1- name : Lint
1+ name : Lint & Static Analysis
22
33on :
44 push :
77 branches : [ main ]
88
99jobs :
10+
11+ # ── 1. CloudFormation lint ─────────────────────────────────────────────────
1012 cfn-lint :
1113 name : CloudFormation Lint
1214 runs-on : ubuntu-latest
@@ -18,3 +20,206 @@ jobs:
1820
1921 - name : Lint CloudFormation template
2022 run : cfn-lint map2-auto-tagger-optimized.yaml
23+
24+ # ── 2. Lambda Python syntax check ─────────────────────────────────────────
25+ python-syntax :
26+ name : Lambda Python Syntax
27+ runs-on : ubuntu-latest
28+ steps :
29+ - uses : actions/checkout@v4
30+
31+ - name : Extract and syntax-check Lambda code from YAML
32+ run : |
33+ python3 - <<'EOF'
34+ import re, sys, py_compile, tempfile, os
35+
36+ with open('map2-auto-tagger-optimized.yaml') as f:
37+ content = f.read()
38+
39+ # Find all ZipFile blocks — extract by tracking indentation boundary
40+ zipfile_positions = [m.start() for m in re.finditer(r'ZipFile: \|', content)]
41+ if not zipfile_positions:
42+ print("ERROR: No ZipFile blocks found in YAML")
43+ sys.exit(1)
44+
45+ blocks = []
46+ for pos in zipfile_positions:
47+ block_start = content.find('\n', pos) + 1
48+ lines = content[block_start:].split('\n')
49+ code_lines = []
50+ for line in lines:
51+ if line == '' or line.startswith(' ' * 10):
52+ code_lines.append(line[10:] if line.startswith(' ' * 10) else '')
53+ else:
54+ break
55+ blocks.append('\n'.join(code_lines))
56+
57+ errors = []
58+ for i, block in enumerate(blocks):
59+ code = block
60+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
61+ f.write(code)
62+ tmp = f.name
63+ try:
64+ py_compile.compile(tmp, doraise=True)
65+ print(f" ✅ Lambda block {i+1}: syntax OK ({len(code.splitlines())} lines)")
66+ except py_compile.PyCompileError as e:
67+ errors.append(f"Lambda block {i+1}: {e}")
68+ print(f" ❌ Lambda block {i+1}: {e}")
69+ finally:
70+ os.unlink(tmp)
71+
72+ if errors:
73+ sys.exit(1)
74+ EOF
75+
76+ # ── 3. Handler regression check ───────────────────────────────────────────
77+ handler-regression :
78+ name : Handler Regression Check
79+ runs-on : ubuntu-latest
80+ steps :
81+ - uses : actions/checkout@v4
82+ with :
83+ fetch-depth : 0
84+
85+ - name : Check for removed handlers without replacement
86+ run : |
87+ python3 - <<'EOF'
88+ import re, sys, subprocess
89+
90+ # Get list of changed files in this PR vs main
91+ result = subprocess.run(
92+ ['git', 'diff', 'origin/main...HEAD', '--', 'map2-auto-tagger-optimized.yaml'],
93+ capture_output=True, text=True
94+ )
95+ diff = result.stdout
96+
97+ if not diff:
98+ print("No changes to map2-auto-tagger-optimized.yaml — skipping")
99+ sys.exit(0)
100+
101+ # Extract removed lines (- prefix, not --- header)
102+ removed = [l[1:] for l in diff.split('\n') if l.startswith('-') and not l.startswith('---')]
103+ added = [l[1:] for l in diff.split('\n') if l.startswith('+') and not l.startswith('+++')]
104+
105+ removed_text = '\n'.join(removed)
106+ added_text = '\n'.join(added)
107+
108+ # Find event handler patterns removed: "elif event_name == 'Xyz'"
109+ removed_handlers = re.findall(r"elif event_name == '([^']+)'", removed_text)
110+ added_handlers = re.findall(r"elif event_name == '([^']+)'", added_text)
111+
112+ warnings = []
113+ for h in removed_handlers:
114+ if h not in added_handlers:
115+ # Check if it still exists in the full file
116+ with open('map2-auto-tagger-optimized.yaml') as f:
117+ full = f.read()
118+ if f"event_name == '{h}'" not in full:
119+ warnings.append(h)
120+
121+ if warnings:
122+ print("❌ The following event handlers were removed with no replacement:")
123+ for h in warnings:
124+ print(f" - {h}")
125+ print()
126+ print("If the universal ARN scanner covers these, add a comment explaining why.")
127+ print("If intentionally removed, document the reason in the PR description.")
128+ sys.exit(1)
129+ else:
130+ if removed_handlers:
131+ print(f"✅ {len(removed_handlers)} handler(s) removed — all confirmed present elsewhere in file")
132+ else:
133+ print("✅ No handlers removed")
134+ EOF
135+
136+ # ── 4. Configurator HTML check ────────────────────────────────────────────
137+ configurator-check :
138+ name : Configurator HTML Check
139+ runs-on : ubuntu-latest
140+ steps :
141+ - uses : actions/checkout@v4
142+
143+ - name : Install Node.js
144+ uses : actions/setup-node@v4
145+ with :
146+ node-version : ' 20'
147+
148+ - name : Check HTML is well-formed
149+ run : |
150+ node - <<'EOF'
151+ const fs = require('fs');
152+
153+ ['configurator.html', 'editor.html'].forEach(file => {
154+ if (!fs.existsSync(file)) {
155+ console.log(` ⚠️ ${file} not found — skipping`);
156+ return;
157+ }
158+ const html = fs.readFileSync(file, 'utf8');
159+
160+ // Check for unclosed script tags
161+ const openScript = (html.match(/<script/g) || []).length;
162+ const closeScript = (html.match(/<\/script>/g) || []).length;
163+ if (openScript !== closeScript) {
164+ console.error(`❌ ${file}: mismatched <script> tags (${openScript} open, ${closeScript} close)`);
165+ process.exit(1);
166+ }
167+
168+ // Check for unclosed style tags
169+ const openStyle = (html.match(/<style/g) || []).length;
170+ const closeStyle = (html.match(/<\/style>/g) || []).length;
171+ if (openStyle !== closeStyle) {
172+ console.error(`❌ ${file}: mismatched <style> tags (${openStyle} open, ${closeStyle} close)`);
173+ process.exit(1);
174+ }
175+
176+ // Check file isn't empty
177+ if (html.trim().length < 100) {
178+ console.error(`❌ ${file}: suspiciously small file (${html.length} bytes)`);
179+ process.exit(1);
180+ }
181+
182+ console.log(` ✅ ${file}: OK (${Math.round(html.length/1024)}KB, ${openScript} script blocks)`);
183+ });
184+ EOF
185+
186+ - name : Extract and syntax-check Lambda code from configurator.html
187+ run : |
188+ python3 - <<'EOF'
189+ import re, sys, py_compile, tempfile, os
190+
191+ with open('configurator.html') as f:
192+ content = f.read()
193+
194+ # Lambda code is inside ZipFile: | blocks within JS template literals
195+ blocks = re.findall(r'ZipFile: \\\|\\n((?:.*\\n)+?)(?=\s*(?:Timeout|Role|Handler|Environment|FunctionName))', content)
196+
197+ if not blocks:
198+ print(" ⚠️ No Lambda ZipFile blocks found in configurator.html — skipping Python check")
199+ sys.exit(0)
200+
201+ errors = []
202+ for i, block in enumerate(blocks):
203+ # Unescape \\n → \n and strip indent
204+ code = block.replace('\\n', '\n').replace("\\'", "'")
205+ code = '\n'.join(
206+ line[10:] if line.startswith(' ' * 10) else line.lstrip()
207+ for line in code.split('\n')
208+ )
209+ if len(code.strip()) < 50:
210+ continue
211+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
212+ f.write(code)
213+ tmp = f.name
214+ try:
215+ py_compile.compile(tmp, doraise=True)
216+ print(f" ✅ Configurator Lambda block {i+1}: syntax OK")
217+ except py_compile.PyCompileError as e:
218+ errors.append(str(e))
219+ print(f" ❌ Configurator Lambda block {i+1}: {e}")
220+ finally:
221+ os.unlink(tmp)
222+
223+ if errors:
224+ sys.exit(1)
225+ EOF
0 commit comments