Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build-and-test:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
Expand Down Expand Up @@ -51,6 +53,7 @@ jobs:
run: pnpm build:demo

browser-release-evidence:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
name: Cross-engine Clipboard / Playwright 1.62.0
runs-on: ubuntu-24.04
timeout-minutes: 60
Expand Down Expand Up @@ -84,6 +87,7 @@ jobs:
run: pnpm --dir tests/browser exec playwright test --config playwright.config.ts

office:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
name: Office / Python ${{ matrix.python-version }}
runs-on: ubuntu-24.04
timeout-minutes: 30
Expand Down
107 changes: 103 additions & 4 deletions src/workflowExactHead.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,47 @@ function workflowJob(source: string, name: string, nextName?: string): string {
return source.slice(start, end);
}

const SUPPORTED_PYTHON_VERSIONS = ['3.11', '3.12', '3.13', '3.14'] as const;

/**
* Resolve every Python minor set the Office job can select, in declared order.
*
* The matrix may be written as a literal sequence or as an expression choosing
* between `fromJSON` payloads per event. Both are decoded to their values so
* this contract asserts the supported minors rather than the syntax that
* happens to express them, and so a legitimate reformatting of the workflow
* cannot turn the suite red on every candidate head at once.
*/
function officeMatrixPythonVersions(job: string): string[][] {
const declaration = /^\s*python-version:[ \t]*(?<value>\S.*?)\s*$/m.exec(job);
expect(
declaration,
`the office job declares no python-version matrix entry:\n${job}`,
).not.toBeNull();
const value = declaration!.groups!.value;

const payloads = [...value.matchAll(/fromJSON\(\s*'(?<json>\[.*?\])'\s*\)/g)].map(
(match) => match.groups!.json,
);
const sources = payloads.length > 0 ? payloads : [value];
Comment thread
seonghobae marked this conversation as resolved.

return sources.map((source) => {
let decoded: unknown;
try {
decoded = JSON.parse(source);
} catch {
throw new Error(
`the office python-version matrix is not resolvable to a version list; observed ${value}`,
);
}
expect(
Array.isArray(decoded) && decoded.length > 0,
`the office python-version matrix must resolve to a non-empty list; observed ${source}`,
).toBe(true);
return (decoded as unknown[]).map(String);
});
}

/** Require checkout -> exact-SHA verification -> first consumer in one job. */
function expectExactCheckoutBeforeConsumer(job: string, consumer: string): void {
const checkout = job.indexOf(`- uses: ${CHECKOUT_PIN}`);
Expand All @@ -70,18 +111,76 @@ const officeJob = workflowJob(workflow, 'office');
describe('exact-head CI workflow contract', () => {
it('cancels only superseded runs for the same repository and PR while keeping full main compatibility coverage', () => {
expect(workflow).toContain(
"group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}",
"group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}",
);
expect(workflow).toContain('cancel-in-progress: true');
expect(officeJob).toContain(
"python-version: ${{ github.event_name == 'pull_request' && fromJSON('[\"3.14\"]') || fromJSON('[\"3.11\", \"3.12\", \"3.13\", \"3.14\"]') }}",
expect(workflow).toContain(
"cancel-in-progress: ${{ github.event_name == 'pull_request' }}",
);
const declaredMatrices = officeMatrixPythonVersions(officeJob);
for (const declared of declaredMatrices) {
const unsupported = declared.filter(
(version) => !SUPPORTED_PYTHON_VERSIONS.includes(version as never),
);
expect(
unsupported,
`the office python-version matrix declares unsupported entries in ${JSON.stringify(declared)}`,
).toEqual([]);
}
expect(
declaredMatrices.at(-1),
'the exhaustive office python-version matrix must cover every supported minor in order',
).toEqual([...SUPPORTED_PYTHON_VERSIONS]);
expect(
declaredMatrices[0],
'the office python-version matrix used for pull requests must include the newest supported minor',
).toContain(SUPPORTED_PYTHON_VERSIONS.at(-1));

expect(releaseWorkflow).toContain(
'group: ${{ github.workflow }}-${{ github.repository }}-${{ github.ref_name }}',
);
expect(releaseWorkflow).toContain('cancel-in-progress: false');
});

it('resolves the office matrix from its value rather than its spelling', () => {
const asJob = (declaration: string): string =>
` office:\n strategy:\n matrix:\n python-version: ${declaration}\n`;

const conditional =
'${{ github.event_name == \'pull_request\' && fromJSON(\'["3.14"]\') || fromJSON(\'["3.11", "3.12", "3.13", "3.14"]\') }}';
expect(officeMatrixPythonVersions(asJob(conditional))).toEqual([
['3.14'],
['3.11', '3.12', '3.13', '3.14'],
]);

const reformatted =
'${{ github.event_name==\'pull_request\' && fromJSON( \'["3.13","3.14"]\' ) || fromJSON( \'["3.11","3.12","3.13","3.14"]\' ) }}';
expect(officeMatrixPythonVersions(asJob(reformatted))).toEqual([
['3.13', '3.14'],
['3.11', '3.12', '3.13', '3.14'],
]);

expect(
officeMatrixPythonVersions(asJob('["3.11", "3.12", "3.13", "3.14"]')),
).toEqual([['3.11', '3.12', '3.13', '3.14']]);

expect(() =>
officeMatrixPythonVersions(' office:\n runs-on: ubuntu-24.04\n'),
).toThrow(/declares no python-version matrix entry/);
expect(() =>
officeMatrixPythonVersions(asJob('${{ steps.resolve.outputs.versions }}')),
).toThrow(/not resolvable to a version list/);
});

it('cancels stale PR work and skips inactive pull requests', () => {
expect(workflow).toContain(
'types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]',
);
for (const job of [buildJob, browserJob, officeJob]) {
expect(job).toContain('!github.event.pull_request.draft');
expect(job).toContain("github.event.action != 'closed'");
}
});

it('uses a fixed runner and checks out the immutable current PR head in every job', () => {
expect(workflow).not.toContain('ubuntu-latest');
for (const job of [buildJob, browserJob, officeJob]) {
Expand Down
Loading