Skip to content

feat: configurator landing page, inline editor flow, CI improvements #23

feat: configurator landing page, inline editor flow, CI improvements

feat: configurator landing page, inline editor flow, CI improvements #23

Workflow file for this run

name: Lint & Static Analysis
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
# ── 1. CloudFormation lint ─────────────────────────────────────────────────
cfn-lint:
name: CloudFormation Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cfn-lint
run: pip install cfn-lint
- name: Lint CloudFormation template
run: cfn-lint map2-auto-tagger-optimized.yaml
# ── 2. Lambda Python syntax check ─────────────────────────────────────────
python-syntax:
name: Lambda Python Syntax
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract and syntax-check Lambda code from YAML
run: |
python3 - <<'EOF'
import re, sys, py_compile, tempfile, os
with open('map2-auto-tagger-optimized.yaml') as f:
content = f.read()
# Find all ZipFile blocks — extract by tracking indentation boundary
zipfile_positions = [m.start() for m in re.finditer(r'ZipFile: \|', content)]
if not zipfile_positions:
print("ERROR: No ZipFile blocks found in YAML")
sys.exit(1)
blocks = []
for pos in zipfile_positions:
block_start = content.find('\n', pos) + 1
lines = content[block_start:].split('\n')
code_lines = []
for line in lines:
if line == '' or line.startswith(' ' * 10):
code_lines.append(line[10:] if line.startswith(' ' * 10) else '')
else:
break
blocks.append('\n'.join(code_lines))
errors = []
for i, block in enumerate(blocks):
code = block
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
tmp = f.name
try:
py_compile.compile(tmp, doraise=True)
print(f" ✅ Lambda block {i+1}: syntax OK ({len(code.splitlines())} lines)")
except py_compile.PyCompileError as e:
errors.append(f"Lambda block {i+1}: {e}")
print(f" ❌ Lambda block {i+1}: {e}")
finally:
os.unlink(tmp)
if errors:
sys.exit(1)
EOF
# ── 3. Handler regression check ───────────────────────────────────────────
handler-regression:
name: Handler Regression Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for removed handlers without replacement
run: |
python3 - <<'EOF'
import re, sys, subprocess
# Get list of changed files in this PR vs main
result = subprocess.run(
['git', 'diff', 'origin/main...HEAD', '--', 'map2-auto-tagger-optimized.yaml'],
capture_output=True, text=True
)
diff = result.stdout
if not diff:
print("No changes to map2-auto-tagger-optimized.yaml — skipping")
sys.exit(0)
# Extract removed lines (- prefix, not --- header)
removed = [l[1:] for l in diff.split('\n') if l.startswith('-') and not l.startswith('---')]
added = [l[1:] for l in diff.split('\n') if l.startswith('+') and not l.startswith('+++')]
removed_text = '\n'.join(removed)
added_text = '\n'.join(added)
# Find event handler patterns removed: "elif event_name == 'Xyz'"
removed_handlers = re.findall(r"elif event_name == '([^']+)'", removed_text)
added_handlers = re.findall(r"elif event_name == '([^']+)'", added_text)
warnings = []
for h in removed_handlers:
if h not in added_handlers:
# Check if it still exists in the full file
with open('map2-auto-tagger-optimized.yaml') as f:
full = f.read()
if f"event_name == '{h}'" not in full:
warnings.append(h)
if warnings:
print("❌ The following event handlers were removed with no replacement:")
for h in warnings:
print(f" - {h}")
print()
print("If the universal ARN scanner covers these, add a comment explaining why.")
print("If intentionally removed, document the reason in the PR description.")
sys.exit(1)
else:
if removed_handlers:
print(f"✅ {len(removed_handlers)} handler(s) removed — all confirmed present elsewhere in file")
else:
print("✅ No handlers removed")
EOF
# ── 4. Configurator HTML check ────────────────────────────────────────────
configurator-check:
name: Configurator HTML Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check HTML is well-formed
run: |
node - <<'EOF'
const fs = require('fs');
['configurator.html'].forEach(file => {
if (!fs.existsSync(file)) {
console.log(` ⚠️ ${file} not found — skipping`);
return;
}
const html = fs.readFileSync(file, 'utf8');
// Check for unclosed script tags
const openScript = (html.match(/<script/g) || []).length;
const closeScript = (html.match(/<\/script>/g) || []).length;
if (openScript !== closeScript) {
console.error(`❌ ${file}: mismatched <script> tags (${openScript} open, ${closeScript} close)`);
process.exit(1);
}
// Check for unclosed style tags
const openStyle = (html.match(/<style/g) || []).length;
const closeStyle = (html.match(/<\/style>/g) || []).length;
if (openStyle !== closeStyle) {
console.error(`❌ ${file}: mismatched <style> tags (${openStyle} open, ${closeStyle} close)`);
process.exit(1);
}
// Check file isn't empty
if (html.trim().length < 100) {
console.error(`❌ ${file}: suspiciously small file (${html.length} bytes)`);
process.exit(1);
}
console.log(` ✅ ${file}: OK (${Math.round(html.length/1024)}KB, ${openScript} script blocks)`);
});
EOF
- name: Extract and syntax-check Lambda code from configurator.html
run: |
python3 - <<'EOF'
import re, sys, py_compile, tempfile, os
with open('configurator.html') as f:
content = f.read()
# Lambda code is inside ZipFile: | blocks within JS template literals
blocks = re.findall(r'ZipFile: \\\|\\n((?:.*\\n)+?)(?=\s*(?:Timeout|Role|Handler|Environment|FunctionName))', content)
if not blocks:
print(" ⚠️ No Lambda ZipFile blocks found in configurator.html — skipping Python check")
sys.exit(0)
errors = []
for i, block in enumerate(blocks):
# Unescape \\n → \n and strip indent
code = block.replace('\\n', '\n').replace("\\'", "'")
code = '\n'.join(
line[10:] if line.startswith(' ' * 10) else line.lstrip()
for line in code.split('\n')
)
if len(code.strip()) < 50:
continue
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
tmp = f.name
try:
py_compile.compile(tmp, doraise=True)
print(f" ✅ Configurator Lambda block {i+1}: syntax OK")
except py_compile.PyCompileError as e:
errors.append(str(e))
print(f" ❌ Configurator Lambda block {i+1}: {e}")
finally:
os.unlink(tmp)
if errors:
sys.exit(1)
EOF
# ── 5. New handler E2E coverage check ─────────────────────────────────────
new-handler-coverage-check:
name: New Handler E2E Coverage Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check new handlers have matching E2E resource scripts
run: |
python3 - <<'EOF'
import re, subprocess, sys
from pathlib import Path
result = subprocess.run(
['git', 'diff', 'origin/main...HEAD', '--', 'map2-auto-tagger-optimized.yaml'],
capture_output=True, text=True
)
diff = result.stdout
if not diff:
print("No changes to map2-auto-tagger-optimized.yaml — skipping")
sys.exit(0)
# Lines added in this PR (+ prefix, excluding +++ header)
added_lines = [l[1:] for l in diff.split('\n')
if l.startswith('+') and not l.startswith('+++')]
# Find newly added handler blocks: "elif event_name == 'Xxx' and event_source == 'yyy'"
handler_pattern = re.compile(
r"elif\s+event_name\s*==\s*'([^']+)'(?:.*?event_source\s*==\s*'([^']+)')?"
)
new_handlers = []
for line in added_lines:
m = handler_pattern.search(line)
if m:
new_handlers.append((m.group(1), m.group(2) or ''))
if not new_handlers:
print("No new event handlers added — skipping coverage check")
sys.exit(0)
print(f"Found {len(new_handlers)} new handler(s) to check:")
# Load all resource_group scripts content once
rg_dir = Path('.github/scripts/resource_groups')
rg_content = ''
if rg_dir.is_dir():
for py_file in rg_dir.glob('*.py'):
rg_content += py_file.read_text()
warnings = []
for event_name, event_source in new_handlers:
# Check for exact name or snake_case variant
snake = re.sub(r'(?<=[a-z])(?=[A-Z])', '_', event_name).lower()
found = (event_name in rg_content or snake in rg_content)
status = "covered" if found else "MISSING"
print(f" [{status}] {event_name} ({event_source or 'source unspecified'})")
if not found:
warnings.append((event_name, event_source))
if warnings:
print()
for event_name, event_source in warnings:
src_hint = event_source or 'unknown'
print(f"⚠️ New handler '{event_name}' ({src_hint}) has no matching E2E test resource.")
print(f" Add resource creation to .github/scripts/resource_groups/<appropriate_module>.py")
print(f" This is a warning — PR can still merge, but E2E coverage will be incomplete.")
print()
sys.exit(0)
EOF
# ── 6. YAML ↔ Configurator sync check ────────────────────────────────────
sync-drift-check:
name: YAML ↔ Configurator Sync Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check IAM permissions and critical handler sync
run: python3 .github/scripts/sync-check.py