diff --git a/.github/workflows/deed-conformance-reusable.yml b/.github/workflows/deed-conformance-reusable.yml new file mode 100644 index 0000000..7f3cfe2 --- /dev/null +++ b/.github/workflows/deed-conformance-reusable.yml @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Fork-local copy of metadatastician/standards/.github/workflows/ +# deed-conformance.yml at 75bd5994470661a099df562d6d5ad2fe744dbd66. +# +# Why a copy. The repository's Actions policy allows a reusable workflow only +# from a repository owned by `hyperpolymath`; a call to another owner fails at +# plan time with zero jobs, so the lane is silently absent from every check +# summary. `standards` was transferred to `metadatastician` on 2026-10-25 and the +# call stopped working. Deleting the lane would be worse than copying it: DEED +# conformance is one row of the verdict grid and one of the three independent +# governance lanes. +# +# This is the CALLER path of the upstream workflow, which is the only path this +# repository ever ran: check out the caller, check out the linter from standards +# at the pinned commit, run it over every committed *.deed file. The upstream +# file's self-test, fixture-corpus and uuid-v8 steps are gated on +# `github.repository == 'hyperpolymath/standards'` and were never executed here, +# so they are not copied. +# +# The pins are the point: the linter still comes from standards at the same +# commit, verified by `scripts/check-governance-contract.sh`, which fails if this +# header's source commit changes or if a caller reaches for the remote reusable +# again. +name: DEED conformance (fork-local) + +on: + workflow_call: + inputs: + require-files: + description: "Fail when the caller has no committed .deed file (a zero-file run is otherwise a pass)." + type: boolean + required: false + default: false + +permissions: {} + +jobs: + lint: + name: DEED syntax / lint + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Check out the caller + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # A checkout of another owner's public repository IS allowed by the + # policy - only reusable workflows are restricted - so the linter itself + # is still the pinned upstream one, not a copy of it. + - name: Check out the DEED linter at the pinned commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 75bd5994470661a099df562d6d5ad2fe744dbd66 + path: .deed-canon + persist-credentials: false + sparse-checkout: | + 1-formats/deed/tools + sparse-checkout-cone-mode: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Lint every committed deed + env: + REQUIRE_FILES: ${{ inputs.require-files || false }} + LINTER: .deed-canon/1-formats/deed/tools/deed_lint.js + run: | + set -euo pipefail + test -s "$LINTER" || { echo "::error::the pinned DEED linter is missing at $LINTER"; exit 1; } + find . -name '*.deed' -not -path './.deed-canon/*' -print | sort > "$RUNNER_TEMP/deeds" + if [ ! -s "$RUNNER_TEMP/deeds" ]; then + if [ "$REQUIRE_FILES" = "true" ]; then + echo "::error::require-files is set and no committed .deed file was found" + exit 1 + fi + echo "no committed .deed files - nothing to lint" + exit 0 + fi + echo "linting $(wc -l < "$RUNNER_TEMP/deeds") file(s)" + # A lint failure must be readable from the checks API, not only from a + # log a reviewer cannot open from every client. + if ! xargs bun "$LINTER" < "$RUNNER_TEMP/deeds"; then + echo "::error::DEED lint failed; see the step log for the file and the reason" + exit 1 + fi diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index e874741..83f2165 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -52,7 +52,13 @@ jobs: deed: name: DEED syntax - uses: metadatastician/standards/.github/workflows/deed-conformance.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + # Fork-local copy of the standards lane at 75bd5994. A reusable workflow + # from another owner is refused by this repository's Actions policy at plan + # time (zero jobs, no error line in any check summary), so the copy is what + # keeps the lane visible after standards moved to metadatastician on + # 2026-10-25. The linter itself is still fetched from standards at that + # commit by the called workflow. + uses: ./.github/workflows/deed-conformance-reusable.yml permissions: contents: read with: diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml new file mode 100644 index 0000000..cd622e0 --- /dev/null +++ b/.github/workflows/scorecard-reusable.yml @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Fork-local copy of metadatastician/standards/.github/workflows/ +# scorecard-reusable.yml at c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d. +# +# Why a copy: the repository's Actions policy allows a reusable workflow only +# from a repository owned by `hyperpolymath`, so the call to `standards` (moved +# to `metadatastician` on 2026-10-25) fails at plan time with zero jobs and the +# lane disappears without a line in any check summary. +# +# The `scorecard` job below is copied verbatim from that commit, including the +# native action-pin reconciliation, because its comments record two incidents +# that a rewrite would reintroduce: a 2-month freeze of the Security tab when +# the SARIF upload was deleted as collateral of an unrelated refactor, and a +# silent re-freeze when a reconciler failure could skip the upload. Read them +# before editing it. +# +# The upstream file's `pull-request` job is NOT copied: `scorecard.yml` has no +# pull-request trigger and `scripts/check-governance-contract.sh` fails if one is +# added, so that job is unreachable here. Adding the trigger means porting it. +name: OSSF Scorecard (fork-local) + +on: + workflow_call: + +# Callers must grant actions: read, contents: read, security-events: write and +# id-token: write ON THE CALLING JOB. A job-level `permissions:` block REPLACES +# the workflow-level one and is never merged with it, so the declaration below +# cannot elevate a caller that omitted them. +permissions: + contents: read + +jobs: + scorecard: + if: github.event_name != 'pull_request' + name: Run Scorecard + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + actions: read + contents: read + security-events: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run Scorecard Analysis + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 + with: + results_file: results.sarif + results_format: sarif + # publish_results feeds the public OpenSSF API and the badge, and is + # INDEPENDENT of results_format -- so emitting SARIF does not cost us + # the badge. Both outputs are live with this shape. + publish_results: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + + # RESTORED 2026-08-25. Commit 5a93d9d5 (PR #393, "Ci/gitleaks self hosted + # fix", 2026-06-21) deleted this step and switched results_format to json + # as collateral of an unrelated gitleaks refactor. Without a SARIF upload + # nothing feeds GitHub code scanning, so every Scorecard alert in the + # estate froze: measured in this repo on 2026-08-25, all 12 Scorecard + # alerts were stuck at 2026-06-03 while Hypatia's updated 2026-08-24. + # The badge kept working, which is why it went unnoticed. + - name: Checkout native-lock reconciliation helper + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: metadatastician/standards + ref: 5a0891e9e58b785181a92e141505ca96ca987b42 # per-file reconciliation (#1168) + path: .standards-scorecard + persist-credentials: false + sparse-checkout: scripts/reconcile-scorecard-actions-lock.rb + sparse-checkout-cone-mode: false + - name: Reconcile native action pins + id: reconcile + # Fail open on the ARTEFACT, never on the outcome. Previously this step + # ran under `set -euo pipefail` with no `continue-on-error`, and the + # upload below consumed results.reconciled.sarif. Any reconciler failure + # therefore skipped the upload entirely, and code scanning silently kept + # serving the PREVIOUS scan's alerts behind a green badge. That is the + # same shape as the ~2-month freeze recorded above (PR #393). + # The upload is now unconditional; the run still fails, at the end of + # the job, once the results are safely published. + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh extension install github/gh-actions-lock --pin v0.2.0 + # Exit 3: the reconciled SARIF was written, but some workflows failed + # native lock verification and keep their findings. Exit 2: usage or + # SARIF-structure error, nothing written. The code is passed on so + # the final step can say which one happened. + rc=0 + ruby .standards-scorecard/scripts/reconcile-scorecard-actions-lock.rb \ + results.sarif results.reconciled.sarif actions-lock-audit.json || rc=$? + printf 'rc=%s\n' "$rc" >> "$GITHUB_OUTPUT" + exit "$rc" + + - name: Select SARIF to upload + id: select-sarif + run: | + set -euo pipefail + if [ -s results.reconciled.sarif ]; then + printf 'file=%s\n' results.reconciled.sarif >> "$GITHUB_OUTPUT" + printf 'reconciled=true\n' >> "$GITHUB_OUTPUT" + else + echo "::warning title=Scorecard reconciliation produced no SARIF::Uploading raw results.sarif. Native action pins will appear UNRECONCILED in this upload." + printf 'file=%s\n' results.sarif >> "$GITHUB_OUTPUT" + printf 'reconciled=false\n' >> "$GITHUB_OUTPUT" + fi + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) + with: + sarif_file: ${{ steps.select-sarif.outputs.file }} + + - name: Upload results artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: scorecard-results + path: | + results.sarif + results.reconciled.sarif + actions-lock-audit.json + retention-days: 90 + + # The upload above can no longer be skipped, so a reconciler outage cannot + # freeze this repo's Security tab. A failed reconciliation is still a + # failure, and is surfaced here rather than swallowed. + - name: Fail if reconciliation did not succeed + if: "!cancelled() && (steps.reconcile.outcome != 'success' || steps.select-sarif.outputs.reconciled != 'true')" + run: | + if [ "${{ steps.reconcile.outputs.rc }}" = "3" ] && [ "${{ steps.select-sarif.outputs.reconciled }}" = "true" ]; then + echo "::error title=Scorecard reconciliation incomplete::Reconciled SARIF was uploaded. Workflows that failed native action-lock verification keep their findings; the reconcile step log names each one. This run fails by design." + else + echo "::error title=Scorecard reconciliation failed::Reconcile step outcome was '${{ steps.reconcile.outcome }}' (exit '${{ steps.reconcile.outputs.rc }}'). Raw SARIF was uploaded so results are not lost, but this run fails by design." + fi + exit 1 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4200498..0a11e5c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -20,7 +20,12 @@ jobs: scorecard: # The default-branch SARIF upload needs security-events: write; never # expose that grant or the publication OIDC token to PR code. - uses: metadatastician/standards/.github/workflows/scorecard-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + # + # Fork-local copy of the standards reusable at c6d78606: a reusable workflow + # owned by another account is refused by this repository's Actions policy at + # plan time, which left this lane dark after standards moved to + # metadatastician on 2026-10-25. + uses: ./.github/workflows/scorecard-reusable.yml permissions: actions: read contents: read diff --git a/docs/maintenance/repository-hygiene.md b/docs/maintenance/repository-hygiene.md index bfae4b3..6fb531a 100644 --- a/docs/maintenance/repository-hygiene.md +++ b/docs/maintenance/repository-hygiene.md @@ -70,6 +70,8 @@ are unaffected: none of them was in a lane. | --- | --- | | `.github/workflows/pr-freshness.yml` | Runs `scripts/maintenance/check-pr-freshness.sh` on every pull request open, push, reopen and ready-for-review. Fails with a check annotation naming how far behind the branch is, or which merge commit breaks rule 2. | | `.github/workflows/branch-hygiene.yml` | Weekly, and on demand: runs the triage and updates **one** rolling issue, closing it when no drifted branch remains. One issue updated in place, because a job that opens an issue every week is the mechanism that produced the pile it is meant to clear. | +| `.github/workflows/deed-conformance-reusable.yml`, `.github/workflows/scorecard-reusable.yml` | Fork-local copies of the two standards reusables, so the lanes keep running whatever the Actions policy says about other owners. Guarded by the contract: source commit, no cross-owner call, and the Scorecard SARIF guards. | +| `scripts/check-governance-contract.sh` (owner rule) | Fails if any workflow calls a reusable workflow that is neither fork-local nor owned by `hyperpolymath` — the failure mode that produces **no jobs and no error line** in any summary. | | `.github/dependabot.yml` | Both ecosystems set `rebase-strategy: auto`. Dependabot does not rebase by default, so with two open updates the second becomes stale the moment the first merges and **Branch freshness** would fail a one-line dependency bump on a rule the bump did not break. Rebasing keeps that check meaning what it says. | | `scripts/tests/test-triage-branches.sh`, `scripts/tests/test-check-pr-freshness.sh` | Both scripts are tested against fixture repositories: merged, drifted-with-nothing-new, drifted-with-a-dropped-file, orphan, unrelated-history and shallow cases. They run with the other shell tests in the `Advisory scan contract` job. | @@ -265,25 +267,55 @@ The repository's Actions policy permits reusable workflows only from repositories owned by `hyperpolymath` — so `hyperpolymath/standards` was allowed and `metadatastician/standards` is not, whatever name the workflow file uses. The `uses:` references therefore cannot be repaired by editing this repository -alone. Three ways out, in increasing order of effort: - -1. **Permit the new owner.** Repository → Settings → Actions → General → - *Access* for reusable workflows, and allow `metadatastician` (or "any - repository"). This is the smallest change and keeps the pins intact. -2. **Move `standards` back** under `hyperpolymath`. Then the existing workflow - files work unchanged, and no pull request is needed. -3. **Run DEED conformance from a checkout** instead of a remote `uses:`, the way - the K9, Hypatia and secret-scanner lanes already do. Correct, but it puts a - copy of someone else's workflow steps in this repository, and that copy then - drifts. - -What was done: every reference was repointed at `metadatastician/standards` +alone, and the account this session runs as is an App installation with +repository `admin` but **no `administration` scope**, so it cannot change the +setting either (`GET /repos/{owner}/{repo}/actions/permissions` → 403). Three +ways out were open: + +1. **Permit the new owner** in Settings → Actions → General. Cleanest, keeps the + pins and the call sites intact — and still worth doing, because it is what + makes a *future* transfer a non-event rather than an outage. +2. **Move `standards` back** under `hyperpolymath`. Then no pull request is + needed at all. +3. **Fork-local copies**, which is what the K9, Hypatia and secret-scanner lanes + already are — three of the five reusables this repository calls were copies + before the transfer, for exactly this class of reason. + +**What was done: (3), because it is the only one that restores the lanes +today without an owner action.** `deed-conformance-reusable.yml` and +`scorecard-reusable.yml` are now fork-local: + +| Lane | Was | Now | Still pinned to | +| --- | --- | --- | --- | +| DEED syntax / lint | `metadatastician/standards/…/deed-conformance.yml@75bd5994` | `./.github/workflows/deed-conformance-reusable.yml` | the linter, fetched from `standards` at `75bd5994` (a *checkout*, which cross-owner is allowed) | +| OpenSSF Scorecard | `metadatastician/standards/…/scorecard-reusable.yml@c6d78606` | `./.github/workflows/scorecard-reusable.yml` | the reconciler helper checkout at `5a0891e9`, `ossf/scorecard-action@fa7117db` | + +Neither copy re-implements anything. The DEED copy is the upstream *caller* +path — the self-test, fixture-corpus and uuid-v8 steps were gated on +`github.repository == 'hyperpolymath/standards'` and never ran here. The +Scorecard copy is the `scorecard` job verbatim, comments included, because those +comments record two freezes of this repository's Security tab; its +`pull-request` job is not copied because `scorecard.yml` has no PR trigger and +the contract fails if one is added. + +Both copies are now guarded by `scripts/check-governance-contract.sh`, which +fails if the recorded source commit changes, if a caller reaches for the remote +reusable again, or if the Scorecard copy loses its SARIF upload, its +reconciliation `continue-on-error`, or its final fail-the-run step. A new +general rule covers the next transfer of any repository: + +> every reusable workflow must be fork-local or owned by the repository owner + +with mutations (`deed-remote`, `scorecard-remote`, `cross-owner-reusable`, +`deed-source-sha`, `scorecard-source-sha`, `scorecard-no-upload`, +`scorecard-quiet-reconcile`) proving each check can fail. + +Before the copies, every reference was repointed at `metadatastician/standards` with the **same pinned SHAs** (verified to resolve in the new location, with -matching commit subjects), `scripts/check-governance-contract.sh` now fails if -any workflow names the pre-transfer owner or if the Scorecard reusable stops -being the pinned file, and the mutation test carries a case for each. The pins -themselves did not move, so no new code is trusted; only the name used to reach -the same commits. +matching commit subjects), and the contract was taught to fail if any workflow +names the pre-transfer owner in a `uses:` or `repository:` field. The pins never +moved, so no new code is trusted; only the name used to reach the same commits +changed, and then only the call sites moved to local copies. What a future reader should not do: "fix" a failed governance run by dropping the `uses:` and inlining a scanner, or by pointing the pin at `@main`. Both turn diff --git a/scripts/check-governance-contract.sh b/scripts/check-governance-contract.sh index 507b5ed..f006452 100755 --- a/scripts/check-governance-contract.sh +++ b/scripts/check-governance-contract.sh @@ -274,11 +274,15 @@ else fail 'the standards renderer checkout exists' fi -stale_owner="$(grep -rn 'hyperpolymath/standards' .github/workflows/ 2>/dev/null || true)" +# Only real reference lines count. A comment may (and the fork-local DEED lane +# does) explain the transfer by naming the old owner; a `uses:` or a checkout +# `repository:` naming it is a broken reference. +stale_owner="$(grep -rnE '^[[:space:]]*(uses:[[:space:]]+[^#]*|repository:)[[:space:]]*hyperpolymath/standards' \ + .github/workflows/ 2>/dev/null || true)" if [ -z "$stale_owner" ]; then - pass 'no workflow still names the pre-transfer standards owner' + pass 'no workflow still references the pre-transfer standards owner' else - fail "workflows still reference hyperpolymath/standards, which no longer resolves for a reusable uses: - $stale_owner" + fail "workflows still reference hyperpolymath/standards in a uses:/repository: field - $stale_owner" fi upload_count="$(grep -c 'uses: github/codeql-action/upload-sarif@' "$REUSABLE" || true)" @@ -325,14 +329,59 @@ not_contains "$secret_workflow" 'secrets:' 'the secret scanner receives no calle not_contains "$secret_workflow" 'continue-on-error:' 'secret scanner failures cannot be advisory' not_contains "$secret_workflow" 'pull_request_target:' 'secret scanning does not use a privileged PR trigger' scorecard_workflow="$(cat .github/workflows/scorecard.yml)" -contains "$scorecard_workflow" 'uses: metadatastician/standards/.github/workflows/scorecard-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ - 'Scorecard uses the audited immutable reusable' +contains "$scorecard_workflow" 'uses: ./.github/workflows/scorecard-reusable.yml' \ + 'Scorecard calls the guarded fork-local reusable' if grep -Eq '^ pull_request(_target)?:' <<<"$scorecard_workflow"; then fail 'Scorecard publication must not run against PR code' else pass 'Scorecard OIDC publication is not enabled for PRs' fi +# The two lanes that used to call a reusable owned by `standards` directly. +# +# The repository's Actions policy allows a reusable workflow only from a +# repository owned by `hyperpolymath`. A call to another owner from a different +# owner fails AT PLAN TIME - zero jobs, no name, no line in any check summary - +# so it is silent. `standards` moved to `metadatastician` on 2026-10-25 and both +# lanes went dark that way. The rule below is the durable part: no workflow here +# may call a reusable workflow from a different owner, whatever the repository +# is called or whoever owns it next. +cross_owner="$(grep -rnE '^[[:space:]]*uses:[[:space:]]+[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/\.github/workflows/' \ + .github/workflows/ 2>/dev/null \ + | grep -vE 'uses:[[:space:]]+\./\.github/workflows/' \ + | grep -vE 'uses:[[:space:]]+hyperpolymath/' || true)" +if [ -z "$cross_owner" ]; then + pass 'every reusable workflow is fork-local or owned by the repository owner' +else + fail "a reusable workflow owned by another account fails at plan time with no jobs - $cross_owner" +fi + +deed_reusable="$(cat .github/workflows/deed-conformance-reusable.yml)" +contains "$deed_reusable" 'deed-conformance.yml at 75bd5994470661a099df562d6d5ad2fe744dbd66' \ + 'the fork-local DEED lane records its immutable standards source' +contains "$deed_reusable" 'ref: 75bd5994470661a099df562d6d5ad2fe744dbd66' \ + 'the DEED linter is still fetched from standards at that commit' +contains "$caller" 'uses: ./.github/workflows/deed-conformance-reusable.yml' \ + 'DEED validation uses the guarded fork-local reusable' +contains "$caller" 'require-files: true' \ + 'a zero-file DEED corpus cannot silently pass' +not_contains "$caller" 'uses: metadatastician/standards/.github/workflows/deed-conformance.yml@' \ + 'the governance lane cannot call the DEED reusable across owners' + +scorecard_reusable="$(cat .github/workflows/scorecard-reusable.yml)" +contains "$scorecard_reusable" 'scorecard-reusable.yml at c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ + 'the fork-local Scorecard lane records its immutable standards source' +# The two guards whose absence froze code scanning for two months upstream, and +# again silently when a reconciler failure could skip the upload. +contains "$scorecard_reusable" 'name: Select SARIF to upload' \ + 'the SARIF upload is fed by an explicit selection step' +contains "$scorecard_reusable" 'uses: github/codeql-action/upload-sarif@' \ + 'the fork-local Scorecard lane still uploads SARIF to code scanning' +contains "$scorecard_reusable" 'Fail if reconciliation did not succeed' \ + 'a failed reconciliation still fails the run instead of passing quietly' +contains "$scorecard_reusable" 'continue-on-error: true' \ + 'the reconciler cannot skip the upload' + if [ "$failures" -gt 0 ]; then printf 'governance contract: %d failure(s)\n' "$failures" >&2 exit 1 diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh index 8945b29..5ce5716 100644 --- a/scripts/tests/test-governance-contract.sh +++ b/scripts/tests/test-governance-contract.sh @@ -14,7 +14,7 @@ reset_fixture() { } reset_fixture bash "$fixture/scripts/check-governance-contract.sh" > "$fixture/log" 2>&1 -for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate baseline-grouping-gate sarif upstream-exception effective-grid jcs-manifest jcs-canonicalizer raw-ijson k9-pin k9-require k9-indeterminate k9-verdict scanner-advisory scanner-secrets scanner-trigger scorecard-trigger stale-standards-owner scorecard-owner; do +for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate baseline-grouping-gate sarif upstream-exception effective-grid jcs-manifest jcs-canonicalizer raw-ijson k9-pin k9-require k9-indeterminate k9-verdict scanner-advisory scanner-secrets scanner-trigger scorecard-trigger stale-standards-owner scorecard-owner deed-remote scorecard-remote deed-source-sha scorecard-source-sha scorecard-no-upload scorecard-quiet-reconcile cross-owner-reusable; do reset_fixture case "$mutation" in allowlist-addition) echo 'frontend/src/fork-only.ts' >> "$fixture/config/hypatia/upstream-typescript-allowlist.txt" ;; @@ -38,8 +38,23 @@ for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate bas # The standards repository was transferred on 2026-10-25 and a reusable # `uses:` no longer follows the old owner, so either of these mutations # leaves a workflow that fails at plan time with no jobs at all. - stale-standards-owner) sed -i 's@metadatastician/standards@hyperpolymath/standards@g' "$fixture/.github/workflows/scorecard.yml" ;; - scorecard-owner) sed -i 's@metadatastician/standards/.github/workflows/scorecard-reusable.yml@metadatastician/standards/.github/workflows/other.yml@' "$fixture/.github/workflows/scorecard.yml" ;; + # The reconciler checkout is the one cross-owner reference that must stay a + # checkout of the current owner; naming the pre-transfer owner there is a + # reference, not a comment. + stale-standards-owner) sed -i 's@repository: metadatastician/standards@repository: hyperpolymath/standards@' "$fixture/.github/workflows/scorecard-reusable.yml" ;; + scorecard-owner) sed -i 's@uses: ./.github/workflows/scorecard-reusable.yml@uses: ./.github/workflows/scorecard-other.yml@' "$fixture/.github/workflows/scorecard.yml" ;; + # The two lanes that went dark on 2026-10-25. A call across owners is + # refused by the Actions policy at plan time with zero jobs, so nothing + # reports it; the contract has to. + deed-remote) sed -i 's|uses: ./.github/workflows/deed-conformance-reusable.yml|uses: metadatastician/standards/.github/workflows/deed-conformance.yml@75bd5994470661a099df562d6d5ad2fe744dbd66|' "$fixture/.github/workflows/hypatia-scan.yml" ;; + scorecard-remote) sed -i 's|uses: ./.github/workflows/scorecard-reusable.yml|uses: metadatastician/standards/.github/workflows/scorecard-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d|' "$fixture/.github/workflows/scorecard.yml" ;; + cross-owner-reusable) sed -i 's|uses: ./.github/workflows/k9-conformance-reusable.yml|uses: someone/else/.github/workflows/k9.yml@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; + # A copy is only a copy while it still comes from the audited commit and + # keeps the guards whose absence froze code scanning upstream. + deed-source-sha) sed -i 's@75bd5994470661a099df562d6d5ad2fe744dbd66@main@' "$fixture/.github/workflows/deed-conformance-reusable.yml" ;; + scorecard-source-sha) sed -i 's@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d@main@' "$fixture/.github/workflows/scorecard-reusable.yml" ;; + scorecard-no-upload) sed -i '/uses: github\/codeql-action\/upload-sarif@/d' "$fixture/.github/workflows/scorecard-reusable.yml" ;; + scorecard-quiet-reconcile) sed -i '/^ - name: Fail if reconciliation did not succeed/,/^ [a-z]/d' "$fixture/.github/workflows/scorecard-reusable.yml" ;; *) echo "ERROR: unknown mutation $mutation" >&2; exit 1 ;; esac if bash "$fixture/scripts/check-governance-contract.sh" > "$fixture/log" 2>&1; then