Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
EVG-20983: Improve deploy script error handling and remove duplicate …
Browse files Browse the repository at this point in the history
…tag push (#2108)
  • Loading branch information
SupaJoon authored Nov 1, 2023
1 parent 6a2f177 commit 1d33a16
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 94 deletions.
142 changes: 142 additions & 0 deletions scripts/deploy/deploy-production.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import prompts from "prompts";
import { evergreenDeploy, localDeploy, ciDeploy } from "./deploy-production";
import { runDeploy } from "./utils/deploy";
import { getCommitMessages, getCurrentlyDeployedCommit } from "./utils/git";
import { tagUtils } from "./utils/git/tag";
import { isRunningOnCI } from "./utils/environment";

jest.mock("prompts");
jest.mock("./utils/git/tag");
jest.mock("./utils/git");
jest.mock("./utils/deploy");
jest.mock("./utils/environment");

describe("deploy-production", () => {
let consoleLogMock;
let processExitMock;
let consoleErrorMock;

beforeEach(() => {
consoleLogMock = jest.spyOn(console, "log").mockImplementation();
consoleErrorMock = jest.spyOn(console, "error").mockImplementation();
processExitMock = jest
.spyOn(process, "exit")
.mockImplementation(() => undefined as never);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("evergreenDeploy", () => {
it("should force deploy if no new commits and user confirms", async () => {
(getCommitMessages as jest.Mock).mockReturnValue("");
(prompts as unknown as jest.Mock).mockResolvedValue({ value: true });

await evergreenDeploy();

expect(tagUtils.deleteTag).toHaveBeenCalled();
expect(tagUtils.pushTags).toHaveBeenCalled();
expect(consoleLogMock).toHaveBeenCalledWith(
"Check Evergreen for deploy progress."
);
});

it("should cancel deploy if no new commits and user denies", async () => {
(getCommitMessages as jest.Mock).mockReturnValue("");
(prompts as unknown as jest.Mock).mockResolvedValue({ value: false });
await evergreenDeploy();
expect(tagUtils.deleteTag).not.toHaveBeenCalled();
expect(tagUtils.pushTags).not.toHaveBeenCalled();
expect(consoleLogMock).toHaveBeenCalledWith(
"Deploy canceled. If systems are experiencing an outage and you'd like to push the deploy directly to S3, run yarn deploy:prod --local."
);
});

it("should deploy if new commits, user confirms and create tag succeeds", async () => {
(getCommitMessages as jest.Mock).mockReturnValue(
"getCommitMessages result"
);
(prompts as unknown as jest.Mock).mockResolvedValue({ value: true });
(tagUtils.createTagAndPush as jest.Mock).mockResolvedValue(true);
const createTagAndPushMock = jest
.spyOn(tagUtils, "createTagAndPush")
.mockImplementation(() => true);
(getCurrentlyDeployedCommit as jest.Mock).mockReturnValue(
"getCurrentlyDeployedCommit mock"
);
await evergreenDeploy();
expect(consoleLogMock).toHaveBeenCalledTimes(2);
expect(consoleLogMock).toHaveBeenCalledWith(
"Currently Deployed Commit: getCurrentlyDeployedCommit mock"
);
expect(consoleLogMock).toHaveBeenCalledWith(
"Commit messages:\ngetCommitMessages result"
);
expect(createTagAndPushMock).toBeCalledTimes(1);
});

it("return exit code 1 if an error is thrown", async () => {
const e = new Error("test error", { cause: "cause of test error" });
(getCommitMessages as jest.Mock).mockReturnValue(
"getCommitMessages result"
);
(prompts as unknown as jest.Mock).mockResolvedValue({ value: true });
(tagUtils.createTagAndPush as jest.Mock).mockImplementation(() => {
throw e;
});
expect(await evergreenDeploy());
expect(consoleErrorMock).toHaveBeenCalledWith(e);
expect(consoleLogMock).toHaveBeenCalledWith("Deploy failed.");
expect(processExitMock).toHaveBeenCalledWith(1);
});
});

describe("localDeploy", () => {
it("should run deploy when user confirms", async () => {
(prompts as unknown as jest.Mock).mockResolvedValue({ value: true });
await localDeploy();
expect(runDeploy).toHaveBeenCalled();
});

it("should not run deploy when user denies", async () => {
(prompts as unknown as jest.Mock).mockResolvedValue({ value: false });
await localDeploy();
expect(runDeploy).not.toHaveBeenCalled();
});

it("logs and error and returns exit code 1 when error is thrown", async () => {
(prompts as unknown as jest.Mock).mockResolvedValue({ value: true });
const e = new Error("error mock");
(runDeploy as jest.Mock).mockImplementation(() => {
throw e;
});
await localDeploy();
expect(consoleErrorMock).toHaveBeenCalledWith(e);
expect(consoleErrorMock).toHaveBeenCalledWith(
"Local deploy failed. Aborting."
);
expect(processExitMock).toHaveBeenCalledWith(1);
});
});

describe("ciDeploy", () => {
it("returns exit code 1 when not running in CI", async () => {
(isRunningOnCI as jest.Mock).mockReturnValue(false);
await ciDeploy();
expect(consoleErrorMock).toHaveBeenCalledWith(
new Error("Not running on CI")
);
expect(consoleErrorMock).toHaveBeenCalledWith(
"CI deploy failed. Aborting."
);
expect(processExitMock).toHaveBeenCalledWith(1);
});

it("should run deploy when running on CI", async () => {
(isRunningOnCI as jest.Mock).mockReturnValue(true);
await ciDeploy();
expect(runDeploy).toHaveBeenCalled();
});
});
});
122 changes: 52 additions & 70 deletions scripts/deploy/deploy-production.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,87 @@
import prompts from "prompts";
import { tagUtils } from "./utils/git/tag";
import { green, underline } from "../utils/colors";
import { isRunningOnCI } from "./utils/environment";
import { getCommitMessages, getCurrentlyDeployedCommit } from "./utils/git";
import { runDeploy } from "./utils/deploy";

const { createNewTag, deleteTag, getLatestTag, pushTags } = tagUtils;
const { createTagAndPush, deleteTag, getLatestTag, pushTags } = tagUtils;
/* Deploy by pushing a git tag, to be picked up and built by Evergreen, and deployed to S3. */
const evergreenDeploy = async () => {
const currentlyDeployedCommit = getCurrentlyDeployedCommit();
console.log(`Currently Deployed Commit: ${currentlyDeployedCommit}`);
try {
const currentlyDeployedCommit = getCurrentlyDeployedCommit();
console.log(`Currently Deployed Commit: ${currentlyDeployedCommit}`);

const commitMessages = getCommitMessages(currentlyDeployedCommit);
const commitMessages = getCommitMessages(currentlyDeployedCommit);

// If there are no commit messages, ask the user if they want to delete and re-push the latest tag, thereby forcing a deploy with no new commits.
if (commitMessages.length === 0) {
const latestTag = getLatestTag();
const response = await prompts({
type: "confirm",
name: "value",
message: `No new commits. Do you want to trigger a deploy on the most recent existing tag? (${latestTag})`,
initial: false,
});
// If there are no commit messages, ask the user if they want to delete and re-push the latest tag, thereby forcing a deploy with no new commits.
if (commitMessages.length === 0) {
const latestTag = getLatestTag();
const { value: shouldForceDeploy } = await prompts({
type: "confirm",
name: "value",
message: `No new commits. Do you want to trigger a deploy on the most recent existing tag? (${latestTag})`,
initial: false,
});

const forceDeploy = response.value;
if (forceDeploy) {
console.log(`Deleting tag (${latestTag}) from remote...`);
deleteTag(latestTag);
console.log("Pushing tags...");
pushTags();
console.log("Check Evergreen for deploy progress.");
if (shouldForceDeploy) {
deleteTag(latestTag);
pushTags();
console.log("Check Evergreen for deploy progress.");
} else {
console.log(
"Deploy canceled. If systems are experiencing an outage and you'd like to push the deploy directly to S3, run yarn deploy:prod --local."
);
}
return;
}

console.log(
"Deploy canceled. If systems are experiencing an outage and you'd like to push the deploy directly to S3, run yarn deploy:prod --local."
);
return;
}

// Print all commits between the last tag and the current commit
console.log(`Commit messages:\n${commitMessages}`);
// Print all commits between the last tag and the current commit
console.log(`Commit messages:\n${commitMessages}`);

const response = await prompts({
type: "confirm",
name: "value",
message: "Are you sure you want to deploy to production?",
});
const { value: shouldCreateTagAndPush } = await prompts({
type: "confirm",
name: "value",
message: "Are you sure you want to deploy to production?",
});

if (response.value) {
try {
console.log("Creating new tag...");
createNewTag();
console.log("Pushing tags...");
pushTags();
console.log("Pushed to remote. Should be deploying soon...");
console.log(
green(
`Track deploy progress at ${underline(
"https://spruce.mongodb.com/commits/spruce?requester=git_tag_request"
)}`
)
);
} catch (err) {
console.error(err);
console.error("Creating tag failed. Aborting.");
process.exit(1);
if (shouldCreateTagAndPush) {
createTagAndPush();
}
} catch (err) {
console.error(err);
console.log("Deploy failed.");
process.exit(1);
}
};

/* Deploy by generating a production build locally and pushing it directly to S3. */
const localDeploy = async () => {
const response = await prompts({
type: "confirm",
name: "value",
message:
"Are you sure you'd like to build Spruce locally and push directly to S3? This is a high-risk operation that requires a correctly configured local environment.",
});

if (response.value) {
try {
try {
const response = await prompts({
type: "confirm",
name: "value",
message:
"Are you sure you'd like to build Spruce locally and push directly to S3? This is a high-risk operation that requires a correctly configured local environment.",
});
if (response.value) {
runDeploy();
} catch (err) {
console.error(err);
console.error("Local deploy failed. Aborting.");
process.exit(1);
}
} catch (err) {
console.error(err);
console.error("Local deploy failed. Aborting.");
process.exit(1);
}
};

/**
* `ciDeploy` is a special deploy function that is only run on CI. It does the actual deploy to S3.
*/
const ciDeploy = async () => {
if (!isRunningOnCI()) {
throw new Error("Not running on CI");
}
try {
const ciDeployOutput = runDeploy();
console.log(ciDeployOutput);
if (!isRunningOnCI()) {
throw new Error("Not running on CI");
}
runDeploy();
} catch (err) {
console.error(err);
console.error("CI deploy failed. Aborting.");
Expand Down
8 changes: 4 additions & 4 deletions scripts/deploy/utils/git/tag/mock-tag-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { yellow } from "../../../../utils/colors";

/**
* `createNewTag` is a helper function that creates a new tag.
* `createTagAndPush` is a helper function that creates a new tag and pushes to remote.
*/
const createNewTag = () => {
console.log(yellow("Dry run mode enabled. Created new tag."));
const createTagAndPush = () => {
console.log(yellow("Dry run mode enabled. Created new tag and pushed."));
};

/**
Expand All @@ -31,4 +31,4 @@ const pushTags = () => {
console.log(yellow("Dry run mode enabled. Pushing tags."));
};

export { createNewTag, getLatestTag, deleteTag, pushTags };
export { createTagAndPush, getLatestTag, deleteTag, pushTags };
Loading

0 comments on commit 1d33a16

Please sign in to comment.