From e1f564513ea5da64861d83a1a639755dd3622f39 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 20:53:46 +0000 Subject: [PATCH 1/2] ci(governance): call the standards DEED and K9 lanes directly Owner decision 2026-11-10: permit metadatastician/standards reusable workflows in this repository's Actions policy and stop keeping fork-local copies of them. - The `deed` and `k9` jobs of `Governance (fork)` now call metadatastician/standards at f85b6c11, one full commit SHA for both. - The two fork-local copies are deleted (deed at 75bd5994, k9 at c6d78606), with the test that covered one K9-copy delta. - The contract check now requires both calls at a full commit SHA and at the same commit. It permits exactly those two cross-owner reusables. The mutation tests cover an unpinned DEED call, an unpinned K9 call, split pins and a call to any other owner. Not changed: the Hypatia lane and the comment poster. The Hypatia copy has two fork behaviours that the standards lane does not offer yet (no SARIF upload on pull requests, the inherited-TypeScript exception). Scorecard and the secret scanner are also still fork-local. This run needs `metadatastician/standards/.github/workflows/*@*` in Settings, Actions, General. Without it the run fails at plan time with zero jobs. Local: scripts/check-governance-contract.sh, 89 checks pass; scripts/tests/test-governance-contract.sh, 37 mutations rejected; the other five shell tests of the workflow-contract job pass. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_0136P7ZoTPXfxg1C85rVNx78 --- .../workflows/deed-conformance-reusable.yml | 90 ----- .github/workflows/hypatia-scan.yml | 27 +- .github/workflows/k9-conformance-reusable.yml | 363 ------------------ docs/maintenance/repository-hygiene.md | 13 +- scripts/check-governance-contract.sh | 66 ++-- scripts/tests/test-governance-contract.sh | 16 +- scripts/tests/test-k9-log-copy.sh | 26 -- 7 files changed, 71 insertions(+), 530 deletions(-) delete mode 100644 .github/workflows/deed-conformance-reusable.yml delete mode 100644 .github/workflows/k9-conformance-reusable.yml delete mode 100644 scripts/tests/test-k9-log-copy.sh diff --git a/.github/workflows/deed-conformance-reusable.yml b/.github/workflows/deed-conformance-reusable.yml deleted file mode 100644 index 7f3cfe21..00000000 --- a/.github/workflows/deed-conformance-reusable.yml +++ /dev/null @@ -1,90 +0,0 @@ -# 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 d6027e57..22873b5b 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,10 +1,14 @@ # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) # -# Fork governance lane. DEED and K9 conformance come from metadatastician/standards; the -# Hypatia verdict grid uses a fork-local copy of the standards reusable so its -# advisory PR scan cannot become a blocking code-scanning-results check. Both -# sources are pinned to commits. The grid covers Hypatia, panic-attack, pons +# Fork governance lane. The DEED and K9 conformance lanes call +# metadatastician/standards directly, at one full commit SHA (owner decision, +# 2026-11-10). The Actions policy of this repository must permit +# `metadatastician/standards/.github/workflows/*@*` for those calls to start. +# The Hypatia verdict grid still uses a fork-local copy of the standards +# reusable so its advisory PR scan cannot become a blocking +# code-scanning-results check. Every source is pinned to a commit. +# The grid covers Hypatia, panic-attack, pons # asinorum and the gitbot-fleet report-backs under .gitbot-fleet/. This workflow # is the fork's; nothing in it is proposed upstream, and upstream's ci.yml is # untouched. @@ -43,7 +47,6 @@ jobs: bash scripts/tests/test-governance-contract.sh bash scripts/tests/test-hypatia-inherited-language-exception.sh bash scripts/tests/test-ci-logging.sh - bash scripts/tests/test-k9-log-copy.sh bash scripts/tests/test-scanner-boundaries.sh # Branch triage and the pull-request freshness rule: the scripts the # hygiene workflows run, tested where every other shell test runs. @@ -52,13 +55,11 @@ jobs: deed: name: DEED syntax - # 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 + # The standards lane, called directly. If the Actions policy stops + # permitting metadatastician/standards, this run fails at plan time with + # zero jobs and no error line in any check summary: read the workflow page. + # The K9 lane below must keep the same pin; the contract check enforces it. + uses: metadatastician/standards/.github/workflows/deed-conformance.yml@f85b6c11e5b02c51b1f0a94d40f39d9d72246396 permissions: contents: read with: @@ -66,7 +67,7 @@ jobs: k9: name: K9 conformance - uses: ./.github/workflows/k9-conformance-reusable.yml + uses: metadatastician/standards/.github/workflows/k9-conformance.yml@f85b6c11e5b02c51b1f0a94d40f39d9d72246396 permissions: contents: read pull-requests: read diff --git a/.github/workflows/k9-conformance-reusable.yml b/.github/workflows/k9-conformance-reusable.yml deleted file mode 100644 index d211040e..00000000 --- a/.github/workflows/k9-conformance-reusable.yml +++ /dev/null @@ -1,363 +0,0 @@ -# Fork-local copy of standards k9-conformance.yml at -# c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d. Deltas: workflow_call only; -# standards checkout/evidence use that immutable source rather than this -# caller's workflow SHA; optional corpus-log copying does not fail an empty -# corpus. Validator, renderer, semantic gate and ratchet are unchanged. -# SPDX-License-Identifier: MPL-2.0 -# De-onboarded from actions.lock: this repository keeps no actions.lock, and -# every action and reusable reference here is a literal full commit SHA. -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -name: k9-conformance - -# Conformance lane for K9 contracts (standards#1058, ruling D173). The -# canonical validator is 1-formats/k9/tools/k9-validate.sh; the grid it -# reports through is scripts/k9-grid.sh (docs/VERDICT-GRID-SPEC.adoc, -# conformance family). docs/CONTRACTILE-DESCRIPTILE-VALIDATION.adoc says -# where this lane sits beside the DEED lane and the Hypatia lane. -# -# In this repo (push / pull_request) it checks the validator's self-test, -# the contract's own typecheck, and every committed K9 file. -# -# As a reusable (workflow_call) it validates the CALLER's committed K9 files -# with the validator, contract and renderer at THIS workflow's own commit -# (job.workflow_sha), so a caller pinned to a standards SHA gets exactly -# that validator. Layers L0–L2 run; L3 (signature) is outside this lane and -# the grid says so. The caller's shrink-only debt ledger -# (.machine_readable/k9-contract-debt.txt) is honoured as -# 1-formats/k9/tools/CONFORMANCE-REPORTING.adoc states it, and compared with -# the PR base: an entry the PR adds exempts nothing unless a commit message or -# the PR body carries `Ratchet-exception: .machine_readable/k9-contract-debt.txt -# — ` (the governance exemption ratchet's form; it watches this ledger -# too). The checkout is full so the changed-file list is the merge-base diff -# and the commit messages are readable; on a push the range is the push's own -# (`before`..`sha`), so a direct push to a ledgered contract, or a direct -# ledger addition, is ratcheted exactly as a pull request is. -# -# Outputs: the `k9-grid` artifact (report.json and badge.json as I-JSON/JCS, -# report.md, the validator logs) and the `verdict` output. The caller posts -# report.md to its pull request and publishes badge.json itself: a reusable -# cannot ask for more permissions than it is given, and this one asks for -# `contents: read` plus `pull-requests: read` (to read the grid comment it -# posted last time, for the "since previous revision" column). -# -# Caller: -# jobs: -# k9: -# uses: metadatastician/standards/.github/workflows/k9-conformance.yml@ -# permissions: -# contents: read -# pull-requests: read -# with: -# require-files: true # fail when the caller has no K9 file at all -# fail-on-indeterminate: true # orange fails the job too (default: only red fails) -# grid-author: github-actions[bot] # the account whose grid comment is the delta baseline - -on: - workflow_call: - inputs: - require-files: - description: "Fail when the caller has no committed K9 file (a zero-file run is otherwise grey, not red)." - type: boolean - required: false - default: false - fail-on-indeterminate: - description: "Fail the job on an orange grid as well as a red one." - type: boolean - required: false - default: false - grid-author: - description: "The account that posts the grid comment in the caller; only its comment is read as the previous revision's baseline, so a contributor's comment cannot pose as one." - type: string - required: false - default: github-actions[bot] - outputs: - verdict: - description: "Grid state: red, orange, yellow, green or grey." - value: ${{ jobs.validate.outputs.verdict }} - -permissions: {} - -jobs: - validate: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - pull-requests: read - outputs: - verdict: ${{ steps.grid.outputs.state }} - env: - # In this repo the PR's own validator is in the first checkout; a caller - # gets validator, contract and renderer from the canon checkout at - # job.workflow_sha. - CANON: ${{ github.repository == 'metadatastician/standards' && '.' || '.k9-canon' }} - NICKEL_VERSION: '1.18.0' - NICKEL_SHA256: '9cba4dd65ae9915ec61f73033aafcff307a377665a83fd8f530df086763318cb' - IJSON_JCS_REV: '0fdeea716fd558e9807362acf6f8202d56d8d88f' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - # Full history: the changed-file list is the merge-base diff and the - # ledger ratchet reads the commit messages, on a pull request (base - # ..head) and on a push (the push's before..sha) alike. - fetch-depth: 0 - persist-credentials: false - - - name: checkout the K9 validator, contract and renderer at this workflow's commit - if: github.repository != 'metadatastician/standards' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: metadatastician/standards - ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d - path: .k9-canon - persist-credentials: false - sparse-checkout: "1-formats/k9\nscripts/k9-grid.sh\nscripts/lib/k9-grid.jq\n" - sparse-checkout-cone-mode: false - - - name: Record which standards commit supplies validator, contract and renderer - env: - # The job context is not available to job-level env, so the canon - # ref is recorded here. A caller gets this workflow's own commit. - WORKFLOW_SHA: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - if [ "$GITHUB_REPOSITORY" = "metadatastician/standards" ]; then - echo "CANON_REF=$HEAD_SHA" >> "$GITHUB_ENV" - else - echo "CANON_REF=$WORKFLOW_SHA" >> "$GITHUB_ENV" - fi - - - name: Install Nickel (pinned, sha256-verified; one version estate-wide) - run: | - set -euo pipefail - # Canonical owner, not the pre-rename `tweag/nickel`: that path now - # answers 301 and depends on GitHub keeping the redirect, which is the - # courtesy this repository has already been burned by once (a transfer - # that `git` and the REST API follow and that other references do not). - # --retry-all-errors, because plain --retry ignores an HTTP status - # (curl exit 22), which is how this step died while the asset itself was - # present. The pinned SHA256 below is what makes the retry safe. - URL="https://github.com/nickel-lang/nickel/releases/download/${NICKEL_VERSION}/nickel-x86_64-linux" - curl -sSfL --proto '=https' --proto-redir '=https' --tlsv1.2 \ - --retry 4 --retry-delay 5 --retry-all-errors \ - --connect-timeout 20 --max-time 300 \ - -o "$RUNNER_TEMP/nickel" "$URL" - echo "${NICKEL_SHA256} ${RUNNER_TEMP}/nickel" | sha256sum -c - - chmod +x "$RUNNER_TEMP/nickel" - echo "$RUNNER_TEMP" >> "$GITHUB_PATH" - - - name: Cache the ijson-jcs canonicaliser - id: jcs-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cargo/bin/ijson-jcs - key: ijson-jcs-${{ env.IJSON_JCS_REV }}-${{ runner.os }} - - - name: Install ijson-jcs (3-practice/JSON-POLICY.adoc, D306) - if: steps.jcs-cache.outputs.cache-hit != 'true' - run: | - set -euo pipefail - # Best effort: the renderer falls back to jq's sorted compact form, - # byte-identical for its ASCII integer-only documents, and the grid - # records which serialiser ran. - cargo install --quiet --git https://github.com/hyperpolymath/ijson-jcs --rev "$IJSON_JCS_REV" --locked ijson-jcs \ - || echo "::warning::ijson-jcs did not install; report.json falls back to jq -S -c" - - - name: Renderer regression tests - if: github.repository == 'metadatastician/standards' - run: bash scripts/tests/k9-grid-test.sh - - - name: Validator self-test - run: | - set +e - bash "$CANON/1-formats/k9/tools/k9-validate.sh" --self-test > "$RUNNER_TEMP/selftest.log" 2>&1 - echo "SELFTEST_CODE=$?" >> "$GITHUB_ENV" - tail -n 5 "$RUNNER_TEMP/selftest.log" - - - name: Contract typecheck - run: | - set +e - nickel typecheck "$CANON/1-formats/k9/spec/contract/k9_contract.ncl" > "$RUNNER_TEMP/typecheck.log" 2>&1 - echo "TYPECHECK_CODE=$?" >> "$GITHUB_ENV" - cat "$RUNNER_TEMP/typecheck.log" - - - name: Enumerate the committed K9 files, the changed files and ledger growth - env: - # A pull request compares with its base; a push with the commit the - # push replaced (all zeros when the ref is new: nothing to compare). - BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - LEDGER: .machine_readable/k9-contract-debt.txt - run: | - set -euo pipefail - # Tracked files only: an untracked file is not a committed contract. - # The two extensions K9-CONTRACT-SPEC.adoc reserves, nothing else. - # A caller with no K9 file at all is a legitimate (grey) corpus, so - # grep's no-match exit is not an error here. - git ls-files -z -- '*.k9.ncl' '*.k9' \ - | tr '\0' '\n' \ - | { grep -v -E '^(\.k9-canon/|9-archive/|1-formats/k9/tools/fixtures/)' || [ $? -eq 1 ]; } \ - | sort > "$RUNNER_TEMP/expected" - echo "$(wc -l < "$RUNNER_TEMP/expected") committed K9 file(s)" - : > "$RUNNER_TEMP/changed" - : > "$RUNNER_TEMP/ledger-growth" - echo "BASE_UNRESOLVED=" >> "$GITHUB_ENV" - case "$BASE_SHA" in *[!0]*) ;; *) BASE_SHA="" ;; esac - [ -n "$BASE_SHA" ] || exit 0 - - # The PR's own changes are those since its merge base, not since the - # base branch tip: a file the base branch changed after the PR - # branched is not this PR's edit. A push compares with `before` - # itself, never a merge base: after a force push the fork point is - # not the state the push replaced, and a ledger entry the old tip - # had removed would come back unseen. The full checkout already - # holds the base commit; the fetch - # is a fallback only, and runs without credentials - # (persist-credentials is off), so it serves public repositories. - if { git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null || git fetch --quiet origin "$BASE_SHA" 2>/dev/null; } \ - && merge_base=$(if [ "$GITHUB_EVENT_NAME" = pull_request ]; then git merge-base "$BASE_SHA" "$HEAD_SHA"; else git rev-parse --verify "$BASE_SHA^{commit}"; fi 2>/dev/null) \ - && git diff --name-only "$merge_base" "$HEAD_SHA" > "$RUNNER_TEMP/changed" 2>/dev/null; then - echo "$(wc -l < "$RUNNER_TEMP/changed") file(s) changed since the merge base $merge_base" - else - echo "::warning::could not resolve the PR base/head range; debt exemptions are reported as indeterminate" - echo "BASE_UNRESOLVED=--base-unresolved" >> "$GITHUB_ENV" - exit 0 - fi - - # The ledger is shrink-only. Entries present at HEAD and absent at - # the merge base are growth; growth exempts nothing unless a commit - # message in the PR or the PR body declares it in the exemption - # ratchet's form, naming this ledger: - # Ratchet-exception: .machine_readable/k9-contract-debt.txt — - # Ratchet-exception(.machine_readable/k9-contract-debt.txt): - # Read ledger paths from stdin and emit sorted unique entries, - # excluding comments and blank lines; an empty ledger succeeds. - entries() { { grep -vE '^[[:space:]]*(#|$)' || [ $? -eq 1 ]; } | sort -u; } - head_entries=$RUNNER_TEMP/ledger-head - base_entries=$RUNNER_TEMP/ledger-base - if [ -f "$LEDGER" ]; then entries < "$LEDGER" > "$head_entries"; else : > "$head_entries"; fi - { git show "$merge_base:$LEDGER" 2>/dev/null || true; } | entries > "$base_entries" - comm -13 "$base_entries" "$head_entries" > "$RUNNER_TEMP/ledger-growth" - [ -s "$RUNNER_TEMP/ledger-growth" ] || exit 0 - declared=no - if git log --format=%B "$merge_base..$HEAD_SHA" | grep -iE '^Ratchet-exception' | grep -qF "$LEDGER"; then - declared=yes - elif [ -f "${GITHUB_EVENT_PATH:-}" ] \ - && jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH" | grep -iE '^Ratchet-exception' | grep -qF "$LEDGER"; then - declared=yes - fi - n=$(wc -l < "$RUNNER_TEMP/ledger-growth") - if [ "$declared" = yes ]; then - echo "::notice::$LEDGER grew by $n entr(y/ies) under a declared Ratchet-exception" - : > "$RUNNER_TEMP/ledger-growth" - else - echo "::error::$LEDGER grew by $n entr(y/ies) with no Ratchet-exception naming it; the new entries exempt nothing" - sed 's/^/ + /' "$RUNNER_TEMP/ledger-growth" - fi - - - name: Validate every committed K9 file (L0–L2, strict) - run: | - set +e - : > "$RUNNER_TEMP/results.jsonl" - worst=0 - while IFS= read -r f; do - [ -n "$f" ] || continue - # `--` ends the options: a tracked file whose name starts with `-` - # is a path, not a flag. - bash "$CANON/1-formats/k9/tools/k9-validate.sh" --strict --layer L2 --json --quiet -- "$f" >> "$RUNNER_TEMP/results.jsonl" 2>> "$RUNNER_TEMP/corpus.log" - rc=$? - [ "$rc" -gt "$worst" ] && worst=$rc - done < "$RUNNER_TEMP/expected" - echo "CORPUS_CODE=$worst" >> "$GITHUB_ENV" - echo "validator worst exit status: $worst" - - - name: Read the grid comment from the previous revision (delta column) - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ github.token }} - COMMENTS_URL: ${{ github.event.pull_request.comments_url }} - MARKER: '' - GRID_AUTHOR: ${{ inputs.grid-author || 'github-actions[bot]' }} - run: | - set -euo pipefail - # Best effort: the caller's posting account writes the grid under - # this marker and regenerates it in place, so its last comment - # carrying the marker is the previous revision's grid and baseline. - # Only that account's comments count: anyone who can comment could - # otherwise plant a marker and a forged baseline. Any failure here - # only costs the "since previous revision" column. - : > "$RUNNER_TEMP/previous.md" - page=1 - while [ "$page" -le 10 ]; do - if ! curl -sSf --retry 2 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ - "$COMMENTS_URL?per_page=100&page=$page" > "$RUNNER_TEMP/comments.json"; then - echo "::notice::could not read the pull request comments; the delta column reports no comparable baseline" - break - fi - jq -r --arg m "$MARKER" --arg a "$GRID_AUTHOR" \ - '[.[] | select((.user.login // "") == $a and ((.body // "") | contains($m)))] | last | .body // empty' \ - "$RUNNER_TEMP/comments.json" > "$RUNNER_TEMP/previous-page.md" - [ -s "$RUNNER_TEMP/previous-page.md" ] && mv "$RUNNER_TEMP/previous-page.md" "$RUNNER_TEMP/previous.md" - [ "$(jq length "$RUNNER_TEMP/comments.json")" -eq 100 ] || break - page=$((page + 1)) - done - [ -s "$RUNNER_TEMP/previous.md" ] && echo "previous grid found" || echo "no previous grid" - - - name: Render the verdict grid - id: grid - env: - REQUIRE_FILES: ${{ inputs.require-files || false }} - PR_NUMBER: ${{ github.event.pull_request.number || 'local' }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - set -euo pipefail - ledger=.machine_readable/k9-contract-debt.txt - args=(--out "$RUNNER_TEMP/k9-grid" --results "$RUNNER_TEMP/results.jsonl" --expected "$RUNNER_TEMP/expected" - --changed "$RUNNER_TEMP/changed" --selftest "$RUNNER_TEMP/selftest.log" --selftest-code "$SELFTEST_CODE" - --typecheck "$RUNNER_TEMP/typecheck.log" --typecheck-code "$TYPECHECK_CODE" --corpus-code "$CORPUS_CODE" - --sha "$HEAD_SHA" --run "$GITHUB_RUN_ID" --url "$RUN_URL" --pr "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" - --canon-ref "$CANON_REF") - [ -f "$ledger" ] && args+=(--ledger "$ledger") - [ -s "$RUNNER_TEMP/ledger-growth" ] && args+=(--ledger-growth "$RUNNER_TEMP/ledger-growth") - [ -s "$RUNNER_TEMP/previous.md" ] && args+=(--previous "$RUNNER_TEMP/previous.md") - [ -n "${BASE_UNRESOLVED:-}" ] && args+=("$BASE_UNRESOLVED") - # With require-files the empty corpus is a failing row in the grid - # itself, so report.md and badge.json carry the red the job reports. - [ "$REQUIRE_FILES" = "true" ] && args+=(--require-files) - bash "$CANON/scripts/k9-grid.sh" render "${args[@]}" - cp "$RUNNER_TEMP/selftest.log" "$RUNNER_TEMP/typecheck.log" "$RUNNER_TEMP/results.jsonl" "$RUNNER_TEMP/k9-grid/" - # The corpus log is optional for an empty corpus. A terminal - # `[ -f ] && cp` returns 1 when absent, failing a valid grey grid. - if [ -f "$RUNNER_TEMP/corpus.log" ]; then - cp "$RUNNER_TEMP/corpus.log" "$RUNNER_TEMP/k9-grid/" - fi - - - name: Archive the grid and its evidence - if: ${{ always() && steps.grid.outcome != 'skipped' }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: k9-grid - path: ${{ runner.temp }}/k9-grid - if-no-files-found: error - retention-days: 90 - - - name: Gate - env: - STATE: ${{ steps.grid.outputs.state }} - FAIL_ON_INDETERMINATE: ${{ inputs.fail-on-indeterminate || false }} - run: | - set -euo pipefail - echo "K9 verdict grid: $STATE" - case "$STATE" in - red) echo "::error::K9 conformance failed; see the k9-grid artifact and the step summary"; exit 1 ;; - orange) - if [ "$FAIL_ON_INDETERMINATE" = "true" ]; then - echo "::error::K9 conformance is indeterminate and fail-on-indeterminate is set"; exit 1 - fi - echo "::warning::K9 conformance is indeterminate: evidence is missing, not clean" ;; - yellow) echo "::notice::K9 conformance passes with ledgered debt or warnings" ;; - green|grey) ;; - *) echo "::error::no verdict was produced"; exit 1 ;; - esac diff --git a/docs/maintenance/repository-hygiene.md b/docs/maintenance/repository-hygiene.md index 6fb531a3..2a24af42 100644 --- a/docs/maintenance/repository-hygiene.md +++ b/docs/maintenance/repository-hygiene.md @@ -70,7 +70,7 @@ 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. | +| `.github/workflows/scorecard-reusable.yml` | Fork-local copy of the standards Scorecard reusable, so the lane keeps running whatever the Actions policy says about other owners. Guarded by the contract: source commit, no cross-owner call, and the Scorecard SARIF guards. The DEED copy that sat beside it was removed on 2026-11-10; see "Update 2026-11-10" below. | | `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. | @@ -290,6 +290,17 @@ today without an owner action.** `deed-conformance-reusable.yml` and | 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` | +**Update 2026-11-10.** The owner chose way out 1: permit the new owner. The DEED +and K9 lanes now call `metadatastician/standards` directly, at one full commit +SHA, and the two fork-local copies (`deed-conformance-reusable.yml`, +`k9-conformance-reusable.yml`) are deleted. This needs the entry +`metadatastician/standards/.github/workflows/*@*` in Settings → Actions → +General. Without it the `Governance (fork)` run fails at plan time with zero +jobs. The contract now permits exactly those two cross-owner calls, each at a +full commit SHA and both at the same commit. The Hypatia, Scorecard and +secret-scanner lanes are still fork-local. The text below describes the state +before that date. + 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 diff --git a/scripts/check-governance-contract.sh b/scripts/check-governance-contract.sh index 79a6c9fa..5438a0cc 100644 --- a/scripts/check-governance-contract.sh +++ b/scripts/check-governance-contract.sh @@ -364,15 +364,20 @@ fi # These are independent conformance families. An empty K9 corpus must be # shown explicitly; missing semantic evidence must not become a green DEED row. -contains "$caller" 'uses: ./.github/workflows/k9-conformance-reusable.yml' \ - 'K9 validation uses the guarded fork-local reusable' -k9_workflow="$(cat .github/workflows/k9-conformance-reusable.yml)" -contains "$k9_workflow" 'ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ - 'the K9 validator and renderer use the immutable standards source' -not_contains "$k9_workflow" 'ref: ${{ job.workflow_sha }}' \ - 'the K9 standards checkout cannot use the caller SHA' -contains "$k9_workflow" 'if [ -f "$RUNNER_TEMP/corpus.log" ]; then' \ - 'optional corpus-log copying cannot fail an empty K9 corpus' +# +# Both lanes call metadatastician/standards directly (owner decision, +# 2026-11-10). A pin that is not a full commit SHA is a float, and two +# different pins are two standards revisions in one verdict. +lane_pin() { + grep -E "^ uses: metadatastician/standards/\.github/workflows/$1@[0-9a-f]{40}\$" <<<"$caller" \ + | sed 's/.*@//' +} +k9_pin="$(lane_pin 'k9-conformance\.yml')" +if [ "$(grep -c . <<<"$k9_pin")" = 1 ]; then + pass 'K9 validation calls the standards lane at one immutable commit' +else + fail 'K9 validation must call metadatastician/standards k9-conformance.yml at one full commit SHA' +fi contains "$caller" 'require-files: true' \ 'the committed K9 corpus cannot silently become empty' contains "$caller" 'fail-on-indeterminate: true' \ @@ -407,36 +412,39 @@ else pass 'Scorecard OIDC publication is not enabled for PRs' fi -# The two lanes that used to call a reusable owned by `standards` directly. +# Reusable workflows owned by another account. # -# 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. +# A call the Actions policy does not permit 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 two lanes went dark that way. On +# 2026-11-10 the owner decided to permit metadatastician/standards reusables +# in the policy. The rule below is the durable part: the only cross-owner reusables +# are the two named standards lanes, each at a full commit SHA. Anything else +# stays fork-local or owned by the repository owner. 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)" + | grep -vE 'uses:[[:space:]]+hyperpolymath/' \ + | grep -vE 'uses:[[:space:]]+metadatastician/standards/\.github/workflows/(deed|k9)-conformance\.yml@[0-9a-f]{40}[[:space:]]*$' || true)" if [ -z "$cross_owner" ]; then - pass 'every reusable workflow is fork-local or owned by the repository owner' + pass 'every reusable workflow is fork-local, owner-owned or a pinned standards lane' else - fail "a reusable workflow owned by another account fails at plan time with no jobs - $cross_owner" + fail "a reusable workflow outside the permitted set 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' +deed_pin="$(lane_pin 'deed-conformance\.yml')" +if [ "$(grep -c . <<<"$deed_pin")" = 1 ]; then + pass 'DEED validation calls the standards lane at one immutable commit' +else + fail 'DEED validation must call metadatastician/standards deed-conformance.yml at one full commit SHA' +fi +if [ -n "$deed_pin" ] && [ "$deed_pin" = "$k9_pin" ]; then + pass 'the DEED and K9 lanes use the same standards commit' +else + fail "the DEED and K9 lanes must use one standards commit - deed=$deed_pin k9=$k9_pin" +fi 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' \ diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh index a0397c92..e877d7ea 100644 --- a/scripts/tests/test-governance-contract.sh +++ b/scripts/tests/test-governance-contract.sh @@ -15,7 +15,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 deed-remote scorecard-remote deed-source-sha scorecard-source-sha scorecard-no-upload scorecard-quiet-reconcile cross-owner-reusable fleet-unpinned fleet-before-contract fleet-dropped-download fleet-download-after-render fleet-download-fatal fleet-committed-evidence fleet-symlinked-evidence fleet-ignores-cancel stale-standards-remote lock-header-without-lock; 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-unpinned scorecard-remote lane-pins-split scorecard-source-sha scorecard-no-upload scorecard-quiet-reconcile cross-owner-reusable fleet-unpinned fleet-before-contract fleet-dropped-download fleet-download-after-render fleet-download-fatal fleet-committed-evidence fleet-symlinked-evidence fleet-ignores-cancel stale-standards-remote lock-header-without-lock; do reset_fixture case "$mutation" in allowlist-addition) echo 'frontend/src/fork-only.ts' >> "$fixture/config/hypatia/upstream-typescript-allowlist.txt" ;; @@ -28,7 +28,7 @@ for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate bas jcs-manifest) sed -i '/- name: Read the pinned ijson-jcs identity/,/- name: Cache the pinned ijson-jcs binary/s@config/format-tools.toml@config/other-tools.toml@' "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; jcs-canonicalizer) sed -i '/- name: Apply upstream-inherited TypeScript exception/,/- name: Restore panic-attack build cache/s@"$HOME/.cargo/bin/ijson-jcs"@@' "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; raw-ijson) sed -i '/- name: Validate findings and count severities/,/- name: Relativize finding paths/s@"$HOME/.cargo/bin/ijson-jcs" canon < hypatia-findings.json > /dev/null@@' "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; - k9-pin) sed -i 's/ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d/ref: main/' "$fixture/.github/workflows/k9-conformance-reusable.yml" ;; + k9-pin) sed -i -E 's|(k9-conformance\.yml)@[0-9a-f]{40}|\1@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; k9-require) sed -i 's/require-files: true/require-files: false/g' "$fixture/.github/workflows/hypatia-scan.yml" ;; k9-indeterminate) sed -i 's/fail-on-indeterminate: true/fail-on-indeterminate: false/' "$fixture/.github/workflows/hypatia-scan.yml" ;; k9-verdict) sed -i 's/${K9_VERDICT:-orange}/${K9_VERDICT:-green}/' "$fixture/.github/workflows/hypatia-scan.yml" ;; @@ -44,12 +44,13 @@ for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate bas # 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" ;; + # A call the Actions policy does not permit is refused at plan time with + # zero jobs, so nothing reports it; the contract has to. The two standards + # lanes are permitted only at a full commit SHA, and only at one commit. + deed-unpinned) sed -i -E 's|(deed-conformance\.yml)@[0-9a-f]{40}|\1@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; + lane-pins-split) sed -i -E 's|(deed-conformance\.yml)@[0-9a-f]{40}|\1@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" ;; + cross-owner-reusable) sed -i -E 's|uses: metadatastician/standards/\.github/workflows/k9-conformance\.yml@[0-9a-f]{40}|uses: someone/else/.github/workflows/k9.yml@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; # Fork delta 4: the fleet report-backs must come from an immutable gitbot-fleet # commit, be downloaded before the grid renders, and never kill the lane. fleet-unpinned) sed -i -E 's|(fleet-report-back-reusable\.yml)@[0-9a-f]{40}|\1@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; @@ -71,7 +72,6 @@ for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate bas lock-header-without-lock) sed -i '1i # This workflow is managed by gh actions-lock.' "$fixture/.github/workflows/ci.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" ;; diff --git a/scripts/tests/test-k9-log-copy.sh b/scripts/tests/test-k9-log-copy.sh deleted file mode 100644 index a008a3ec..00000000 --- a/scripts/tests/test-k9-log-copy.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Exercise the actual optional-log branch from the fork-local K9 workflow. -set -euo pipefail -ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" -fixture=$(mktemp -d) -trap 'rm -rf "$fixture"' EXIT -copy_log=$(awk ' - /^ if \[ -f "\$RUNNER_TEMP\/corpus.log" \]; then/ { capture = 1 } - capture { sub(/^ /, ""); print } - capture && /^fi$/ { exit } -' "$ROOT/.github/workflows/k9-conformance-reusable.yml") -[[ -n "$copy_log" ]] || { echo 'optional K9 copy branch is missing' >&2; exit 1; } -mkdir -p "$fixture/k9-grid" -RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" -echo 'ok - empty K9 corpus needs no log' -printf '%s\n' 'fixture validator log' > "$fixture/corpus.log" -RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" -cmp "$fixture/corpus.log" "$fixture/k9-grid/corpus.log" -echo 'ok - present K9 corpus log is retained' -rm -rf "$fixture/k9-grid" -if RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" 2>/dev/null; then - echo 'ERROR: failed K9 log copy was hidden' >&2 - exit 1 -fi -echo 'ok - genuine K9 copy errors still fail' From 82de0cef8b5753e2c9da77c1030a2d3e5e36679e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 20:56:09 +0000 Subject: [PATCH 2/2] fix(governance): report a missing lane pin by name, not by a silent exit `lane_pin` ended in a `grep` that matched nothing when a lane had no full commit SHA. Under `set -euo pipefail` the script then stopped with exit 1 and no message, before the check that names the fault. The mutation tests still passed, because they only asked for a non-zero exit. - `lane_pin` now prints nothing for a missing call and never fails. - The tests now require the named error for the three lane-pin mutations. They fail against the earlier script. - The new lines use `[[ ]]` and a local variable, which clears the five SonarCloud findings on pull request 320 (shell:S7679, shell:S7688). Local: check-governance-contract.sh, 89 checks pass; test-governance-contract.sh, 37 mutations rejected. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_0136P7ZoTPXfxg1C85rVNx78 --- scripts/check-governance-contract.sh | 11 +++++++---- scripts/tests/test-governance-contract.sh | 10 ++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/check-governance-contract.sh b/scripts/check-governance-contract.sh index 5438a0cc..637191df 100644 --- a/scripts/check-governance-contract.sh +++ b/scripts/check-governance-contract.sh @@ -368,12 +368,15 @@ fi # Both lanes call metadatastician/standards directly (owner decision, # 2026-11-10). A pin that is not a full commit SHA is a float, and two # different pins are two standards revisions in one verdict. +# Print the pin of each call to the named standards lane, one per line. No +# call is no output, never a failed command: the checks below report it. lane_pin() { - grep -E "^ uses: metadatastician/standards/\.github/workflows/$1@[0-9a-f]{40}\$" <<<"$caller" \ + local lane=$1 + { grep -E "^ uses: metadatastician/standards/\.github/workflows/${lane}@[0-9a-f]{40}\$" <<<"$caller" || true; } \ | sed 's/.*@//' } k9_pin="$(lane_pin 'k9-conformance\.yml')" -if [ "$(grep -c . <<<"$k9_pin")" = 1 ]; then +if [[ "$(grep -c . <<<"$k9_pin" || true)" == 1 ]]; then pass 'K9 validation calls the standards lane at one immutable commit' else fail 'K9 validation must call metadatastician/standards k9-conformance.yml at one full commit SHA' @@ -433,12 +436,12 @@ else fi deed_pin="$(lane_pin 'deed-conformance\.yml')" -if [ "$(grep -c . <<<"$deed_pin")" = 1 ]; then +if [[ "$(grep -c . <<<"$deed_pin" || true)" == 1 ]]; then pass 'DEED validation calls the standards lane at one immutable commit' else fail 'DEED validation must call metadatastician/standards deed-conformance.yml at one full commit SHA' fi -if [ -n "$deed_pin" ] && [ "$deed_pin" = "$k9_pin" ]; then +if [[ -n "$deed_pin" && "$deed_pin" == "$k9_pin" ]]; then pass 'the DEED and K9 lanes use the same standards commit' else fail "the DEED and K9 lanes must use one standards commit - deed=$deed_pin k9=$k9_pin" diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh index e877d7ea..8ebbc135 100644 --- a/scripts/tests/test-governance-contract.sh +++ b/scripts/tests/test-governance-contract.sh @@ -85,6 +85,16 @@ for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate bas if [[ "$mutation" == allowlist-addition ]]; then grep -Fq 'ERROR: the complete allowlist must match the pinned upstream TypeScript snapshot' "$fixture/log" fi + # A lane without a full pin must be named, not die on a failed command. + if [[ "$mutation" == k9-pin ]]; then + grep -Fq 'ERROR: K9 validation must call metadatastician/standards k9-conformance.yml at one full commit SHA' "$fixture/log" + fi + if [[ "$mutation" == deed-unpinned ]]; then + grep -Fq 'ERROR: DEED validation must call metadatastician/standards deed-conformance.yml at one full commit SHA' "$fixture/log" + fi + if [[ "$mutation" == lane-pins-split ]]; then + grep -Fq 'ERROR: the DEED and K9 lanes must use one standards commit' "$fixture/log" + fi if [[ "$mutation" == baseline-*-gate ]]; then grep -Fq 'ERROR: the advisory baseline gate requires nonblocking mode and either findings or a committed baseline' "$fixture/log" fi