-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
move bash script to cancel preparation runs to tested node script
- Loading branch information
Showing
6 changed files
with
280 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
scripts/release/__tests__/cancel-preparation-runs.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters