Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ name: Governance Reusable Workflow
on:
workflow_call:
inputs:
hypatia-ref:
description: >-
Hypatia commit the baseline job builds, as a 40-hex SHA, or "HEAD"
for a canary run. Kept in step with hypatia-scan-reusable.yml's
default; a floating HEAD here broke every baseline caller when
hypatia#895 changed the emitted severity of `warn` findings.
type: string
default: 51ab6496bf47e30a0576d503df31fec30f3cfe56
runs-on:
description: Runner label for all governance jobs
type: string
Expand Down Expand Up @@ -222,20 +230,25 @@ jobs:
elixir-version: '1.19.4'
otp-version: '28.3'

- name: Resolve Hypatia HEAD commit
- name: Resolve Hypatia scanner commit
if: needs.workflow-staleness.outputs.has_baseline == 'true'
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)
# Resolved before the cache step because cache restore happens before
# the clone, so the key must hash the commit, not a cloned tree.
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"
echo "Resolved hypatia HEAD: $sha"
echo "Resolved hypatia scanner commit: $sha"

- name: Cache Hex/Mix and Scanner Build
if: needs.workflow-staleness.outputs.has_baseline == 'true'
Expand Down Expand Up @@ -1399,19 +1412,14 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: hyperpolymath/standards
# ⚠ ENFORCED, no longer a request: scripts/check-lock-gate-pin-freshness.sh
# fails Self Test unless this commit already contains everything on
# main across the sparse-checkout list below. Change one of those
# files and the NEXT pull request must bump this line — it cannot be
# this PR's own merge commit, which does not exist yet, so the pin is
# one change behind by construction and that is the intended shape.
# Pinned to an immutable commit for the same reason as the dupkey
# helpers in workflow-lint: following `main` would let an edit in
# standards change the verdict of every already-pinned caller with no
# review in their repositories. This pin is invisible to the caller's
# `uses:` ref and to actions.lock — it is a third, independent pin,
# which is precisely how it went stale across standards#946.
ref: 5f82b635c5da5c3df44d5a0caa8983d9b95e0db9
# The reusable's OWN commit — the one the caller's `uses:` ref
# resolved to — so the lock tooling is exactly what the caller
# pinned, never older and never following `main`. This replaced a
# hardcoded third pin that was one change behind by construction and
# went stale twice (standards#946, then #1119);
# scripts/check-lock-gate-pin-freshness.sh accepts only this
# expression or a fresh 40-hex SHA.
ref: ${{ job.workflow_sha }}
path: .standards-lock
persist-credentials: false
sparse-checkout: |
Expand Down
7 changes: 6 additions & 1 deletion scripts/apply-baseline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,16 @@ ANNOTATED="$(jq -n \
# referencing `.file_pattern` there would error with "Cannot index
# string". Capture the entry pattern first, then reference $pat
# inside the test() regex argument.
# Hypatia emitted `warn` for advisory findings until hypatia#895
# (2026-10-01), which now emits them as `medium`. Baselines written before
# then acknowledge `warn`; treating the two as one tier keeps those
# acknowledgements matching instead of re-reporting every one as new.
def sev_tier: if . == "warn" then "medium" else . end;
def match_entry(f):
f as $finding
| $baseline
| map(select(
.severity == $finding.severity
(.severity | sev_tier) == ($finding.severity | sev_tier)
# `rule_module` is a string OR a list of strings.
#
# ⚠ THE LIST FORM EXISTS BECAUSE ONE DEFECT CAN BE EMITTED BY TWO
Expand Down
18 changes: 16 additions & 2 deletions scripts/check-lock-gate-pin-freshness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# ---------------
# `governance-reusable.yml` stages the lock-gate tooling from a THIRD pin: not
# the caller's `uses:` ref and not the lockfile's record of it, but a SHA
# hardcoded inside the callee for its own `actions/checkout`. A called reusable
# workflow has no context exposing its own commit, so the hardcode is forced.
# hardcoded inside the callee for its own `actions/checkout`. That hardcode was
# believed forced; `job.workflow_sha` (the reusable's own commit) removes it, and
# this guard accepts that expression as fresh by construction.
#
# That third pin is invisible to every other control. When standards#946 fixed
# `scripts/update-actions-lock.sh`, this pin still pointed at the commit BEFORE
Expand Down Expand Up @@ -77,6 +78,8 @@ step_block() {
' "$(workflow_path)"
}

# Assert the lock-gate step stages from job.workflow_sha, or from a 40-hex
# commit that already contains COMPARE over the staged paths; exit 1 otherwise.
main() {
local compare="${1:-origin/main}"
local wf block pin paths diverged
Expand All @@ -91,6 +94,17 @@ rename it here too rather than deleting the assertion"

pin="$(printf '%s\n' "$block" | sed -n 's/^[[:space:]]*ref:[[:space:]]*\([^[:space:]#]*\).*/\1/p' | head -1)"
[ -n "$pin" ] || fail "step '$STEP_NAME' has no 'ref:' — it would follow the default branch"
# `job.workflow_sha` is the reusable workflow's OWN commit — the one the
# caller's `uses:` ref resolved to. Staging from it means the tooling is the
# tooling of the caller's pin, never older, so there is no third pin to go
# stale and nothing to compare. (`github.workflow_sha` / `github.sha` name
# the CALLER's commit, not this file's, and stay refused below.)
local raw_ref
raw_ref="$(printf '%s\n' "$block" | sed -n 's/^[[:space:]]*ref:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
if printf '%s' "$raw_ref" | grep -Eq '^\$\{\{[[:space:]]*job\.workflow_sha[[:space:]]*\}\}$'; then
echo "PASS: lock gate is staged from job.workflow_sha (the reusable's own commit) — fresh by construction"
return 0
fi
printf '%s' "$pin" | grep -Eq '^[0-9a-f]{40}$' ||
fail "step '$STEP_NAME' is pinned to '$pin', not an immutable 40-hex commit"

Expand Down
11 changes: 7 additions & 4 deletions scripts/check-package-policy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,20 @@ 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
[ -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
# 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
Expand Down
25 changes: 25 additions & 0 deletions scripts/tests/apply-baseline-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ EOF
assert_status "file_pattern matches nested file" \
"$WORK/findings2.json" "$WORK/baseline2.json" "1,0"

# === Case 2b: severity tier — a `warn` acknowledgement still matches the
# same finding now emitted as `medium` (hypatia#895), and vice versa; a
# different tier must not match. Real shape from standards' own baseline.
cat > "$WORK/findings2b.json" <<'EOF'
[{"severity":"medium","rule_module":"research_extensions","type":"RE001","file":".github/workflows/mirror-reusable.yml"}]
EOF
cat > "$WORK/baseline2b-warn.json" <<'EOF'
[{"severity":"warn","rule_module":"research_extensions","type":"RE001","file":".github/workflows/mirror-reusable.yml"}]
EOF
cat > "$WORK/baseline2b-high.json" <<'EOF'
[{"severity":"high","rule_module":"research_extensions","type":"RE001","file":".github/workflows/mirror-reusable.yml"}]
EOF
cat > "$WORK/findings2b-warn.json" <<'EOF'
[{"severity":"warn","rule_module":"research_extensions","type":"RE001","file":".github/workflows/mirror-reusable.yml"}]
EOF
cat > "$WORK/baseline2b-medium.json" <<'EOF'
[{"severity":"medium","rule_module":"research_extensions","type":"RE001","file":".github/workflows/mirror-reusable.yml"}]
EOF
assert_status "warn acknowledgement matches a medium finding" \
"$WORK/findings2b.json" "$WORK/baseline2b-warn.json" "1,0"
assert_status "medium acknowledgement matches a warn finding" \
"$WORK/findings2b-warn.json" "$WORK/baseline2b-medium.json" "1,0"
assert_status "a different tier does not match" \
"$WORK/findings2b.json" "$WORK/baseline2b-high.json" "0,1"

# === Case 3: file_pattern MUST NOT match unrelated file (regression
# against the always-matches bug from `.file_pattern` inside test()) ===
cat > "$WORK/findings3.json" <<'EOF'
Expand Down
13 changes: 13 additions & 0 deletions scripts/tests/check-lock-gate-pin-freshness-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ export LOCK_GATE_WORKFLOW="$WORK/.github/workflows/governance-reusable.yml"
out="$(run "$NEW")"; rc=$?
check "missing ref: is refused" 1 "$rc" "$out" "would follow the default branch"

# 11. The reusable's own commit is fresh by construction and needs no compare.
write_fixture '${{ job.workflow_sha }}'
out="$(run "$NEW")"; rc=$?
check "job.workflow_sha is accepted" 0 "$rc" "$out" "fresh by construction"

# 12. The CALLER's commit is not the reusable's: both forms stay refused.
write_fixture '${{ github.workflow_sha }}'
out="$(run "$NEW")"; rc=$?
check "github.workflow_sha (caller's) is refused" 1 "$rc" "$out" "not an immutable 40-hex commit"
write_fixture '${{ github.sha }}'
out="$(run "$NEW")"; rc=$?
check "github.sha (caller's) is refused" 1 "$rc" "$out" "not an immutable 40-hex commit"

# 10. Staging nothing must not be a free pass.
cat > "$WORK/.github/workflows/governance-reusable.yml" <<YAML
jobs:
Expand Down
4 changes: 2 additions & 2 deletions scripts/tests/governance-gates-505-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
7 changes: 5 additions & 2 deletions scripts/tests/governance-reusable-contract-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ lock_gate_block="$(awk '
' "$GOVERNANCE")"
[ -n "$lock_gate_block" ] ||
fail "governance workflow has no step named 'Checkout standards for the lock gate'"
printf '%s\n' "$lock_gate_block" | grep -Eq '^[[:space:]]*ref:[[:space:]]*[0-9a-f]{40}[[:space:]]*$' ||
fail "the lock gate is not staged from an immutable 40-hex commit"
# Immutable either way: a literal 40-hex commit, or job.workflow_sha (the
# reusable's own commit, i.e. exactly the caller's pin). Never a branch, and
# never github.workflow_sha / github.sha, which name the CALLER's commit.
printf '%s\n' "$lock_gate_block" | grep -Eq '^[[:space:]]*ref:[[:space:]]*([0-9a-f]{40}|\$\{\{ job\.workflow_sha \}\})[[:space:]]*$' ||
fail "the lock gate is not staged from an immutable 40-hex commit or job.workflow_sha"
if printf '%s\n' "$lock_gate_block" | grep -Eq '^[[:space:]]*ref:[[:space:]]*main[[:space:]]*$'; then
fail "the lock gate follows moving main"
fi
Expand Down
2 changes: 1 addition & 1 deletion tests/test_governance_reusable_shape.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ njobs=$(job_ids | wc -l); nro=$(grep -c 'runs-on: ${{ inputs.runs-on }}' "$F")
B=$(job_block actions-lock-verify)
[ -n "$B" ] && ok "actions-lock-verify job exists" || bad "actions-lock-verify job missing"
printf '%s' "$B" | grep -q 'check-actions-lock-gate.sh' && ok "actions-lock-verify runs the tested gate script" || bad "actions-lock-verify does not run check-actions-lock-gate.sh"
printf '%s' "$B" | grep -Eq 'ref: [0-9a-f]{40}$' && ok "actions-lock-verify pins standards at an immutable commit" || bad "actions-lock-verify standards checkout not pinned to an immutable commit"
printf '%s' "$B" | grep -Eq 'ref: ([0-9a-f]{40}|\$\{\{ job\.workflow_sha \}\})$' && ok "actions-lock-verify pins standards at an immutable commit" || bad "actions-lock-verify standards checkout not pinned to an immutable commit"
printf '%s' "$B" | grep -q 'ref: main' && bad "actions-lock-verify floats a standards checkout at main" || ok "actions-lock-verify has no floating ref: main"
printf '%s' "$B" | grep -q 'ACTIONS_LOCK_VERIFIER=' && ok "gate is pointed at the fetched verifier" || bad "ACTIONS_LOCK_VERIFIER not set for the gate"

Expand Down
Loading