Skip to content

Commit 431adbb

Browse files
refactor(governance): subsume language-policy.yml + add deno-ci-reusable (semantics-level fix for estate-template drift) (#168)
## Why Multiple PRs in the estate inbox are failing on a single bug class: **per-repo workflow templates copy-drift away from the canonical estate policy**. The repro that surfaced it was self-inflicted: PR #167 to this repo failed its own \`Check for Banned Languages\` step because \`language-policy.yml\` does a naive \`find -name \"*.py\"\` that bypasses \`.hypatia-baseline.json\` — even though the file in question is already acknowledged there. The same gap appears in absolute-zero, maa-framework, and any other repo that still ships a local copy of either workflow. Patching each copy individually treats the symptom; the foundational fix is to delete the copies and consume a single reusable workflow. ## What changes | File | Change | |---|---| | \`.github/workflows/governance-reusable.yml\` | language-policy job extended: now also catches Makefiles, Java/Kotlin, Swift, Flutter/Dart, V-lang manifests, ATS2 sources, and \`package.json\` with runtime \`\"dependencies\"\`. All go through the existing \`enforce()\` / \`is_exempt()\` helpers — so every newly-covered check honours \`.hypatia-baseline.json\`, \`.hypatia-ignore\`, and inline \`# hypatia:ignore …\` pragmas. | | \`.github/workflows/language-policy.yml\` | Deleted. \`governance.yml\` (the thin wrapper) already calls \`governance-reusable.yml\` via \`workflow_call\`, so estate coverage is unchanged — but the bug class becomes structurally impossible: the naive find-based check no longer exists to be drift-copied. | | \`.github/workflows/deno-ci-reusable.yml\` | **New**. Replaces the broken \`rescript-deno-ci.yml\` template that's been failing across the estate. Detects Deno targets/tests/config first and only runs lint/fmt/test/check when applicable. Declares top-level \`permissions:\`. Drops the banned \`npx rescript\` step and the vestigial security-grep job. | ## What was broken in the old \`rescript-deno-ci.yml\` Observed across absolute-zero #41 and tma-mark2 #41: * \`deno test --allow-all\` ran unconditionally and exit-1'd on \`No test modules found\` in repos without Deno tests. * \`deno lint\` / \`deno fmt --check\` ran unconditionally and exit-1'd on \`No target files found\` in repos without JS/TS targets. * Missing top-level \`permissions:\` declaration — tripped governance-reusable's workflow-security-linter. * Still ran \`npm install && npx rescript\` even though ReScript was banned in new code 2026-04-30 (per \`.claude/CLAUDE.md\`). * Vestigial \`security\` job that grep'd for permission strings and did nothing with the output. ## Downstream rollout Each repo carrying \`rescript-deno-ci.yml\` or its own \`language-policy.yml\` is now eligible to delete its copy and consume the reusable: \`\`\`yaml jobs: deno-ci: uses: hyperpolymath/standards/.github/workflows/deno-ci-reusable.yml@main \`\`\` Follow-up PRs will land per-repo against absolute-zero, tma-mark2, etc. ## Relationship to PR #167 #167 patches the tsconfig/rescript-config check in this same file to honour \`.hypatia-baseline.json\`. This PR is orthogonal: #167 fixes a single step's exemption logic; this PR consolidates two workflows so the duplication itself goes away. Either can land first; rebase is trivial. ## Test plan - [ ] Governance reusable still passes on this PR (its own CI run will exercise the consolidated language-policy job, including against this repo's \`a2ml-templates/state-scm-to-v2.py\` baseline entry — which the reusable already honours). - [ ] Workflow-security-linter check passes (deno-ci-reusable has SPDX + top-level \`permissions:\`). - [ ] Once merged, run a fanout PR converting \`absolute-zero\` + \`tma-mark2\` to the wrappers and confirm green.
1 parent 91d8b88 commit 431adbb

3 files changed

Lines changed: 151 additions & 193 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# deno-ci-reusable.yml — Reusable Deno CI bundle (RSR).
3+
#
4+
# Replaces the per-repo `rescript-deno-ci.yml` template that copy-drifted
5+
# across the estate with several systemic bugs:
6+
#
7+
# * `deno test` ran unconditionally and failed `No test modules found`
8+
# on repos with no Deno tests.
9+
# * `deno lint` / `deno fmt --check` ran unconditionally and failed
10+
# `No target files found` on repos with no JS/TS targets.
11+
# * No top-level `permissions:` declaration, tripping the workflow
12+
# security linter in governance-reusable.yml.
13+
# * Still invoked `npx rescript` even though ReScript is banned in new
14+
# code as of 2026-04-30 (see standards/.claude/CLAUDE.md).
15+
# * The vestigial "security" job grep-audited for permission strings
16+
# without doing anything useful with the result.
17+
#
18+
# Each step now early-exits cleanly when the inputs don't exist, so the
19+
# job is a no-op on repos that don't yet ship Deno code but pass cleanly
20+
# when they start to.
21+
#
22+
# Caller example (single wrapper, mirrors governance.yml's pattern):
23+
#
24+
# jobs:
25+
# deno-ci:
26+
# uses: hyperpolymath/standards/.github/workflows/deno-ci-reusable.yml@main
27+
28+
name: Deno CI (reusable)
29+
30+
on:
31+
workflow_call:
32+
inputs:
33+
runs-on:
34+
description: Runner label for the deno-ci job
35+
type: string
36+
required: false
37+
default: ubuntu-latest
38+
deno-version:
39+
description: Deno version selector
40+
type: string
41+
required: false
42+
default: v2.x
43+
44+
permissions:
45+
contents: read
46+
47+
jobs:
48+
deno-ci:
49+
name: Deno CI
50+
runs-on: ${{ inputs.runs-on }}
51+
permissions:
52+
contents: read
53+
steps:
54+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
55+
with:
56+
repository: ${{ github.repository }}
57+
ref: ${{ github.ref }}
58+
59+
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
60+
with:
61+
deno-version: ${{ inputs.deno-version }}
62+
63+
- name: Detect Deno targets
64+
id: detect
65+
run: |
66+
# `deno.json` (or its legacy `deno.jsonc`) is the canonical
67+
# signal that this repo opts into Deno tooling. We also count
68+
# raw .ts/.js files as a fallback for repos still mid-migration
69+
# to deno.json. Either way, the per-step guards below honour
70+
# any `include` / `exclude` scoping the consumer declared in
71+
# deno.json.
72+
has_config=false
73+
has_targets=false
74+
has_tests=false
75+
if [ -f deno.json ] || [ -f deno.jsonc ]; then has_config=true; fi
76+
if find . -path ./node_modules -prune -o -path ./.git -prune \
77+
-o \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.mjs" \) \
78+
-type f -print 2>/dev/null | head -1 | grep -q .; then
79+
has_targets=true
80+
fi
81+
if find . -path ./node_modules -prune -o -path ./.git -prune \
82+
-o \( -name "*_test.ts" -o -name "*.test.ts" -o -name "*_test.js" -o -name "*.test.js" \) \
83+
-type f -print 2>/dev/null | head -1 | grep -q .; then
84+
has_tests=true
85+
fi
86+
echo "has_config=$has_config" >> "$GITHUB_OUTPUT"
87+
echo "has_targets=$has_targets" >> "$GITHUB_OUTPUT"
88+
echo "has_tests=$has_tests" >> "$GITHUB_OUTPUT"
89+
echo "deno-ci: config=$has_config targets=$has_targets tests=$has_tests"
90+
91+
- name: Deno lint
92+
if: steps.detect.outputs.has_targets == 'true' || steps.detect.outputs.has_config == 'true'
93+
run: deno lint
94+
95+
- name: Deno fmt check
96+
if: steps.detect.outputs.has_targets == 'true' || steps.detect.outputs.has_config == 'true'
97+
run: deno fmt --check
98+
99+
- name: Deno test
100+
if: steps.detect.outputs.has_tests == 'true'
101+
run: deno test --allow-all --coverage=coverage
102+
103+
- name: Deno type check
104+
if: steps.detect.outputs.has_targets == 'true'
105+
# Soft-pass: `deno check` exits non-zero on unresolved imports we
106+
# don't yet require contributors to vendor. We surface output for
107+
# diagnostics but don't fail the gate.
108+
run: deno check . || echo "::warning::deno check reported issues (non-blocking)."
109+
110+
- name: Summary
111+
run: |
112+
if [ "${{ steps.detect.outputs.has_config }}" = "false" ] \
113+
&& [ "${{ steps.detect.outputs.has_targets }}" = "false" ]; then
114+
echo "deno-ci: no Deno targets detected — skipped all checks."
115+
else
116+
echo "deno-ci: passed (lint / fmt / type / test where applicable)."
117+
fi

.github/workflows/governance-reusable.yml

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ jobs:
236236
# 3. Inline `# hypatia:ignore ...` pragma in the file's first
237237
# 8 lines — the same escape the Hypatia scanner itself
238238
# honours.
239-
- name: Check for ReScript / Go / Python (banned language files)
239+
- name: Check banned-language files (ReScript / Go / Python / Java / Kotlin / Swift / Dart / V-lang / ATS2 / Makefile)
240240
run: |
241241
rule_module="cicd_rules"
242242
rule_type="banned_language_file"
@@ -300,15 +300,36 @@ jobs:
300300
echo "✅ No non-exempt ${label}"
301301
}
302302
303-
RES_FILES=$(find . -name "*.res" | grep -v node_modules || true)
304-
GO_FILES=$(find . -name "*.go" || true)
305-
PY_FILES=$(find . -name "*.py" | grep -v salt | grep -v _states \
303+
RES_FILES=$(find . -name "*.res" -not -path "./node_modules/*" -not -path "./.git/*" || true)
304+
GO_FILES=$(find . -name "*.go" -not -path "./.git/*" || true)
305+
PY_FILES=$(find . -name "*.py" -not -path "./.git/*" \
306+
| grep -v salt | grep -v _states \
306307
| grep -v _modules | grep -v pillar | grep -v venv \
307308
| grep -v __pycache__ || true)
309+
MAKE_FILES=$(find . \( -name "Makefile" -o -name "Makefile.*" -o -name "*.mk" \) \
310+
-not -path "./.git/*" -not -path "./.github/*" || true)
311+
JAVA_FILES=$(find . \( -name "*.java" -o -name "*.kt" -o -name "*.kts" \) \
312+
-not -path "./.git/*" || true)
313+
SWIFT_FILES=$(find . -name "*.swift" -not -path "./.git/*" || true)
314+
DART_FILES=$(find . \( -name "*.dart" -o -name "pubspec.yaml" \) \
315+
-not -path "./.git/*" || true)
316+
# V-lang detected by manifest (v.mod / vpkg.json); the .v extension
317+
# collides with Verilog so we never key on it.
318+
VMOD_FILES=$(find . \( -name "v.mod" -o -name "vpkg.json" \) \
319+
-not -path "./node_modules/*" -not -path "./.git/*" || true)
320+
# ATS2 source extensions: rejected in favour of Idris2 / Rust/SPARK.
321+
ATS2_FILES=$(find . \( -name "*.dats" -o -name "*.sats" -o -name "*.hats" \) \
322+
-not -path "./.git/*" || true)
308323
309324
enforce "ReScript files" "use AffineScript instead" "$RES_FILES"
310325
enforce "Go files" "use Rust/WASM instead" "$GO_FILES"
311326
enforce "Python files" "only allowed for SaltStack" "$PY_FILES"
327+
enforce "Makefiles" "use Mustfile/justfile instead" "$MAKE_FILES"
328+
enforce "Java/Kotlin files" "use Rust/Tauri/Dioxus instead" "$JAVA_FILES"
329+
enforce "Swift files" "use Tauri/Dioxus instead" "$SWIFT_FILES"
330+
enforce "Flutter/Dart files" "use Tauri/Dioxus instead (Google lock-in)" "$DART_FILES"
331+
enforce "V-lang manifests (v.mod / vpkg.json)" "V-lang is banned since 2026-04-10 — migrate to Zig" "$VMOD_FILES"
332+
enforce "ATS2 files (.dats / .sats / .hats)" "use Idris2 or Rust/SPARK instead" "$ATS2_FILES"
312333
313334
- name: Check for npm/bun artifacts
314335
# standards#67 — npm-avoidant: package-lock.json must never be tracked
@@ -340,6 +361,15 @@ jobs:
340361
printf '%s\n' "$NPMRC_FILES"
341362
FAILED=1
342363
fi
364+
# Root package.json with runtime "dependencies" — moved here from
365+
# the now-deleted language-policy.yml. devDependencies-only is
366+
# tolerated for transitional tooling (e.g., a legacy bundler
367+
# invoked from Justfile), but a `"dependencies"` field
368+
# indicates Node.js runtime use, which Deno replaces.
369+
if [ -f package.json ] && grep -q '"dependencies"[[:space:]]*:[[:space:]]*{[^}]' package.json; then
370+
echo "❌ package.json with runtime \"dependencies\" detected. Use deno.json imports instead."
371+
FAILED=1
372+
fi
343373
if [ -n "$FAILED" ]; then
344374
echo "See hyperpolymath/standards docs/JS-RUNTIME-POLICY.adoc for remediation."
345375
exit 1

.github/workflows/language-policy.yml

Lines changed: 0 additions & 189 deletions
This file was deleted.

0 commit comments

Comments
 (0)