diff --git a/scripts/check-workflow-staleness.sh b/scripts/check-workflow-staleness.sh index 51faa3ea..3244cdb7 100755 --- a/scripts/check-workflow-staleness.sh +++ b/scripts/check-workflow-staleness.sh @@ -454,8 +454,16 @@ fi for wf in "$REPO_ROOT"/.github/workflows/*.yml "$REPO_ROOT"/.github/workflows/*.yaml; do [ -f "$wf" ] || continue - # Rule: no_scorecard_sarif_code_scanning (structural — independent of pins) - if grep -q "ossf/scorecard-action@" "$wf" && grep -q "github/codeql-action/upload-sarif@" "$wf"; then + # Rule: no_scorecard_sarif_code_scanning (structural — independent of pins). + # Exempt scorecard-reusable.yml when this IS the standards repo: that file + # is the canonical implementation of the pattern this rule detects, and no + # other repo ever carries these two lines locally (they call it via + # `uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@sha` + # instead). Without this guard the rule tripped on its own canonical source + # unconditionally, the same false-positive the sibling + # no_retired_scorecard_enforcer rule above already guards against. + if { [ "$IS_STANDARDS" = "false" ] || [ "$(basename "$wf")" != "scorecard-reusable.yml" ]; } \ + && grep -q "ossf/scorecard-action@" "$wf" && grep -q "github/codeql-action/upload-sarif@" "$wf"; then echo "::error file=$wf::OSSF Scorecard must not upload SARIF to GitHub Code Scanning unless it runs for every PR head commit." FAILED=1 fi diff --git a/scripts/filter-sarif-by-baseline.sh b/scripts/filter-sarif-by-baseline.sh index 98d61b2d..57f6fd9f 100755 Binary files a/scripts/filter-sarif-by-baseline.sh and b/scripts/filter-sarif-by-baseline.sh differ diff --git a/tools/policy/check-language-policy.sh b/tools/policy/check-language-policy.sh new file mode 100755 index 00000000..205ce015 --- /dev/null +++ b/tools/policy/check-language-policy.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Language-policy drift gate. +# +# WHY THIS EXISTS. The estate's language policy is duplicated into ~372 per-repo +# `.claude/CLAUDE.md` files across 131 repos. On 2026-08-26 a census found 868 of them +# still listed **Bun as BANNED** with Deno as its replacement - the exact inverse of the +# standing ruling - and nothing had ever detected it. Correcting `standards` fixes one copy; +# agents read the local one. +# +# WHY ASSERTIONS, NOT A DIFF. The copies are legitimately not identical: repos carry their +# own exemption tables, architecture notes and carve-outs. A byte-for-byte generator would +# be permanently red. So this gate asserts the INVARIANTS the policy must satisfy, whatever +# the surrounding wording. +# +# Exit 0 = compliant. Exit 1 = drift. Every failure prints file:line. +set -uo pipefail +status=0 +files=$(git ls-files '*CLAUDE.md' 2>/dev/null | grep -v node_modules) +[ -z "$files" ] && { echo "no CLAUDE.md tracked - nothing to check"; exit 0; } + +fail(){ printf ' \033[31mFAIL\033[0m %s\n %s\n' "$1" "$2"; status=1; } + +for f in $files; do + echo "checking $f" + + # --- must NOT appear ------------------------------------------------------- + # 1. Bun banned. This is the inversion that went undetected across 868 files. + if grep -nF -- '| Bun | Deno |' "$f" >/dev/null; then + fail "$f:$(grep -nF -- '| Bun | Deno |' "$f" | head -1 | cut -d: -f1)" \ + 'Bun is listed as BANNED with Deno as replacement - inverted. Bun is tier 1.' + fi + # 2. The rule that told repos not to declare dependencies at all. hyperpolymath/ubicity + # a phrase inside a blockquote or quotation marks is HISTORY, not policy + live(){ grep -vE '^[[:space:]]*>' "$1" | grep -vE '"[^"]*'"$2"'[^"]*"|“[^”]*'"$2"'[^”]*”'; } + # imported zod and glob, shipped no manifest, and could not build under ANY toolchain. + if live "$f" 'No package.json for runtime deps' | grep -qF 'No package.json for runtime deps'; then + fail "$f:$(grep -nF 'No package.json for runtime deps' "$f" | head -1 | cut -d: -f1)" \ + 'Forbids declaring dependencies. Bun is npm-compatible; a manifest is REQUIRED.' + fi + if live "$f" 'deno.json imports' | grep -qF 'deno.json imports'; then + fail "$f:$(grep -nF 'deno.json imports' "$f" | head -1 | cut -d: -f1)" \ + 'Directs dependency declaration into deno.json. Use package.json + bun.lock.' + fi + # 3. No tool description may advertise TypeScript. Owner ruling 2026-08-27: + # "no typescript ... that should not exist at all." + if grep -nE 'Executes .\.ts. directly|JS/TS runtime' "$f" >/dev/null; then + fail "$f:$(grep -nE 'Executes .\.ts. directly|JS/TS runtime' "$f" | head -1 | cut -d: -f1)" \ + 'Advertises TypeScript execution. TypeScript is banned; do not describe tools as TS runtimes.' + fi + # 4. Blanking scars. A bulk purge substituted a token with an EMPTY STRING, which also + # produced `rm -rf /lib` in wordpress-tools (the lethal shape is /path -> /path). + if awk -F'|' 'NF>=4 && $2 ~ /^[[:space:]]*$/{exit 0} END{exit 1}' "$f"; then + fail "$f" 'Policy table row with an EMPTY first cell - blanking scar from a bulk substitution.' + fi + if grep -nF '| **** |' "$f" >/dev/null; then + fail "$f:$(grep -nF '| **** |' "$f" | head -1 | cut -d: -f1)" \ + 'Empty bold cell (****) - the language name was blanked out.' + fi + if grep -nE '\*\*No new +files\*\*|Only where +cannot' "$f" >/dev/null; then + fail "$f" 'Enforcement rule with a blanked language name.' + fi + # 5. A rule may not ban the language it mandates. + if grep -nE '^\| AffineScript \| AffineScript \|' "$f" >/dev/null; then + fail "$f" 'BANNED table maps AffineScript to itself - it bans the mandated language.' + fi + + # --- must appear, if the file carries a language-policy table --------------- + if grep -qE '^### (ALLOWED|BANNED)' "$f"; then + { grep -qE '^\|[[:space:]]*\*\*Bun\*\*[[:space:]]*\|' "$f" || grep -qiE '^[-*][[:space:]]+\*{0,2}Bun\*{0,2}\b' "$f"; } || \ + fail "$f" 'No Bun row in ALLOWED. Bun is the tier-1 JS runtime and package manager.' + { grep -qE '^\|[[:space:]]*\*{0,2}Deno\*{0,2}[[:space:]]*\|[[:space:]]*\*{0,2}Bun\*{0,2}[[:space:]]*\|' "$f" || grep -qiE '^[-*][[:space:]]+Deno[[:space:]]*\(use Bun\)' "$f"; } || \ + fail "$f" 'Deno is not listed in BANNED with Bun as its replacement (ruling 2026-08-26).' + fi +done + +if [ $status -eq 0 ]; then echo "language policy OK"; else + echo + echo "Language-policy drift detected. Canonical source: hyperpolymath/standards .claude/CLAUDE.md" + echo "Fix the local copy; do not weaken this gate." +fi +exit $status diff --git a/tools/policy/check-workflows-parse.sh b/tools/policy/check-workflows-parse.sh new file mode 100755 index 00000000..773a906b --- /dev/null +++ b/tools/policy/check-workflows-parse.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Fail if any GitHub Actions workflow does not parse. +# +# WHY THIS EXISTS. Measured across the estate on 2026-08-27: **481 workflow files in +# 134 repos do not parse at all**. A workflow that cannot be loaded produces NO check +# run, so it is invisible to `?status=failure` sweeps and to `gh pr checks` — the gate +# simply never runs, and its absence looks exactly like success. +# +# One was root-caused to a literal BACKSPACE byte (0x08) committed inside a regex. +# The other 480 are structural YAML: 245 "mapping values are not allowed in this +# context", 137 "could not find expected ':'", 73 block-mapping errors, 6 unterminated +# quotes. +# +# Exit 0 = every workflow parses. Exit 1 = at least one does not. +set -uo pipefail + +parser="" +if command -v yq >/dev/null 2>&1; then parser=yq +elif command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then parser=python +elif command -v ruby >/dev/null 2>&1; then parser=ruby +else + echo "::warning::no YAML parser available (yq, python3+pyyaml, or ruby) — cannot verify workflows" + exit 0 +fi + +parse_ok() { + case "$parser" in + yq) yq '.' "$1" >/dev/null 2>&1 ;; + python) python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' "$1" >/dev/null 2>&1 ;; + ruby) ruby -ryaml -e 'YAML.safe_load(File.read(ARGV[0]), aliases: true)' "$1" >/dev/null 2>&1 ;; + esac +} + +status=0; checked=0 +while IFS= read -r f; do + [ -f "$f" ] || continue + checked=$((checked + 1)) + if ! parse_ok "$f"; then + status=1 + printf '::error file=%s::workflow does not parse — it produces NO check run, so this gate never executes\n' "$f" + case "$parser" in + yq) yq '.' "$f" 2>&1 | head -2 | sed 's/^/ /' ;; + python) python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' "$f" 2>&1 | tail -2 | sed 's/^/ /' ;; + ruby) ruby -ryaml -e 'YAML.safe_load(File.read(ARGV[0]), aliases: true)' "$f" 2>&1 | head -2 | sed 's/^/ /' ;; + esac + # control characters are a common, easily-missed cause + if grep -qP '[\x00-\x08\x0B\x0C\x0E-\x1F]' "$f" 2>/dev/null; then + echo " ⚠ contains CONTROL CHARACTERS — YAML forbids them; see empty-linter" + grep -nP '[\x00-\x08\x0B\x0C\x0E-\x1F]' "$f" | head -3 | cat -v | sed 's/^/ /' + fi + fi +done < <(git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' '**/.github/workflows/*.yml' '**/.github/workflows/*.yaml' 2>/dev/null | sort -u) + +if [ "$checked" -eq 0 ]; then echo "no workflows tracked — nothing to check"; exit 0; fi +if [ "$status" -eq 0 ]; then echo "✅ all $checked workflow(s) parse"; else + echo + echo "A workflow that does not parse produces no check run. Its gate has never run," + echo "and its silence is indistinguishable from success. Fix the YAML; do not delete" + echo "the check." +fi +exit $status