Skip to content

Commit c7d4c5f

Browse files
authored
Merge pull request #47530 from Expensify/Rory-FixProdDeployGitHubComments
[No QA] Fix prod deploy GitHub comments
2 parents 6fd16ac + 0183ca5 commit c7d4c5f

File tree

3 files changed

+158
-40
lines changed

3 files changed

+158
-40
lines changed

.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,83 @@
11
import * as core from '@actions/core';
22
import * as github from '@actions/github';
3+
import type {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types';
34
import {getJSONInput} from '@github/libs/ActionUtils';
45
import GithubUtils from '@github/libs/GithubUtils';
56
import GitUtils from '@github/libs/GitUtils';
67

8+
type WorkflowRun = RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']['workflow_runs'][number];
9+
10+
const BUILD_AND_DEPLOY_JOB_NAME_PREFIX = 'Build and deploy';
11+
12+
/**
13+
* This function checks if a given release is a valid baseTag to get the PR list with `git log baseTag...endTag`.
14+
*
15+
* The rules are:
16+
* - production deploys can only be compared with other production deploys
17+
* - staging deploys can be compared with other staging deploys or production deploys.
18+
* The reason is that the final staging release in each deploy cycle will BECOME a production release.
19+
* For example, imagine a checklist is closed with version 9.0.20-6; that's the most recent staging deploy, but the release for 9.0.20-6 is now finalized, so it looks like a prod deploy.
20+
* When 9.0.21-0 finishes deploying to staging, the most recent prerelease is 9.0.20-5. However, we want 9.0.20-6...9.0.21-0,
21+
* NOT 9.0.20-5...9.0.21-0 (so that the PR CP'd in 9.0.20-6 is not included in the next checklist)
22+
*/
23+
async function isReleaseValidBaseForEnvironment(releaseTag: string, isProductionDeploy: boolean) {
24+
if (!isProductionDeploy) {
25+
return true;
26+
}
27+
const isPrerelease = (
28+
await GithubUtils.octokit.repos.getReleaseByTag({
29+
owner: github.context.repo.owner,
30+
repo: github.context.repo.repo,
31+
tag: releaseTag,
32+
})
33+
).data.prerelease;
34+
return !isPrerelease;
35+
}
36+
37+
/**
38+
* Was a given platformDeploy workflow run successful on at least one platform?
39+
*/
40+
async function wasDeploySuccessful(runID: number) {
41+
const jobsForWorkflowRun = (
42+
await GithubUtils.octokit.actions.listJobsForWorkflowRun({
43+
owner: github.context.repo.owner,
44+
repo: github.context.repo.repo,
45+
// eslint-disable-next-line @typescript-eslint/naming-convention
46+
run_id: runID,
47+
filter: 'latest',
48+
})
49+
).data.jobs;
50+
return jobsForWorkflowRun.some((job) => job.name.startsWith(BUILD_AND_DEPLOY_JOB_NAME_PREFIX) && job.conclusion === 'success');
51+
}
52+
53+
/**
54+
* This function checks if a given deploy workflow is a valid basis for comparison when listing PRs merged between two versions.
55+
* It returns the reason a version should be skipped, or an empty string if the version should not be skipped.
56+
*/
57+
async function shouldSkipVersion(lastSuccessfulDeploy: WorkflowRun, inputTag: string, isProductionDeploy: boolean): Promise<string> {
58+
if (!lastSuccessfulDeploy?.head_branch) {
59+
// This should never happen. Just doing this to appease TS.
60+
return '';
61+
}
62+
63+
// we never want to compare a tag with itself. This check is necessary because prod deploys almost always have the same version as the last staging deploy.
64+
// In this case, the next for wrong environment fails because the release that triggered that staging deploy is now finalized, so it looks like a prod deploy.
65+
if (lastSuccessfulDeploy?.head_branch === inputTag) {
66+
return `Same as input tag ${inputTag}`;
67+
}
68+
if (!(await isReleaseValidBaseForEnvironment(lastSuccessfulDeploy?.head_branch, isProductionDeploy))) {
69+
return 'Was a staging deploy, we only want to compare with other production deploys';
70+
}
71+
if (!(await wasDeploySuccessful(lastSuccessfulDeploy.id))) {
72+
return 'Was an unsuccessful deploy, nothing was deployed in that version';
73+
}
74+
return '';
75+
}
76+
777
async function run() {
878
try {
979
const inputTag = core.getInput('TAG', {required: true});
10-
const isProductionDeploy = getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false);
80+
const isProductionDeploy = !!getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false);
1181
const deployEnv = isProductionDeploy ? 'production' : 'staging';
1282

1383
console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`);
@@ -27,33 +97,26 @@ async function run() {
2797

2898
// Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully
2999
let lastSuccessfulDeploy = completedDeploys.shift();
30-
while (
31-
lastSuccessfulDeploy?.head_branch &&
32-
((
33-
await GithubUtils.octokit.repos.getReleaseByTag({
34-
owner: github.context.repo.owner,
35-
repo: github.context.repo.repo,
36-
tag: lastSuccessfulDeploy.head_branch,
37-
})
38-
).data.prerelease === isProductionDeploy ||
39-
!(
40-
await GithubUtils.octokit.actions.listJobsForWorkflowRun({
41-
owner: github.context.repo.owner,
42-
repo: github.context.repo.repo,
43-
// eslint-disable-next-line @typescript-eslint/naming-convention
44-
run_id: lastSuccessfulDeploy.id,
45-
filter: 'latest',
46-
})
47-
).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))
48-
) {
49-
console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`);
50-
lastSuccessfulDeploy = completedDeploys.shift();
51-
}
52100

53101
if (!lastSuccessfulDeploy) {
54102
throw new Error('Could not find a prior successful deploy');
55103
}
56104

105+
let reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
106+
while (lastSuccessfulDeploy && reason) {
107+
console.log(
108+
`Deploy of tag ${lastSuccessfulDeploy.head_branch} was not valid as a base for comparison, looking at the next one. Reason: ${reason}`,
109+
lastSuccessfulDeploy.html_url,
110+
);
111+
lastSuccessfulDeploy = completedDeploys.shift();
112+
113+
if (!lastSuccessfulDeploy) {
114+
throw new Error('Could not find a prior successful deploy');
115+
}
116+
117+
reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
118+
}
119+
57120
const priorTag = lastSuccessfulDeploy.head_branch;
58121
console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`);
59122
const prList = await GitUtils.getPullRequestsMergedBetween(priorTag ?? '', inputTag);

.github/actions/javascript/getDeployPullRequestList/index.js

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11502,10 +11502,68 @@ const github = __importStar(__nccwpck_require__(5438));
1150211502
const ActionUtils_1 = __nccwpck_require__(6981);
1150311503
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
1150411504
const GitUtils_1 = __importDefault(__nccwpck_require__(1547));
11505+
const BUILD_AND_DEPLOY_JOB_NAME_PREFIX = 'Build and deploy';
11506+
/**
11507+
* This function checks if a given release is a valid baseTag to get the PR list with `git log baseTag...endTag`.
11508+
*
11509+
* The rules are:
11510+
* - production deploys can only be compared with other production deploys
11511+
* - staging deploys can be compared with other staging deploys or production deploys.
11512+
* The reason is that the final staging release in each deploy cycle will BECOME a production release.
11513+
* For example, imagine a checklist is closed with version 9.0.20-6; that's the most recent staging deploy, but the release for 9.0.20-6 is now finalized, so it looks like a prod deploy.
11514+
* When 9.0.21-0 finishes deploying to staging, the most recent prerelease is 9.0.20-5. However, we want 9.0.20-6...9.0.21-0,
11515+
* NOT 9.0.20-5...9.0.21-0 (so that the PR CP'd in 9.0.20-6 is not included in the next checklist)
11516+
*/
11517+
async function isReleaseValidBaseForEnvironment(releaseTag, isProductionDeploy) {
11518+
if (!isProductionDeploy) {
11519+
return true;
11520+
}
11521+
const isPrerelease = (await GithubUtils_1.default.octokit.repos.getReleaseByTag({
11522+
owner: github.context.repo.owner,
11523+
repo: github.context.repo.repo,
11524+
tag: releaseTag,
11525+
})).data.prerelease;
11526+
return !isPrerelease;
11527+
}
11528+
/**
11529+
* Was a given platformDeploy workflow run successful on at least one platform?
11530+
*/
11531+
async function wasDeploySuccessful(runID) {
11532+
const jobsForWorkflowRun = (await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({
11533+
owner: github.context.repo.owner,
11534+
repo: github.context.repo.repo,
11535+
// eslint-disable-next-line @typescript-eslint/naming-convention
11536+
run_id: runID,
11537+
filter: 'latest',
11538+
})).data.jobs;
11539+
return jobsForWorkflowRun.some((job) => job.name.startsWith(BUILD_AND_DEPLOY_JOB_NAME_PREFIX) && job.conclusion === 'success');
11540+
}
11541+
/**
11542+
* This function checks if a given deploy workflow is a valid basis for comparison when listing PRs merged between two versions.
11543+
* It returns the reason a version should be skipped, or an empty string if the version should not be skipped.
11544+
*/
11545+
async function shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy) {
11546+
if (!lastSuccessfulDeploy?.head_branch) {
11547+
// This should never happen. Just doing this to appease TS.
11548+
return '';
11549+
}
11550+
// we never want to compare a tag with itself. This check is necessary because prod deploys almost always have the same version as the last staging deploy.
11551+
// In this case, the next for wrong environment fails because the release that triggered that staging deploy is now finalized, so it looks like a prod deploy.
11552+
if (lastSuccessfulDeploy?.head_branch === inputTag) {
11553+
return `Same as input tag ${inputTag}`;
11554+
}
11555+
if (!(await isReleaseValidBaseForEnvironment(lastSuccessfulDeploy?.head_branch, isProductionDeploy))) {
11556+
return 'Was a staging deploy, we only want to compare with other production deploys';
11557+
}
11558+
if (!(await wasDeploySuccessful(lastSuccessfulDeploy.id))) {
11559+
return 'Was an unsuccessful deploy, nothing was deployed in that version';
11560+
}
11561+
return '';
11562+
}
1150511563
async function run() {
1150611564
try {
1150711565
const inputTag = core.getInput('TAG', { required: true });
11508-
const isProductionDeploy = (0, ActionUtils_1.getJSONInput)('IS_PRODUCTION_DEPLOY', { required: false }, false);
11566+
const isProductionDeploy = !!(0, ActionUtils_1.getJSONInput)('IS_PRODUCTION_DEPLOY', { required: false }, false);
1150911567
const deployEnv = isProductionDeploy ? 'production' : 'staging';
1151011568
console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`);
1151111569
const completedDeploys = (await GithubUtils_1.default.octokit.actions.listWorkflowRuns({
@@ -11520,25 +11578,18 @@ async function run() {
1152011578
.filter((workflowRun) => workflowRun.conclusion !== 'cancelled');
1152111579
// Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully
1152211580
let lastSuccessfulDeploy = completedDeploys.shift();
11523-
while (lastSuccessfulDeploy?.head_branch &&
11524-
((await GithubUtils_1.default.octokit.repos.getReleaseByTag({
11525-
owner: github.context.repo.owner,
11526-
repo: github.context.repo.repo,
11527-
tag: lastSuccessfulDeploy.head_branch,
11528-
})).data.prerelease === isProductionDeploy ||
11529-
!(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({
11530-
owner: github.context.repo.owner,
11531-
repo: github.context.repo.repo,
11532-
// eslint-disable-next-line @typescript-eslint/naming-convention
11533-
run_id: lastSuccessfulDeploy.id,
11534-
filter: 'latest',
11535-
})).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))) {
11536-
console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`);
11537-
lastSuccessfulDeploy = completedDeploys.shift();
11538-
}
1153911581
if (!lastSuccessfulDeploy) {
1154011582
throw new Error('Could not find a prior successful deploy');
1154111583
}
11584+
let reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
11585+
while (lastSuccessfulDeploy && reason) {
11586+
console.log(`Deploy of tag ${lastSuccessfulDeploy.head_branch} was not valid as a base for comparison, looking at the next one. Reason: ${reason}`, lastSuccessfulDeploy.html_url);
11587+
lastSuccessfulDeploy = completedDeploys.shift();
11588+
if (!lastSuccessfulDeploy) {
11589+
throw new Error('Could not find a prior successful deploy');
11590+
}
11591+
reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
11592+
}
1154211593
const priorTag = lastSuccessfulDeploy.head_branch;
1154311594
console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`);
1154411595
const prList = await GitUtils_1.default.getPullRequestsMergedBetween(priorTag ?? '', inputTag);

.github/workflows/platformDeploy.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
secrets: inherit
3939

4040
android:
41+
# WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly
4142
name: Build and deploy Android
4243
needs: validateActor
4344
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }}
@@ -122,6 +123,7 @@ jobs:
122123
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
123124

124125
desktop:
126+
# WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly
125127
name: Build and deploy Desktop
126128
needs: validateActor
127129
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }}
@@ -165,6 +167,7 @@ jobs:
165167
GITHUB_TOKEN: ${{ github.token }}
166168

167169
iOS:
170+
# WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly
168171
name: Build and deploy iOS
169172
needs: validateActor
170173
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }}
@@ -276,6 +279,7 @@ jobs:
276279
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
277280

278281
web:
282+
# WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly
279283
name: Build and deploy Web
280284
needs: validateActor
281285
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }}

0 commit comments

Comments
 (0)