@@ -73,6 +73,7 @@ const CLI_BIN = join(
7373const coreFeatures = [ scaffolderPlugin ] ;
7474
7575type Status = "pass" | "fail-load" | "fail-start" | "fail-bundle" | "error" ;
76+ type BackendStartResult = { ok : boolean ; skipped ?: boolean ; error ?: string } ;
7677type Report = {
7778 cliVersion : string ;
7879 backend : {
@@ -81,19 +82,34 @@ type Report = {
8182 skipped : string [ ] ;
8283 errors : PluginError [ ] ;
8384 } ;
84- backendStart : { ok : boolean ; skipped ?: boolean ; error ?: string } ;
85+ backendStart : BackendStartResult ;
8586 frontend : { total : number ; valid : number ; errors : PluginError [ ] } ;
8687 status : Status ;
8788} ;
8889
8990// execFileSync (args array, no shell) so workspace names / OCI refs can never be
9091// interpolated into a shell command as this grows beyond a single fixed plugin.
91- function run ( file : string , args : string [ ] , env ?: NodeJS . ProcessEnv ) : string {
92- return execFileSync ( file , args , {
93- encoding : "utf-8" ,
94- stdio : "pipe" ,
95- env : { ...process . env , ...env } ,
96- } ) . trim ( ) ;
92+ function run ( file : string , args : string [ ] ) : string {
93+ return execFileSync ( file , args , { encoding : "utf-8" , stdio : "pipe" } ) . trim ( ) ;
94+ }
95+
96+ // Any failure — bad args, install CLI crash, boot error before the report is built —
97+ // still produces a results.json (status: error), so a consumer never reads a stale
98+ // "pass" or finds no report at all.
99+ async function writeErrorReport (
100+ out : string ,
101+ cliVersion : string ,
102+ message : string ,
103+ ) : Promise < void > {
104+ const report : Report = {
105+ cliVersion,
106+ backend : { total : 0 , loaded : 0 , skipped : [ ] , errors : [ ] } ,
107+ backendStart : { ok : false , error : message } ,
108+ frontend : { total : 0 , valid : 0 , errors : [ ] } ,
109+ status : "error" ,
110+ } ;
111+ await writeFile ( out , JSON . stringify ( report , null , 2 ) ) ;
112+ console . error ( `▶ report → ${ out } (status: error)\n${ message } ` ) ;
97113}
98114
99115function computeStatus (
@@ -122,9 +138,7 @@ async function extractPlugins(root: string, dynamicPlugins: string): Promise<voi
122138}
123139
124140// Boot the loaded backend features in-process to confirm they integrate.
125- async function startBackend (
126- loaded : LoadedPlugin [ ] ,
127- ) : Promise < { ok : boolean ; skipped ?: boolean ; error ?: string } > {
141+ async function startBackend ( loaded : LoadedPlugin [ ] ) : Promise < BackendStartResult > {
128142 // No backend plugins (e.g. a frontend-only workspace) — boot wasn't attempted, not a
129143 // failure. Flag it so results.json doesn't read like the backend crashed.
130144 if ( loaded . length === 0 ) return { ok : true , skipped : true } ;
@@ -172,11 +186,11 @@ async function main(): Promise<number> {
172186 const dynamicPlugins = values [ "dynamic-plugins" ] ;
173187
174188 if ( ! dynamicPlugins ) {
175- console . error ( "Provide --dynamic-plugins <dynamic-plugins.yaml>." ) ;
189+ await writeErrorReport ( out , "unknown" , "Provide --dynamic-plugins <dynamic-plugins.yaml>." ) ;
176190 return 2 ;
177191 }
178192 if ( ! existsSync ( dynamicPlugins ) ) {
179- console . error ( `dynamic-plugins file not found: ${ dynamicPlugins } ` ) ;
193+ await writeErrorReport ( out , "unknown" , `dynamic-plugins file not found: ${ dynamicPlugins } ` ) ;
180194 return 2 ;
181195 }
182196
@@ -204,18 +218,18 @@ async function main(): Promise<number> {
204218 // Let extracted plugins (under a temp dir) resolve their @backstage/* peers here.
205219 patchModuleResolution ( HARNESS_NODE_MODULES ) ;
206220
207- const skipped = manifest . backend
208- . filter ( ( p ) => KNOWN_FAILURES . has ( p . dirName ) )
209- . map ( ( p ) => p . dirName ) ;
221+ // Partition in one pass so `skipped` and `backendPlugins` stay complementary.
222+ const skipped : string [ ] = [ ] ;
223+ const backendPlugins : PluginEntry [ ] = [ ] ;
224+ for ( const p of manifest . backend ) {
225+ if ( KNOWN_FAILURES . has ( p . dirName ) ) skipped . push ( p . dirName ) ;
226+ else backendPlugins . push ( p ) ;
227+ }
210228 if ( skipped . length > 0 ) {
211229 console . warn (
212230 `⚠ skipped ${ skipped . length } known-failure backend plugin(s): ${ skipped . join ( ", " ) } ` ,
213231 ) ;
214232 }
215-
216- const backendPlugins = manifest . backend . filter (
217- ( p ) => ! KNOWN_FAILURES . has ( p . dirName ) ,
218- ) ;
219233 const { loaded, errors : loadErrors } = loadBackendPlugins ( backendPlugins ) ;
220234 const start = await startBackend ( loaded ) ;
221235 const frontend = validateFrontends ( manifest . frontend ) ;
@@ -258,18 +272,8 @@ async function main(): Promise<number> {
258272 ) ;
259273 return report . status === "pass" ? 0 : 1 ;
260274 } catch ( err ) {
261- // Any failure before the report is built (e.g. the install CLI failing on a bad
262- // OCI ref) still writes a results.json, so a consumer never reads a stale "pass".
263- const message = err instanceof Error ? err . message : String ( err ) ;
264- const report : Report = {
265- cliVersion,
266- backend : { total : 0 , loaded : 0 , skipped : [ ] , errors : [ ] } ,
267- backendStart : { ok : false , error : message } ,
268- frontend : { total : 0 , valid : 0 , errors : [ ] } ,
269- status : "error" ,
270- } ;
271- await writeFile ( out , JSON . stringify ( report , null , 2 ) ) ;
272- console . error ( `▶ report → ${ out } (status: error)\n${ message } ` ) ;
275+ // e.g. the install CLI failing on a bad OCI ref — see writeErrorReport.
276+ await writeErrorReport ( out , cliVersion , err instanceof Error ? err . message : String ( err ) ) ;
273277 return 1 ;
274278 } finally {
275279 if ( tempDir ) await rm ( tempDir , { recursive : true , force : true } ) ;
0 commit comments