Dogfood Gate #437
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. | |
| # 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] | |
| permissions: | |
| actions: read | |
| contents: read | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Job 1: A2ML manifest validation | |
| # --------------------------------------------------------------------------- | |
| a2ml-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 A2ML files | |
| id: detect | |
| run: | | |
| COUNT=$(find . -type f \( -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 A2ML manifests | |
| if: steps.detect.outputs.count > 0 | |
| run: bash .github/hooks/validate-a2ml.sh | |
| - name: Write summary | |
| run: | | |
| A2ML_COUNT="${{ steps.detect.outputs.count }}" | |
| if [ "$A2ML_COUNT" -eq 0 ]; then | |
| cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" | |
| ## A2ML Validation | |
| :warning: **No .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`<reponame>_chora.deed`) at its root. | |
| Copy one from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). | |
| EOF | |
| else | |
| echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Scanned **${A2ML_COUNT}** manifest file(s) (.deed, or legacy .a2ml). 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 | |
| run: bash .github/hooks/validate-k9.sh | |
| - 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 .a2ml/.deed manifest files found.** Every RSR-compliant repo should have a repo deed (`<reponame>_chora.deed`) at its root. | |
| 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: 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: | | |
| # Inline invisible character detection (from empty-linter's core patterns). | |
| # Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens, | |
| # non-breaking spaces, null bytes, and other invisible Unicode in source files. | |
| python3 - <<'PY' | |
| import os | |
| from pathlib import Path | |
| root = Path(os.environ["GITHUB_WORKSPACE"]) | |
| skipped_dirs = { | |
| ".cache", ".deno", ".elixir_ls", ".git", ".lake", ".zig-cache", | |
| "_build", "build", "coverage", "deps", "dist", "external_corpora", | |
| "node_modules", "out", "target", "vendor", "zig-cache", "zig-out", | |
| } | |
| intentional_fixture_dirs = { | |
| ("tests", "fixtures", "bom-detection"), | |
| ("tests", "fixtures", "empty-linter"), | |
| } | |
| # Generated files are not linted for invisible characters: their | |
| # content comes from tooling/history, not authors. CHANGELOG.md is | |
| # regenerated by the changelog bot from squash-merge commit messages | |
| # that contain GitHub's ZWSP-defused @-mentions (@\u200buser) — linting | |
| # it flips main red on every bot regeneration (#192 did exactly | |
| # that: 206 hits, zero of them authored). Estate-grade fix (strip | |
| # at generation in canonical cliff.toml) is tracked upstream. | |
| generated_files = {"CHANGELOG.md"} | |
| source_suffixes = { | |
| ".adoc", ".adb", ".ads", ".agda", ".c", ".cc", ".clj", ".cljs", | |
| ".cpp", ".erl", ".ex", ".exs", ".fs", ".fsi", ".fsx", ".gleam", | |
| ".h", ".hh", ".hpp", ".hrl", ".hs", ".idr", ".java", ".jl", | |
| ".js", ".json", ".kt", ".kts", ".lean", ".lua", ".md", ".ml", | |
| ".php", ".r", ".rb", ".res", ".rs", ".scala", ".sh", ".swift", | |
| ".toml", ".ts", ".v", ".yaml", ".yml", ".zig", | |
| } | |
| invisible_codepoints = { | |
| 0x00A0, 0x00AD, 0x2060, 0xFEFF, | |
| *range(0x200B, 0x2010), | |
| *range(0x202A, 0x2030), | |
| *range(0x2066, 0x206A), | |
| } | |
| def command_escape(value): | |
| return str(value).replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") | |
| def property_escape(value): | |
| return command_escape(value).replace(":", "%3A").replace(",", "%2C") | |
| # Runtime regression for GitHub workflow-command property delimiters. | |
| assert property_escape("docs/a,b::c.md") == "docs/a%2Cb%3A%3Ac.md" | |
| def intentionally_invalid_fixture(relative): | |
| return any(relative.parts[:len(prefix)] == prefix for prefix in intentional_fixture_dirs) | |
| findings = [] | |
| errors = [] | |
| for directory, dirnames, filenames in os.walk(root, topdown=True): | |
| dirnames[:] = [name for name in dirnames if name not in skipped_dirs] | |
| directory_path = Path(directory) | |
| for filename in filenames: | |
| path = directory_path / filename | |
| relative = path.relative_to(root) | |
| if ( | |
| path.is_symlink() | |
| or path.suffix.lower() not in source_suffixes | |
| or intentionally_invalid_fixture(relative) | |
| or relative.as_posix() in generated_files | |
| ): | |
| continue | |
| try: | |
| data = path.read_bytes() | |
| except OSError as error: | |
| errors.append((relative, f"could not read file: {error}")) | |
| continue | |
| reasons = set() | |
| if data.startswith(b"\xef\xbb\xbf"): | |
| reasons.add("leading UTF-8 BOM") | |
| if any(byte <= 0x08 or byte in (0x0B, 0x0C) or 0x0E <= byte <= 0x1F for byte in data): | |
| reasons.add("C0 control character") | |
| try: | |
| text_content = data.decode("utf-8", errors="strict") | |
| except UnicodeDecodeError as error: | |
| errors.append((relative, f"invalid UTF-8 at byte {error.start}")) | |
| continue | |
| if any(ord(character) in invisible_codepoints for character in text_content): | |
| reasons.add("invisible Unicode code point") | |
| if reasons: | |
| findings.append((relative, ", ".join(sorted(reasons)))) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: | |
| output.write(f"findings={len(findings)}\n") | |
| output.write(f"exit_code={2 if errors else 0}\n") | |
| output.write("ready=true\n") | |
| for relative, reasons in findings: | |
| print(f"::warning file={property_escape(relative)}::Invisible characters detected: {command_escape(reasons)}") | |
| for relative, reason in errors: | |
| print(f"::error file={property_escape(relative)}::Invisible-character scan failed: {command_escape(reason)}") | |
| PY | |
| - name: Write summary | |
| run: | | |
| if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then | |
| FINDINGS="${{ steps.lint.outputs.findings }}" | |
| EXIT_CODE="${{ steps.lint.outputs.exit_code }}" | |
| if [ "$EXIT_CODE" -ne 0 ] 2>/dev/null; then | |
| echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo ":x: Scanner execution failed; see error annotations above." >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| 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" | |
| exit 1 | |
| 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: 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" | |
| if [ -f ".well-known/groove/manifest.json" ]; then | |
| HAS_MANIFEST="true" | |
| # Validate the manifest JSON | |
| if ! jq empty .well-known/groove/manifest.json 2>/dev/null; then | |
| echo "::error file=.well-known/groove/manifest.json::Invalid JSON in Groove manifest" | |
| else | |
| SVC_ID=$(jq -r '.service_id // "unknown"' .well-known/groove/manifest.json) | |
| 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 (has server/listener code) | |
| HAS_SERVER="false" | |
| if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --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 .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 (.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: Dogfooding summary | |
| # --------------------------------------------------------------------------- | |
| dogfood-summary: | |
| name: Dogfooding compliance summary | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [a2ml-validate, k9-validate, empty-lint, groove-check] | |
| if: always() | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7.0.1 | |
| - name: Generate dogfooding scorecard | |
| run: | | |
| SCORE=0 | |
| MAX=5 | |
| # A2ML manifest present? | |
| if find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then | |
| SCORE=$((SCORE + 1)) | |
| A2ML_STATUS=":white_check_mark:" | |
| else | |
| A2ML_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 ".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 | |
| cat <<EOF >> "$GITHUB_STEP_SUMMARY" | |
| ## Dogfooding Scorecard | |
| **Score: ${SCORE}/${MAX}** | |
| | Tool/Format | Status | Notes | | |
| |-------------|--------|-------| | |
| | AI manifest (0-AI-MANIFEST.deed, or legacy .a2ml) | ${A2ML_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 | | |
| --- | |
| *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.* | |
| *Dogfooding is guinea pig fooding — we test our tools on ourselves.* | |
| EOF | |