|
|
@@ -8,6 +8,43 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{
|
|
|
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
|
|
|
|
|
|
describe('CI workflow', () => {
|
|
|
+ it.each(['ci.yml', 'ci-master.yml', 'e2e.yml', 'release.yml', 'release-vendor.yml'])(
|
|
|
+ '%s cancels superseded validation runs without crossing workflow or ref boundaries', (name) => {
|
|
|
+ const workflow = loadWorkflow('.github/workflows/' + name)
|
|
|
+ expect(workflow.concurrency).toEqual({
|
|
|
+ group: '${{ github.workflow }}-${{ github.ref }}',
|
|
|
+ 'cancel-in-progress': true,
|
|
|
+ })
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ it('cancels reusable CI builds without cancelling release-owned builds', () => {
|
|
|
+ const workflow = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml')
|
|
|
+ expect(workflow.concurrency).toEqual({
|
|
|
+ group: 'build-single-exe-${{ github.workflow }}-${{ github.ref }}',
|
|
|
+ 'cancel-in-progress': '${{ !inputs.release }}',
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not cancel protected publication or deployment transactions', () => {
|
|
|
+ for (const name of ['release-publish.yml', 'release-vendor-publish.yml']) {
|
|
|
+ const publish = workflowJob(loadWorkflow('.github/workflows/' + name), 'publish')
|
|
|
+ expect(publish.concurrency).toMatchObject({ 'cancel-in-progress': false })
|
|
|
+ }
|
|
|
+ for (const name of ['python-release.yml', 'node-addon-system-release.yml', 'docs-pages.yml']) {
|
|
|
+ expect(loadWorkflow('.github/workflows/' + name).concurrency).toMatchObject({ 'cancel-in-progress': false })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ it('skips coverage-history uploads on cancellation but retains Wine cleanup', () => {
|
|
|
+ const coverage = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'windows-coverage')
|
|
|
+ const wine = workflowJob(loadWorkflow('.github/workflows/ci-master.yml'), 'windows')
|
|
|
+ expect(coverage.steps).toContainEqual(expect.objectContaining({
|
|
|
+ name: 'Save coverage duration history', if: '${{ !cancelled() }}',
|
|
|
+ }))
|
|
|
+ expect(wine.steps).toContainEqual(expect.objectContaining({ name: 'Shut down wineserver', if: 'always()' }))
|
|
|
+ })
|
|
|
+
|
|
|
it('isolates every pnpm action setup destination per runner', () => {
|
|
|
const files = ['.github/workflows/ci.yml', '.github/workflows/ci-master.yml']
|
|
|
const setups: Array<{ jobName: string; step: unknown }> = []
|
|
|
@@ -346,7 +383,7 @@ describe('CI workflow', () => {
|
|
|
)
|
|
|
})
|
|
|
|
|
|
- it('exempts push from cancellation in ci-master, so one master merge does not cancel the running drill', () => {
|
|
|
+ it('cancels superseded master runs without changing the post-merge job inventory', () => {
|
|
|
const workflow = loadWorkflow('.github/workflows/ci-master.yml')
|
|
|
const prWorkflow = loadWorkflow('.github/workflows/ci.yml')
|
|
|
if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) {
|
|
|
@@ -356,21 +393,11 @@ describe('CI workflow', () => {
|
|
|
throw new TypeError('ci workflow must define jobs')
|
|
|
}
|
|
|
|
|
|
- // Cancellation applies to the whole superseded RUN, so this has to be
|
|
|
- // decided at workflow level and gated on the event: a job-level group
|
|
|
- // cannot exempt its job from its run being cancelled. Only push is exempt —
|
|
|
- // a drill takes longer than the interval between master merges. The negated
|
|
|
- // form is load-bearing: `== 'pull_request'` would also stop cancelling
|
|
|
- // workflow_dispatch, and a re-dispatched runner benchmark holds up to 12
|
|
|
- // larger runners for 15 minutes in this same group on master.
|
|
|
- expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}")
|
|
|
-
|
|
|
- // The PR-only ci.yml still cancels a superseded run on a new push, so a
|
|
|
- // fresh head does not stack a second full 9-job run behind a stale one.
|
|
|
- // Unlike ci-master it has no push carve-out: every PR event supersedes.
|
|
|
- expect(prWorkflow.concurrency).toMatchObject({
|
|
|
+ expect(workflow.concurrency).toEqual({
|
|
|
+ group: '${{ github.workflow }}-${{ github.ref }}',
|
|
|
'cancel-in-progress': true,
|
|
|
})
|
|
|
+ expect(prWorkflow.concurrency).toEqual(workflow.concurrency)
|
|
|
|
|
|
// The exact event sets are what keep master-only jobs out of the PR check
|
|
|
// panel: ci-master triggers only on push(master) + workflow_dispatch and
|
|
|
@@ -382,13 +409,12 @@ describe('CI workflow', () => {
|
|
|
expect(Object.keys(workflow.on).sort()).toEqual(['push', 'workflow_dispatch'])
|
|
|
expect(Object.keys(prWorkflow.on)).toEqual(['pull_request'])
|
|
|
|
|
|
- // Neither drill may carry a job-level group: it would not exempt the job
|
|
|
- // from run-scoped cancellation.
|
|
|
+ // Drills share the parent run’s supersession policy.
|
|
|
for (const name of ['serial-linux-selfhosted', 'serial-windows']) {
|
|
|
const job = workflow.jobs[name]
|
|
|
if (!isRecord(job)) throw new TypeError(`${name} must be defined`)
|
|
|
expect(job.concurrency).toBeUndefined()
|
|
|
- // Both stay master-push-only; that is what makes the push carve-out safe.
|
|
|
+ // Standby drills remain post-merge work, but share run cancellation.
|
|
|
expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
|
|
|
}
|
|
|
|
|
|
@@ -409,10 +435,7 @@ describe('CI workflow', () => {
|
|
|
.sort()
|
|
|
expect(pushReachable).toEqual(['python-runtime', 'serial-linux-selfhosted', 'serial-windows', 'windows'])
|
|
|
|
|
|
- // Why workflow_dispatch must keep cancelling: each benchmark fans out to a
|
|
|
- // dozen larger runners at once, in this same group on master. If it stopped
|
|
|
- // cancelling, a re-dispatch would queue ahead of a drill instead of
|
|
|
- // replacing the stale measurement.
|
|
|
+ // Manual benchmarks retain their bounded fan-out.
|
|
|
for (const name of ['larger-runner-benchmark', 'consolidated-runner-benchmark']) {
|
|
|
const job = workflow.jobs[name]
|
|
|
if (!isRecord(job) || !isRecord(job.strategy)) {
|