Skip to content

Commit f48a6a5

Browse files
hyunsiesclaude
andcommitted
ci: expand lint workflow with Python syntax, handler regression, and HTML checks
- Python syntax: extracts Lambda code from YAML ZipFile blocks and compiles - Handler regression: flags event handlers removed without replacement - HTML check: validates script/style tag balance in configurator.html + editor.html - cfn-lint: unchanged, already passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent ce08629 commit f48a6a5

1 file changed

Lines changed: 206 additions & 1 deletion

File tree

.github/workflows/lint.yml

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Lint
1+
name: Lint & Static Analysis
22

33
on:
44
push:
@@ -7,6 +7,8 @@ on:
77
branches: [ main ]
88

99
jobs:
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

Comments
 (0)