-
Notifications
You must be signed in to change notification settings - Fork 25
EVG-20983: Improve deploy script error handling and remove duplicate tag push #2108
Changes from 14 commits
73de282
595d835
6c7c300
929c982
5e6d058
2131f24
6b23c4f
5592ca8
bc7a38e
9ea58a9
90df75e
d4075a8
7cbe8a2
27b550a
2e6a895
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
}); | ||
}); | ||
}); |
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, | ||
}); | ||
Comment on lines
+19
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We ran into an issue last week when the deployer just hit enter and breezed through this message. Could we maybe have it default to false so that a user needs to be a little more careful to go through with this action? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good idea but I'll like to complete it in a separate ticket: https://jira.mongodb.org/browse/EVG-21115 |
||
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the duplicate tag push. |
||
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."); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😍