From b16251994b9a9508f58075f94753022b49d60ea7 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:26:33 +0100 Subject: [PATCH 1/5] fix(gates): pin what reusables fetch; gate packaging on capability Three checks were red on most estate PRs because pinned reusable workflows pulled unpinned things at run time, and the Guix gate contradicted the canon. - hypatia-scan-reusable: new `hypatia-ref` input, default 51ab6496 (the hypatia#895 warn->medium fix). "HEAD" keeps a canary path. Value is validated as 40-hex or HEAD. - governance-reusable: every standards sparse checkout uses job.workflow_sha (the reusable's own commit) instead of `ref: main`, so a caller's pin pins the scripts too. gh-actions-lock installed --pin v0.1.6. The package-policy step ships check-rsr-profile.sh and the gates table. - check-package-policy.sh: packaging is required only where the rsr-profile declares reproducible-build or container (rsr-criteria-v2 1.2.1/1.2.3/8.1.4 are gated, not universal). Real packaging passes before the profile is read; every Containerfile is tried; .clusterfuzzlite/ is ignored; a stub guix.scm fails only where the capability is declared; an unresolvable profile is named in a warning; the Nix ban still fails regardless of profile. Census over 325 local governance callers: 86 red before, 20 after (all Nix-only, removed by the per-repo sweep); no previously-green repo turns red. Tests: governance-gates-505 39/39, with a mutant (REQUIRED forced empty) killed by 8 cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016L7GFo3yGQ2vK9YgKL2wsP --- .github/workflows/governance-reusable.yml | 35 ++-- .github/workflows/hypatia-scan-reusable.yml | 27 ++- scripts/check-package-policy.sh | 198 ++++++++++++++------ scripts/tests/governance-gates-505-test.sh | 72 +++++++ 4 files changed, 256 insertions(+), 76 deletions(-) diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 70626f399..0b28143b5 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -289,14 +289,16 @@ jobs: # A reusable workflow only auto-checks-out its own YAML, not sibling # scripts. Sparse-check-out standards' scripts/ to get apply-baseline.sh, - # mirroring the language-policy job below. Pinned to main because - # github.workflow_sha resolves to the *caller* repo's SHA (which would 404). + # mirroring the language-policy job below. Pinned to job.workflow_sha (this + # reusable's own standards commit), NOT main: a script fetched from main + # changed every pinned caller's verdict on each standards push. (Unlike + # github.workflow_sha, which is the caller's SHA, job.workflow_sha is ours.) - name: Check out standards for the baseline filter if: needs.workflow-staleness.outputs.has_baseline == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: .standards-checkout sparse-checkout: | scripts @@ -726,10 +728,11 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: .standards-checkout sparse-checkout: | scripts + .machine_readable/template-capability-gates.toml sparse-checkout-cone-mode: false - name: Enforce Guix-only packaging policy @@ -738,9 +741,15 @@ jobs: # checkout before scanning: the gate walks the whole caller tree, so # a packaging file shipped inside .standards-checkout/ would satisfy # the policy on the caller's behalf (same trap as the baseline job). - cp .standards-checkout/scripts/check-package-policy.sh "$RUNNER_TEMP/" + # The capability resolver (check-rsr-profile.sh) and its gate table travel + # with the checker, in the same relative layout, so applicability is read + # from the same standards commit as the policy itself. + pkg="$RUNNER_TEMP/pkg-policy" + mkdir -p "$pkg/scripts" "$pkg/.machine_readable" + cp .standards-checkout/scripts/check-package-policy.sh .standards-checkout/scripts/check-rsr-profile.sh "$pkg/scripts/" + cp .standards-checkout/.machine_readable/template-capability-gates.toml "$pkg/.machine_readable/" rm -rf .standards-checkout - bash "$RUNNER_TEMP/check-package-policy.sh" . + bash "$pkg/scripts/check-package-policy.sh" . security-policy: name: Security policy checks @@ -1051,7 +1060,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: .standards-checkout sparse-checkout: | scripts @@ -1464,7 +1473,9 @@ jobs: # redundant direct-SHA conversion; restoring their locked version # refs made GitHub's native resolver accept them again. if [ -f .github/workflows/actions.lock ]; then - gh extension install github/gh-actions-lock + # Pinned: an unpinned install let a new verifier release change every + # caller's verdict overnight. Bump deliberately, with the gate tests. + gh extension install github/gh-actions-lock --pin v0.1.6 fi # The gate delegates lockfile verification to the authoritative @@ -1530,7 +1541,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: standards - name: Run trusted-base check on caller repo run: | @@ -1561,7 +1572,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: standards - name: Run licence-consistency check on caller repo run: | @@ -1591,7 +1602,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: .standards-checkout sparse-checkout: | scripts @@ -1636,7 +1647,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/standards - ref: main + ref: ${{ job.workflow_sha }} path: .standards-checkout sparse-checkout: | scripts diff --git a/.github/workflows/hypatia-scan-reusable.yml b/.github/workflows/hypatia-scan-reusable.yml index e92b86ca2..a3e8ebd1a 100644 --- a/.github/workflows/hypatia-scan-reusable.yml +++ b/.github/workflows/hypatia-scan-reusable.yml @@ -12,6 +12,15 @@ on: type: boolean required: false default: false + hypatia-ref: + description: >- + Hypatia commit to build the scanner from. Defaults to a vetted SHA so a + bad hypatia main cannot turn every caller red at once (09-12..14 and + 09-26..30 outages). Canary callers pass "HEAD" to trial the tip before + the default is moved here. + type: string + required: false + default: 51ab6496bf47e30a0576d503df31fec30f3cfe56 permissions: actions: read @@ -36,15 +45,21 @@ jobs: elixir-version: '1.19.4' otp-version: '28.3' - - name: Resolve Hypatia HEAD commit + - name: Resolve Hypatia scanner commit id: hypatia-rev + env: + HYPATIA_REF: ${{ inputs.hypatia-ref }} run: | - # Pin the cache to the *current* Hypatia main tip. Resolved before the - # cache step because cache restore happens before the clone, so the key - # cannot hash a not-yet-cloned tree — it must hash the remote ref. - sha=$(git ls-remote https://github.com/hyperpolymath/hypatia.git HEAD | cut -f1) + # The scanner is PINNED (input hypatia-ref, default a vetted SHA). Only + # an explicit "HEAD" floats, for canary callers. Resolved before the + # cache step because the cache key must hash the commit. + if [ "$HYPATIA_REF" = "HEAD" ]; then + sha=$(git ls-remote https://github.com/hyperpolymath/hypatia.git HEAD | cut -f1) + else + sha="$HYPATIA_REF" + fi if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then - echo "ERROR: could not resolve hypatia HEAD via git ls-remote" >&2 + echo "ERROR: hypatia-ref must be a 40-hex commit SHA or HEAD (got: $HYPATIA_REF)" >&2 exit 1 fi echo "sha=$sha" >> "$GITHUB_OUTPUT" diff --git a/scripts/check-package-policy.sh b/scripts/check-package-policy.sh index 4e50f4759..f82330cb6 100755 --- a/scripts/check-package-policy.sh +++ b/scripts/check-package-policy.sh @@ -2,7 +2,8 @@ # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell # -# check-package-policy.sh — gate on the Guix-primary / Nix-fallback policy. +# check-package-policy.sh — gate on the Guix-primary packaging policy, applied +# only where the repo's rsr-profile declares reproducible-build or container. # # Replaces the echo-only "Enforce Guix primary / Nix fallback" step in # governance-reusable.yml, whose every branch echoed and which terminated with @@ -114,56 +115,136 @@ find_first() { printf '%s' "$out" } -GUIX="$(find_first -name guix.scm -o -name manifest.scm -o -name channels.scm -o -name .guix-channel)" +# --------------------------------------------------------------------------- +# APPLICABILITY (2026-10-01, owner decision "only repos that need it"). +# Packaging is NOT a universal criterion. rsr-criteria-v2.a2ml gates 1.2.1 +# guix-primary and 8.1.4 no-scaffold-stub on `reproducible-build`, and 1.2.3 +# container-rootless on `container`; a criterion applies iff its gate is +# `universal` OR the repo's rsr-profile declares the gating capability. This +# script previously demanded packaging of every repo, contradicting the canon +# for docs, proof and Julia libraries — 86/325 governance callers red, every +# one of them with no rsr-profile at all. +# +# Capabilities are resolved by scripts/check-rsr-profile.sh, the reference +# implementation of preset + capabilities + add - remove, not re-derived here. +# No profile ⇒ no declared capability ⇒ not applicable (a notice names the file +# to add). The Nix ban is a removal ruling, not a capability, so it still fails +# whatever the profile says. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RSR_PROFILE_CHECKER="${RSR_PROFILE_CHECKER:-$SCRIPT_DIR/check-rsr-profile.sh}" + +# Print the repo's effective capabilities, one per line; nothing if it has no +# profile. A profile the reference checker cannot resolve declares nothing it +# can read, so it counts as undeclared but is NAMED in a warning (e.g. an +# explicit `capabilities = []`, which check-rsr-profile.sh rejects). Only a +# missing resolver, a deployment defect, returns 1. +effective_capabilities() { + local f found="" out + for f in "$ROOT"/.machine_readable/rsr-profile.a2ml "$ROOT"/machine-readable/rsr-profile.a2ml; do + [ -f "$f" ] && found="$f" && break + done + [ -n "$found" ] || return 0 + if [ ! -f "$RSR_PROFILE_CHECKER" ]; then + echo "::error::check-package-policy: capability resolver missing at $RSR_PROFILE_CHECKER" >&2 + return 1 + fi + out="$(bash "$RSR_PROFILE_CHECKER" "$ROOT" 2>&1 || true)" + if ! printf '%s\n' "$out" | grep -q '^effective capabilities:'; then + echo "::warning::check-package-policy: ${found#"$ROOT"/} could not be resolved ($(printf '%s\n' "$out" | grep -m1 ERROR || echo "no effective capabilities line")); treating it as declaring no packaging capability." >&2 + return 0 + fi + printf '%s\n' "$out" | sed -n 's/^effective capabilities: //p' | tr ' ' '\n' | sed '/^$/d' +} + +# Every match, not the first: a repo's real build/container/Containerfile must +# not be shadowed by an earlier-sorting fuzzing image (.clusterfuzzlite/ ships +# a deliberately minimal OSS-Fuzz Containerfile that is not packaging). +find_all() { + find "$ROOT" \( "${PRUNE[@]}" -o -path "$ROOT/.clusterfuzzlite" \) -prune -o \( "$@" \) -print 2>/dev/null | sort || true +} + +# A scaffold stub per criterion 8.1.4: an unfilled placeholder or `(source #f)`. +is_stub_guix() { grep -qE '\(source #f\)|\{\{[A-Z_]+\}\}' "$1"; } + +GUIX_ALL="$(find_all -name guix.scm -o -name manifest.scm -o -name channels.scm -o -name .guix-channel)" +GUIX="" GUIX_STUB="" +while IFS= read -r f; do + [ -n "$f" ] || continue + if [ "$(basename "$f")" = guix.scm ] && is_stub_guix "$f"; then + GUIX_STUB="${GUIX_STUB:-$f}" + else + GUIX="$f"; break + fi +done <<< "$GUIX_ALL" NIX="$(find_first -name flake.nix -o -name default.nix -o -name shell.nix)" -# Sealed container — the policy's named escape hatch, previously undetectable. -# `Containerfile*` and `Dockerfile*` both count: the estate standardises on -# Podman/Containerfile, but a repo already carrying a Dockerfile is served by -# the same escape hatch and should not be told it has no packaging. -CONTAINER="$(find_first -name 'Containerfile*' -o -name 'Dockerfile*')" +# Sealed container — the policy's named escape hatch. `Containerfile*` and +# `Dockerfile*` both count. +CONTAINERS="$(find_all -name 'Containerfile*' -o -name 'Dockerfile*')" if [ -n "$GUIX" ]; then echo "✅ Guix package management detected (primary): ${GUIX#"$ROOT"/}" exit 0 fi -# A Containerfile only counts if it BUILDS something. The estate scaffold ships -# a template whose every install/build line is a commented `# TODO:` example — -# measured 2026-07-27: 17 of 60 estate Containerfiles are that stub. Accepting -# them on presence alone reproduces exactly the fault this script was written to -# remove (standards#505 accepted any *.scm as "Guix detected"). A stub provides -# no environment, so it is not packaging. -# -# The predicate is deliberately cheap and syntactic: at least one ACTIVE -# RUN / ENTRYPOINT / CMD instruction. It cannot prove the image is useful, but -# it does separate "someone filled this in" from "this is the untouched -# template", which is the distinction that matters at gate time. -if [ -n "$CONTAINER" ]; then - if grep -qE '^[[:space:]]*(RUN|ENTRYPOINT|CMD)[[:space:]]' "$CONTAINER"; then - echo "✅ Sealed-container packaging detected (escape hatch): ${CONTAINER#"$ROOT"/}" - echo "::notice::Guix is the estate primary; a sealed container is the" \ - "accepted escape hatch for the not-in-Guix / non-free tail." - exit 0 +# A Containerfile only counts if it BUILDS something: at least one ACTIVE +# RUN / ENTRYPOINT / CMD instruction. The estate scaffold ships a template whose +# every install/build line is a commented `# TODO:` example (17/60 measured +# 2026-07-27); presence alone would reproduce the fault of standards#505. +# Every Containerfile is tried; any one with active instructions satisfies. +CONTAINER="" CONTAINER_STUB="" +while IFS= read -r f; do + [ -n "$f" ] || continue + if grep -qE '^[[:space:]]*(RUN|ENTRYPOINT|CMD)[[:space:]]' "$f"; then + CONTAINER="$f"; break fi - echo "::warning::${CONTAINER#"$ROOT"/} is the UNFILLED scaffold template —" \ + CONTAINER_STUB="${CONTAINER_STUB:-$f}" +done <<< "$CONTAINERS" + +if [ -n "$CONTAINER" ]; then + echo "✅ Sealed-container packaging detected (escape hatch): ${CONTAINER#"$ROOT"/}" + echo "::notice::Guix is the estate primary; a sealed container is the" \ + "accepted escape hatch for the not-in-Guix / non-free tail." + exit 0 +fi +if [ -n "$CONTAINER_STUB" ]; then + echo "::warning::${CONTAINER_STUB#"$ROOT"/} is the UNFILLED scaffold template —" \ "every install/build step is a commented '# TODO:' example, so it" \ "provides no environment and does not satisfy the policy." - CONTAINER="" +fi + +# Applicability is resolved only now: a repo with real packaging passed above +# whatever its profile says, so a profile defect can never redden it. +CAPS="$(effective_capabilities)" || exit 1 +REQUIRED="" +if printf '%s +' "$CAPS" | grep -qxE 'reproducible-build|container'; then + REQUIRED="$(printf '%s +' "$CAPS" | grep -xE 'reproducible-build|container' | paste -sd ' ' -)" +fi + +# Only a stub guix.scm. Before capability gating this passed on presence, and +# ~90 repos rely on that; 8.1.4 is gated on reproducible-build, so the stub +# only fails where that capability (or container) is declared. +if [ -n "$GUIX_STUB" ]; then + if [ -z "$REQUIRED" ]; then + echo "::notice::${GUIX_STUB#"$ROOT"/} is a scaffold stub (criterion 8.1.4)." \ + "Not enforced: this repo declares neither reproducible-build nor container." + echo "✅ Packaging not applicable (no packaging capability declared)." + exit 0 + fi + echo "::error::${GUIX_STUB#"$ROOT"/} is a scaffold stub (placeholder or (source #f))," \ + "and this repo declares: $REQUIRED. A stub builds nothing (criterion 8.1.4)." + echo "Make the guix.scm real, or add a Containerfile with active RUN/CMD steps." + exit 1 fi # Nix-only. Under the 2026-05-18 ruling this is NOT compliance — Nix is not a -# tier — but it is also not the same as having no packaging at all, and the -# repos in this state are overwhelmingly there because a *sweep put them there* -# rather than through any author's choice. So it warns until the retirement -# date, then fails. It never prints a ✅. +# tier — and the 2026-07-28 ruling removes it from the estate outright. That is +# a ban, not a capability, so it applies whatever the profile declares. # # ⚠ SEQUENCING — read before changing ENFORCE_NIX_RETIREMENT_FROM. -# Nix retirement must TRAIL per-repo Guix functionality. Campaign #102 closed -# COMPLETED having hand-diffed 277 candidates and removed exactly ONE flake; -# ~270 repos carry a `guix.scm` that is a non-functional scaffold stub, so for -# them "delete the flake" means "have no working packaging". Measured over the -# local estate checkout: 22 repos are Nix-only and would fail the moment this -# date passes. Setting a date in the past makes that immediate, with no grace. +# Nix retirement must TRAIL per-repo Guix functionality: for a repo whose +# guix.scm is a stub, "delete the flake" means "have no working packaging". if [ -n "$NIX" ]; then ENFORCE_NIX_RETIREMENT_FROM="${ENFORCE_NIX_RETIREMENT_FROM:-2026-06-01}" require_date ENFORCE_NIX_RETIREMENT_FROM "$ENFORCE_NIX_RETIREMENT_FROM" @@ -179,37 +260,38 @@ if [ -n "$NIX" ]; then echo "::error::Nix-only packaging is not compliant: ${NIX#"$ROOT"/}" echo echo "Estate policy (3-practice/LANGUAGE-POLICY.adoc, RULED 2026-05-18) is Guix primary" - echo "+ sealed-container escape; NO Nix mirror. Replace the flake with:" - echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary)" - echo " Containerfile (escape hatch)" - echo - echo "HARDENED 2026-07-28 (owner ruling): Nix is REMOVED from the estate, not" - echo "tolerated. Retire the flake opportunistically whenever you touch a repo." - echo - echo "But removal is not the whole job: a repo whose guix.scm is a scaffold stub" - echo "has no working packaging once the flake is gone. Make the Guix side real" - echo "(or fill the Containerfile, which is Podman-verifiable where Guix is not" - echo "installable) IN THE SAME CHANGE as retiring the mirror. Do not leave the" - echo "repo unpackaged, and do not allowlist the flake instead" - echo "(spec/scaffold-stub-debt.adoc, step 3)." + echo "+ sealed-container escape; NO Nix mirror. HARDENED 2026-07-28: Nix is REMOVED." + if [ -z "$REQUIRED" ]; then + echo "This repo declares no packaging capability, so deleting the flake is the whole fix." + else + echo "This repo declares: $REQUIRED — replace the flake with a real guix.scm or" + echo "an active Containerfile IN THE SAME CHANGE (spec/scaffold-stub-debt.adoc, step 3)." + fi exit 1 fi -# Violation: neither packaging system is present. +if [ -z "$REQUIRED" ]; then + echo "::notice::No packaging, and none required: the repo's rsr-profile declares" \ + "neither reproducible-build nor container." \ + "A repo that ships a build should declare one in .machine_readable/rsr-profile.a2ml." + echo "✅ Packaging not applicable (no packaging capability declared)." + exit 0 +fi + +# Violation: packaging declared, none present. if [[ "$TODAY" < "$ENFORCE_PACKAGE_POLICY_FROM" ]]; then - echo "::warning::No packaging found (no Guix, no sealed container) — this" \ + echo "::warning::No packaging found but the profile declares: $REQUIRED — this" \ "becomes a BLOCKING failure on $ENFORCE_PACKAGE_POLICY_FROM (today is $TODAY)." - # Never claim a pass while the policy is unmet. echo "NOT YET ENFORCED: package policy unmet but inside the grace window." exit 0 fi -echo "::error::Package policy violation: no packaging found." +echo "::error::Package policy violation: the profile declares $REQUIRED, but no packaging was found." echo -echo "Estate policy (3-practice/LANGUAGE-POLICY.adoc, RULED 2026-05-18) is Guix primary" -echo "+ sealed-container escape; NO Nix mirror. Add one of:" -echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary)" -echo " Containerfile (escape hatch)" +echo "Add one of:" +echo " guix.scm | manifest.scm | channels.scm | .guix-channel (primary; not a stub)" +echo " Containerfile with active RUN/CMD/ENTRYPOINT (escape hatch)" +echo "or remove the capability from .machine_readable/rsr-profile.a2ml if it is not real." echo -echo "Files inside .git/ node_modules/ deps/ .lake/ vendor/ do not count." +echo "Files inside .git/ node_modules/ deps/ .lake/ vendor/ .clusterfuzzlite/ do not count." exit 1 diff --git a/scripts/tests/governance-gates-505-test.sh b/scripts/tests/governance-gates-505-test.sh index f154fdc66..4763a6ed2 100755 --- a/scripts/tests/governance-gates-505-test.sh +++ b/scripts/tests/governance-gates-505-test.sh @@ -54,6 +54,15 @@ mkrepo() { printf '%s' "$d" } +# declare — write an rsr-profile declaring capabilities, +# so a fixture opts into the packaging criterion (gated, not universal). +declare() { + local d="$1"; shift + mkdir -p "$d/.machine_readable" + local caps; caps="$(printf '"%s", ' "$@")" + printf '[rsr-profile]\ncapabilities = [%s]\n' "${caps%, }" > "$d/.machine_readable/rsr-profile.a2ml" +} + BEFORE="2026-08-01" # inside the grace window (cutoff 2026-08-21) AFTER="2026-09-01" # past the cutoff @@ -141,6 +150,65 @@ assert "day before cutoff still in grace" 0 "NOT YET ENFORCED" \ env DOCS_TODAY="2026-08-20" "$DOCS" "$r" # Anti-disarm: a malformed cutoff must refuse to run, not silently grace. +# --- Applicability (2026-10-01): packaging is gated on reproducible-build or +# container, per rsr-criteria-v2 1.2.1 / 1.2.3 / 8.1.4. Not universal. +r=$(mkrepo pkg-none-undeclared README.adoc) +assert "no packaging + no profile is NOT applicable (passes)" 0 "Packaging not applicable" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +r=$(mkrepo pkg-none-docs README.adoc) +declare "$r" docs-site +assert "no packaging + profile without the capability passes" 0 "Packaging not applicable" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +# stub_guix — write a template-style guix.scm whose package has no source. +stub_guix() { printf '(package\n (name "x")\n (source #f))\n' > "$1"; } +r=$(mkrepo pkg-stub-undeclared README.adoc); mkdir -p "$r/build"; stub_guix "$r/build/guix.scm" +assert "stub guix.scm, capability undeclared: notice, pass" 0 "scaffold stub" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +r=$(mkrepo pkg-stub-declared README.adoc); mkdir -p "$r/build"; stub_guix "$r/build/guix.scm" +declare "$r" reproducible-build +assert "stub guix.scm, reproducible-build declared: BLOCKS (8.1.4)" 1 "scaffold stub" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +# Planted positive: declared container + TODO-only template must still fail. +r=$(mkrepo pkg-container-todo README.adoc) +printf 'FROM cgr.dev/chainguard/wolfi-base\n# TODO: RUN apk add ...\n' > "$r/Containerfile" +declare "$r" container +assert "declared container + TODO-only Containerfile BLOCKS" 1 "Package policy violation" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +# Every Containerfile is tried; .clusterfuzzlite/ never counts and never shadows. +r=$(mkrepo pkg-container-multi README.adoc) +mkdir -p "$r/.clusterfuzzlite" "$r/build/container" +printf 'FROM gcr.io/oss-fuzz-base/base-builder\nRUN echo fuzz\n' > "$r/.clusterfuzzlite/Containerfile" +printf 'FROM x\n# TODO\n' > "$r/a.Containerfile" +printf 'FROM x\nRUN true\n' > "$r/build/container/Containerfile" +declare "$r" container +assert "active Containerfile found past a stub; .clusterfuzzlite ignored" 0 "build/container/Containerfile" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +r=$(mkrepo pkg-fuzz-only README.adoc) +mkdir -p "$r/.clusterfuzzlite" +printf 'FROM gcr.io/oss-fuzz-base/base-builder\nRUN echo fuzz\n' > "$r/.clusterfuzzlite/Containerfile" +declare "$r" container +assert ".clusterfuzzlite/Containerfile alone does not satisfy" 1 "Package policy violation" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + +r=$(mkrepo pkg-bad-profile README.adoc) +mkdir -p "$r/.machine_readable"; printf '[rsr-profile]\nrole = "x"\n' > "$r/.machine_readable/rsr-profile.a2ml" +assert "unresolvable profile is NAMED in a warning, read as undeclared" 0 "could not be resolved" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" +# A profile defect must never redden a repo whose packaging is real. +: > "$r/guix.scm" +assert "unresolvable profile + real guix.scm passes before the profile is read" 0 "Guix package management detected" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" +rm "$r/guix.scm" +# A missing resolver is a deployment defect and must refuse. +assert "missing capability resolver refuses" 1 "capability resolver missing" \ + env PKG_TODAY="$AFTER" RSR_PROFILE_CHECKER=/nonexistent "$PKG" "$r" + assert "malformed cutoff refuses to run" 1 "is not YYYY-MM-DD" \ env ENFORCE_CONTRIBUTING_FROM="soon" "$DOCS" "$r" assert "missing repo root errors" 1 "is not a directory" \ @@ -168,6 +236,7 @@ assert "Nix-only packaging warns before retirement" 0 "NOT YET ENFORCED" \ # Same repo, both sides of the cutoff — the self-flipping proof. r=$(mkrepo pkg-none README.adoc) +declare "$r" reproducible-build assert "no packaging warns pre-cutoff (no pass claimed)" 0 "NOT YET ENFORCED" \ env PKG_TODAY="$BEFORE" "$PKG" "$r" assert "no packaging BLOCKS post-cutoff" 1 "Package policy violation" \ @@ -176,15 +245,18 @@ assert "no packaging BLOCKS post-cutoff" 1 "Package policy violation" \ # Tightened predicate: a stray Guile file is not packaging. The replaced step # accepted this via `find . -name "*.scm"`. r=$(mkrepo pkg-stray-scm src/helpers.scm) +declare "$r" reproducible-build assert "stray .scm does NOT satisfy the policy" 1 "Package policy violation" \ env PKG_TODAY="$AFTER" "$PKG" "$r" # Vendored trees must not satisfy the policy on the repo's behalf. r=$(mkrepo pkg-vendored node_modules/foo/guix.scm) +declare "$r" reproducible-build assert "guix.scm in node_modules does not count" 1 "Package policy violation" \ env PKG_TODAY="$AFTER" "$PKG" "$r" r=$(mkrepo pkg-deps deps/bar/flake.nix) +declare "$r" reproducible-build assert "flake.nix in deps/ does not count" 1 "Package policy violation" \ env PKG_TODAY="$AFTER" "$PKG" "$r" From 8f277c2c3170934eccd004a6fd6678a2072de672 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:30:30 +0100 Subject: [PATCH 2/5] fix(gates): a missing profile resolver warns, never reddens Callers pinned to a pre-2026-10-01 governance-reusable copy check-package-policy.sh alone, so the resolver is absent by construction. Measured over the 325 local callers in that lone-file shape: 305 pass, 20 Nix-only fail, same as the full-layout run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016L7GFo3yGQ2vK9YgKL2wsP --- scripts/check-package-policy.sh | 11 +++++++---- scripts/tests/governance-gates-505-test.sh | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/check-package-policy.sh b/scripts/check-package-policy.sh index f82330cb6..952ac1b97 100755 --- a/scripts/check-package-policy.sh +++ b/scripts/check-package-policy.sh @@ -136,8 +136,8 @@ RSR_PROFILE_CHECKER="${RSR_PROFILE_CHECKER:-$SCRIPT_DIR/check-rsr-profile.sh}" # Print the repo's effective capabilities, one per line; nothing if it has no # profile. A profile the reference checker cannot resolve declares nothing it # can read, so it counts as undeclared but is NAMED in a warning (e.g. an -# explicit `capabilities = []`, which check-rsr-profile.sh rejects). Only a -# missing resolver, a deployment defect, returns 1. +# explicit `capabilities = []`, which check-rsr-profile.sh rejects). A missing +# resolver is handled the same way, also with a named warning. effective_capabilities() { local f found="" out for f in "$ROOT"/.machine_readable/rsr-profile.a2ml "$ROOT"/machine-readable/rsr-profile.a2ml; do @@ -145,8 +145,11 @@ effective_capabilities() { done [ -n "$found" ] || return 0 if [ ! -f "$RSR_PROFILE_CHECKER" ]; then - echo "::error::check-package-policy: capability resolver missing at $RSR_PROFILE_CHECKER" >&2 - return 1 + # Callers pinned to a governance-reusable from before 2026-10-01 copy this + # script alone, so the resolver is absent there by construction. Reddening + # them would turn a deployment-shape gap into a policy failure. + echo "::warning::check-package-policy: capability resolver missing at $RSR_PROFILE_CHECKER (caller pin predates it); ${found#"$ROOT"/} not read, treating it as declaring no packaging capability." >&2 + return 0 fi out="$(bash "$RSR_PROFILE_CHECKER" "$ROOT" 2>&1 || true)" if ! printf '%s\n' "$out" | grep -q '^effective capabilities:'; then diff --git a/scripts/tests/governance-gates-505-test.sh b/scripts/tests/governance-gates-505-test.sh index 4763a6ed2..2dc42aebe 100755 --- a/scripts/tests/governance-gates-505-test.sh +++ b/scripts/tests/governance-gates-505-test.sh @@ -205,8 +205,8 @@ assert "unresolvable profile is NAMED in a warning, read as undeclared" 0 "could assert "unresolvable profile + real guix.scm passes before the profile is read" 0 "Guix package management detected" \ env PKG_TODAY="$AFTER" "$PKG" "$r" rm "$r/guix.scm" -# A missing resolver is a deployment defect and must refuse. -assert "missing capability resolver refuses" 1 "capability resolver missing" \ +# Old-shape callers ship this script without its resolver: warn, never redden. +assert "missing capability resolver warns, reads as undeclared" 0 "capability resolver missing" \ env PKG_TODAY="$AFTER" RSR_PROFILE_CHECKER=/nonexistent "$PKG" "$r" assert "malformed cutoff refuses to run" 1 "is not YYYY-MM-DD" \ From da6dfa57feccbff88eb08e0a7243e49a247c80b9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:19:47 +0100 Subject: [PATCH 3/5] Update scripts/check-package-policy.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- scripts/check-package-policy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-package-policy.sh b/scripts/check-package-policy.sh index 952ac1b97..694df97f4 100755 --- a/scripts/check-package-policy.sh +++ b/scripts/check-package-policy.sh @@ -197,7 +197,7 @@ fi CONTAINER="" CONTAINER_STUB="" while IFS= read -r f; do [ -n "$f" ] || continue - if grep -qE '^[[:space:]]*(RUN|ENTRYPOINT|CMD)[[:space:]]' "$f"; then + if grep -qiE '^[[:space:]]*(RUN|ENTRYPOINT|CMD)[[:space:]]' "$f"; then CONTAINER="$f"; break fi CONTAINER_STUB="${CONTAINER_STUB:-$f}" From a1b7b6d24672f6c9888d517549353b3b2a232a16 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:24:28 +0100 Subject: [PATCH 4/5] Update scripts/tests/governance-gates-505-test.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- scripts/tests/governance-gates-505-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests/governance-gates-505-test.sh b/scripts/tests/governance-gates-505-test.sh index 2dc42aebe..3924914ef 100755 --- a/scripts/tests/governance-gates-505-test.sh +++ b/scripts/tests/governance-gates-505-test.sh @@ -183,7 +183,7 @@ assert "declared container + TODO-only Containerfile BLOCKS" 1 "Package policy v r=$(mkrepo pkg-container-multi README.adoc) mkdir -p "$r/.clusterfuzzlite" "$r/build/container" printf 'FROM gcr.io/oss-fuzz-base/base-builder\nRUN echo fuzz\n' > "$r/.clusterfuzzlite/Containerfile" -printf 'FROM x\n# TODO\n' > "$r/a.Containerfile" +printf 'FROM x\n# TODO\n' > "$r/Containerfile.template" printf 'FROM x\nRUN true\n' > "$r/build/container/Containerfile" declare "$r" container assert "active Containerfile found past a stub; .clusterfuzzlite ignored" 0 "build/container/Containerfile" \ From 373e8204b483c8cb228b5592e7b7f3da7de11045 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:26:44 +0000 Subject: [PATCH 5/5] fix(packaging): enforce Nix retirement before Guix stub exemption --- scripts/check-package-policy.sh | 32 +++++++++++----------- scripts/tests/governance-gates-505-test.sh | 10 +++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/scripts/check-package-policy.sh b/scripts/check-package-policy.sh index 694df97f4..9f6f8dc68 100755 --- a/scripts/check-package-policy.sh +++ b/scripts/check-package-policy.sh @@ -225,22 +225,6 @@ if printf '%s ' "$CAPS" | grep -xE 'reproducible-build|container' | paste -sd ' ' -)" fi -# Only a stub guix.scm. Before capability gating this passed on presence, and -# ~90 repos rely on that; 8.1.4 is gated on reproducible-build, so the stub -# only fails where that capability (or container) is declared. -if [ -n "$GUIX_STUB" ]; then - if [ -z "$REQUIRED" ]; then - echo "::notice::${GUIX_STUB#"$ROOT"/} is a scaffold stub (criterion 8.1.4)." \ - "Not enforced: this repo declares neither reproducible-build nor container." - echo "✅ Packaging not applicable (no packaging capability declared)." - exit 0 - fi - echo "::error::${GUIX_STUB#"$ROOT"/} is a scaffold stub (placeholder or (source #f))," \ - "and this repo declares: $REQUIRED. A stub builds nothing (criterion 8.1.4)." - echo "Make the guix.scm real, or add a Containerfile with active RUN/CMD steps." - exit 1 -fi - # Nix-only. Under the 2026-05-18 ruling this is NOT compliance — Nix is not a # tier — and the 2026-07-28 ruling removes it from the estate outright. That is # a ban, not a capability, so it applies whatever the profile declares. @@ -273,6 +257,22 @@ if [ -n "$NIX" ]; then exit 1 fi +# Only a stub guix.scm. Before capability gating this passed on presence, and +# ~90 repos rely on that; 8.1.4 is gated on reproducible-build, so the stub +# only fails where that capability (or container) is declared. +if [ -n "$GUIX_STUB" ]; then + if [ -z "$REQUIRED" ]; then + echo "::notice::${GUIX_STUB#"$ROOT"/} is a scaffold stub (criterion 8.1.4)." \ + "Not enforced: this repo declares neither reproducible-build nor container." + echo "✅ Packaging not applicable (no packaging capability declared)." + exit 0 + fi + echo "::error::${GUIX_STUB#"$ROOT"/} is a scaffold stub (placeholder or (source #f))," \ + "and this repo declares: $REQUIRED. A stub builds nothing (criterion 8.1.4)." + echo "Make the guix.scm real, or add a Containerfile with active RUN/CMD steps." + exit 1 +fi + if [ -z "$REQUIRED" ]; then echo "::notice::No packaging, and none required: the repo's rsr-profile declares" \ "neither reproducible-build nor container." \ diff --git a/scripts/tests/governance-gates-505-test.sh b/scripts/tests/governance-gates-505-test.sh index 3924914ef..a645b396d 100755 --- a/scripts/tests/governance-gates-505-test.sh +++ b/scripts/tests/governance-gates-505-test.sh @@ -234,6 +234,16 @@ assert "Nix-only packaging BLOCKS after retirement" 1 "Nix-only packaging is not assert "Nix-only packaging warns before retirement" 0 "NOT YET ENFORCED" \ env PKG_TODAY="2026-05-31" "$PKG" "$r" +# A Guix scaffold must not hide Nix-only packaging when no profile is present. +r=$(mkrepo pkg-nix-stub-undeclared flake.nix guix.scm) +stub_guix "$r/guix.scm" +assert "Nix + Guix stub, no profile: warns before retirement" 0 "NOT YET ENFORCED" \ + env PKG_TODAY="2026-05-31" "$PKG" "$r" +assert "Nix + Guix stub, no profile: BLOCKS on retirement cutoff" 1 "Nix-only packaging is not compliant" \ + env PKG_TODAY="2026-06-01" "$PKG" "$r" +assert "Nix + Guix stub, no profile: BLOCKS after retirement" 1 "Nix-only packaging is not compliant" \ + env PKG_TODAY="$AFTER" "$PKG" "$r" + # Same repo, both sides of the cutoff — the self-flipping proof. r=$(mkrepo pkg-none README.adoc) declare "$r" reproducible-build