Skip to content

Commit

Permalink
Merge pull request #49013 from Expensify/Rory-FixDeployShit
Browse files Browse the repository at this point in the history
Fix GitHub Release Bugs

(cherry picked from commit 2add16c)

(CP triggered by luacmartins)
  • Loading branch information
chiragsalian authored and OSBotify committed Sep 11, 2024
1 parent 3861082 commit 19253e0
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 239 deletions.
Original file line number Diff line number Diff line change
@@ -1,79 +1,9 @@
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 deploy 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});
Expand All @@ -82,62 +12,56 @@ async function run() {

console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`);

const platformDeploys = (
await GithubUtils.octokit.actions.listWorkflowRuns({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
workflow_id: 'platformDeploy.yml',
status: 'completed',
})
).data.workflow_runs
// Note: we filter out cancelled runs instead of looking only for success runs
// because if a build fails on even one platform, then it will have the status 'failure'
.filter((workflowRun) => workflowRun.conclusion !== 'cancelled');

const deploys = (
await GithubUtils.octokit.actions.listWorkflowRuns({
let priorTag: string | undefined;
let foundCurrentRelease = false;
await GithubUtils.paginate(
GithubUtils.octokit.repos.listReleases,
{
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
workflow_id: 'deploy.yml',
status: 'completed',
})
).data.workflow_runs
// Note: we filter out cancelled runs instead of looking only for success runs
// because if a build fails on even one platform, then it will have the status 'failure'
.filter((workflowRun) => workflowRun.conclusion !== 'cancelled');

// W've combined platformDeploy.yml and deploy.yml
// TODO: Remove this once there are successful staging and production deploys using the new deploy.yml workflow
const completedDeploys = [...deploys, ...platformDeploys];
completedDeploys.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());

// 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();

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);
per_page: 100,
},
({data}, done) => {
// For production deploys, look only at 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
const filteredData = isProductionDeploy ? data.filter((release) => !release.prerelease) : data;

// Release was in the last page, meaning the previous release is the first item in this page
if (foundCurrentRelease) {
priorTag = data.at(0)?.tag_name;
done();
return filteredData;
}

// Search for the index of input tag
const indexOfCurrentRelease = filteredData.findIndex((release) => release.tag_name === inputTag);

// If it happens to be at the end of this page, then the previous tag will be in the next page.
// Set a flag showing we found it so we grab the first release of the next page
if (indexOfCurrentRelease === filteredData.length - 1) {
foundCurrentRelease = true;
return filteredData;
}

// If it's anywhere else in this page, the the prior release is the next item in the page
if (indexOfCurrentRelease > 0) {
priorTag = filteredData.at(indexOfCurrentRelease + 1)?.tag_name;
done();
}

// Release not in this page (or we're done)
return filteredData;
},
);

if (!priorTag) {
throw new Error('Something went wrong and the prior tag could not be found.');
}

const priorTag = lastSuccessfulDeploy.head_branch;
console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`);
const prList = await GitUtils.getPullRequestsMergedBetween(priorTag ?? '', inputTag);
const prList = await GitUtils.getPullRequestsMergedBetween(priorTag, inputTag);
console.log('Found the pull request list: ', prList);
core.setOutput('PR_LIST', prList);
} catch (error) {
Expand Down
126 changes: 33 additions & 93 deletions .github/actions/javascript/getDeployPullRequestList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11502,111 +11502,51 @@ 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 deploy 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 deployEnv = isProductionDeploy ? 'production' : 'staging';
console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`);
const platformDeploys = (await GithubUtils_1.default.octokit.actions.listWorkflowRuns({
let priorTag;
let foundCurrentRelease = false;
await GithubUtils_1.default.paginate(GithubUtils_1.default.octokit.repos.listReleases, {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
workflow_id: 'platformDeploy.yml',
status: 'completed',
})).data.workflow_runs
// Note: we filter out cancelled runs instead of looking only for success runs
// because if a build fails on even one platform, then it will have the status 'failure'
.filter((workflowRun) => workflowRun.conclusion !== 'cancelled');
const deploys = (await GithubUtils_1.default.octokit.actions.listWorkflowRuns({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
workflow_id: 'deploy.yml',
status: 'completed',
})).data.workflow_runs
// Note: we filter out cancelled runs instead of looking only for success runs
// because if a build fails on even one platform, then it will have the status 'failure'
.filter((workflowRun) => workflowRun.conclusion !== 'cancelled');
// W've combined platformDeploy.yml and deploy.yml
// TODO: Remove this once there are successful staging and production deploys using the new deploy.yml workflow
const completedDeploys = [...deploys, ...platformDeploys];
completedDeploys.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
// 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();
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');
per_page: 100,
}, ({ data }, done) => {
// For production deploys, look only at 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
const filteredData = isProductionDeploy ? data.filter((release) => !release.prerelease) : data;
// Release was in the last page, meaning the previous release is the first item in this page
if (foundCurrentRelease) {
priorTag = data.at(0)?.tag_name;
done();
return filteredData;
}
// Search for the index of input tag
const indexOfCurrentRelease = filteredData.findIndex((release) => release.tag_name === inputTag);
// If it happens to be at the end of this page, then the previous tag will be in the next page.
// Set a flag showing we found it so we grab the first release of the next page
if (indexOfCurrentRelease === filteredData.length - 1) {
foundCurrentRelease = true;
return filteredData;
}
reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy);
// If it's anywhere else in this page, the the prior release is the next item in the page
if (indexOfCurrentRelease > 0) {
priorTag = filteredData.at(indexOfCurrentRelease + 1)?.tag_name;
done();
}
// Release not in this page (or we're done)
return filteredData;
});
if (!priorTag) {
throw new Error('Something went wrong and the prior tag could not be found.');
}
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);
const prList = await GitUtils_1.default.getPullRequestsMergedBetween(priorTag, inputTag);
console.log('Found the pull request list: ', prList);
core.setOutput('PR_LIST', prList);
}
Expand Down
Loading

0 comments on commit 19253e0

Please sign in to comment.