ci(antipattern): TS check reads .claude/CLAUDE.md exemption table #196
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
| # SPDX-License-Identifier: PMPL-1.0-or-later | |
| # RSR Anti-Pattern CI Check | |
| # SPDX-License-Identifier: PMPL-1.0-or-later | |
| # | |
| # Enforces: No TypeScript, No Go, No Python (except SaltStack), No npm | |
| # Allows: ReScript, Deno, WASM, Rust, OCaml, Haskell, Guile/Scheme | |
| name: RSR Anti-Pattern Check | |
| on: | |
| push: | |
| branches: [main, master, develop] | |
| pull_request: | |
| branches: [main, master, develop] | |
| permissions: read-all | |
| jobs: | |
| antipattern-check: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Check for TypeScript | |
| run: | | |
| python3 << 'PYEOF' | |
| import re, sys, fnmatch, pathlib | |
| # Universal builtin allowlist — bridges that need no per-repo declaration. | |
| # Files matching any of these patterns are always allowed. | |
| BUILTIN_GLOBS = [ | |
| '*.d.ts', | |
| '**/bindings/**', | |
| '**/tests/**', '**/test/**', | |
| '**/scripts/**', | |
| '**/mcp-adapter/**', | |
| '**/*vscode*/**', | |
| '**/cli/**', | |
| '**/mod.ts', | |
| '**/lsp-server.ts', '**/lsp_server.ts', '**/lsp.ts', '**/*-lsp.ts', | |
| '**/deno-*/**', | |
| '**/node_modules/**', | |
| '**/vendor/**', | |
| '**/examples/**', | |
| '**/ffi/**', | |
| ] | |
| # Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table. | |
| # Single source of truth — adding a row here unblocks CI for that path. | |
| # Format expected: | |
| # ### TypeScript Exemptions ... | |
| # | Path | Files | Rationale | Unblock condition | | |
| # |---|---|---|---| | |
| # | `path/to/file.ts` | 1 | ... | ... | | |
| # | `dir/*.ts` | 6 | ... | ... | | |
| exemptions = [] | |
| claude_md = pathlib.Path('.claude/CLAUDE.md') | |
| if claude_md.exists(): | |
| in_table = False | |
| for line in claude_md.read_text(encoding='utf-8').splitlines(): | |
| if re.search(r'TypeScript [Ee]xemptions', line): | |
| in_table = True | |
| continue | |
| if in_table and line.startswith(('### ', '## ', '# ')): | |
| break | |
| if in_table and line.startswith('|'): | |
| m = re.match(r'\|\s*`([^`]+)`', line) | |
| if m: | |
| exemptions.append(m.group(1)) | |
| # Find all .ts and .tsx files | |
| found = [] | |
| for ext in ('ts', 'tsx'): | |
| found.extend(str(p) for p in pathlib.Path('.').rglob(f'*.{ext}')) | |
| def allowed(path): | |
| p = path.lstrip('./') | |
| for g in BUILTIN_GLOBS + exemptions: | |
| if fnmatch.fnmatchcase(p, g): | |
| return True | |
| # also treat glob ending with / as a directory prefix | |
| base = g.rstrip('/').rstrip('*').rstrip('/') | |
| if base and (p == base or p.startswith(base + '/')): | |
| return True | |
| return False | |
| bad = sorted(f for f in found if not allowed(f)) | |
| if bad: | |
| print("❌ TypeScript files detected outside the allowlist.\n") | |
| for f in bad: | |
| print(f" {f}") | |
| print() | |
| print("To resolve, either:") | |
| print(" (a) migrate the file to AffineScript") | |
| print(" (see Human_Programming_Guide.adoc migration chapter), OR") | |
| print(" (b) move it to an allowlisted bridge path") | |
| print(" (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, deno-*/, etc.), OR") | |
| print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md") | |
| print(" with rationale + unblock condition.") | |
| if exemptions: | |
| print(f"\n(Currently {len(exemptions)} exemption(s) parsed from .claude/CLAUDE.md.)") | |
| sys.exit(1) | |
| print(f"✅ No TypeScript files outside allowlist ({len(exemptions)} per-repo exemption(s) parsed).") | |
| PYEOF | |
| - name: Check for Go | |
| run: | | |
| if find . -name "*.go" | grep -q .; then | |
| echo "❌ Go files detected - use Rust/WASM instead" | |
| find . -name "*.go" | |
| exit 1 | |
| fi | |
| echo "✅ No Go files" | |
| - name: Check for Python (non-SaltStack) | |
| run: | | |
| PY_FILES=$(find . -name "*.py" | grep -v salt | grep -v _states | grep -v _modules | grep -v pillar | grep -v venv | grep -v __pycache__ || true) | |
| if [ -n "$PY_FILES" ]; then | |
| echo "❌ Python files detected - only allowed for SaltStack" | |
| echo "$PY_FILES" | |
| exit 1 | |
| fi | |
| echo "✅ No non-SaltStack Python files" | |
| - name: Check for npm lockfiles | |
| run: | | |
| if [ -f "package-lock.json" ] || [ -f "yarn.lock" ]; then | |
| echo "❌ npm/yarn lockfile detected - use Deno instead" | |
| exit 1 | |
| fi | |
| echo "✅ No npm lockfiles" | |
| - name: Check for tsconfig | |
| run: | | |
| if [ -f "tsconfig.json" ]; then | |
| echo "❌ tsconfig.json detected - use ReScript instead" | |
| exit 1 | |
| fi | |
| echo "✅ No tsconfig.json" | |
| - name: Verify Deno presence (if package.json exists) | |
| run: | | |
| if [ -f "package.json" ]; then | |
| if [ ! -f "deno.json" ] && [ ! -f "deno.jsonc" ]; then | |
| echo "⚠️ Warning: package.json without deno.json - migration recommended" | |
| fi | |
| fi | |
| echo "✅ Deno configuration check complete" | |
| - name: Summary | |
| run: | | |
| echo "╔════════════════════════════════════════════════════════════╗" | |
| echo "║ RSR Anti-Pattern Check Passed ✅ ║" | |
| echo "║ ║" | |
| echo "║ Allowed: ReScript, Deno, WASM, Rust, OCaml, Haskell, ║" | |
| echo "║ Guile/Scheme, SaltStack (Python) ║" | |
| echo "║ ║" | |
| echo "║ Blocked: TypeScript, Go, npm, Python (non-Salt) ║" | |
| echo "╚════════════════════════════════════════════════════════════╝" |