From ae283b5f87aa172e26693bbe3a954caa7e5ed141 Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 22 Oct 2026 01:20:51 +0200 Subject: [PATCH 1/3] build(go): build with Go 1.27.3 to remove a reachable standard-library vulnerability govulncheck reports GO-2026-6809 (crypto/x509 distrusts compromised intermediate certificates) as reachable in Go 1.27.2 through the CLI's diagnostic output path, so the release candidate and every source gate fail at go:vulncheck. Go 1.27.3 contains the fix. - go.mod: toolchain go1.27.3; the go directive and every module requirement are unchanged. - release/change-record.v2.json: the toolchain addition now states that release binaries are built with Go 1.27.3 and why; the pinned copy in internal/tools/releasechange/record_test.go is regenerated from it. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree f7cfc5f6bd9a with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561 browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so the go:vulncheck step ran govulncheck against a vulnerability database snapshot fetched with curl (modified 2026-10-21T00:27:29Z) and found no vulnerabilities. With the same snapshot, Go 1.27.2 is reported affected by GO-2026-6809. --- go.mod | 2 +- internal/tools/releasechange/record_test.go | 2 +- release/change-record.v2.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 232c7401..bbc688b0 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/research-engineering/agentic-proofkit go 1.27 -toolchain go1.27.2 +toolchain go1.27.3 tool ( github.com/rhysd/actionlint/cmd/actionlint diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index abc79175..7f417c3f 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -241,7 +241,7 @@ var currentAdditions = []Change{ {ChangeID: "proofkit.workflow.agent-trajectory-witness", Summary: "REQ-PROOFKIT-WORKFLOW-020 adds a whole agent trajectory witness that drives a fixture repository through the public CLI only, from install bootstrap to current proof, with a route ledger, negative controls and an edge-local invalidation matrix; the installed npm carrier and the host wheel's module and console-script carriers execute it, while the CPython 3.11.0 floor smoke keeps its finite carrier cases. It proves CLI admission, linkage, currentness and route closure, not native test truth."}, {ChangeID: "proofkit.release-evidence.hardening", Summary: "Release tooling admits npm absence only from bounded strict E404 JSON, rejects failed or incomplete Go build-info metadata instead of emitting an empty SBOM dependency inventory, bounds the root-package verifier to 128 MiB compressed, 512 MiB expanded and 4096 entries, rejects duplicate wheel filenames in PyPI registry evidence, reads each wheel once in the Python package verifier and installs a private copy of the admitted bytes in its local consumer check, requires installed-CLI help on stdout with empty stderr, and passes the workflow credential to curl on stdin instead of argv. The release guide defines the provider-bound default signing policy; independently signed annotated tags remain optional. Source-checkout coverage tooling rejects partial, aborted, expired or failed native test observations."}, {ChangeID: "proofkit.browser.consumer-hosting-boundary", Summary: "Remote reading of specification views is documented as a consumer hosting concern: publish the existing --format html exports from a consumer-owned static host behind a consumer-owned access gateway, use rendered-artifact-freshness for hosted copies and secret-scan as an advisory byte check. requirement-browser-server and view keep serving only literal loopback hosts."}, - {ChangeID: "proofkit.toolchain.runtime-and-build-updates", Summary: "Release binaries are built with Go 1.27.2 instead of 1.27.1 and bundle golang.org/x/mod 0.42.0, golang.org/x/net 0.61.0, golang.org/x/sys 0.49.0, golang.org/x/text 0.43.0 and github.com/mattn/go-isatty 0.0.24; no runtime module was added or removed. LICENSE now also names the Go standard library among the bundled BSD-3-Clause components; its reproduced Go Authors notice already covered it. The development and CI toolchain moves to npm 12.2.0, Node 26.11.1, CPython 3.15.0, Playwright 1.63.0 and axe-core 4.14.0 with pinned GitHub Actions, and the Python launcher's POSIX path imports neither subprocess nor typing."}, + {ChangeID: "proofkit.toolchain.runtime-and-build-updates", Summary: "Release binaries are built with Go 1.27.3 instead of 1.27.1, whose crypto/x509 fix removes the reachable standard-library vulnerability GO-2026-6809 that govulncheck reports for 1.27.2, and bundle golang.org/x/mod 0.42.0, golang.org/x/net 0.61.0, golang.org/x/sys 0.49.0, golang.org/x/text 0.43.0 and github.com/mattn/go-isatty 0.0.24; no runtime module was added or removed. LICENSE now also names the Go standard library among the bundled BSD-3-Clause components; its reproduced Go Authors notice already covered it. The development and CI toolchain moves to npm 12.2.0, Node 26.11.1, CPython 3.15.0, Playwright 1.63.0 and axe-core 4.14.0 with pinned GitHub Actions, and the Python launcher's POSIX path imports neither subprocess nor typing."}, } var currentMigrationSteps = []string{ diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 12b449c1..55484259 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -180,7 +180,7 @@ }, { "changeId": "proofkit.toolchain.runtime-and-build-updates", - "summary": "Release binaries are built with Go 1.27.2 instead of 1.27.1 and bundle golang.org/x/mod 0.42.0, golang.org/x/net 0.61.0, golang.org/x/sys 0.49.0, golang.org/x/text 0.43.0 and github.com/mattn/go-isatty 0.0.24; no runtime module was added or removed. LICENSE now also names the Go standard library among the bundled BSD-3-Clause components; its reproduced Go Authors notice already covered it. The development and CI toolchain moves to npm 12.2.0, Node 26.11.1, CPython 3.15.0, Playwright 1.63.0 and axe-core 4.14.0 with pinned GitHub Actions, and the Python launcher's POSIX path imports neither subprocess nor typing." + "summary": "Release binaries are built with Go 1.27.3 instead of 1.27.1, whose crypto/x509 fix removes the reachable standard-library vulnerability GO-2026-6809 that govulncheck reports for 1.27.2, and bundle golang.org/x/mod 0.42.0, golang.org/x/net 0.61.0, golang.org/x/sys 0.49.0, golang.org/x/text 0.43.0 and github.com/mattn/go-isatty 0.0.24; no runtime module was added or removed. LICENSE now also names the Go standard library among the bundled BSD-3-Clause components; its reproduced Go Authors notice already covered it. The development and CI toolchain moves to npm 12.2.0, Node 26.11.1, CPython 3.15.0, Playwright 1.63.0 and axe-core 4.14.0 with pinned GitHub Actions, and the Python launcher's POSIX path imports neither subprocess nor typing." } ], "migration": { From e672f6b16bbb530614d925b5e8548dd81e8f1f50 Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 22 Oct 2026 03:00:15 +0200 Subject: [PATCH 2/3] ci(platform): verify Linux arm64 on native ARM runners Add the required job "quality / platform smoke / linux-arm64" on the GitHub-hosted ubuntu-24.04-arm runner. Its first step fails unless uname -m reports aarch64. It then sets up the same pinned Node 26.11.1, verified npm, Go and Python 3.15.0 toolchains as the macOS platform jobs, runs the rootpath, repositorytransaction, processgroup and projectstatus packages natively, runs npm run npm:version and npm run platform:smoke, builds the complete current release wheel set, and runs the CPython 3.11.0 installed-wheel smoke, which selects the manylinux_2_17_aarch64 wheel and the pinned linux/arm64 CPython 3.11.0 image digest. The required aggregate needs the job and requires its success. The workflow oracles pin the job's name, runner, 15-minute timeout and exact ordered steps, the five required leaf jobs and the aggregate shell. They refuse mutants that move the job to another runner, drop, move or weaken its architecture check, drop or narrow its invariant, smoke, wheel-set or CPython 3.11.0 steps, or remove it from the aggregate needs or result checks. The Python runtime oracle counts the job as a pinned Python consumer. README states that required CI executes Linux arm64 on ubuntu-24.04-arm and x64 on ubuntu-24.04, that CI does not execute a host at the manylinux 2.17 floor, and that the CPython 3.11.0 smoke runs on both Linux architectures; its projection test follows. REQ-PROOFKIT-QUALITY-011 now names the Linux arm64 platform job, its architecture check and its CPython 3.11.0 step, and five required leaf jobs instead of four. CONTRIBUTING lists ubuntu-24.04-arm among the source-check targets. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 557350793e08 with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561 browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. The new arm64 job's success is proven only by hosted CI on this PR. --- .github/workflows/ci.yml | 57 +++ CONTRIBUTING.md | 12 +- README.md | 14 +- .../proofkit-supply-chain-quality/overview.md | 27 +- .../requirements.v2.json | 2 +- internal/tools/pythonpackage/metadata_test.go | 21 +- scripts/workflow_package_gate_oracle_test.go | 409 +++++++++++++++--- scripts/workflow_python_minimum_test.go | 173 +++++--- scripts/workflow_python_runtime_test.go | 12 +- 9 files changed, 578 insertions(+), 149 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ee24f87..345a9a6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,6 +258,61 @@ jobs: npm run npm:version npm run platform:smoke + platform-smoke-linux-arm64: + name: quality / platform smoke / linux-arm64 + runs-on: ubuntu-24.04-arm + timeout-minutes: 15 + steps: + - name: Verify native Linux architecture + run: | + set -euo pipefail + machine="$(uname -m)" + printf 'uname -m=%s\n' "$machine" + test "$machine" = "aarch64" + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 26.11.1 + package-manager-cache: false + + - name: Install verified npm + uses: ./.github/actions/setup-verified-npm + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.15.0' + + - name: Run Linux filesystem and process invariants + run: go test ./internal/kernel/rootpath ./internal/kernel/repositorytransaction ./internal/kernel/processgroup ./internal/command/projectstatus -count=1 + + - name: Run platform smoke + run: | + set -euo pipefail + npm run npm:version + npm run platform:smoke + + - name: Build release wheel set + run: | + set -euo pipefail + go run ./internal/tools/packagebuild all + go run ./internal/tools/pythonpackage build + + - name: Verify Python 3.11.0 installed wheel + run: go run ./internal/tools/pythonpackage verify-minimum + browser-runtime: name: quality / browser runtime runs-on: ubuntu-24.04 @@ -324,6 +379,7 @@ jobs: - source-quality - platform-smoke - platform-smoke-intel + - platform-smoke-linux-arm64 steps: - name: Verify required quality results run: | @@ -332,4 +388,5 @@ jobs: test "${{ needs.source-quality.result }}" = "success" test "${{ needs.platform-smoke.result }}" = "success" test "${{ needs.platform-smoke-intel.result }}" = "success" + test "${{ needs.platform-smoke-linux-arm64.result }}" = "success" printf 'OK: all required quality checks passed\n' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8245077f..059450b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,12 +115,12 @@ SZOMB. A truncated record or a permission error remains an ordinary observation failure: the observer runs only before the sole reap, so the unreaped child still pins its PID and group and retained cleanup may signal them. Linux uses fresh `waitid(P_PID, WEXITED|WNOWAIT|WNOHANG)` records. Source-check targets -are the configured Ubuntu 24.04, macos-15 (arm64) and macos-15-intel (x86_64) -jobs and the native Linux wheel verifier profile, each requiring actual -host/API qualification. A CI label, cross-build or one local run is not native -qualification of another host. macOS 13 and 14 are not executed: no hosted -macOS 13 image exists, and GitHub retires `macos-14` on 2026-11-02 after failing -brownouts (actions/runner-images#13518). +are the configured Ubuntu 24.04 (x86_64), ubuntu-24.04-arm (arm64), macos-15 +(arm64) and macos-15-intel (x86_64) jobs and the native Linux wheel verifier +profile, each requiring actual host/API qualification. A CI label, cross-build +or one local run is not native qualification of another host. macOS 13 and 14 +are not executed: no hosted macOS 13 image exists, and GitHub retires +`macos-14` on 2026-11-02 after failing brownouts (actions/runner-images#13518). This package is outside the CLI dependency closure on all four shipped targets; consumer wheel tags and platform minima are unchanged. Import or payload changes require a fresh closure proof, and unsupported hosts fail before child Start. diff --git a/README.md b/README.md index 0eca95d6..431e9cd8 100644 --- a/README.md +++ b/README.md @@ -149,15 +149,17 @@ Supported binary targets are macOS 15 or later on arm64 or x64, executed by required CI on the GitHub-hosted `macos-15` and `macos-15-intel` runners. Darwin binaries declare a macOS 13.0 minimum in their Mach-O build metadata; macOS 13 and 14 are binary-compatible by that metadata only, not executed or verified. -Linux manylinux 2.17 or later is supported on arm64 or x64. Windows is unsupported. The Python -runner requires Python 3.11 or later and wraps the same Go CLI; it is not a -Python SDK. +Linux manylinux 2.17 or later is supported on arm64 or x64, executed by +required CI on the GitHub-hosted `ubuntu-24.04-arm` and `ubuntu-24.04` runners. +The manylinux 2.17 floor is the wheel-tag declaration; CI does not execute a +host at that floor. Windows is unsupported. The Python runner requires Python +3.11 or later and wraps the same Go CLI; it is not a Python SDK. Linux wheel installation requires pip 20.3 or later for the manylinux tag. Use an installer compatible with the selected Python. The bounded compatibility -smoke exercises the initial Python 3.11 release with a checksum-pinned compatible -installer. This is compatibility evidence, not a recommendation to use an -obsolete interpreter. +smoke exercises the initial Python 3.11 release on Linux arm64 and x64 with a +checksum-pinned compatible installer. This is compatibility evidence, not a +recommendation to use an obsolete interpreter. After an exact Python package version is available from an admitted channel, use one complete package-manager chain: diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 831b31f3..9b39c2a3 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -73,18 +73,21 @@ vulnerability absence, or consumer rollout safety by itself. tests does not prove assertion-branch execution, mutation adequacy, or exhaustive command semantics. - `REQ-PROOFKIT-QUALITY-011`: CI separates the OS-independent full - source/package gate from macOS platform smoke, executes the complete Go - package set through its owner command, uses explicit hosted runner labels - instead of floating latest labels, and exposes one fail-closed aggregate gate - whose exact expression and script reject neutralization and require every - required leaf check to finish with success, while the aggregate job omits - job defaults and `continue-on-error` entirely. Exact workflow-level bash - defaults plus a closed inventory require exact provider-check names and - scalar runners, bind each macOS job's (required arm64 `macos-15` and x86_64 - `macos-15-intel`) presence-aware exact ordered step inventory and exact - local setup-action bytes to the fail-closed package-script owner command, - require every required leaf job and step to - omit conditions except for exact named owner conditions, and reject + source/package gate from macOS and Linux arm64 platform smoke, executes the + complete Go package set through its owner command, uses explicit hosted + runner labels instead of floating latest labels, and exposes one fail-closed + aggregate gate whose exact expression and script reject neutralization and + require every required leaf check to finish with success, while the + aggregate job omits job defaults and `continue-on-error` entirely. Exact + workflow-level bash defaults plus a closed inventory require exact + provider-check names and scalar runners, bind each platform job's (required + arm64 `macos-15`, x86_64 `macos-15-intel` and aarch64 `ubuntu-24.04-arm`) + presence-aware exact ordered step inventory, whose first step checks the + native architecture, and exact local setup-action bytes to the fail-closed + package-script owner command, require the minimum-version CPython installed-wheel + smoke in both the Linux x86_64 source job and the Linux arm64 job, require + every required leaf job and step to omit conditions except for exact named + owner conditions, and reject dynamically false or explicit-null conditions, dual `run`/`uses` keys, non-string runner labels, nested or top-level semantic shadow steps, reusable-job substitution, inherited or local environment entries, shell, diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v2.json b/docs/specs/proofkit-supply-chain-quality/requirements.v2.json index a49c6300..5a455c75 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v2.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v2.json @@ -21,7 +21,7 @@ , {"requirementId":"REQ-PROOFKIT-QUALITY-008","statementCompletion":"GitHub Actions workflow syntax and expression semantics are checked by actionlint in the local package gate.","fields":{"nonClaims":["This requirement does not claim workflow runtime success, external action safety, branch protection, or release approval."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-008"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} , {"requirementId":"REQ-PROOFKIT-QUALITY-009","statementCompletion":"Performance-sensitive parser and serializer paths expose benchmark entrypoints without making wall-clock budgets a required PR gate before stable baselines exist.","fields":{"nonClaims":["This requirement does not claim current performance maximum, CI wall-clock budget enforcement, or regression absence without benchmark evidence."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-009"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} , {"requirementId":"REQ-PROOFKIT-QUALITY-010","statementCompletion":"Coverage metrics report requirement, binding, witness, CLI inventory linkage, descriptor-owned command proof-route candidates, and a separate execution-backed command-oracle ledger. The ledger must bind every candidate commandRef, selector, concrete negative case, falsification event, assertion oracle, expected public outcome, and owner invariant to one immutable exact-file materialized source snapshot; execute exactly the selected active Go tests through canonically ordered package-scoped argv vectors that cannot select same-named non-candidate tests in another package; require unique package start and pass events, unique test run and pass events, owner-reserved cooperative attributes for every candidate, bounded cancellable subprocesses, and immediate process-group termination on output-limit violation; reject missing, unknown, duplicate, skipped, failed, reordered, context-invalid, spoofed-output, cross-test, source-drift, producer-unreachable candidate projections, and correlated identity substitutions; publish and invalidate local diagnostic and metrics artifacts atomically through repository-confined non-symlink destinations; on supported release platforms, open retained input artifacts descriptor-first through a nonblocking no-follow boundary before type, identity, and exact byte-limit checks so named pipes, symlinks, path substitution, and post-read mutation cannot block or escape, and fail closed instead of using a weaker file-open path elsewhere; revalidate current candidate, corpus, runtime, and source identities before and after artifact publication; and fail closed when any public command lacks execution-backed evidence. An independently authored versioned counterfeit corpus must bijectively cover every shipped evidence class, required policy axis, record coordinate, and declared substitution axis with positive controls and checked-in exact expected decisions evaluated through the production admission and lifecycle owners rather than a generated expectation registry. Static route metadata, prose, legacy source markers, test existence, and failure-capable syntax remain proof-route candidates and cannot satisfy the execution-backed closure.","fields":{"nonClaims":["Successful selected tests do not prove assertion-branch execution, mutation adequacy, line coverage, dead-code absence, exhaustive command semantics, malicious-test resistance, producer authentication, receipt freshness, merge satisfaction, or production readiness."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-010"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} - , {"requirementId":"REQ-PROOFKIT-QUALITY-011","statementCompletion":"CI separates the full source/package gate with an explicit native Linux Python 3.11.0 installed-wheel subcheck from the platform-specific macOS execution smoke, runs the complete processgroup package on each macOS lane without selector-based narrowing alongside the existing filesystem checks, executes the complete Go package set through the owner command rather than a partial package list, requires the local composed Go gate and CI source job to run non-mutating go mod tidy -diff followed by go mod verify before Go tests with either failure stopping the chain, requires exact owner-reviewed root check and go:check AND-only command chains, forbids pre/post npm lifecycle script keys by presence for check and every direct npm-run target in those chains or CI source steps without redefining unrelated npm scripts, uses exact scalar hosted runners and provider-check names, requires each macOS job's presence-aware exact ordered step inventory, with distinct required macos-15 arm64 and macos-15-intel x86_64 jobs, and exact repository-local setup-action bytes to execute the exact fail-closed platform-smoke package-script owner command, and exposes one exact fail-closed aggregate quality gate under exact bash workflow run defaults with no inherited working-directory or environment entries; a closed inventory requires every required leaf job and step to omit conditions except for exact owner-reviewed named conditions, every required leaf job and the aggregate job omit job defaults, reusable-workflow substitution, and continue-on-error and have no job environment entries, all of their steps omit shell, working-directory, and continue-on-error and have no step environment entries, and the aggregate's reviewed expression and script require every exact required leaf check to finish with success; workflow mutation fixtures preserve nil versus empty values, field-presence bits and detached mutable data before applying negative changes; neutralizing wrappers, dynamically false or explicit-null conditions, execution overrides, dual run/uses keys, extra disjunctions, non-string runner labels, nested or top-level semantic shadow steps, no-op or reusable-job substitution, skipped, failed, or cancelled leaf jobs cannot satisfy branch-protection evidence. The unconditional minimum-Python owner step immediately follows package artifact verification and precedes receipts and closeout; omission, neutralization or failure cannot yield source-quality success, and the same four required leaf jobs remain in the aggregate.","fields":{"nonClaims":["This requirement does not claim provider-run success, branch-protection settings, Windows support, performance budget enforcement, or release approval."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-011"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} + , {"requirementId":"REQ-PROOFKIT-QUALITY-011","statementCompletion":"CI separates the full source/package gate with an explicit native Linux x86_64 Python 3.11.0 installed-wheel subcheck from the platform-specific macOS and Linux arm64 execution smoke, runs the complete processgroup package on each macOS and Linux arm64 platform lane without selector-based narrowing alongside the existing filesystem checks, executes the complete Go package set through the owner command rather than a partial package list, requires the local composed Go gate and CI source job to run non-mutating go mod tidy -diff followed by go mod verify before Go tests with either failure stopping the chain, requires exact owner-reviewed root check and go:check AND-only command chains, forbids pre/post npm lifecycle script keys by presence for check and every direct npm-run target in those chains or CI source steps without redefining unrelated npm scripts, uses exact scalar hosted runners and provider-check names, requires each platform job's presence-aware exact ordered step inventory, with distinct required macos-15 arm64, macos-15-intel x86_64 and ubuntu-24.04-arm aarch64 jobs whose first step fails unless uname -m reports the job's native architecture, and exact repository-local setup-action bytes to execute the exact fail-closed platform-smoke package-script owner command, and exposes one exact fail-closed aggregate quality gate under exact bash workflow run defaults with no inherited working-directory or environment entries; a closed inventory requires every required leaf job and step to omit conditions except for exact owner-reviewed named conditions, every required leaf job and the aggregate job omit job defaults, reusable-workflow substitution, and continue-on-error and have no job environment entries, all of their steps omit shell, working-directory, and continue-on-error and have no step environment entries, and the aggregate's reviewed expression and script require every exact required leaf check to finish with success; workflow mutation fixtures preserve nil versus empty values, field-presence bits and detached mutable data before applying negative changes; neutralizing wrappers, dynamically false or explicit-null conditions, execution overrides, dual run/uses keys, extra disjunctions, non-string runner labels, nested or top-level semantic shadow steps, no-op or reusable-job substitution, skipped, failed, or cancelled leaf jobs cannot satisfy branch-protection evidence. The unconditional minimum-Python owner step immediately follows package artifact verification in the source job and precedes receipts and closeout, and in the Linux arm64 job it immediately follows a build of the complete current release wheel set; omission or neutralization of either step cannot yield source-quality success, failure of either step cannot yield success of the job that runs it, and none of them can yield required-aggregate success; exactly five required leaf jobs remain in the aggregate.","fields":{"nonClaims":["This requirement does not claim provider-run success, branch-protection settings, Windows support, performance budget enforcement, or release approval."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-011"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} , {"requirementId":"REQ-PROOFKIT-QUALITY-012","statementCompletion":"Release and adoption channel identifiers are admitted by one canonical authority vocabulary that separates authorityChannel values from display labels, publisher environments, statuses, registry sub-vocabulary, and candidate evidence classes.","fields":{"nonClaims":["This requirement does not claim provider-run success, registry publication, GitHub Release creation, consumer adoption, rollout approval, or release approval."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-012"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} , {"requirementId":"REQ-PROOFKIT-QUALITY-013","statementCompletion":"Self-hosting workflow package-gate evidence is checked by a typed workflow oracle that proves the package gate is reachable, exact, advisory, and guarded only by an absent condition or the exact owner-reviewed condition for every trust-significant job and step; the oracle checks the complete required CI and release job inventories, rejects neutralizing or dynamically false expression variants, requires any workflow run defaults to retain exact bash without a working-directory override, admits only the exact owner-reviewed workflow and step environment entries, requires the gate job to omit job defaults and continue-on-error and have no job environment entries, requires every step in that job to omit shell, working-directory, and continue-on-error, and orders the gate before evidence publication instead of relying on workflow text search. The CI source job includes the exact minimum-Python installed-wheel invocation after package artifacts and before self-hosting receipts, coverage and closeout; its execution requirement does not promote local advisory receipts into authenticated provider evidence.","fields":{"nonClaims":["This requirement does not claim full GitHub Actions interpretation, provider-run success, branch-protection enforcement, required-check enforcement, external action safety, registry publication, release approval, or rollout approval."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-013"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"medium","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} , {"requirementId":"REQ-PROOFKIT-QUALITY-014","statementCompletion":"Release authority consumers compare downstream policy against the admitted typed release-authority projection and admitted report digest instead of rereading caller-owned raw releaseAuthorityInput fields after validation.","fields":{"nonClaims":["This requirement does not claim public SDK stability, registry publication, package installation, consumer dependency policy, rollout approval, merge approval, provider provenance, proof freshness, or native witness execution."],"externalNonClaimRefs":["NC-PROOFKIT-QUALITY-014"],"proofBindingRefs":["proofkit/requirement-bindings.json"],"ownerId":"proofkit.supply-chain-quality","claimLevel":"blocking","riskClass":"high","nonClaimRefs":[],"lifecycle":{"state":"active"},"deferral":null,"updatePolicy":{"reviewOwnerId":"proofkit.supply-chain-quality","requiresImpactDeclaration":true,"requiresProofBindingReview":true}}} diff --git a/internal/tools/pythonpackage/metadata_test.go b/internal/tools/pythonpackage/metadata_test.go index 5a3f6ddb..bd36e638 100644 --- a/internal/tools/pythonpackage/metadata_test.go +++ b/internal/tools/pythonpackage/metadata_test.go @@ -181,20 +181,29 @@ func TestREADMEPlatformAndPythonProjection(t *testing.T) { if ownerMinimums["darwin"] != "13" { t.Fatalf("Darwin wheel-tag minimum %s no longer matches the README executed/metadata-only split", ownerMinimums["darwin"]) } + // Each OS names its required CI runners in the owner architecture order; + // a changed architecture set requires re-authoring the runner lists. + for _, goos := range []string{"darwin", "linux"} { + if strings.Join(ownerArchitectures[goos], " ") != "arm64 x64" { + t.Fatalf("%s architectures %v no longer match the README runner lists", goos, ownerArchitectures[goos]) + } + } expectedBlock := strings.Join([]string{ fmt.Sprintf("Supported binary targets are macOS 15 or later on %s, executed by", strings.Join(ownerArchitectures["darwin"], " or ")), "required CI on the GitHub-hosted `macos-15` and `macos-15-intel` runners. Darwin", "binaries declare a macOS 13.0 minimum in their Mach-O build metadata; macOS 13", "and 14 are binary-compatible by that metadata only, not executed or verified.", - fmt.Sprintf("Linux manylinux %s or later is supported on %s. Windows is unsupported. The Python", ownerMinimums["linux"], strings.Join(ownerArchitectures["linux"], " or ")), - "runner requires Python 3.11 or later and wraps the same Go CLI; it is not a", - "Python SDK.", + fmt.Sprintf("Linux manylinux %s or later is supported on %s, executed by", ownerMinimums["linux"], strings.Join(ownerArchitectures["linux"], " or ")), + "required CI on the GitHub-hosted `ubuntu-24.04-arm` and `ubuntu-24.04` runners.", + fmt.Sprintf("The manylinux %s floor is the wheel-tag declaration; CI does not execute a", ownerMinimums["linux"]), + "host at that floor. Windows is unsupported. The Python runner requires Python", + "3.11 or later and wraps the same Go CLI; it is not a Python SDK.", "", "Linux wheel installation requires pip 20.3 or later for the manylinux tag.", "Use an installer compatible with the selected Python. The bounded compatibility", - "smoke exercises the initial Python 3.11 release with a checksum-pinned compatible", - "installer. This is compatibility evidence, not a recommendation to use an", - "obsolete interpreter.", + "smoke exercises the initial Python 3.11 release on Linux arm64 and x64 with a", + "checksum-pinned compatible installer. This is compatibility evidence, not a", + "recommendation to use an obsolete interpreter.", "", "After an exact Python package version is available from an admitted channel,", "use one complete package-manager chain:", diff --git a/scripts/workflow_package_gate_oracle_test.go b/scripts/workflow_package_gate_oracle_test.go index 8f7b5331..a1da8a7e 100644 --- a/scripts/workflow_package_gate_oracle_test.go +++ b/scripts/workflow_package_gate_oracle_test.go @@ -669,6 +669,25 @@ func needsList(raw any) []string { } } +// withoutNeed removes exactly one owner need so a mutant omits only that job. +func withoutNeed(t *testing.T, raw any, job string) []any { + t.Helper() + values, ok := raw.([]any) + if !ok { + t.Fatalf("owner needs=%#v, want a list", raw) + } + result := make([]any, 0, len(values)) + for _, value := range values { + if value != job { + result = append(result, value) + } + } + if len(result) != len(values)-1 { + t.Fatalf("owner needs=%#v must contain %q exactly once", raw, job) + } + return result +} + func nestedValue(raw any, path []string) any { value := raw for _, key := range path { @@ -734,7 +753,7 @@ func TestCIWorkflowDeclaresFailClosedRequiredAggregate(t *testing.T) { if !usesAlwaysStatusCheck(gate.If) { t.Fatalf("ci-required-gate if=%q, want always() so failed or skipped needs are inspected", gate.If) } - wantNeeds := []string{"browser-runtime", "platform-smoke", "platform-smoke-intel", "source-quality"} + wantNeeds := []string{"browser-runtime", "platform-smoke", "platform-smoke-intel", "platform-smoke-linux-arm64", "source-quality"} if got := needsList(gate.Needs); !reflect.DeepEqual(got, wantNeeds) { t.Fatalf("ci-required-gate needs=%#v, want %#v", got, wantNeeds) } @@ -906,6 +925,34 @@ func TestCIRequiredAggregateRejectsExecutionOverrides(t *testing.T) { workflow.Jobs["platform-smoke-intel"] = job }, }, + { + name: "required Linux arm64 leaf job continue on error", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke-linux-arm64"] + job.ContinueOnError = "${{ 1 == 1 }}" + workflow.Jobs["platform-smoke-linux-arm64"] = job + }, + }, + { + name: "required Linux arm64 leaf job environment", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke-linux-arm64"] + job.Env = map[string]any{"GOARCH": "amd64"} + workflow.Jobs["platform-smoke-linux-arm64"] = job + }, + }, + { + name: "required Linux arm64 leaf step environment", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke-linux-arm64"] + for index := range job.Steps { + if job.Steps[index].Name == "Verify Python 3.11.0 installed wheel" { + job.Steps[index].Env = map[string]any{"DOCKER_DEFAULT_PLATFORM": "linux/amd64"} + } + } + workflow.Jobs["platform-smoke-linux-arm64"] = job + }, + }, { name: "required leaf job null continue on error", mutate: func(workflow *githubWorkflow) { @@ -1348,7 +1395,7 @@ func TestCIRequiredAggregateRejectsPlatformSmokeSubstitution(t *testing.T) { }, "x86_64 job dropped from aggregate needs": func(workflow *githubWorkflow) { gate := workflow.Jobs["ci-required-gate"] - gate.Needs = []any{"browser-runtime", "source-quality", "platform-smoke"} + gate.Needs = withoutNeed(t, gate.Needs, "platform-smoke-intel") workflow.Jobs["ci-required-gate"] = gate }, "x86_64 result unchecked by aggregate": func(workflow *githubWorkflow) { @@ -1359,7 +1406,7 @@ func TestCIRequiredAggregateRejectsPlatformSmokeSubstitution(t *testing.T) { "x86_64 job removed consistently": func(workflow *githubWorkflow) { delete(workflow.Jobs, "platform-smoke-intel") gate := workflow.Jobs["ci-required-gate"] - gate.Needs = []any{"browser-runtime", "source-quality", "platform-smoke"} + gate.Needs = withoutNeed(t, gate.Needs, "platform-smoke-intel") gate.Steps[0].Run = strings.Replace(gate.Steps[0].Run, intelAggregateLine, "", 1) workflow.Jobs["ci-required-gate"] = gate }, @@ -1375,6 +1422,198 @@ func TestCIRequiredAggregateRejectsPlatformSmokeSubstitution(t *testing.T) { } }) } + // The Linux arm64 claim depends on its own required native ARM runner: the + // job must prove aarch64 in-job before checkout, run the owner invariant, + // smoke and CPython 3.11.0 steps exactly, and stay in the aggregate. + linuxJob := linuxArm64PlatformSmokeJob + linuxStepIndex := func(job githubJob, name string) int { + index, err := uniqueStepIndex(job.Steps, name) + if err != nil || index < 0 { + t.Fatalf("locate %s step %q: index=%d err=%v", linuxJob, name, index, err) + } + return index + } + linuxArchitectureLine := "test \"$machine\" = \"aarch64\"\n" + linuxAggregateLine := "test \"${{ needs.platform-smoke-linux-arm64.result }}\" = \"success\"\n" + if job := base.Jobs[linuxJob]; !strings.Contains(job.Steps[linuxStepIndex(job, "Verify native Linux architecture")].Run, linuxArchitectureLine) { + t.Fatal("owner Linux arm64 job lacks the aarch64 assertion line") + } + if !strings.Contains(base.Jobs["ci-required-gate"].Steps[0].Run, linuxAggregateLine) { + t.Fatal("owner aggregate lacks the Linux arm64 result line") + } + linuxCases := []struct { + name string + mutate func(*githubWorkflow) + }{ + {"x64 runner", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.RunsOn = "ubuntu-24.04" + workflow.Jobs[linuxJob] = job + }}, + {"Darwin arm64 runner", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.RunsOn = "macos-15" + workflow.Jobs[linuxJob] = job + }}, + {"older ARM runner image", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.RunsOn = "ubuntu-22.04-arm" + workflow.Jobs[linuxJob] = job + }}, + {"non-string runner label", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.RunsOn = []any{"ubuntu-24.04-arm", false} + workflow.Jobs[linuxJob] = job + }}, + {"timeout lowered", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.TimeoutMinutes = 1 + workflow.Jobs[linuxJob] = job + }}, + {"provider check name drift", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.Name = "quality / platform smoke" + workflow.Jobs[linuxJob] = job + }}, + {"architecture check omitted", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify native Linux architecture") + job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) + workflow.Jobs[linuxJob] = job + }}, + {"architecture assertion removed", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify native Linux architecture") + job.Steps[index].Run = strings.Replace(job.Steps[index].Run, linuxArchitectureLine, "", 1) + workflow.Jobs[linuxJob] = job + }}, + {"architecture assertion expects x86_64", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify native Linux architecture") + job.Steps[index].Run = strings.Replace(job.Steps[index].Run, linuxArchitectureLine, "test \"$machine\" = \"x86_64\"\n", 1) + workflow.Jobs[linuxJob] = job + }}, + {"architecture assertion masked", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify native Linux architecture") + job.Steps[index].Run = strings.Replace(job.Steps[index].Run, linuxArchitectureLine, "test \"$machine\" = \"aarch64\" || true\n", 1) + workflow.Jobs[linuxJob] = job + }}, + {"architecture check moved after checkout", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify native Linux architecture") + checkout := linuxStepIndex(job, "Checkout") + job.Steps[index], job.Steps[checkout] = job.Steps[checkout], job.Steps[index] + workflow.Jobs[linuxJob] = job + }}, + {"Darwin architecture check substituted", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify native Linux architecture") + job.Steps[index] = workflow.Jobs["platform-smoke"].Steps[0] + workflow.Jobs[linuxJob] = job + }}, + {"process package omitted", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + for i := range job.Steps { + job.Steps[i].Run = strings.ReplaceAll(job.Steps[i].Run, " ./internal/kernel/processgroup", "") + } + workflow.Jobs[linuxJob] = job + }}, + {"process suite narrowed", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Run Linux filesystem and process invariants") + job.Steps[index].Run += " -run '^TestLinuxObservationClassification$'" + workflow.Jobs[linuxJob] = job + }}, + {"invariants omitted", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Run Linux filesystem and process invariants") + job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) + workflow.Jobs[linuxJob] = job + }}, + {"platform smoke neutralized", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.Steps[linuxStepIndex(job, "Run platform smoke")].Run = "true" + workflow.Jobs[linuxJob] = job + }}, + {"release wheel set narrowed to current", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Build release wheel set") + job.Steps[index].Run = strings.Replace(job.Steps[index].Run, "packagebuild all", "packagebuild current", 1) + workflow.Jobs[linuxJob] = job + }}, + {"CPython 3.11.0 smoke omitted", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + index := linuxStepIndex(job, "Verify Python 3.11.0 installed wheel") + job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) + workflow.Jobs[linuxJob] = job + }}, + {"no-op replaces job steps", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.Steps = []githubStep{{Name: "No-op", Run: "true", runPresent: true}} + workflow.Jobs[linuxJob] = job + }}, + {"Darwin step list substituted", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.Steps = workflow.Jobs["platform-smoke"].Steps + workflow.Jobs[linuxJob] = job + }}, + {"reusable job substitution", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.Steps = nil + job.Uses = "example/ci/.github/workflows/no-op.yml@0123456789012345678901234567890123456789" + workflow.Jobs[linuxJob] = job + }}, + {"inserted semantic shadow step", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.Steps = append([]githubStep{{Name: "Shadow platform smoke", Run: "npm pkg set scripts.platform:smoke=true", runPresent: true}}, job.Steps...) + workflow.Jobs[linuxJob] = job + }}, + {"job continue on error", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.ContinueOnError = true + workflow.Jobs[linuxJob] = job + }}, + {"job condition", func(workflow *githubWorkflow) { + job := workflow.Jobs[linuxJob] + job.If = "${{ github.event_name == 'never' }}" + workflow.Jobs[linuxJob] = job + }}, + {"dropped from aggregate needs", func(workflow *githubWorkflow) { + gate := workflow.Jobs["ci-required-gate"] + gate.Needs = withoutNeed(t, gate.Needs, linuxJob) + workflow.Jobs["ci-required-gate"] = gate + }}, + {"result unchecked by aggregate", func(workflow *githubWorkflow) { + gate := workflow.Jobs["ci-required-gate"] + gate.Steps[0].Run = strings.Replace(gate.Steps[0].Run, linuxAggregateLine, "", 1) + workflow.Jobs["ci-required-gate"] = gate + }}, + {"result masked by aggregate", func(workflow *githubWorkflow) { + gate := workflow.Jobs["ci-required-gate"] + gate.Steps[0].Run = strings.Replace(gate.Steps[0].Run, linuxAggregateLine, strings.TrimSuffix(linuxAggregateLine, "\n")+" || true\n", 1) + workflow.Jobs["ci-required-gate"] = gate + }}, + {"removed consistently", func(workflow *githubWorkflow) { + delete(workflow.Jobs, linuxJob) + gate := workflow.Jobs["ci-required-gate"] + gate.Needs = withoutNeed(t, gate.Needs, linuxJob) + gate.Steps[0].Run = strings.Replace(gate.Steps[0].Run, linuxAggregateLine, "", 1) + workflow.Jobs["ci-required-gate"] = gate + }}, + } + for _, test := range linuxCases { + t.Run(linuxJob+"/"+test.name, func(t *testing.T) { + workflow := cloneWorkflow(t, base) + test.mutate(&workflow) + if reflect.DeepEqual(workflow, base) { + t.Fatal("Linux arm64 mutation did not change the owner workflow") + } + if err := validateCIRequiredAggregate(workflow); err == nil { + t.Fatal("Linux arm64 platform-smoke weakening was admitted") + } + }) + } t.Run("package script owner neutralized", func(t *testing.T) { var manifest map[string]any if err := json.Unmarshal(packageManifest, &manifest); err != nil { @@ -1614,6 +1853,7 @@ test "${{ needs.browser-runtime.result }}" = "success" test "${{ needs.source-quality.result }}" = "success" test "${{ needs.platform-smoke.result }}" = "success" test "${{ needs.platform-smoke-intel.result }}" = "success" +test "${{ needs.platform-smoke-linux-arm64.result }}" = "success" printf 'OK: all required quality checks passed\n'` const requiredPlatformSmokeShell = `set -euo pipefail @@ -1621,6 +1861,25 @@ npm run npm:version npm run platform:smoke ` +// The exact required leaf jobs of the CI aggregate, in needsList order. +var ciRequiredLeafJobs = []string{"browser-runtime", "platform-smoke", "platform-smoke-intel", "platform-smoke-linux-arm64", "source-quality"} + +const linuxArm64PlatformSmokeJob = "platform-smoke-linux-arm64" + +// The Linux arm64 job builds the complete current release wheel set so the +// CPython 3.11.0 installed smoke can select and rebuild its native wheel. +const requiredReleaseWheelSetShell = `set -euo pipefail +go run ./internal/tools/packagebuild all +go run ./internal/tools/pythonpackage build +` + +const requiredMinimumPythonCommand = "go run ./internal/tools/pythonpackage verify-minimum" + +// Every platform job runs the same filesystem and process packages natively; +// their Darwin or Linux build-tagged tests qualify that host for the source +// tools' process lifecycle. +const requiredPlatformInvariantCommand = "go test ./internal/kernel/rootpath ./internal/kernel/repositorytransaction ./internal/kernel/processgroup ./internal/command/projectstatus -count=1" + func readPackageManifestForTest(t *testing.T) []byte { t.Helper() raw, err := os.ReadFile(filepath.Join("..", "package.json")) @@ -1677,6 +1936,15 @@ var platformSmokeArchitectures = map[string]string{ "platform-smoke-intel": "x86_64", } +// The Linux arm64 job first proves in-job that the runner is natively +// aarch64, so the runner label is not the only evidence. The job declares no +// container, so its run steps execute on the runner host. +const requiredLinuxArm64ArchitectureShell = `set -euo pipefail +machine="$(uname -m)" +printf 'uname -m=%s\n' "$machine" +test "$machine" = "aarch64" +` + func requiredDarwinArchitectureShell(architecture string) string { return `set -euo pipefail machine="$(uname -m)" @@ -1689,17 +1957,70 @@ test "${translated:-0}" = "0" // Both Darwin architectures run the same owner step list. Only the first // step's expected architecture and the job topology (name, runner label, -// timeout) distinguish arm64 from x86_64. +// timeout) distinguish arm64 from x86_64. The Linux arm64 job runs the same +// setup, invariant packages and platform smoke after its own architecture +// check, then the CPython 3.11.0 installed smoke over a fresh release wheel set. func validateExactPlatformSmokeSteps(jobID string, job githubJob) error { - architecture, ok := platformSmokeArchitectures[jobID] - if !ok { - return fmt.Errorf("ci workflow %s has no owner Darwin architecture", jobID) - } - expected := []githubStep{ - { + var expected []githubStep + if jobID == linuxArm64PlatformSmokeJob { + expected = append(expected, githubStep{ + Name: "Verify native Linux architecture", + Run: requiredLinuxArm64ArchitectureShell, + }) + expected = append(expected, platformSmokeSetupSteps()...) + expected = append(expected, + githubStep{Name: "Run Linux filesystem and process invariants", Run: requiredPlatformInvariantCommand}, + githubStep{Name: "Run platform smoke", Run: requiredPlatformSmokeShell}, + githubStep{Name: "Build release wheel set", Run: requiredReleaseWheelSetShell}, + githubStep{Name: "Verify Python 3.11.0 installed wheel", Run: requiredMinimumPythonCommand}, + ) + } else { + architecture, ok := platformSmokeArchitectures[jobID] + if !ok { + return fmt.Errorf("ci workflow %s has no owner platform architecture", jobID) + } + expected = append(expected, githubStep{ Name: "Verify native Darwin architecture", Run: requiredDarwinArchitectureShell(architecture), - }, + }) + expected = append(expected, platformSmokeSetupSteps()...) + expected = append(expected, + githubStep{Name: "Run Darwin filesystem and process invariants", Run: requiredPlatformInvariantCommand}, + githubStep{Name: "Run platform smoke", Run: requiredPlatformSmokeShell}, + ) + } + if len(job.Steps) != len(expected) { + return fmt.Errorf( + "ci workflow %s step count=%d, want exact %d", + jobID, + len(job.Steps), + len(expected), + ) + } + for index, want := range expected { + actual := job.Steps[index] + if actual.Name != want.Name || + actual.Uses != want.Uses || + actual.Run != want.Run || + !reflect.DeepEqual(actual.With, want.With) || + actual.runPresent != (want.Run != "") || + actual.usesPresent != (want.Uses != "") || + actual.withPresent != (want.With != nil) { + return fmt.Errorf( + "ci workflow %s step %d differs from exact owner step %q", + jobID, + index, + want.Name, + ) + } + } + return nil +} + +// The pinned setup steps every platform job runs between its architecture +// check and its first repository command. +func platformSmokeSetupSteps() []githubStep { + return []githubStep{ { Name: "Checkout", Uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", @@ -1730,41 +2051,7 @@ func validateExactPlatformSmokeSteps(jobID string, job githubJob) error { Uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97", With: map[string]any{"python-version": "3.15.0"}, }, - { - Name: "Run Darwin filesystem and process invariants", - Run: "go test ./internal/kernel/rootpath ./internal/kernel/repositorytransaction ./internal/kernel/processgroup ./internal/command/projectstatus -count=1", - }, - { - Name: "Run platform smoke", - Run: requiredPlatformSmokeShell, - }, } - if len(job.Steps) != len(expected) { - return fmt.Errorf( - "ci workflow %s step count=%d, want exact %d", - jobID, - len(job.Steps), - len(expected), - ) - } - for index, want := range expected { - actual := job.Steps[index] - if actual.Name != want.Name || - actual.Uses != want.Uses || - actual.Run != want.Run || - !reflect.DeepEqual(actual.With, want.With) || - actual.runPresent != (want.Run != "") || - actual.usesPresent != (want.Uses != "") || - actual.withPresent != (want.With != nil) { - return fmt.Errorf( - "ci workflow %s step %d differs from exact owner step %q", - jobID, - index, - want.Name, - ) - } - } - return nil } func validateCIRequiredAggregate(workflow githubWorkflow) error { @@ -1790,7 +2077,7 @@ func validateCIRequiredAggregate(workflow githubWorkflow) error { if err := validateOptionalWorkflowRunDefaults(workflow.Defaults); err != nil { return fmt.Errorf("ci workflow run defaults: %w", err) } - wantNeeds := []string{"browser-runtime", "platform-smoke", "platform-smoke-intel", "source-quality"} + wantNeeds := ciRequiredLeafJobs for _, jobID := range append(append([]string{}, wantNeeds...), "ci-required-gate") { job, ok := workflow.Jobs[jobID] if !ok { @@ -1808,11 +2095,12 @@ func validateCIRequiredAggregate(workflow githubWorkflow) error { "ci workflow", workflow, map[string]string{ - "browser-runtime": "", - "platform-smoke": "", - "platform-smoke-intel": "", - "source-quality": "", - "ci-required-gate": "always()", + "browser-runtime": "", + "platform-smoke": "", + "platform-smoke-intel": "", + "platform-smoke-linux-arm64": "", + "source-quality": "", + "ci-required-gate": "always()", }, map[string]map[string]string{ "browser-runtime": { @@ -1827,11 +2115,12 @@ func validateCIRequiredAggregate(workflow githubWorkflow) error { runner string timeoutMinutes int }{ - "source-quality": {name: "quality / source", runner: "ubuntu-24.04", timeoutMinutes: 20}, - "platform-smoke": {name: "quality / platform smoke / macos-15", runner: "macos-15", timeoutMinutes: 12}, - "platform-smoke-intel": {name: "quality / platform smoke / macos-15-intel", runner: "macos-15-intel", timeoutMinutes: 20}, - "browser-runtime": {name: "quality / browser runtime", runner: "ubuntu-24.04", timeoutMinutes: 20}, - "ci-required-gate": {name: "quality / required aggregate", runner: "ubuntu-24.04", timeoutMinutes: 5}, + "source-quality": {name: "quality / source", runner: "ubuntu-24.04", timeoutMinutes: 20}, + "platform-smoke": {name: "quality / platform smoke / macos-15", runner: "macos-15", timeoutMinutes: 12}, + "platform-smoke-intel": {name: "quality / platform smoke / macos-15-intel", runner: "macos-15-intel", timeoutMinutes: 20}, + "platform-smoke-linux-arm64": {name: "quality / platform smoke / linux-arm64", runner: "ubuntu-24.04-arm", timeoutMinutes: 15}, + "browser-runtime": {name: "quality / browser runtime", runner: "ubuntu-24.04", timeoutMinutes: 20}, + "ci-required-gate": {name: "quality / required aggregate", runner: "ubuntu-24.04", timeoutMinutes: 5}, } for jobID, expected := range exactJobTopology { job := workflow.Jobs[jobID] @@ -1851,13 +2140,15 @@ func validateCIRequiredAggregate(workflow githubWorkflow) error { ) } } - for _, jobID := range []string{"platform-smoke", "platform-smoke-intel"} { + for _, jobID := range []string{"platform-smoke", "platform-smoke-intel", linuxArm64PlatformSmokeJob} { if err := validateExactPlatformSmokeSteps(jobID, workflow.Jobs[jobID]); err != nil { return err } } - if err := validateCIMinimumPythonStep(workflow.Jobs["source-quality"]); err != nil { - return err + for _, jobID := range ciMinimumPythonJobIDs() { + if err := validateCIMinimumPythonStep(jobID, workflow.Jobs[jobID]); err != nil { + return err + } } if err := validateCILegacyUnicodeStep(workflow.Jobs["source-quality"]); err != nil { return err diff --git a/scripts/workflow_python_minimum_test.go b/scripts/workflow_python_minimum_test.go index 4605d38d..017d8065 100644 --- a/scripts/workflow_python_minimum_test.go +++ b/scripts/workflow_python_minimum_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os/exec" "path/filepath" + "slices" "strings" "testing" ) @@ -13,49 +14,85 @@ func TestCIMinimumFailureCannotSatisfyAggregate(t *testing.T) { if err := validateCIRequiredAggregate(workflow); err != nil { t.Fatal(err) } - for _, status := range []string{"success", "failure", "skipped", "cancelled"} { - t.Run(status, func(t *testing.T) { - script := workflow.Jobs["ci-required-gate"].Steps[0].Run - for _, job := range []string{"source-quality", "browser-runtime", "platform-smoke", "platform-smoke-intel"} { - value := "success" - if job == "source-quality" { - value = status + // Both jobs that run the CPython 3.11.0 installed smoke are required + // leaves: a minimum failure in either job is a job result the aggregate + // must refuse, whatever the other leaves report. + leaves := []string{"source-quality", "browser-runtime", "platform-smoke", "platform-smoke-intel", "platform-smoke-linux-arm64"} + for _, failing := range ciMinimumPythonJobIDs() { + for _, status := range []string{"success", "failure", "skipped", "cancelled"} { + t.Run(failing+"/"+status, func(t *testing.T) { + script := workflow.Jobs["ci-required-gate"].Steps[0].Run + for _, job := range leaves { + value := "success" + if job == failing { + value = status + } + script = strings.ReplaceAll(script, "${{ needs."+job+".result }}", value) } - script = strings.ReplaceAll(script, "${{ needs."+job+".result }}", value) - } - command := exec.CommandContext(t.Context(), "bash", "-c", script) - output, err := command.CombinedOutput() - if (err == nil) != (status == "success") { - t.Fatalf("source-quality %s aggregate error=%v", status, err) - } - if status != "success" && strings.Contains(string(output), "OK:") { - t.Fatal("failed minimum lane emitted aggregate success") - } - }) + if strings.Contains(script, "${{") { + t.Fatalf("aggregate script references an unsubstituted job result: %q", script) + } + command := exec.CommandContext(t.Context(), "bash", "-c", script) + output, err := command.CombinedOutput() + if (err == nil) != (status == "success") { + t.Fatalf("%s %s aggregate error=%v", failing, status, err) + } + if status != "success" && strings.Contains(string(output), "OK:") { + t.Fatal("failed minimum lane emitted aggregate success") + } + }) + } + } +} + +// Each required job that runs the CPython 3.11.0 installed smoke names the +// step that produces its current release wheel set and the steps the smoke +// must precede. +var ciMinimumPythonJobs = map[string]struct { + producer string + successors []string +}{ + "source-quality": { + producer: "Build and verify package artifacts", + successors: []string{"Verify self-hosting receipts", "Verify self-hosting coverage", "Verify release closeout"}, + }, + linuxArm64PlatformSmokeJob: {producer: "Build release wheel set"}, +} + +func ciMinimumPythonJobIDs() []string { + ids := make([]string, 0, len(ciMinimumPythonJobs)) + for id := range ciMinimumPythonJobs { + ids = append(ids, id) } + slices.Sort(ids) + return ids } -func validateCIMinimumPythonStep(job githubJob) error { +func validateCIMinimumPythonStep(jobID string, job githubJob) error { + owner, ok := ciMinimumPythonJobs[jobID] + if !ok { + return fmt.Errorf("CI job %s has no owner Python minimum step", jobID) + } index, err := uniqueStepIndex(job.Steps, "Verify Python 3.11.0 installed wheel") if err != nil || index < 0 { - return fmt.Errorf("CI must contain exactly one Python minimum installed-wheel step") + return fmt.Errorf("CI job %s must contain exactly one Python minimum installed-wheel step", jobID) } step := job.Steps[index] - if step.Run != "go run ./internal/tools/pythonpackage verify-minimum" || !step.runPresent || + if step.Run != requiredMinimumPythonCommand || !step.runPresent || step.If != "" || step.ifPresent || step.ContinueOnError != nil || step.continueOnErrorPresent || step.Uses != "" || step.usesPresent || step.With != nil || step.withPresent || step.Env != nil || step.Shell != nil || step.shellPresent || step.WorkingDirectory != nil || step.workingDirectoryPresent || step.TimeoutMinutes != nil || step.timeoutMinutesPresent || step.ID != "" || step.idPresent { - return fmt.Errorf("CI Python minimum step must be the exact unconditional owner invocation") + return fmt.Errorf("CI job %s Python minimum step must be the exact unconditional owner invocation", jobID) } - buildIndex, err := uniqueStepIndex(job.Steps, "Build and verify package artifacts") + buildIndex, err := uniqueStepIndex(job.Steps, owner.producer) if err != nil || buildIndex < 0 || index != buildIndex+1 { - return fmt.Errorf("CI Python minimum step must immediately follow current package artifacts") + return fmt.Errorf("CI job %s Python minimum step must immediately follow %s", jobID, owner.producer) } - for _, successor := range []string{"Verify self-hosting receipts", "Verify self-hosting coverage", "Verify release closeout"} { + for _, successor := range owner.successors { next, err := uniqueStepIndex(job.Steps, successor) if err != nil || next <= index { - return fmt.Errorf("CI Python minimum step must precede %s", successor) + return fmt.Errorf("CI job %s Python minimum step must precede %s", jobID, successor) } } return nil @@ -63,48 +100,68 @@ func validateCIMinimumPythonStep(job githubJob) error { func TestCIMinimumPythonStepRejectsOmissionAndNeutralization(t *testing.T) { base := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) - if err := validateCIMinimumPythonStep(base.Jobs["source-quality"]); err != nil { - t.Fatal(err) + for _, jobID := range ciMinimumPythonJobIDs() { + if err := validateCIMinimumPythonStep(jobID, base.Jobs[jobID]); err != nil { + t.Fatal(err) + } } mutations := []struct { name string + jobs []string change func(*githubJob, int) }{ - {"omitted", func(job *githubJob, index int) { job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) }}, - {"duplicate", func(job *githubJob, index int) { job.Steps = append(job.Steps, job.Steps[index]) }}, - {"before artifact", func(job *githubJob, index int) { + {"omitted", nil, func(job *githubJob, index int) { job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) }}, + {"duplicate", nil, func(job *githubJob, index int) { job.Steps = append(job.Steps, job.Steps[index]) }}, + {"before artifact", nil, func(job *githubJob, index int) { job.Steps[index], job.Steps[index-1] = job.Steps[index-1], job.Steps[index] }}, - {"after receipts", func(job *githubJob, index int) { + {"after receipts", []string{"source-quality"}, func(job *githubJob, index int) { job.Steps[index], job.Steps[index+1] = job.Steps[index+1], job.Steps[index] }}, - {"no-op", func(job *githubJob, index int) { job.Steps[index].Run = "true" }}, - {"masked exit", func(job *githubJob, index int) { job.Steps[index].Run += " || true" }}, - {"background", func(job *githubJob, index int) { job.Steps[index].Run += " &" }}, - {"conditional", func(job *githubJob, index int) { job.Steps[index].If = "false" }}, - {"null condition", func(job *githubJob, index int) { job.Steps[index].ifPresent = true }}, - {"continue on error", func(job *githubJob, index int) { job.Steps[index].ContinueOnError = true }}, - {"null continue", func(job *githubJob, index int) { job.Steps[index].continueOnErrorPresent = true }}, - {"shadow environment", func(job *githubJob, index int) { job.Steps[index].Env = map[string]any{"PATH": "/other"} }}, - {"uses", func(job *githubJob, index int) { job.Steps[index].Uses = "other/action@main" }}, - {"wrong tool", func(job *githubJob, index int) { job.Steps[index].Run = "go run ./internal/tools/pythonpackage verify" }}, - } - for _, mutation := range mutations { - t.Run(mutation.name, func(t *testing.T) { - workflow := cloneWorkflow(t, base) - job := workflow.Jobs["source-quality"] - index, err := uniqueStepIndex(job.Steps, "Verify Python 3.11.0 installed wheel") - if err != nil { - t.Fatal(err) - } - mutation.change(&job, index) - workflow.Jobs["source-quality"] = job - if err := validateCIMinimumPythonStep(job); err == nil { - t.Fatal("minimum step mutation admitted by independent owner predicate") + {"before platform smoke", []string{linuxArm64PlatformSmokeJob}, func(job *githubJob, index int) { + step := job.Steps[index] + job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) + smoke, err := uniqueStepIndex(job.Steps, "Run platform smoke") + if err != nil || smoke < 0 { + panic("owner Linux arm64 workflow lost Run platform smoke") } - if err := validateCIRequiredAggregate(workflow); err == nil { - t.Fatal("minimum step mutation admitted by aggregate oracle") + job.Steps = slices.Insert(job.Steps, smoke, step) + }}, + {"wheel set omitted", []string{linuxArm64PlatformSmokeJob}, func(job *githubJob, index int) { + job.Steps = append(job.Steps[:index-1], job.Steps[index:]...) + }}, + {"no-op", nil, func(job *githubJob, index int) { job.Steps[index].Run = "true" }}, + {"masked exit", nil, func(job *githubJob, index int) { job.Steps[index].Run += " || true" }}, + {"background", nil, func(job *githubJob, index int) { job.Steps[index].Run += " &" }}, + {"conditional", nil, func(job *githubJob, index int) { job.Steps[index].If = "false" }}, + {"null condition", nil, func(job *githubJob, index int) { job.Steps[index].ifPresent = true }}, + {"continue on error", nil, func(job *githubJob, index int) { job.Steps[index].ContinueOnError = true }}, + {"null continue", nil, func(job *githubJob, index int) { job.Steps[index].continueOnErrorPresent = true }}, + {"shadow environment", nil, func(job *githubJob, index int) { job.Steps[index].Env = map[string]any{"PATH": "/other"} }}, + {"uses", nil, func(job *githubJob, index int) { job.Steps[index].Uses = "other/action@main" }}, + {"wrong tool", nil, func(job *githubJob, index int) { job.Steps[index].Run = "go run ./internal/tools/pythonpackage verify" }}, + } + for _, jobID := range ciMinimumPythonJobIDs() { + for _, mutation := range mutations { + if mutation.jobs != nil && !slices.Contains(mutation.jobs, jobID) { + continue } - }) + t.Run(jobID+"/"+mutation.name, func(t *testing.T) { + workflow := cloneWorkflow(t, base) + job := workflow.Jobs[jobID] + index, err := uniqueStepIndex(job.Steps, "Verify Python 3.11.0 installed wheel") + if err != nil { + t.Fatal(err) + } + mutation.change(&job, index) + workflow.Jobs[jobID] = job + if err := validateCIMinimumPythonStep(jobID, job); err == nil { + t.Fatal("minimum step mutation admitted by independent owner predicate") + } + if err := validateCIRequiredAggregate(workflow); err == nil { + t.Fatal("minimum step mutation admitted by aggregate oracle") + } + }) + } } } diff --git a/scripts/workflow_python_runtime_test.go b/scripts/workflow_python_runtime_test.go index d09e4f4e..6f2aa03f 100644 --- a/scripts/workflow_python_runtime_test.go +++ b/scripts/workflow_python_runtime_test.go @@ -21,7 +21,7 @@ var hostPythonConsumer = regexp.MustCompile(`\bpython3\b|npm run (check|go:test| // Jobs whose run steps consume the host Python. Every other job must not. var expectedPythonJobs = map[string][]string{ - "ci.yml": {"platform-smoke", "platform-smoke-intel", "source-quality"}, + "ci.yml": {"platform-smoke", "platform-smoke-intel", "platform-smoke-linux-arm64", "source-quality"}, "release.yml": {"candidate", "publish-pypi", "publish-readiness"}, } @@ -175,6 +175,16 @@ func TestWorkflowPythonRuntimeRejectsUnpinnedConsumers(t *testing.T) { {"x86_64 platform runtime drift", "ci.yml", "platform-smoke-intel", func(job *githubJob) { job.Steps[setupStep(*job)].With["python-version"] = "3.14.7" }}, + {"Linux arm64 platform setup omitted", "ci.yml", "platform-smoke-linux-arm64", func(job *githubJob) { + index := setupStep(*job) + job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) + }}, + {"Linux arm64 platform setup after smoke", "ci.yml", "platform-smoke-linux-arm64", func(job *githubJob) { + index := setupStep(*job) + step := job.Steps[index] + job.Steps = append(job.Steps[:index], job.Steps[index+1:]...) + job.Steps = append(job.Steps, step) + }}, {"new unpinned consumer", "release.yml", "release-metadata", func(job *githubJob) { job.Steps = append(job.Steps, githubStep{Name: "Read version", Run: "python3 -c 'print(1)'", runPresent: true}) }}, From 993d28587e878316cf6b89f1cc57dad57e7243c2 Mon Sep 17 00:00:00 2001 From: iperev Date: Sun, 25 Oct 2026 20:55:57 +0100 Subject: [PATCH 3/3] fix: retain schema security prerequisite in ARM validation --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b847083..7770ca24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -417,9 +417,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ {