Skip to content

Commit 259484d

Browse files
committed
ci: mermaids with colors
1 parent f1988d2 commit 259484d

2 files changed

Lines changed: 102 additions & 52 deletions

File tree

.github/workflows/dockerfile-deps.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,19 @@ jobs:
3939
run: |
4040
echo "::error::Dockerfile dependencies validation failed. See job summary for details."
4141
exit 1
42+
43+
# TODO: Optionally add PR comment when validation fails
44+
# - name: Comment on PR with changes (if validation failed)
45+
# if: failure() && github.event_name == 'pull_request'
46+
# uses: actions/github-script@v7
47+
# with:
48+
# script: |
49+
# const fs = require('fs');
50+
# const report = fs.readFileSync('report.md', 'utf8');
51+
#
52+
# await github.rest.issues.createComment({
53+
# owner: context.repo.owner,
54+
# repo: context.repo.repo,
55+
# issue_number: context.issue.number,
56+
# body: `## Dockerfile Dependencies Changed\n\n${report}\n\n---\n*This check ensures Dockerfile dependencies are tracked and validated against the source of truth.*`
57+
# });

tools/dockerfile-deps.py

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
1. Extract build stage dependencies
77
2. Generate a dependency graph visualization
88
3. Validate build order using topological sort
9-
4. Compare against a checked-in "facit" (source of truth)
9+
4. Compare against a checked-in source of truth
1010
5. Output results suitable for GitHub Actions summaries
1111
1212
Usage:
13-
# Generate facit file
14-
python3 dockerfile-deps.py --generate-facit
13+
# Generate source of truth file
14+
python3 dockerfile-deps.py --generate-sot
1515
16-
# Validate against facit
16+
# Validate against source of truth
1717
python3 dockerfile-deps.py --validate
1818
1919
# Just show the analysis
@@ -154,37 +154,37 @@ def to_dict(self) -> dict:
154154
"file_dependencies": {k: sorted(list(v)) for k, v in self.file_dependencies.items()}
155155
}
156156

157-
def compare_with_facit(self, facit_path: Path) -> Tuple[bool, List[str]]:
158-
"""Compare current dependencies with facit file"""
159-
if not facit_path.exists():
160-
return False, [f"❌ Facit file not found: {facit_path}"]
157+
def compare_with_source_of_truth(self, sot_path: Path) -> Tuple[bool, List[str]]:
158+
"""Compare current dependencies with source of truth file"""
159+
if not sot_path.exists():
160+
return False, [f"❌ Source of truth file not found: {sot_path}"]
161161

162-
with open(facit_path) as f:
163-
facit = json.load(f)
162+
with open(sot_path) as f:
163+
sot = json.load(f)
164164

165165
errors = []
166166

167167
# Compare stages
168-
facit_stages = set(facit.get("stages", {}).keys())
168+
sot_stages = set(sot.get("stages", {}).keys())
169169
current_stages = set(self.stages.keys())
170170

171-
if facit_stages != current_stages:
172-
added = current_stages - facit_stages
173-
removed = facit_stages - current_stages
171+
if sot_stages != current_stages:
172+
added = current_stages - sot_stages
173+
removed = sot_stages - current_stages
174174

175175
if added:
176176
errors.append(f"❌ New stages added: {', '.join(sorted(added))}")
177177
if removed:
178178
errors.append(f"❌ Stages removed: {', '.join(sorted(removed))}")
179179

180180
# Compare stage dependencies for common stages
181-
for stage in facit_stages & current_stages:
182-
facit_deps = set(facit.get("dependencies", {}).get(stage, []))
181+
for stage in sot_stages & current_stages:
182+
sot_deps = set(sot.get("dependencies", {}).get(stage, []))
183183
current_deps = self.dependencies.get(stage, set())
184184

185-
if facit_deps != current_deps:
186-
added_deps = current_deps - facit_deps
187-
removed_deps = facit_deps - current_deps
185+
if sot_deps != current_deps:
186+
added_deps = current_deps - sot_deps
187+
removed_deps = sot_deps - current_deps
188188

189189
if added_deps or removed_deps:
190190
errors.append(f"❌ Stage dependencies changed for '{stage}':")
@@ -194,13 +194,13 @@ def compare_with_facit(self, facit_path: Path) -> Tuple[bool, List[str]]:
194194
errors.append(f" Removed stages: {', '.join(sorted(removed_deps))}")
195195

196196
# Compare file dependencies for common stages
197-
for stage in facit_stages & current_stages:
198-
facit_files = set(facit.get("file_dependencies", {}).get(stage, []))
197+
for stage in sot_stages & current_stages:
198+
sot_files = set(sot.get("file_dependencies", {}).get(stage, []))
199199
current_files = self.file_dependencies.get(stage, set())
200200

201-
if facit_files != current_files:
202-
added_files = current_files - facit_files
203-
removed_files = facit_files - current_files
201+
if sot_files != current_files:
202+
added_files = current_files - sot_files
203+
removed_files = sot_files - current_files
204204

205205
if added_files or removed_files:
206206
errors.append(f"❌ File dependencies changed for '{stage}':")
@@ -212,16 +212,50 @@ def compare_with_facit(self, facit_path: Path) -> Tuple[bool, List[str]]:
212212
return len(errors) == 0, errors
213213

214214
def generate_mermaid(self) -> str:
215-
"""Generate Mermaid flowchart from dependencies"""
215+
"""Generate Mermaid flowchart from dependencies with color coding"""
216216
lines = ["graph TD"]
217217

218+
# Define color classes for different stage types
219+
lines.append(" classDef base fill:#3b82f6,stroke:#1e40af,color:#fff")
220+
lines.append(" classDef deps fill:#06b6d4,stroke:#0891b2,color:#fff")
221+
lines.append(" classDef test fill:#10b981,stroke:#059669,color:#fff")
222+
lines.append(" classDef lint fill:#f97316,stroke:#ea580c,color:#fff")
223+
lines.append(" classDef build fill:#8b5cf6,stroke:#7c3aed,color:#fff")
224+
lines.append(" classDef artifact fill:#ec4899,stroke:#db2777,color:#fff")
225+
lines.append(" classDef publish fill:#ef4444,stroke:#dc2626,color:#fff")
226+
lines.append(" classDef validate fill:#eab308,stroke:#ca8a04,color:#000")
227+
lines.append("")
228+
229+
def get_stage_class(stage: str) -> str:
230+
"""Determine the CSS class for a stage based on its name"""
231+
if stage.endswith("-base") or stage == "rust-base":
232+
return "base"
233+
elif stage.endswith("-deps"):
234+
return "deps"
235+
elif ".test" in stage:
236+
return "test"
237+
elif ".lint" in stage:
238+
return "lint"
239+
elif ".build" in stage:
240+
return "build"
241+
elif ".artifact" in stage:
242+
return "artifact"
243+
elif ".publish" in stage:
244+
return "publish"
245+
elif "validate" in stage:
246+
return "validate"
247+
return "base" # default
248+
218249
# Add nodes (exclude "all" stage as it's just a collection target)
219250
for stage in self.stages:
220251
if stage == "all":
221252
continue
222253
# Sanitize stage names for Mermaid
223254
node_id = stage.replace("-", "_").replace(".", "_")
224-
lines.append(f" {node_id}[\"{stage}\"]")
255+
stage_class = get_stage_class(stage)
256+
lines.append(f" {node_id}[\"{stage}\"]:::{stage_class}")
257+
258+
lines.append("")
225259

226260
# Add edges (dependencies) - skip anything involving "all"
227261
for stage, deps in self.dependencies.items():
@@ -236,7 +270,7 @@ def generate_mermaid(self) -> str:
236270

237271
return "\n".join(lines)
238272

239-
def generate_report(self, facit_path: Path = None) -> str:
273+
def generate_report(self, sot_path: Path = None) -> str:
240274
"""Generate a human-readable report"""
241275
lines = []
242276
lines.append("# Dockerfile Dependency Analysis")
@@ -245,28 +279,28 @@ def generate_report(self, facit_path: Path = None) -> str:
245279
lines.append(f"**Total Stages:** {len(self.stages)}")
246280
lines.append("")
247281

248-
# Facit validation
249-
if facit_path:
250-
is_valid, errors = self.compare_with_facit(facit_path)
282+
# Source of truth validation
283+
if sot_path:
284+
is_valid, errors = self.compare_with_source_of_truth(sot_path)
251285
if is_valid:
252-
lines.append("## Facit Validation: ✅ PASS")
286+
lines.append("## Source of Truth Validation: ✅ PASS")
253287
lines.append("")
254-
lines.append(f"Dependencies match the expected structure in `{facit_path}`")
288+
lines.append(f"Dependencies match the expected structure in `{sot_path}`")
255289
else:
256-
lines.append("## Facit Validation: ❌ FAIL")
290+
lines.append("## Source of Truth Validation: ❌ FAIL")
257291
lines.append("")
258292
for error in errors:
259293
lines.append(error)
260294
lines.append("")
261-
lines.append("**Action required:** Update the facit file or fix the Dockerfile:")
295+
lines.append("**Action required:** Update the source of truth or fix the Dockerfile:")
262296
lines.append("")
263297
lines.append("```bash")
264-
lines.append("# If changes are intentional, update facit:")
265-
lines.append("python3 tools/dockerfile-deps.py --generate-facit")
298+
lines.append("# If changes are intentional, update source of truth:")
299+
lines.append("python3 tools/dockerfile-deps.py --generate-sot")
266300
lines.append("")
267-
lines.append("# Then commit the updated facit:")
301+
lines.append("# Then commit the updated source of truth:")
268302
lines.append("git add .dockerfile-deps.json")
269-
lines.append("git commit -m 'chore: update dockerfile dependencies facit'")
303+
lines.append("git commit -m 'chore: update dockerfile dependencies source of truth'")
270304
lines.append("```")
271305
lines.append("")
272306

@@ -324,27 +358,27 @@ def generate_report(self, facit_path: Path = None) -> str:
324358

325359
def main():
326360
parser = argparse.ArgumentParser(
327-
description="Analyze Dockerfile dependencies and validate against facit"
361+
description="Analyze Dockerfile dependencies and validate against source of truth"
328362
)
329363
parser.add_argument(
330364
"--dockerfile",
331365
default="Dockerfile",
332366
help="Path to Dockerfile (default: Dockerfile)"
333367
)
334368
parser.add_argument(
335-
"--facit",
369+
"--sot",
336370
default=".dockerfile-deps.json",
337-
help="Path to facit file (default: .dockerfile-deps.json)"
371+
help="Path to source of truth file (default: .dockerfile-deps.json)"
338372
)
339373
parser.add_argument(
340-
"--generate-facit",
374+
"--generate-sot",
341375
action="store_true",
342-
help="Generate facit file from current Dockerfile"
376+
help="Generate source of truth file from current Dockerfile"
343377
)
344378
parser.add_argument(
345379
"--validate",
346380
action="store_true",
347-
help="Validate Dockerfile against facit"
381+
help="Validate Dockerfile against source of truth"
348382
)
349383
parser.add_argument(
350384
"--mermaid-only",
@@ -355,7 +389,7 @@ def main():
355389
args = parser.parse_args()
356390

357391
dockerfile = Path(args.dockerfile)
358-
facit_path = Path(args.facit)
392+
sot_path = Path(args.sot)
359393

360394
if not dockerfile.exists():
361395
print(f"Error: {dockerfile} not found", file=sys.stderr)
@@ -364,11 +398,11 @@ def main():
364398
analyzer = DockerfileAnalyzer(str(dockerfile))
365399
analyzer.parse()
366400

367-
if args.generate_facit:
368-
# Generate and save facit
369-
with open(facit_path, 'w') as f:
401+
if args.generate_sot:
402+
# Generate and save source of truth
403+
with open(sot_path, 'w') as f:
370404
json.dump(analyzer.to_dict(), f, indent=2, sort_keys=True)
371-
print(f"✅ Generated facit file: {facit_path}")
405+
print(f"✅ Generated source of truth file: {sot_path}")
372406
sys.exit(0)
373407

374408
if args.mermaid_only:
@@ -380,13 +414,13 @@ def main():
380414

381415
# Generate report
382416
if args.validate:
383-
report = analyzer.generate_report(facit_path)
417+
report = analyzer.generate_report(sot_path)
384418
print(report)
385419

386420
# Exit with error if validation failed
387-
facit_valid, _ = analyzer.compare_with_facit(facit_path)
421+
sot_valid, _ = analyzer.compare_with_source_of_truth(sot_path)
388422
topo_valid, _ = analyzer.validate_dependencies()
389-
sys.exit(0 if (facit_valid and topo_valid) else 1)
423+
sys.exit(0 if (sot_valid and topo_valid) else 1)
390424
else:
391425
report = analyzer.generate_report()
392426
print(report)

0 commit comments

Comments
 (0)