661. Extract build stage dependencies
772. Generate a dependency graph visualization
883. 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
10105. Output results suitable for GitHub Actions summaries
1111
1212Usage:
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
325359def 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