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
126 changes: 69 additions & 57 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ permissions:
jobs:
workflow-staleness:
name: Check Workflow Staleness
runs-on: ubuntu-latest
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 10
outputs:
has_baseline: ${{ steps.check.outputs.has_baseline }}
Expand Down Expand Up @@ -135,7 +135,7 @@ jobs:
GH_TOKEN: ${{ secrets.HYPATIA_SCAN_PAT }}
if: ${{ env.GH_TOKEN == '' }}
run: |
echo "::notice::Live Actions policy was not evaluated: HYPATIA_SCAN_PAT was not supplied by the caller. The separate tree allowlist gate still ran."
echo "::warning::Live Actions policy was not evaluated: HYPATIA_SCAN_PAT was not supplied by the caller. The separate tree allowlist gate still ran."
# shellcheck disable=SC2016
printf '%s\n' \
'### Live Actions policy not evaluated' \
Expand All @@ -160,7 +160,7 @@ jobs:
rc=$?
set -e
if [ "$rc" -eq 3 ]; then
echo "::notice::Live Actions policy was not evaluated: the supplied credential could not read the Administration endpoint."
echo "::warning::Live Actions policy was not evaluated: the supplied credential could not read the Administration endpoint."
printf '%s\n' \
'### Live Actions policy not evaluated' \
'' \
Expand All @@ -179,7 +179,7 @@ jobs:
# repo without a baseline blocked forever, by construction. The job now
# always runs; the EXPENSIVE steps are guarded individually, so it still
# costs nothing when there is no baseline to validate.
runs-on: ubuntu-latest
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 15
steps:
- name: Checkout caller repository
Expand Down Expand Up @@ -753,17 +753,17 @@ jobs:
FAILED=false
WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true)
if [ -n "$WEAK_CRYPTO" ]; then
echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:"
echo "::warning::Weak crypto (MD5/SHA1) detected — ADVISORY, does not fail this job. Use SHA256+:"
echo "$WEAK_CRYPTO"
fi
HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true)
if [ -n "$HTTP_URLS" ]; then
echo "⚠️ HTTP URLs found. Use HTTPS:"
echo "::warning::HTTP URLs found — ADVISORY, does not fail this job. Use HTTPS:"
echo "$HTTP_URLS"
fi
SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true)
if [ -n "$SECRETS" ]; then
echo "❌ Potential hardcoded secrets detected!"
echo "::error::Potential hardcoded secrets detected — this FAILS the job:"
FAILED=true
fi
if [ "$FAILED" = true ]; then
Expand Down Expand Up @@ -1123,7 +1123,6 @@ jobs:
path: .standards-dupkey
sparse-checkout: |
scripts/check-workflow-duplicate-keys.sh
scripts/update-actions-lock.sh
tools/policy/check-workflows-parse.sh
sparse-checkout-cone-mode: false
# ⚠ Not fatal if the file is absent. This checkout is pinned to
Expand Down Expand Up @@ -1177,18 +1176,6 @@ jobs:
# The lockfile gate below runs in a consumer checkout, where the
# standards helper is not present. Preserve the canonical helper
# before removing this sparse standards checkout.
if [ "$GITHUB_REPOSITORY" = hyperpolymath/standards ]; then
LOCK_SCRIPT="scripts/update-actions-lock.sh"
echo "Using this repository's own actions-lock verifier (standards self-lint)."
else
LOCK_SCRIPT=".standards-dupkey/scripts/update-actions-lock.sh"
fi
if [ ! -f "$LOCK_SCRIPT" ]; then
echo "::error::actions-lock verifier not found — neither fetched from" \
"standards@main nor present locally."
exit 1
fi
cp "$LOCK_SCRIPT" "$RUNNER_TEMP/update-actions-lock.sh"
rm -rf .standards-dupkey
bash "$RUNNER_TEMP/dupkeys.sh" .github/workflows

Expand Down Expand Up @@ -1220,43 +1207,6 @@ jobs:
done
[ $failed -eq 1 ] && { echo "Add SPDX header + permissions:"; exit 1; }
echo "All workflows have SPDX headers + permissions"
- name: Check locked or SHA-pinned actions
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ -f .github/workflows/actions.lock ]; then
# actions.lock is the authoritative immutable resolution for both
# direct actions and their transitive dependencies. Do not also
# rewrite direct refs to raw SHAs: gh actions-lock omits refs that
# no tag or branch contains, and GitHub then rejects the workflow
# at startup. Measured in oikosbot PR #78 on 2026-08-29: five
# previously executable workflows became startup_failure after the
# redundant direct-SHA conversion; restoring their locked version
# refs made GitHub's native resolver accept them again.
gh extension install github/gh-actions-lock
bash "$RUNNER_TEMP/update-actions-lock.sh" --verify-local
echo "Immutable direct and transitive lockfile coverage verified"
else
unpinned=$(grep -rnE --include='*.yml' --include='*.yaml' \
"^[[:space:]]+uses:" .github/workflows/ | \
grep -v "@[a-f0-9]\{40\}" | \
grep -v "uses: \./\|uses: docker://\|uses: actions/github-script\|uses: hyperpolymath/standards/" || true)
if [ -n "$unpinned" ]; then
echo "ERROR: no .github/workflows/actions.lock in THIS TREE, and these refs are not SHA-pinned."
echo " Prefer \`gh actions-lock\` — it also locks the transitive dependencies"
echo " of composite actions, which an inline SHA cannot express."
echo " Do NOT do both: gh actions-lock refuses a ref no tag or branch contains,"
echo " so inline pinning REMOVES actions from the lockfile."
echo "$unpinned"
exit 1
fi
echo "All actions are SHA-pinned"
fi
# The step above proves a pin has the right SHAPE. It cannot prove the
# SHA EXISTS — a fabricated 40-hex string passes it. Measured 2026-07-28:
# 112 of 613 unique estate pins (18%) do not resolve, in 876 committed
# files. An unresolvable `uses:` yields NO check run, so those repos look
# green while the job never ran. This step closes that gap.
- name: Checkout standards for the pin-existence gate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -1284,6 +1234,68 @@ jobs:
fi
echo "No critical duplicates found"

# actions-lock-verify — its own context so a lockfile red names its cause
# (spec 2026-09-02-cicd-regularisation-design §6.4, R2). Formerly a step
# buried inside workflow-lint, where "Workflow security linter" red could
# mean SPDX, permissions, duplicate keys, parse, pin-resolve OR the lock.
# The gate logic lives in scripts/check-actions-lock-gate.sh (tested by
# scripts/tests/check-actions-lock-gate-test.sh): lock present → the
# authoritative `gh actions-lock --verify-local`; lock absent → unpinned
# refs are red now, all-pinned-but-no-lock is a ::warning until
# ENFORCE_ACTIONS_LOCK_FROM (2026-10-01), red after.
#
# Standards is checked out at `job.workflow_sha` = the SHA of THIS reusable
# as pinned by the caller (verified 2026-09-02 on consumer run logs), so the
# gate script and the YAML move together — one speed, no `ref: main` float.
actions-lock-verify:
name: Actions lockfile verify
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout caller repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ github.sha }}
- name: Checkout standards for the lock gate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: hyperpolymath/standards
ref: ${{ job.workflow_sha }}
path: .standards-lock
persist-credentials: false
sparse-checkout: |
scripts/check-actions-lock-gate.sh
scripts/update-actions-lock.sh
sparse-checkout-cone-mode: false
- name: Install gh actions-lock
env:
GH_TOKEN: ${{ github.token }}
run: gh extension install github/gh-actions-lock
- name: Verify actions.lock (or SHA pins during the grace window)
env:
GH_TOKEN: ${{ github.token }}
run: |
set -uo pipefail
if [ "$GITHUB_REPOSITORY" = hyperpolymath/standards ]; then
SRC=scripts
echo "Using this repository's own gate + verifier (standards self-lint)."
else
SRC=.standards-lock/scripts
fi
for f in check-actions-lock-gate.sh update-actions-lock.sh; do
if [ ! -f "$SRC/$f" ]; then
echo "::error::actions-lock gate: $f not found in $SRC (standards checkout at job.workflow_sha failed?)"
exit 1
fi
cp "$SRC/$f" "$RUNNER_TEMP/$f"
done
rm -rf .standards-lock
ACTIONS_LOCK_VERIFIER="$RUNNER_TEMP/update-actions-lock.sh" \
bash "$RUNNER_TEMP/check-actions-lock-gate.sh" .github/workflows

trusted-base:
name: Trusted-base reduction policy
runs-on: ${{ inputs.runs-on }}
Expand Down
10 changes: 7 additions & 3 deletions config/rulesets/gates.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"context_shape": "job name; for a reusable caller it is '<caller job id> / <reusable job name>'",
"integration_id": 15368,
"if_no_run_yet": "omit that file's contexts and report it; never write a context nothing has emitted",
"if_zero_contexts_overall": "do not write the required_status_checks rule at all; report the repo as UNGATED"
"if_zero_contexts_overall": "do not write the required_status_checks rule at all; report the repo as UNGATED",
"never_required_match": "an entry in never_required_contexts matches either the whole context or the part after the first \" / \" (the reusable job name); it is removed from the derived set, never typed",
"context_freeze": "reusable job `name:` values and caller job ids are FROZEN until this derivation replaces every typed ruleset: renaming either turns a live required context into a phantom on every consumer. Adding a job is allowed. Guard: standards tests/test_governance_reusable_shape.sh"
},
"profiles": {
"base": {
Expand Down Expand Up @@ -93,7 +95,9 @@
],
"never_required_contexts": [
"Allowlist Preflight",
"SonarCloud Code Analysis",
"CodeQL (default setup)"
"Code quality + docs",
"CodeQL (default setup)",
"Live Actions policy (credentialed advisory)",
"SonarCloud Code Analysis"
]
}
22 changes: 22 additions & 0 deletions docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,25 @@ Facts found while writing the canonical artefacts; each overrides the section it
| 7.6 ADR template | `docs/decisions/ADR-<num>.adoc` | ADR files are `ADR-<num>-<slug>.adoc`; the live template 404s on every repo. Canon = code-search URL `search?q=ADR-<num>+path%3Adocs%2Fdecisions&type=code` | `ADR-003.adoc` on standards = HTTP 404, 2026-09-02 |
| 7.2 allowlist enforcement | "prune" implied enforcement | `verified_allowed` is true, so verified creators bypass the list; prune is hygiene, R1 is enforced by deleting workflows. **O12 added**: flip `verified_allowed` to false after a `uses:` census | `config/settings/actions-allowlist.json` |
| 7.3 direct push | unstated | With every bypass at `pull_request` and no `always` actor, main is PR-only for everyone including the owner; emergency path = disable the ruleset. FYI posted on #715 | `config/rulesets/base.json` |

## 14. Fold table — estate policy files → governance-reusable jobs (step 2f)

Doc only; step 5 deletes the left column once the right column is green on the pilots.
Job names are the frozen contexts (§6.2; guard = `tests/test_governance_reusable_shape.sh`).

| Estate file (repos) | Governance job (context) | Fold decision |
|---|---|---|
| `security-policy.yml` (61) | `Security policy checks` | Fold. Weak crypto / HTTP URL findings are ADVISORY `::warning`; hardcoded secrets FAIL. Delete file |
| `runtime-policy.yml` (47), `language-policy.yml` (12), `ts-blocker.yml` (7) | `Language / package anti-pattern policy` | Fold. Delete files |
| `npm-bun-blocker.yml` (7) | — | DROP: contradicts the Bun ruling; no job carries it |
| `rsr-antipattern.yml` (24) | `Language / package anti-pattern policy` | DROP the file (its reusable does not exist); the job already covers the anti-pattern list |
| `wellknown-enforcement.yml` (60) | `Well-Known (RFC 9116 + RSR)` | Fold. Open: is `security.txt` MUST or SHOULD → owner ruling **O13** (#715). Until ruled the job keeps today's severity |
| `estate-rules.yml` (40) | `Trusted-base reduction policy` + `Licence consistency` | Fold; the two jobs are the surviving halves. Delete file |
| `guix-policy.yml` (25), `guix-nix-policy.yml` (35) | `Guix packaging policy (Nix retired)` | Fold. Presence-only checks in the files were fake; the job's checks must keep a planted positive |
| `container-policy.yml` (7) | `Security policy checks` | Fold the digest-pin rule into the job as a new sub-check with a planted positive; delete file |
| `workflow-linter.yml` (141) | `Workflow security linter` + `Actions lockfile verify` | Fold. Inline `uses:` are linted too (the file missed them). Lock verification is its own context (PR 2a) |
| `dogfood-gate.yml` (240, 164 variants): invisible-character job | new governance job (name TBD in PR 2b, e.g. `Invisible characters`) | REMAKE as a GATE. Reference implementation = double-track-browser #90: `grep -aP '(*UTF)[…]'`, in-step probe that the pattern fires on a planted NBSP, `::error` + exit 1 on findings |
| `dogfood-gate.yml`: groove / A2ML / K9 checks | — | Keep as the `*-ecosystem/validate-action` opt-ins (§5.6); not governance |
| `cicd-suite/spdx-license-check`, `palimpsest-license` action | `Licence consistency` | Fold. SPDX line-1 rule lives in one place |
| `Validate Hypatia Baseline` (governance) vs `hypatia-scan` threshold | `Validate Hypatia Baseline` + `scan / Hypatia Neurosymbolic Analysis` | Align: governance validates baseline file shape only (info); the severity threshold (high) is owned by `hypatia-scan-reusable`. One owner per rule, no double jeopardy |
| `Live Actions policy (credentialed advisory)`, `Code quality + docs`, `Allowlist Preflight` | (advisory) | Never required contexts (`config/rulesets/gates.json`). They warn, they do not gate |
74 changes: 74 additions & 0 deletions scripts/check-actions-lock-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# check-actions-lock-gate.sh — the `actions-lock-verify` GATE (R2: SHA pins +
# actions.lock everywhere; spec 2026-09-02-cicd-regularisation-design §6.4).
#
# Three outcomes, never a silent pass:
# lockfile present → run the authoritative verifier (`gh actions-lock
# --verify-local` via scripts/update-actions-lock.sh) and
# propagate its exit status. A corrupted lock goes RED.
# lockfile absent, → RED: an unpinned `uses:` is a violation today, lock or
# unpinned refs no lock.
# lockfile absent, → grace window: `::warning` + "NOT YET ENFORCED" and exit
# all SHA-pinned 0 until ENFORCE_ACTIONS_LOCK_FROM; `::error` + exit 1
# from that date. The sweep (spec §10 step 5) lands the
# lockfiles before the date; the date makes the gate
# real without red-flooding 300 repos on day one.
#
# Test seams (used by scripts/tests/check-actions-lock-gate-test.sh):
# LOCK_TODAY override today's date (YYYY-MM-DD)
# ENFORCE_ACTIONS_LOCK_FROM override the cutoff (default 2026-10-01)
# ACTIONS_LOCK_VERIFIER path to update-actions-lock.sh (default: sibling)
#
# Usage: check-actions-lock-gate.sh [WORKFLOWS_DIR] (default .github/workflows)
set -uo pipefail

WF_DIR="${1:-.github/workflows}"
TODAY="${LOCK_TODAY:-$(date -u +%F)}"
ENFORCE_FROM="${ENFORCE_ACTIONS_LOCK_FROM:-2026-10-01}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERIFIER="${ACTIONS_LOCK_VERIFIER:-$SCRIPT_DIR/update-actions-lock.sh}"

if [ ! -d "$WF_DIR" ]; then

Check failure on line 34 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2B&open=AaBiDSoiIbU0N4BlL_2B&pullRequest=718
echo "::error::actions-lock gate: workflows directory not found: $WF_DIR"

Check warning on line 35 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2C&open=AaBiDSoiIbU0N4BlL_2C&pullRequest=718
exit 2
fi

if [ -f "$WF_DIR/actions.lock" ]; then

Check failure on line 39 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2D&open=AaBiDSoiIbU0N4BlL_2D&pullRequest=718
if [ ! -f "$VERIFIER" ]; then

Check failure on line 40 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2E&open=AaBiDSoiIbU0N4BlL_2E&pullRequest=718
echo "::error::actions-lock gate: lockfile present but verifier not found at $VERIFIER"

Check warning on line 41 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2F&open=AaBiDSoiIbU0N4BlL_2F&pullRequest=718
exit 2
fi
echo "Lockfile present: running the authoritative verifier ($VERIFIER --verify-local)."
bash "$VERIFIER" --verify-local
rc=$?
if [ "$rc" -ne 0 ]; then

Check failure on line 47 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2G&open=AaBiDSoiIbU0N4BlL_2G&pullRequest=718
echo "::error::actions-lock gate: lockfile verification FAILED (exit $rc). Regenerate with scripts/update-actions-lock.sh in the same PR as the uses: change."

Check warning on line 48 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2H&open=AaBiDSoiIbU0N4BlL_2H&pullRequest=718
exit "$rc"
fi
echo "Immutable direct and transitive lockfile coverage verified."
exit 0
fi

# No lockfile. Unpinned refs are a violation regardless of the grace window.
unpinned=$(grep -rnE --include='*.yml' --include='*.yaml' "^[[:space:]]+-?[[:space:]]*uses:" "$WF_DIR" \
| grep -vE "@[a-f0-9]{40}([[:space:]]|$)" \
| grep -vE "uses:[[:space:]]+(\./|docker://|actions/github-script|hyperpolymath/standards/)" || true)
if [ -n "$unpinned" ]; then

Check failure on line 59 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2I&open=AaBiDSoiIbU0N4BlL_2I&pullRequest=718
echo "::error::actions-lock gate: no $WF_DIR/actions.lock AND these refs are not SHA-pinned:"

Check warning on line 60 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2J&open=AaBiDSoiIbU0N4BlL_2J&pullRequest=718
echo "$unpinned"
echo " Prefer \`gh actions-lock\` (scripts/update-actions-lock.sh): it also locks the"
echo " transitive dependencies of composite actions, which an inline SHA cannot express."
exit 1
fi

if [[ "$TODAY" < "$ENFORCE_FROM" ]]; then
echo "::warning::actions-lock gate: no $WF_DIR/actions.lock. All refs are SHA-pinned, but the lockfile becomes REQUIRED on $ENFORCE_FROM (today is $TODAY). Run scripts/update-actions-lock.sh."
echo "NOT YET ENFORCED: lockfile missing but inside the grace window."
exit 0
fi

echo "::error::actions-lock gate: no $WF_DIR/actions.lock and the grace window closed on $ENFORCE_FROM (today is $TODAY). Run scripts/update-actions-lock.sh and commit the lockfile."

Check warning on line 73 in scripts/check-actions-lock-gate.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaBiDSoiIbU0N4BlL_2K&open=AaBiDSoiIbU0N4BlL_2K&pullRequest=718
exit 1
Loading
Loading