Skip to content

Commit

Permalink
move bash script to cancel preparation runs to tested node script
Browse files Browse the repository at this point in the history
  • Loading branch information
JReinhold committed Oct 4, 2023
1 parent ed18a24 commit 930adf4
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 14 deletions.
17 changes: 3 additions & 14 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,9 @@ jobs:
gh run watch ${{ github.run_id }}
- name: Cancel all release preparation runs
run: |
# Get a list of all running or pending release preparation runs
# combining both the prepare-patch-release.yml and prepare-non-patch-release.yml workflows
RUNNING_RELEASE_PREPARATIONS=$(
{
gh run list --limit 50 --workflow=prepare-patch-release.yml --json databaseId,status
gh run list --limit 50 --workflow=prepare-non-patch-release.yml --json databaseId,status
} | jq -rc '.[] | select(.status | contains("in_progress", "pending", "queued", "requested", "waiting")) | .databaseId'
)
# Loop through each run and pass it to the "gh run cancel" command
while IFS= read -r databaseId; do
gh run cancel "$databaseId"
done <<< "$RUNNING_RELEASE_PREPARATIONS"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn release:cancel-preparation-runs

- name: Checkout ${{ github.ref_name }}
uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint:js:cmd": "cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives",
"lint:package": "sort-package-json",
"migrate-docs": "node --require esbuild-register ./ts-to-ts49.ts",
"release:cancel-preparation-runs": "ts-node --swc ./release/cancel-preparation-runs.ts",
"release:ensure-next-ahead": "ts-node --swc ./release/ensure-next-ahead.ts",
"release:generate-pr-description": "ts-node --swc ./release/generate-pr-description.ts",
"release:get-changelog-from-file": "ts-node --swc ./release/get-changelog-from-file.ts",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@jest/globals": "^29.3.1",
"@nx/workspace": "16.2.1",
"@octokit/graphql": "^5.0.5",
"@octokit/request": "^8.1.2",
"@storybook/eslint-config-storybook": "^3.1.2",
"@storybook/jest": "next",
"@storybook/linter-config": "^3.1.2",
Expand Down
107 changes: 107 additions & 0 deletions scripts/release/__tests__/cancel-preparation-runs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* eslint-disable global-require */
/* eslint-disable no-underscore-dangle */
import {
PREPARE_NON_PATCH_WORKFLOW_PATH,
PREPARE_PATCH_WORKFLOW_PATH,
run as cancelPreparationWorkflows,
} from '../cancel-preparation-runs';
import * as github_ from '../utils/github-client';

jest.mock('../utils/github-client');

const github = jest.mocked(github_);

jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});

describe('Cancel preparation runs', () => {
beforeEach(() => {
jest.clearAllMocks();
github.githubRestClient.mockImplementation(((route: string, options: any) => {
switch (route) {
case 'GET /repos/{owner}/{repo}/actions/workflows':
return {
data: {
workflows: [
{
id: 1,
path: PREPARE_PATCH_WORKFLOW_PATH,
},
{
id: 2,
path: PREPARE_NON_PATCH_WORKFLOW_PATH,
},
],
},
};
case 'GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs':
return {
data: {
workflow_runs: [
{
id: options.workflow_id === 1 ? 100 : 200,
status: 'in_progress',
},
{
id: options.workflow_id === 1 ? 150 : 250,
status: 'completed',
},
],
},
};
case 'POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel':
return undefined; // success
default:
throw new Error(`Unexpected route: ${route}`);
}
}) as any);
});

it('should fail early when no GH_TOKEN is set', async () => {
delete process.env.GH_TOKEN;
await expect(cancelPreparationWorkflows()).rejects.toThrowErrorMatchingInlineSnapshot(
`"GH_TOKEN environment variable must be set, exiting."`
);
});

it('should cancel all running preparation workflows in GitHub', async () => {
process.env.GH_TOKEN = 'MY_SECRET';

await expect(cancelPreparationWorkflows()).resolves.toBeUndefined();

expect(github.githubRestClient).toHaveBeenCalledTimes(5);
expect(github.githubRestClient).toHaveBeenCalledWith(
'POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel',
{
owner: 'storybookjs',
repo: 'storybook',
run_id: 100,
}
);
expect(github.githubRestClient).toHaveBeenCalledWith(
'POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel',
{
owner: 'storybookjs',
repo: 'storybook',
run_id: 200,
}
);
expect(github.githubRestClient).not.toHaveBeenCalledWith(
'POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel',
{
owner: 'storybookjs',
repo: 'storybook',
run_id: 150,
}
);
expect(github.githubRestClient).not.toHaveBeenCalledWith(
'POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel',
{
owner: 'storybookjs',
repo: 'storybook',
run_id: 250,
}
);
});
});
107 changes: 107 additions & 0 deletions scripts/release/cancel-preparation-runs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* This script cancels all running preparation workflows in GitHub.
* It will fetch all active runs for the preparation workflows, and cancel them.
*/
/* eslint-disable no-console */
import chalk from 'chalk';
import program from 'commander';
import dedent from 'ts-dedent';
import { githubRestClient } from './utils/github-client';

program
.name('cancel-preparation-workflows')
.description('cancel all running preparation workflows in GitHub');

export const PREPARE_PATCH_WORKFLOW_PATH = '.github/workflows/prepare-patch-release.yml';
export const PREPARE_NON_PATCH_WORKFLOW_PATH = '.github/workflows/prepare-non-patch-release.yml';

export const run = async () => {
if (!process.env.GH_TOKEN) {
throw new Error('GH_TOKEN environment variable must be set, exiting.');
}

console.log(`🔎 Looking for workflows to cancel...`);
const allWorkflows = await githubRestClient('GET /repos/{owner}/{repo}/actions/workflows', {
owner: 'storybookjs',
repo: 'storybook',
});

const preparePatchWorkflowId = allWorkflows.data.workflows.find(
({ path }) => path === PREPARE_PATCH_WORKFLOW_PATH
)?.id;
const prepareNonPatchWorkflowId = allWorkflows.data.workflows.find(
({ path }) => path === PREPARE_NON_PATCH_WORKFLOW_PATH
)?.id;

console.log(`Found workflow IDs for the preparation workflows:
${chalk.blue(PREPARE_PATCH_WORKFLOW_PATH)}: ${chalk.green(preparePatchWorkflowId)}
${chalk.blue(PREPARE_NON_PATCH_WORKFLOW_PATH)}: ${chalk.green(prepareNonPatchWorkflowId)}`);

if (!preparePatchWorkflowId || !prepareNonPatchWorkflowId) {
throw new Error(dedent`🚨 Could not find workflow IDs for the preparation workflows
- Looked for paths: "${chalk.blue(PREPARE_PATCH_WORKFLOW_PATH)}" and "${chalk.blue(
PREPARE_NON_PATCH_WORKFLOW_PATH
)}", are they still correct?
- Found workflows:
${JSON.stringify(allWorkflows.data.workflows, null, 2)}`);
}

console.log('🔍 Fetching patch and non-patch runs for preparation workflows...');
const [patchRuns, nonPatchRuns] = await Promise.all([
githubRestClient('GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs', {
owner: 'storybookjs',
repo: 'storybook',
workflow_id: preparePatchWorkflowId,
}),
githubRestClient('GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs', {
owner: 'storybookjs',
repo: 'storybook',
workflow_id: prepareNonPatchWorkflowId,
}),
]);
console.log('✅ Successfully fetched patch and non-patch runs for preparation workflows.');

const runsToCancel = patchRuns.data.workflow_runs
.concat(nonPatchRuns.data.workflow_runs)
.filter(({ status }) =>
['in_progress', 'pending', 'queued', 'requested', 'waiting'].includes(status)
);

if (runsToCancel.length === 0) {
console.log('👍 No runs to cancel.');
return;
}

console.log(`🔍 Found ${runsToCancel.length} runs to cancel. Cancelling them now:
${runsToCancel
.map((r) => `${chalk.green(r.path)} - ${chalk.green(r.id)}: ${chalk.blue(r.status)}`)
.join('\n ')}`);

const result = await Promise.allSettled(
runsToCancel.map((r) =>
githubRestClient('POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel', {
owner: 'storybookjs',
repo: 'storybook',
run_id: r.id,
})
)
);

if (result.some((r) => r.status === 'rejected')) {
console.warn('⚠️ Some runs could not be cancelled:');
result.forEach((r, index) => {
if (r.status === 'rejected') {
console.warn(`Run ID: ${runsToCancel[index].id} - Reason: ${r.reason}`);
}
});
} else {
console.log('✅ Successfully cancelled all preparation runs.');
}
};

if (require.main === module) {
run().catch((err) => {
console.error(err);
// this is non-critical work, so we don't want to fail the CI build if this fails
});
}
9 changes: 9 additions & 0 deletions scripts/release/utils/github-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable no-console */
import type { GraphQlQueryResponseData } from '@octokit/graphql';
import { graphql } from '@octokit/graphql';
import { request } from '@octokit/request';
import fetch from 'node-fetch';

export interface PullRequest {
number: number;
Expand All @@ -14,6 +16,13 @@ export const githubGraphQlClient = graphql.defaults({
headers: { authorization: `token ${process.env.GH_TOKEN}` },
});

export const githubRestClient = request.defaults({
request: {
fetch,
},
headers: { authorization: `token ${process.env.GH_TOKEN}`, 'X-GitHub-Api-Version': '2022-11-28' },
});

export async function getUnpickedPRs(
baseBranch: string,
verbose?: boolean
Expand Down
52 changes: 52 additions & 0 deletions scripts/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2536,6 +2536,17 @@ __metadata:
languageName: node
linkType: hard

"@octokit/endpoint@npm:^9.0.0":
version: 9.0.1
resolution: "@octokit/endpoint@npm:9.0.1"
dependencies:
"@octokit/types": ^12.0.0
is-plain-object: ^5.0.0
universal-user-agent: ^6.0.0
checksum: 757505b1cd634bcd7b71a18c8fe07dfda47790598ddd0d9d13f47d68713070f49953a672ac40ec39787defc2a7e07d08dca97756def7b907118f8f8d4c653f5c
languageName: node
linkType: hard

"@octokit/graphql@npm:^4.3.1, @octokit/graphql@npm:^4.5.8":
version: 4.8.0
resolution: "@octokit/graphql@npm:4.8.0"
Expand Down Expand Up @@ -2572,6 +2583,13 @@ __metadata:
languageName: node
linkType: hard

"@octokit/openapi-types@npm:^19.0.0":
version: 19.0.0
resolution: "@octokit/openapi-types@npm:19.0.0"
checksum: 8270e0a224bbef6d1c82396cda873a3528111cb25a772184b39e1fbada4e6433b41c5f4634ecf204e8a2816a802048197e0132b7615b579fab217f7c1e29545b
languageName: node
linkType: hard

"@octokit/plugin-paginate-rest@npm:^2.16.8, @octokit/plugin-paginate-rest@npm:^2.2.0":
version: 2.21.3
resolution: "@octokit/plugin-paginate-rest@npm:2.21.3"
Expand Down Expand Up @@ -2636,6 +2654,17 @@ __metadata:
languageName: node
linkType: hard

"@octokit/request-error@npm:^5.0.0":
version: 5.0.1
resolution: "@octokit/request-error@npm:5.0.1"
dependencies:
"@octokit/types": ^12.0.0
deprecation: ^2.0.0
once: ^1.4.0
checksum: e72a4627120de345b54876a1f007664095e5be9d624fce2e14fccf7668cd8f5e4929d444d8fc085d48e1fb5cd548538453974aab129a669101110d6679dce6c6
languageName: node
linkType: hard

"@octokit/request@npm:^5.4.0, @octokit/request@npm:^5.6.0, @octokit/request@npm:^5.6.3":
version: 5.6.3
resolution: "@octokit/request@npm:5.6.3"
Expand Down Expand Up @@ -2664,6 +2693,19 @@ __metadata:
languageName: node
linkType: hard

"@octokit/request@npm:^8.1.2":
version: 8.1.2
resolution: "@octokit/request@npm:8.1.2"
dependencies:
"@octokit/endpoint": ^9.0.0
"@octokit/request-error": ^5.0.0
"@octokit/types": ^12.0.0
is-plain-object: ^5.0.0
universal-user-agent: ^6.0.0
checksum: 49219eb71b856acecc8268f05a7a7d074488f9eaeb59439f5c8872e5b4555a4e6c0cf0ebcadf0983466f88957e74c27765f582e473b0dd89b453274501f0dc37
languageName: node
linkType: hard

"@octokit/rest@npm:^16.43.0 || ^17.11.0 || ^18.12.0, @octokit/rest@npm:^18.12.0":
version: 18.12.0
resolution: "@octokit/rest@npm:18.12.0"
Expand All @@ -2688,6 +2730,15 @@ __metadata:
languageName: node
linkType: hard

"@octokit/types@npm:^12.0.0":
version: 12.0.0
resolution: "@octokit/types@npm:12.0.0"
dependencies:
"@octokit/openapi-types": ^19.0.0
checksum: 6e5b68f8855775638db53244348d2ca07896d36a15aad41d3cb652fafaa1b307c3b6222319174dd5716accd9076e101da838b82f988a7c380a8e9d188e3aadf1
languageName: node
linkType: hard

"@octokit/types@npm:^4.1.6":
version: 4.1.10
resolution: "@octokit/types@npm:4.1.10"
Expand Down Expand Up @@ -2906,6 +2957,7 @@ __metadata:
"@jest/globals": ^29.3.1
"@nx/workspace": 16.2.1
"@octokit/graphql": ^5.0.5
"@octokit/request": ^8.1.2
"@storybook/eslint-config-storybook": ^3.1.2
"@storybook/jest": next
"@storybook/linter-config": ^3.1.2
Expand Down

0 comments on commit 930adf4

Please sign in to comment.