Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions office/tests/test_python_support_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ def _office_matrix_python_versions(office_job: str) -> tuple[tuple[str, ...], ..
"the office job declares no python-version matrix entry"
)
matrix_value = value_match.group("value")
if matrix_value.startswith("${{"):
assert re.fullmatch(
r"\$\{\{\s*github\.event_name\s*==\s*'pull_request'\s*&&\s*"
r"fromJSON\(\s*'\[.*?\]'\s*\)\s*\|\|\s*"
r"fromJSON\(\s*'\[.*?\]'\s*\)\s*\}\}",
matrix_value,
), "the office matrix must select its first list only for pull_request events"

payloads = re.findall(r"fromJSON\(\s*'(\[.*?\])'\s*\)", matrix_value) or [
matrix_value
Expand All @@ -71,6 +78,25 @@ def _office_matrix_python_versions(office_job: str) -> tuple[tuple[str, ...], ..
return tuple(declared)


def test_office_matrix_rejects_changed_event_predicates() -> None:
"""Identical version payloads cannot conceal a changed event partition."""
expression = (
"${{ github.event_name == 'pull_request' && fromJSON('[\"3.14\"]') "
"|| fromJSON('[\"3.11\",\"3.12\",\"3.13\",\"3.14\"]') }}"
)
assert _office_matrix_python_versions(f"python-version: {expression}") == (
("3.14",), SUPPORTED_PYTHON_VERSIONS
)
for predicate in ("push", "workflow_dispatch"):
changed = expression.replace("'pull_request'", repr(predicate))
try:
_office_matrix_python_versions(f"python-version: {changed}")
except AssertionError as error:
assert "first list only for pull_request" in str(error)
else:
raise AssertionError(f"accepted an unsupported event predicate: {predicate}")


def test_python_support_range_matches_classifiers_and_ci_matrix() -> None:
"""Require package metadata and the Office CI job to cover the same minors."""

Expand Down
123 changes: 119 additions & 4 deletions src/workflowExactHead.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,60 @@ 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.
* Conditional declarations must use the supported PR-versus-other-event
* partition; unknown predicates fail closed rather than assigning event meaning
* to arbitrary payload order.
*/
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;

if (value.startsWith('${{')) {
// Only this event partition establishes the positional PR/push obligations.
expect(
value,
'the office matrix must select its first list only for pull_request events',
).toMatch(
/^\$\{\{\s*github\.event_name\s*==\s*'pull_request'\s*&&\s*fromJSON\(\s*'\[.*?\]'\s*\)\s*\|\|\s*fromJSON\(\s*'\[.*?\]'\s*\)\s*\}\}$/u,
);
}

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 +124,79 @@ 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(/first list only for pull_request/);
expect(() =>
officeMatrixPythonVersions(asJob(conditional.replace("'pull_request'", "'push'"))),
).toThrow(/first list only for pull_request/);
});

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