feat: configurator landing page, inline editor flow, CI improvements #23
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
| 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 |