Skip to content

ci: bump actions/upload-artifact SHA to current v4 #76

ci: bump actions/upload-artifact SHA to current v4

ci: bump actions/upload-artifact SHA to current v4 #76

Workflow file for this run

# SPDX-License-Identifier: PMPL-1.0-or-later
name: RSR Language Policy
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
check-banned-patterns:
name: Check for banned languages/patterns
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Check for TypeScript
run: |
python3 << 'PYEOF'
import re, sys, pathlib
# Universal allowlist — bridges and conventions that need no per-repo declaration.
# Implemented as explicit string predicates rather than glob patterns so that
# top-level directories (e.g. tests/foo.ts) are matched the same as nested ones,
# which fnmatch's * cannot do reliably.
DIR_NAMES_ALLOWED = {
'bindings', 'tests', 'test', 'scripts',
'mcp-adapter', 'cli', 'vendor', 'examples', 'ffi',
'node_modules', 'benchmarks',
}
def builtin_allowed(p):
# `p` is a posix-style path with no leading ./
# 1. Type declaration files
if p.endswith('.d.ts'):
return True
# 2. Canonical Deno entrypoint filenames
base = p.rsplit('/', 1)[-1]
if base == 'mod.ts':
return True
# 3. LSP server files (filename suffixes)
if base in ('lsp-server.ts', 'lsp_server.ts', 'lsp.ts') or base.endswith('-lsp.ts'):
return True
# 4. Benchmark files (filename suffixes)
if base.endswith('.bench.ts') or base.endswith('_bench.ts'):
return True
# 5. Any directory segment (excluding basename) matches an allowed dir
segs = p.split('/')
for s in segs[:-1]:
if s in DIR_NAMES_ALLOWED:
return True
# vscode-anything or anything-vscode
if 'vscode' in s:
return True
# deno-named subprojects
if s.startswith('deno-'):
return True
return False
# Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
# This is the documented single source of truth: adding one row here unblocks CI.
# Glob characters: '*' and '**' both mean "any chars including /". This loose
# interpretation matches user intent when an exemption row reads, e.g.,
# `affinescript-deno-test/*.ts` (covering nested files too).
def glob_to_regex(g):
out = []
for c in g.lstrip('./'):
if c == '*': out.append('.*')
elif c == '?': out.append('.')
elif c in '.+(){}[]|^$\\': out.append(re.escape(c))
else: out.append(c)
return re.compile('^' + ''.join(out) + '$')
exemption_patterns = []
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:
exemption_patterns.append((m.group(1), glob_to_regex(m.group(1))))
def exempt(p):
for raw, regex in exemption_patterns:
if regex.match(p):
return True
# Also allow exact-path matches and prefix matches for paths
# ending in `/`
if p == raw.lstrip('./'):
return True
if raw.endswith('/') and p.startswith(raw.lstrip('./')):
return True
return False
# Find all .ts and .tsx files (excluding common dot-dirs that find normally skips)
found = []
for ext in ('ts', 'tsx'):
for p in pathlib.Path('.').rglob(f'*.{ext}'):
parts = p.parts
if any(part.startswith('.') and part not in ('.', '..') for part in parts):
continue
found.append(p.as_posix().lstrip('./'))
bad = sorted(f for f in found if not (builtin_allowed(f) or exempt(f)))
if bad:
print("❌ TypeScript files detected outside the allowlist.\n")
for f in bad:
print(f" {f}")
print()
print("To resolve, choose one:")
print(" (a) migrate the file to AffineScript")
print(" (see Human_Programming_Guide.adoc 'Migrating from -script Languages')")
print(" (b) move to an allowlisted bridge path")
print(" (bindings/, tests/, test/, scripts/, benchmarks/, mcp-adapter/,")
print(" *vscode*/, cli/, deno-*/, vendor/, examples/, ffi/)")
print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
print(" with rationale + unblock condition")
if exemption_patterns:
print(f"\n(Currently {len(exemption_patterns)} exemption(s) parsed from .claude/CLAUDE.md.)")
sys.exit(1)
print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).")
PYEOF
# 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 "::error::Go files found! Use Rust instead (per RSR policy)"
exit 1
fi
echo "✓ No Go files found"
- name: Check for npm/Node artifacts
run: |
if [ -f "package-lock.json" ] || [ -d "node_modules" ]; then
echo "::error::npm artifacts found! Use Deno instead (per RSR policy)"
exit 1
fi
echo "✓ No npm artifacts found"
- name: Check for Python (non-SaltStack)
run: |
banned_py=$(find . -name "*.py" | grep -v -E "(salt|pillar|states|_modules|_states)" | head -1)
if [ -n "$banned_py" ]; then
echo "::error::Python files found outside SaltStack! Use ReScript/Rust (per RSR policy)"
exit 1
fi
echo "✓ No banned Python files found"
- name: Check for Makefiles
run: |
if [ -f "Makefile" ] || [ -f "makefile" ] || find . -name "*.mk" | grep -q .; then
echo "::error::Makefile found! Use Justfile or Mustfile instead (per RSR policy)"
exit 1
fi
echo "✓ No Makefiles found"