Repository navigation
chore(deps): bump the actions group with 3 updates #180
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This workflow is managed by gh actions-lock. | |
| # SPDX-License-Identifier: MPL-2.0 | |
| # This workflow is managed by gh actions-lock. | |
| # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> | |
| # | |
| # dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate | |
| # Validates that the repo uses hyperpolymath's own formats and tools. | |
| # Companion to static-analysis-gate.yml (security) — this is for format compliance. | |
| name: Dogfood Gate | |
| on: | |
| pull_request: | |
| branches: ['**'] | |
| push: | |
| branches: [main, master] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Job 1: DEED manifest validation | |
| # --------------------------------------------------------------------------- | |
| deed-validate: | |
| name: Validate DEED manifests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Check for manifest files (.a2ml/.deed) | |
| id: detect | |
| run: | | |
| COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) | |
| echo "count=$COUNT" >> "$GITHUB_OUTPUT" | |
| if [ "$COUNT" -eq 0 ]; then | |
| echo "::warning::No .a2ml/.deed manifest files found. Every RSR repo should have a repo deed (<reponame>_chora.deed); legacy 0-AI-MANIFEST.a2ml accepted mid-migration — standards #837" | |
| fi | |
| - name: Validate DEED manifests | |
| if: steps.detect.outputs.count > 0 | |
| uses: hyperpolymath/deed-ecosystem/validate-action@main | |
| with: | |
| path: '.' | |
| strict: 'false' | |
| - name: Write summary | |
| run: | | |
| MANIFEST_COUNT="${{ steps.detect.outputs.count }}" | |
| if [ "$MANIFEST_COUNT" -eq 0 ]; then | |
| cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" | |
| ## DEED Manifest Validation | |
| :warning: **No .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`<reponame>_chora.deed`) at its root. | |
| Copy it from [dicta-task](https://github.com/hyperpolymath/dicta-task). Manifests are validated against the DEED grammar (standards `1-formats/deed/`); the `.a2ml` → `.deed` extension migration is tracked in standards #837 — the deed validator scans both. | |
| EOF | |
| else | |
| echo "## DEED Manifest Validation" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Scanned **${MANIFEST_COUNT}** .a2ml/.deed manifest(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Job 2: K9 contract validation | |
| # --------------------------------------------------------------------------- | |
| k9-validate: | |
| name: Validate K9 contracts | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Check for K9 files | |
| id: detect | |
| run: | | |
| COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l) | |
| CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \ | |
| -not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \ | |
| -not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l) | |
| echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT" | |
| echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT" | |
| if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then | |
| echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts." | |
| fi | |
| - name: Validate K9 contracts | |
| if: steps.detect.outputs.k9_count > 0 | |
| uses: hyperpolymath/k9-ecosystem/validate-action@main | |
| with: | |
| path: '.' | |
| strict: 'false' | |
| - name: Write summary | |
| run: | | |
| K9_COUNT="${{ steps.detect.outputs.k9_count }}" | |
| CFG_COUNT="${{ steps.detect.outputs.config_count }}" | |
| if [ "$K9_COUNT" -eq 0 ]; then | |
| cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" | |
| ## K9 Contract Validation | |
| :warning: **No K9 contract files found.** Run `k9iser` to generate contracts. | |
| Generate contracts with: `k9iser generate .` | |
| EOF | |
| else | |
| echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Job 3: Empty-linter — invisible character detection | |
| # --------------------------------------------------------------------------- | |
| empty-lint: | |
| name: "🔴 GATE: Empty-linter (invisible characters)" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Scan for invisible characters | |
| id: lint | |
| run: | | |
| RESULTS_FILE="$RUNNER_TEMP/empty-lint-results.bin" | |
| BLOCKING_FILE="$RUNNER_TEMP/empty-lint-blocking-results.bin" | |
| if ! scripts/check-invisible-characters.sh \ | |
| "$GITHUB_WORKSPACE" "$RESULTS_FILE" "$BLOCKING_FILE"; then | |
| echo "::error::Invisible-character scanner failed; refusing a partial pass" | |
| exit 2 | |
| fi | |
| FINDINGS=0 | |
| while IFS= read -r -d '' filepath; do | |
| FINDINGS=$((FINDINGS + 1)) | |
| REL_PATH="${filepath#"$GITHUB_WORKSPACE"/}" | |
| SAFE_PATH="${REL_PATH//'%'/'%25'}" | |
| SAFE_PATH="${SAFE_PATH//$'\r'/'%0D'}" | |
| SAFE_PATH="${SAFE_PATH//$'\n'/'%0A'}" | |
| SAFE_PATH="${SAFE_PATH//':'/'%3A'}" | |
| SAFE_PATH="${SAFE_PATH//','/'%2C'}" | |
| echo "::warning file=${SAFE_PATH}::Invisible Unicode or C0 characters detected" | |
| done < "$RESULTS_FILE" | |
| BLOCKING=0 | |
| while IFS= read -r -d '' filepath; do | |
| BLOCKING=$((BLOCKING + 1)) | |
| REL_PATH="${filepath#"$GITHUB_WORKSPACE"/}" | |
| SAFE_PATH="${REL_PATH//'%'/'%25'}" | |
| SAFE_PATH="${SAFE_PATH//$'\r'/'%0D'}" | |
| SAFE_PATH="${SAFE_PATH//$'\n'/'%0A'}" | |
| SAFE_PATH="${SAFE_PATH//':'/'%3A'}" | |
| SAFE_PATH="${SAFE_PATH//','/'%2C'}" | |
| echo "::error file=${SAFE_PATH}::C0 control character or NUL byte detected" | |
| done < "$BLOCKING_FILE" | |
| echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT" | |
| echo "blocking=$BLOCKING" >> "$GITHUB_OUTPUT" | |
| echo "ready=true" >> "$GITHUB_OUTPUT" | |
| if [ "$BLOCKING" -gt 0 ]; then | |
| echo "## Empty-linter: BLOCKED — $BLOCKING file(s) contain C0/NUL corruption" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| - name: Write summary | |
| run: | | |
| if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then | |
| FINDINGS="${{ steps.lint.outputs.findings }}" | |
| if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then | |
| echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo ":white_check_mark: No invisible character issues found." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| else | |
| echo "## Empty-Linter" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Skipped: empty-linter not available." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Job 4: Groove manifest check (for repos that should expose services) | |
| # --------------------------------------------------------------------------- | |
| groove-check: | |
| name: "🟡 CHECK: Groove manifest check" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Check for Groove manifest | |
| id: groove | |
| run: | | |
| # Check for static or dynamic Groove endpoints | |
| HAS_MANIFEST="false" | |
| HAS_GROOVE_CODE="false" | |
| # Canonical manifest location is www/.well-known/groove/ (issue #53); | |
| # the repository-root path is accepted, with a warning, during the | |
| # migration window. | |
| MANIFEST="" | |
| if [ -f "www/.well-known/groove/manifest.json" ]; then | |
| MANIFEST="www/.well-known/groove/manifest.json" | |
| elif [ -f ".well-known/groove/manifest.json" ]; then | |
| MANIFEST=".well-known/groove/manifest.json" | |
| echo "::warning::Groove manifest at legacy root .well-known/ — canonical location is www/.well-known/ (run scripts/migrate-wellknown-to-www.sh)" | |
| fi | |
| if [ -n "$MANIFEST" ]; then | |
| HAS_MANIFEST="true" | |
| # Validate the manifest JSON | |
| if ! jq empty "$MANIFEST" 2>/dev/null; then | |
| # Gate, don't annotate: an unparseable manifest is a real error, | |
| # not a warning — same behaviour as the standalone | |
| # groove-check.yml (this job had drifted to annotation-only, | |
| # the class of "check that cannot fail" from nexia-list#49). | |
| echo "::error file=$MANIFEST::Invalid JSON in Groove manifest" | |
| exit 1 | |
| else | |
| SVC_ID=$(jq -r '.service_id // "unknown"' "$MANIFEST") | |
| echo "service_id=$SVC_ID" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| # Check for Groove endpoint code (Rust, Elixir, Zig, V) | |
| if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then | |
| HAS_GROOVE_CODE="true" | |
| fi | |
| # Check if this repo likely serves HTTP in production. Key on | |
| # production HTTP-server framework markers only. A bare `TcpListener` | |
| # is deliberately NOT a signal: it is dominated by test/utility use | |
| # (e.g. a #[cfg(test)] loopback fake), so matching it produced a | |
| # spurious groove nudge on client-only apps. A real hand-rolled Rust | |
| # server still pairs the listener with `axum::serve`/`hyper::Server`, | |
| # which are matched here. | |
| HAS_SERVER="false" | |
| if grep -rl 'Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web\|hyper::Server\|rocket::build\|warp::serve' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null | head -1 | grep -q .; then | |
| HAS_SERVER="true" | |
| fi | |
| echo "has_manifest=$HAS_MANIFEST" >> "$GITHUB_OUTPUT" | |
| echo "has_groove_code=$HAS_GROOVE_CODE" >> "$GITHUB_OUTPUT" | |
| echo "has_server=$HAS_SERVER" >> "$GITHUB_OUTPUT" | |
| if [ "$HAS_SERVER" = "true" ] && [ "$HAS_MANIFEST" = "false" ] && [ "$HAS_GROOVE_CODE" = "false" ]; then | |
| echo "::warning::This repo has server code but no Groove endpoint. Add www/.well-known/groove/manifest.json for service discovery." | |
| fi | |
| - name: Write summary | |
| run: | | |
| echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Static manifest (www/.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY" | |
| # --------------------------------------------------------------------------- | |
| # Job 5: eclexiaiser manifest validation | |
| # --------------------------------------------------------------------------- | |
| eclexiaiser-validate: | |
| name: Validate eclexiaiser manifest | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Check and validate eclexiaiser manifest | |
| id: eclex | |
| run: | | |
| if [ ! -f "eclexiaiser.toml" ]; then | |
| # Check if repo has a Containerfile — if so, recommend eclexiaiser | |
| if [ -f "Containerfile" ]; then | |
| echo "::warning::Containerfile present but no eclexiaiser.toml. Run \`eclexiaiser init\` to scaffold energy/carbon budgets." | |
| fi | |
| echo "has_manifest=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "has_manifest=true" >> "$GITHUB_OUTPUT" | |
| # Validate eclexiaiser.toml structure (bash + grep; NO Python per estate policy). | |
| # Structural presence checks only — deep schema validation is eclexiaiser's own job. | |
| err=0 | |
| grep -qE '^[[:space:]]*\[project\]' eclexiaiser.toml || { echo "::error file=eclexiaiser.toml::[project] section is required"; err=1; } | |
| grep -qE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]+"' eclexiaiser.toml || { echo "::error file=eclexiaiser.toml::a non-empty name is required"; err=1; } | |
| grep -qE '^[[:space:]]*\[\[functions\]\]' eclexiaiser.toml || { echo "::error file=eclexiaiser.toml::at least one [[functions]] entry is required"; err=1; } | |
| if [ "$err" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| fns=$(grep -cE '^[[:space:]]*\[\[functions\]\]' eclexiaiser.toml) | |
| echo "Valid: eclexiaiser.toml structure present (${fns} function block(s))" | |
| - name: Write summary | |
| run: | | |
| if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then | |
| echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo ":ballot_box_with_check: No eclexiaiser.toml. Add one with \`eclexiaiser init\` for energy/carbon tracking." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Job 6: Canon lockstep — does this template still implement the canon? | |
| # | |
| # The canon<->spine binding. hyperpolymath/standards publishes canon.lock: the | |
| # released identity of the law, with a sha256 per law artefact. This repo | |
| # declares the SAME hashes in .machine_readable/rsr-profile.a2ml [canon]. | |
| # | |
| # Before this job, the declaration was two free-text strings | |
| # (`spec = "rsr-criteria-v2"`, `declares-against = "2.0.0-draft"`) and nothing | |
| # failed when the canon changed. This makes it a comparison. | |
| # | |
| # Deliberately a bare `run:` step with NO `uses:`. Adding an action reference | |
| # would have required an actions.lock row, and this repo's lock records | |
| # FLOATING refs ('actions/checkout@v7.0.1') where the canon's records SHAs - | |
| # so a pinned `uses:` here would mismatch the lock and fail at LOAD time with | |
| # `jobs=0` and no annotation. Read-only over the public internet needs no auth. | |
| # --------------------------------------------------------------------------- | |
| canon-lockstep: | |
| name: Canon lockstep | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Fetch the canon's released identity | |
| id: canon | |
| run: | | |
| set -euo pipefail | |
| LOCK="$RUNNER_TEMP/canon.lock" | |
| URL="https://raw.githubusercontent.com/hyperpolymath/standards/main/canon.lock" | |
| if ! curl -fsSL --retry 3 --max-time 30 "$URL" -o "$LOCK"; then | |
| echo "::error::could not fetch canon.lock from $URL" | |
| echo "The canon does not publish a released identity, so this repo" | |
| echo "cannot be shown to implement it. Fix at source: add canon.lock." | |
| exit 1 | |
| fi | |
| # Read (not compute) the criteria hash canon.lock asserts. | |
| # | |
| # awk only JOINS the record — the artefact entries are inline tables | |
| # that span lines — then grep -oE extracts the hash. Deliberately the | |
| # same two-step the canon's own check-canon-lockstep.sh uses, so | |
| # there is one technique across both repos. | |
| # | |
| # Two traps this avoids, both found by running it rather than reading | |
| # it. (1) An earlier version used `gsub(/.*"|"/,"",s)` to strip the | |
| # quotes; the `.*"` is GREEDY, so it ate the entire string and the | |
| # hash came out EMPTY. (2) awk interval expressions are not portable | |
| # across mawk/gawk builds; grep -oE is. | |
| rec="$(awk ' | |
| /^[[:space:]]*#/ { next } | |
| /^criteria[[:space:]]*=/ { on=1 } | |
| on { | |
| line=$0; sub(/#.*/,"",line); rec = rec " " line | |
| if (line ~ /}/) { on=0 } | |
| } | |
| END { print rec }' "$LOCK")" | |
| want="$(printf '%s\n' "$rec" \ | |
| | grep -oE 'sha256[[:space:]]*=[[:space:]]*"[0-9a-f]{64}"' \ | |
| | grep -oE '[0-9a-f]{64}' | head -1)" | |
| if [ -z "$want" ]; then | |
| echo "::error::canon.lock carries no criteria sha256 — malformed, or" | |
| echo "the criteria record no longer starts a line with 'criteria ='." | |
| exit 1 | |
| fi | |
| echo "want=$want" >> "$GITHUB_OUTPUT" | |
| - name: Compare this repo's declared pin | |
| env: | |
| WANT: ${{ steps.canon.outputs.want }} | |
| # This job has NO `uses:` steps, deliberately: any added action would | |
| # have to be pinned in actions.lock, and `uses ⊆ actions.lock` is already | |
| # failing on main, so this job must not make it worse. That means no | |
| # actions/checkout — and therefore NO WORKING TREE. Which is what | |
| # broke this job: it read "machine-readable/rsr-profile.a2ml" from a | |
| # directory that was never populated, so it failed 100% of the time | |
| # with "no rsr-profile.a2ml", and would have whatever the pin said. | |
| # | |
| # Fetch the profile by SHA instead, exactly as canon.lock is fetched | |
| # above. Deliberately NOT github.sha: on pull_request that is the | |
| # ephemeral MERGE commit, which is not on the remote and would 404. | |
| # The event's head sha is the commit that actually exists there. | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| run: | | |
| set -euo pipefail | |
| PROFILE="$RUNNER_TEMP/rsr-profile.a2ml" | |
| fetched="" | |
| for p in ".machine_readable/rsr-profile.a2ml" "machine-readable/rsr-profile.a2ml"; do | |
| URL="https://raw.githubusercontent.com/hyperpolymath/dicta-task/${HEAD_SHA}/${p}" | |
| if curl -fsSL --retry 3 --max-time 30 "$URL" -o "$PROFILE" 2>/dev/null; then | |
| fetched="$p"; break | |
| fi | |
| done | |
| if [ -z "$fetched" ]; then | |
| echo "::error::no rsr-profile.a2ml at ${HEAD_SHA:0:7} — this repo cannot declare a canon pin" | |
| echo "Looked for .machine_readable/rsr-profile.a2ml and machine-readable/rsr-profile.a2ml." | |
| exit 1 | |
| fi | |
| echo "profile: $fetched @ ${HEAD_SHA:0:7}" | |
| # ALL THREE PINS, BOTH SIDES. | |
| # | |
| # This step used to compare only criteria_sha256, while this repo's own | |
| # rsr-profile.a2ml stated that "these values ... equal canon.lock | |
| # [canon.artifacts]" — plural. The claim was wider than the check, so | |
| # `version` and `gates_sha256` could sit at stale values and nothing | |
| # reported it. They did: canon 2.0.1 changed gates_sha256, and this repo | |
| # kept asserting 2.0.0 and the superseded hash. A pin nothing verifies | |
| # is not a binding. See hyperpolymath/standards docs/AUDIT.adoc F8. | |
| # | |
| # Both readers below are the technique the canon's own | |
| # check-canon-lockstep.sh uses, so there is one idiom across both repos. | |
| canon_key() { | |
| awk -v key="$1" ' | |
| /^[[:space:]]*#/ { next } | |
| /^\[/ { f = ($0 == "[canon]") ? 1 : 0; next } | |
| f && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { | |
| sub(/^[^=]*=[[:space:]]*/, ""); gsub(/^["[:space:]]+|["[:space:]]+$/, ""); print; exit | |
| }' "$2" | |
| } | |
| # The artefact records are inline tables spanning lines, so awk JOINS the | |
| # record and grep extracts the hash. Deliberately not anchored to the | |
| # sha256 being on any particular line of the record. | |
| artefact_hash() { | |
| awk -v key="$1" ' | |
| $0 ~ "^" key "[[:space:]]*=" { f = 1 } | |
| f { buf = buf " " $0 } | |
| f && /sha256[[:space:]]*=/ { print buf; exit } | |
| ' "$2" | grep -oE '[0-9a-f]{64}' | head -1 | |
| } | |
| want_crit="$WANT" | |
| want_gates="$(artefact_hash gates "$RUNNER_TEMP/canon.lock")" | |
| want_ver="$(canon_key version "$RUNNER_TEMP/canon.lock")" | |
| got_crit="$(canon_key criteria_sha256 "$PROFILE")" | |
| got_gates="$(canon_key gates_sha256 "$PROFILE")" | |
| got_ver="$(canon_key version "$PROFILE")" | |
| # An unreadable pin must not pass. Hashing or parsing nothing yields an | |
| # empty string, and `"" = ""` is a green build with nothing behind it — | |
| # the same class of bug as this job reading a file that was never there. | |
| for pair in "want_crit:$want_crit" "want_gates:$want_gates" "want_ver:$want_ver" \ | |
| "got_crit:$got_crit" "got_gates:$got_gates" "got_ver:$got_ver"; do | |
| if [ -z "${pair#*:}" ]; then | |
| echo "::error::could not read ${pair%%:*} — refusing to compare an empty value" | |
| exit 1 | |
| fi | |
| done | |
| ok=1 | |
| row() { | |
| mark=":x:" | |
| if [ "$2" = "$3" ]; then mark=":white_check_mark:"; else ok=0; fi | |
| printf '| `%s` | `%s` | `%s` | %s |\n' "$1" "${2:0:16}" "${3:0:16}" "$mark" | |
| } | |
| { | |
| echo "## Canon lockstep" | |
| echo "" | |
| echo "| pin | canon.lock asserts | this repo declares | |" | |
| echo "|---|---|---|---|" | |
| row version "$want_ver" "$got_ver" | |
| row criteria_sha256 "$want_crit" "$got_crit" | |
| row gates_sha256 "$want_gates" "$got_gates" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$ok" = "1" ]; then | |
| echo ":white_check_mark: This template implements the canon at HEAD." | |
| exit 0 | |
| fi | |
| echo "::error::canon lockstep broken — the template does not implement the canon at HEAD" | |
| { | |
| echo "" | |
| echo ":x: **Lockstep broken.**" | |
| echo "" | |
| echo "The canon's law changed and this template has not adopted it." | |
| echo "Order matters (\`canon.lock [canon.lockstep].order\` is" | |
| echo "\`spine-adopts-then-canon-releases\`): re-pin \`[canon]\` in" | |
| echo "\`.machine_readable/rsr-profile.a2ml\` to the new hashes and fix any" | |
| echo "criteria this repo now fails, BEFORE the canon release lands." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| # --------------------------------------------------------------------------- | |
| # Job 7: Dogfooding summary | |
| # --------------------------------------------------------------------------- | |
| dogfood-summary: | |
| name: "ℹ️ ADVISORY: Dogfooding compliance summary (non-gating)" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [deed-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate, canon-lockstep] | |
| if: always() | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Generate dogfooding scorecard | |
| run: | | |
| SCORE=0 | |
| MAX=6 | |
| # DEED manifest present? (.a2ml legacy extension accepted during standards #837 migration) | |
| if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then | |
| SCORE=$((SCORE + 1)) | |
| DEED_STATUS=":white_check_mark:" | |
| else | |
| DEED_STATUS=":x:" | |
| fi | |
| # K9 contracts present? | |
| if find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | head -1 | grep -q .; then | |
| SCORE=$((SCORE + 1)) | |
| K9_STATUS=":white_check_mark:" | |
| else | |
| K9_STATUS=":x:" | |
| fi | |
| # .editorconfig present? | |
| if [ -f ".editorconfig" ]; then | |
| SCORE=$((SCORE + 1)) | |
| EC_STATUS=":white_check_mark:" | |
| else | |
| EC_STATUS=":x:" | |
| fi | |
| # Groove manifest or code? | |
| if [ -f "www/.well-known/groove/manifest.json" ] || [ -f ".well-known/groove/manifest.json" ] || grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' . 2>/dev/null | head -1 | grep -q .; then | |
| SCORE=$((SCORE + 1)) | |
| GROOVE_STATUS=":white_check_mark:" | |
| else | |
| GROOVE_STATUS=":ballot_box_with_check:" | |
| fi | |
| # VeriSimDB integration? | |
| if grep -rl 'verisimdb\|VeriSimDB' --include='*.toml' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.rs' --include='*.ex' . 2>/dev/null | head -1 | grep -q .; then | |
| SCORE=$((SCORE + 1)) | |
| VSDB_STATUS=":white_check_mark:" | |
| else | |
| VSDB_STATUS=":ballot_box_with_check:" | |
| fi | |
| # eclexiaiser energy tracking? | |
| if [ -f "eclexiaiser.toml" ]; then | |
| SCORE=$((SCORE + 1)) | |
| ECLEX_STATUS=":white_check_mark:" | |
| else | |
| ECLEX_STATUS=":ballot_box_with_check:" | |
| fi | |
| cat <<EOF >> "$GITHUB_STEP_SUMMARY" | |
| ## Dogfooding Scorecard | |
| **Score: ${SCORE}/${MAX}** | |
| | Tool/Format | Status | Notes | | |
| |-------------|--------|-------| | |
| | DEED repo deed (`<reponame>_chora.deed`) | ${DEED_STATUS} | Required for all RSR repos | | |
| | K9 contracts | ${K9_STATUS} | Required for repos with config files | | |
| | .editorconfig | ${EC_STATUS} | Required for all repos | | |
| | Groove endpoint | ${GROOVE_STATUS} | Required for service repos | | |
| | VeriSimDB integration | ${VSDB_STATUS} | Required for stateful repos | | |
| | eclexiaiser | ${ECLEX_STATUS} | Energy/carbon budgets for container services | | |
| --- | |
| *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/dicta-task) workflow.* | |
| *Dogfooding is guinea pig fooding — we test our tools on ourselves.* | |
| EOF | |
| rsr-score-self: | |
| name: RSR oracle — dogfood this repo | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| repository: hyperpolymath/standards | |
| path: _standards | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| repository: hyperpolymath/hypatia | |
| path: _hypatia | |
| - name: Setup Elixir for the oracle | |
| uses: erlef/setup-beam@v1.24.1 | |
| with: | |
| elixir-version: '1.19.4' | |
| otp-version: '28.3' | |
| - name: Compile the oracle | |
| working-directory: _hypatia | |
| run: mix deps.get && mix compile | |
| - name: Score this repo against the canon | |
| working-directory: _hypatia | |
| run: | | |
| # --ssot is explicit until the hypatia M-3 fix lands (the | |
| # --standards default still points at the pre-reorg path). | |
| mix hypatia.rsr_score .. \ | |
| --ssot ../_standards/0-canon/rsr/rsr-criteria-v2.a2ml \ | |
| --write | |
| - name: Show the scorecard | |
| if: always() | |
| run: | | |
| if [ -f .machine_readable/descriptiles/SCORECARD.a2ml ]; then | |
| cat .machine_readable/descriptiles/SCORECARD.a2ml | |
| else | |
| echo "::warning::no scorecard written — the oracle failed; see the step log" | |
| fi | |
| # Deliberately NO --fail-under: the point is to publish the honest | |
| # scorecard (provisional=true until the coverage worklist is triaged), | |
| # not to gate the spine on itself. Tightens in the PR that lands the | |
| # first coverage tranche. |