Skip to content

Commit

Permalink
Merge pull request #47530 from Expensify/Rory-FixProdDeployGitHubComm…
Browse files Browse the repository at this point in the history
…ents

[No QA] Fix prod deploy GitHub comments

(cherry picked from commit c7d4c5f)

(CP triggered by Beamanator)
  • Loading branch information
AndrewGable authored and OSBotify committed Aug 17, 2024
1 parent 826ed72 commit 2802c8c
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,83 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import type {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types';
import {getJSONInput} from '@github/libs/ActionUtils';
import GithubUtils from '@github/libs/GithubUtils';
import GitUtils from '@github/libs/GitUtils';

type WorkflowRun = RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']['workflow_runs'][number];

const BUILD_AND_DEPLOY_JOB_NAME_PREFIX = 'Build and deploy';

/**
* This function checks if a given release is a valid baseTag to get the PR list with `git log baseTag...endTag`.
*
* The rules are:
* - production deploys can only be compared with other production deploys
* - staging deploys can be compared with other staging deploys or production deploys.
* The reason is that the final staging release in each deploy cycle will BECOME a production release.
* 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.
* 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,
* 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)
*/
async function isReleaseValidBaseForEnvironment(releaseTag: string, isProductionDeploy: boolean) {
if (!isProductionDeploy) {
return true;
}
const isPrerelease = (
await GithubUtils.octokit.repos.getReleaseByTag({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
tag: releaseTag,
})
).data.prerelease;
return !isPrerelease;
}

/**
* Was a given platformDeploy workflow run successful on at least one platform?
*/
async function wasDeploySuccessful(runID: number) {
const jobsForWorkflowRun = (
await GithubUtils.octokit.actions.listJobsForWorkflowRun({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
run_id: runID,
filter: 'latest',
})
).data.jobs;
return jobsForWorkflowRun.some((job) => job.name.startsWith(BUILD_AND_DEPLOY_JOB_NAME_PREFIX) && job.conclusion === 'success');
}

/**
* This function checks if a given deploy workflow is a valid basis for comparison when listing PRs merged between two versions.
* It returns the reason a version should be skipped, or an empty string if the version should not be skipped.
*/
async function shouldSkipVersion(lastSuccessfulDeploy: WorkflowRun, inputTag: string, isProductionDeploy: boolean): Promise<string> {
if (!lastSuccessfulDeploy?.head_branch) {
// This should never happen. Just doing this to appease TS.
return '';
}

// 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.
// 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.
if (lastSuccessfulDeploy?.head_branch === inputTag) {
return `Same as input tag ${inputTag}`;
}
if (!(await isReleaseValidBaseForEnvironment(lastSuccessfulDeploy?.head_branch, isProductionDeploy))) {
return 'Was a staging deploy, we only want to compare with other production deploys';
}
if (!(await wasDeploySuccessful(lastSuccessfulDeploy.id))) {
return 'Was an unsuccessful deploy, nothing was deployed in that version';
}
return '';
}

async function run() {
try {
const inputTag = core.getInput('TAG', {required: true});
const isProductionDeploy = getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false);
const isProductionDeploy = !!getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false);
const deployEnv = isProductionDeploy ? 'production' : 'staging';

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

// Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully
let lastSuccessfulDeploy = completedDeploys.shift();
while (
lastSuccessfulDeploy?.head_branch &&
((
await GithubUtils.octokit.repos.getReleaseByTag({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
tag: lastSuccessfulDeploy.head_branch,
})
).data.prerelease === isProductionDeploy ||
!(
await GithubUtils.octokit.actions.listJobsForWorkflowRun({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
run_id: lastSuccessfulDeploy.id,
filter: 'latest',
})
).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))
) {
console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`);
lastSuccessfulDeploy = completedDeploys.shift();
}

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

let reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
while (lastSuccessfulDeploy && reason) {
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,
);
lastSuccessfulDeploy = completedDeploys.shift();

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

reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
}

const priorTag = lastSuccessfulDeploy.head_branch;
console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`);
const prList = await GitUtils.getPullRequestsMergedBetween(priorTag ?? '', inputTag);
Expand Down
85 changes: 68 additions & 17 deletions .github/actions/javascript/getDeployPullRequestList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11502,10 +11502,68 @@ const github = __importStar(__nccwpck_require__(5438));
const ActionUtils_1 = __nccwpck_require__(6981);
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
const GitUtils_1 = __importDefault(__nccwpck_require__(1547));
const BUILD_AND_DEPLOY_JOB_NAME_PREFIX = 'Build and deploy';
/**
* This function checks if a given release is a valid baseTag to get the PR list with `git log baseTag...endTag`.
*
* The rules are:
* - production deploys can only be compared with other production deploys
* - staging deploys can be compared with other staging deploys or production deploys.
* The reason is that the final staging release in each deploy cycle will BECOME a production release.
* 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.
* 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,
* 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)
*/
async function isReleaseValidBaseForEnvironment(releaseTag, isProductionDeploy) {
if (!isProductionDeploy) {
return true;
}
const isPrerelease = (await GithubUtils_1.default.octokit.repos.getReleaseByTag({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
tag: releaseTag,
})).data.prerelease;
return !isPrerelease;
}
/**
* Was a given platformDeploy workflow run successful on at least one platform?
*/
async function wasDeploySuccessful(runID) {
const jobsForWorkflowRun = (await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
run_id: runID,
filter: 'latest',
})).data.jobs;
return jobsForWorkflowRun.some((job) => job.name.startsWith(BUILD_AND_DEPLOY_JOB_NAME_PREFIX) && job.conclusion === 'success');
}
/**
* This function checks if a given deploy workflow is a valid basis for comparison when listing PRs merged between two versions.
* It returns the reason a version should be skipped, or an empty string if the version should not be skipped.
*/
async function shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy) {
if (!lastSuccessfulDeploy?.head_branch) {
// This should never happen. Just doing this to appease TS.
return '';
}
// 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.
// 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.
if (lastSuccessfulDeploy?.head_branch === inputTag) {
return `Same as input tag ${inputTag}`;
}
if (!(await isReleaseValidBaseForEnvironment(lastSuccessfulDeploy?.head_branch, isProductionDeploy))) {
return 'Was a staging deploy, we only want to compare with other production deploys';
}
if (!(await wasDeploySuccessful(lastSuccessfulDeploy.id))) {
return 'Was an unsuccessful deploy, nothing was deployed in that version';
}
return '';
}
async function run() {
try {
const inputTag = core.getInput('TAG', { required: true });
const isProductionDeploy = (0, ActionUtils_1.getJSONInput)('IS_PRODUCTION_DEPLOY', { required: false }, false);
const isProductionDeploy = !!(0, ActionUtils_1.getJSONInput)('IS_PRODUCTION_DEPLOY', { required: false }, false);
const deployEnv = isProductionDeploy ? 'production' : 'staging';
console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`);
const completedDeploys = (await GithubUtils_1.default.octokit.actions.listWorkflowRuns({
Expand All @@ -11520,25 +11578,18 @@ async function run() {
.filter((workflowRun) => workflowRun.conclusion !== 'cancelled');
// Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully
let lastSuccessfulDeploy = completedDeploys.shift();
while (lastSuccessfulDeploy?.head_branch &&
((await GithubUtils_1.default.octokit.repos.getReleaseByTag({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
tag: lastSuccessfulDeploy.head_branch,
})).data.prerelease === isProductionDeploy ||
!(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
run_id: lastSuccessfulDeploy.id,
filter: 'latest',
})).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))) {
console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`);
lastSuccessfulDeploy = completedDeploys.shift();
}
if (!lastSuccessfulDeploy) {
throw new Error('Could not find a prior successful deploy');
}
let reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
while (lastSuccessfulDeploy && reason) {
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);
lastSuccessfulDeploy = completedDeploys.shift();
if (!lastSuccessfulDeploy) {
throw new Error('Could not find a prior successful deploy');
}
reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
}
const priorTag = lastSuccessfulDeploy.head_branch;
console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`);
const prList = await GitUtils_1.default.getPullRequestsMergedBetween(priorTag ?? '', inputTag);
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/platformDeploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
secrets: inherit

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

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

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

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

0 comments on commit 2802c8c

Please sign in to comment.