@@ -21,6 +21,16 @@ static async Task Main(string[] args)
2121 string docRootDir = Path . GetFullPath ( Path . Combine ( scriptsDir , ".." ) ) ;
2222 string ReferenceDir ( string lang ) => Path . Combine ( docRootDir , "docs" , "reference" , lang ) ;
2323
24+ // Strict mode: a language that fails to generate is a HARD ERROR (non-zero exit) instead
25+ // of silently keeping the previously-committed docs. Auto-on in CI (GitHub sets CI=true)
26+ // so a broken extractor fails the workflow rather than shipping stale reference pages;
27+ // off locally so a dev missing Docker/npm/python can still regenerate the subset they can.
28+ // Pass --strict to force it on anywhere.
29+ bool strict = args . Contains ( "--strict" )
30+ || ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "CI" ) ) ;
31+ args = args . Where ( a => a != "--strict" ) . ToArray ( ) ;
32+ var failures = new List < string > ( ) ;
33+
2434 // CLI-only mode (used by the per-OS CI matrix to capture native help text).
2535 if ( args . Length == 2 && args [ 0 ] == "cli" ) {
2636 Console . WriteLine ( "Generating CLI Reference Only..." ) ;
@@ -51,23 +61,23 @@ static async Task Main(string[] args)
5161
5262 // Unified pipeline: extractor → adapter → ApiModel → MarkdownWriter, one per language.
5363 if ( Enabled ( "cs" ) )
54- await RunLanguageAsync ( "C#" , ReferenceDir ( "cs" ) ,
64+ await RunLanguageAsync ( "C#" , ReferenceDir ( "cs" ) , failures , strict ,
5565 async ( ) => { var e = await CSharpExtractor . ExtractAsync ( ) ; return e == null ? null : CSharpAdapter . Build ( e ) ; } ) ;
5666
5767 if ( Enabled ( "js" ) )
58- await RunLanguageAsync ( "JavaScript" , ReferenceDir ( "js" ) ,
68+ await RunLanguageAsync ( "JavaScript" , ReferenceDir ( "js" ) , failures , strict ,
5969 async ( ) => { var e = await TypeScriptExtractor . ExtractAsync ( ) ; return e == null ? null : TypeScriptAdapter . Build ( e ) ; } ) ;
6070
6171 if ( Enabled ( "cpp" ) )
6272 {
6373 CppExtractResult ? cppExtract = null ;
64- await RunLanguageAsync ( "C++" , ReferenceDir ( "cpp" ) ,
74+ await RunLanguageAsync ( "C++" , ReferenceDir ( "cpp" ) , failures , strict ,
6575 async ( ) => { cppExtract = await CppExtractor . ExtractAsync ( ) ; return cppExtract == null ? null : CppAdapter . Build ( cppExtract ) ; } ,
6676 afterWrite : dir => CppAdapter . WriteCApiPage ( cppExtract ! , dir ) ) ;
6777 }
6878
6979 if ( Enabled ( "py" ) )
70- await RunLanguageAsync ( "Python" , ReferenceDir ( "py" ) ,
80+ await RunLanguageAsync ( "Python" , ReferenceDir ( "py" ) , failures , strict ,
7181 async ( ) => { var e = await PythonExtractor . ExtractAsync ( ) ; return e == null ? null : PythonAdapter . Build ( e ) ; } ) ;
7282
7383 Console . WriteLine ( "Updating CLI Reference..." ) ;
@@ -77,34 +87,57 @@ await RunLanguageAsync("Python", ReferenceDir("py"),
7787 Console . WriteLine ( $ " CLI reference failed: { ex . Message } ") ;
7888 }
7989
90+ // In strict mode (CI), surface every failed language at once and exit non-zero so the
91+ // workflow fails loudly instead of committing stale docs for the broken language(s).
92+ if ( failures . Count > 0 )
93+ {
94+ Console . Error . WriteLine ( $ "\n { failures . Count } reference(s) failed (strict mode):") ;
95+ foreach ( var f in failures )
96+ Console . Error . WriteLine ( $ " - { f } ") ;
97+ Environment . Exit ( 1 ) ;
98+ }
99+
80100 Console . WriteLine ( "Done!" ) ;
81101 }
82102
83103 /// <summary>
84- /// Run one language pipeline. On any failure (tool missing, network down, empty model)
85- /// the existing committed docs are left untouched so the site keeps building.
104+ /// Run one language pipeline. On any failure (tool missing, network down, empty model) the
105+ /// existing committed docs are left untouched so the site keeps building — UNLESS
106+ /// <paramref name="strict"/> is set, in which case the failure is recorded in
107+ /// <paramref name="failures"/> and turned into a non-zero exit by the caller (CI behaviour).
86108 /// </summary>
87- static async Task RunLanguageAsync ( string display , string outputDir , Func < Task < ApiModel ? > > build , Action < string > ? afterWrite = null )
109+ static async Task RunLanguageAsync ( string display , string outputDir , List < string > failures , bool strict , Func < Task < ApiModel ? > > build , Action < string > ? afterWrite = null )
88110 {
111+ // In strict mode, "keep existing docs" is itself the failure we want to catch.
112+ string Skip ( string reason )
113+ {
114+ if ( strict ) {
115+ Console . WriteLine ( $ " { display } : { reason } — FAILING (strict mode).") ;
116+ failures . Add ( $ "{ display } : { reason } ") ;
117+ } else {
118+ Console . WriteLine ( $ " { display } : { reason } — keeping existing docs.") ;
119+ }
120+ return reason ;
121+ }
122+
89123 Console . WriteLine ( $ "Updating { display } Reference...") ;
90124 try {
91125 var model = await build ( ) ;
92126 if ( model == null ) {
93- Console . WriteLine ( $ " { display } : extractor unavailable — keeping existing docs. ") ;
127+ Skip ( " extractor produced no model ") ;
94128 return ;
95129 }
96130 var typeCount = model . Namespaces . Sum ( n => n . Types . Count ) ;
97131 if ( typeCount == 0 ) {
98- Console . WriteLine ( $ " { display } : model has no types — keeping existing docs. ") ;
132+ Skip ( " model has no types") ;
99133 return ;
100134 }
101135 Util . EnsureEmptyDirectory ( outputDir ) ;
102136 MarkdownWriter . Write ( model , outputDir ) ;
103137 afterWrite ? . Invoke ( outputDir ) ;
104138 Console . WriteLine ( $ " { display } : wrote { typeCount } type page(s) to { outputDir } .") ;
105139 } catch ( Exception ex ) {
106- Console . WriteLine ( $ " { display } reference failed: { ex . Message } ") ;
107- Console . WriteLine ( $ " Keeping existing { display } docs.") ;
140+ Skip ( $ "reference failed: { ex . Message } ") ;
108141 }
109142 }
110143
0 commit comments