@@ -348,8 +348,8 @@ test('producer maps each platform to its resolved lookup and matrix artifact nam
348348 } ,
349349 } ) ;
350350 assert . equal ( result . status , 0 , result . stderr ) ;
351- const matrixLine = fs . readFileSync ( outputPath , 'utf8' ) . trim ( ) ;
352- assert . match ( matrixLine , / ^ m a t r i x = / ) ;
351+ const outputLines = fs . readFileSync ( outputPath , 'utf8' ) . trim ( ) . split ( '\n' ) ;
352+ const matrixLine = outputLines . find ( ( line ) => line . startsWith ( ' matrix=' ) ) ;
353353 const matrix = JSON . parse ( matrixLine . slice ( 'matrix=' . length ) ) ;
354354 assert . deepEqual (
355355 matrix . include . map ( ( { platform, artifactName } ) => ( { platform, artifactName } ) ) ,
@@ -358,13 +358,110 @@ test('producer maps each platform to its resolved lookup and matrix artifact nam
358358 { platform : 'android' , artifactName : 'fingerprint.android-hash.android' } ,
359359 ] ,
360360 ) ;
361+ assert . deepEqual (
362+ outputLines . find ( ( line ) => line . startsWith ( 'has-work=' ) ) ,
363+ 'has-work=true' ,
364+ ) ;
361365 assert . deepEqual ( fs . readFileSync ( resolverLog , 'utf8' ) . trim ( ) . split ( '\n' ) , [ 'ios' , 'android' ] ) ;
362366 assert . deepEqual ( fs . readFileSync ( nodeLog , 'utf8' ) . trim ( ) . split ( '\n' ) , [
363367 '.github/actions/setup-fixture-app/trusted-artifact.mjs find octo/repo fingerprint.ios-hash.ios current-head' ,
364368 '.github/actions/setup-fixture-app/trusted-artifact.mjs find octo/repo fingerprint.android-hash.android current-head' ,
365369 ] ) ;
366370} ) ;
367371
372+ test ( 'fingerprint matrix covers all four cache combinations and gates has-work on both-cached' , ( t ) => {
373+ const workflow = parse ( fs . readFileSync ( '.github/workflows/test-app-build-cache.yml' , 'utf8' ) ) ;
374+ const fingerprintStep = workflow . jobs . fingerprint . steps . find ( ( step ) => step . id === 'fingerprint' ) ;
375+ // #2034: an empty `include` matrix is legal JSON but GitHub Actions rejects
376+ // it as `strategy.matrix`, so the `release` job must be skipped -- not
377+ // handed a zero-length matrix -- whenever both fixtures are cached.
378+ assert . equal (
379+ workflow . jobs . fingerprint . outputs [ 'has-work' ] ,
380+ '${{ steps.fingerprint.outputs.has-work }}' ,
381+ ) ;
382+ assert . equal ( workflow . jobs . release . if , "needs.fingerprint.outputs.has-work == 'true'" ) ;
383+
384+ const run = fingerprintStep . run
385+ . replaceAll ( '${{ github.event.pull_request.head.sha || github.sha }}' , 'current-head' )
386+ . replaceAll ( '${{ github.repository }}' , 'octo/repo' )
387+ . replaceAll ( '${{ github.event_name }}' , 'push' )
388+ . replaceAll ( '${{ github.event.pull_request.head.repo.full_name }}' , '' ) ;
389+
390+ const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'fixture-matrix-combinations-' ) ) ;
391+ t . after ( ( ) => fs . rmSync ( tempRoot , { force : true , recursive : true } ) ) ;
392+ const actionDir = path . join ( tempRoot , '.github/actions/setup-fixture-app' ) ;
393+ const binDir = path . join ( tempRoot , 'bin' ) ;
394+ fs . mkdirSync ( actionDir , { recursive : true } ) ;
395+ fs . mkdirSync ( binDir ) ;
396+ fs . writeFileSync (
397+ path . join ( actionDir , 'resolve-artifact-name.sh' ) ,
398+ [ '#!/bin/sh' , 'printf "fingerprint.%s-hash.%s\\n" "$1" "$1"' , '' ] . join ( '\n' ) ,
399+ ) ;
400+ fs . chmodSync ( path . join ( actionDir , 'resolve-artifact-name.sh' ) , 0o755 ) ;
401+ fs . writeFileSync (
402+ path . join ( binDir , 'node' ) ,
403+ [
404+ '#!/bin/sh' ,
405+ // Invoked as: node trusted-artifact.mjs find <repo> <name> <sha>
406+ 'name="$4"' ,
407+ 'case "$name" in' ,
408+ ' *.ios) [ "$TEST_IOS_CACHED" = 1 ] && printf "111"; true ;;' ,
409+ ' *.android) [ "$TEST_ANDROID_CACHED" = 1 ] && printf "222"; true ;;' ,
410+ 'esac' ,
411+ '' ,
412+ ] . join ( '\n' ) ,
413+ ) ;
414+ fs . chmodSync ( path . join ( binDir , 'node' ) , 0o755 ) ;
415+
416+ const runFingerprint = ( iosCached , androidCached ) => {
417+ const outputPath = path . join ( tempRoot , `output-${ iosCached } -${ androidCached } ` ) ;
418+ const result = spawnSync ( 'bash' , [ '-c' , run ] , {
419+ cwd : tempRoot ,
420+ encoding : 'utf8' ,
421+ env : {
422+ ...process . env ,
423+ GITHUB_OUTPUT : outputPath ,
424+ PATH : `${ binDir } :${ process . env . PATH } ` ,
425+ TEST_ANDROID_CACHED : androidCached ? '1' : '0' ,
426+ TEST_IOS_CACHED : iosCached ? '1' : '0' ,
427+ } ,
428+ } ) ;
429+ assert . equal ( result . status , 0 , result . stderr ) ;
430+ const lines = fs . readFileSync ( outputPath , 'utf8' ) . trim ( ) . split ( '\n' ) ;
431+ const matrixLine = lines . find ( ( line ) => line . startsWith ( 'matrix=' ) ) ;
432+ const hasWorkLine = lines . find ( ( line ) => line . startsWith ( 'has-work=' ) ) ;
433+ return {
434+ hasWork : hasWorkLine . slice ( 'has-work=' . length ) ,
435+ matrix : JSON . parse ( matrixLine . slice ( 'matrix=' . length ) ) ,
436+ } ;
437+ } ;
438+
439+ const bothCached = runFingerprint ( true , true ) ;
440+ assert . deepEqual ( bothCached . matrix , { include : [ ] } ) ;
441+ assert . equal ( bothCached . hasWork , 'false' ) ;
442+
443+ const iosOnlyCached = runFingerprint ( true , false ) ;
444+ assert . deepEqual (
445+ iosOnlyCached . matrix . include . map ( ( entry ) => entry . platform ) ,
446+ [ 'android' ] ,
447+ ) ;
448+ assert . equal ( iosOnlyCached . hasWork , 'true' ) ;
449+
450+ const androidOnlyCached = runFingerprint ( false , true ) ;
451+ assert . deepEqual (
452+ androidOnlyCached . matrix . include . map ( ( entry ) => entry . platform ) ,
453+ [ 'ios' ] ,
454+ ) ;
455+ assert . equal ( androidOnlyCached . hasWork , 'true' ) ;
456+
457+ const neitherCached = runFingerprint ( false , false ) ;
458+ assert . deepEqual (
459+ neitherCached . matrix . include . map ( ( entry ) => entry . platform ) ,
460+ [ 'ios' , 'android' ] ,
461+ ) ;
462+ assert . equal ( neitherCached . hasWork , 'true' ) ;
463+ } ) ;
464+
368465test ( 'artifact name resolver scopes both platforms and rejects invalid output' , ( t ) => {
369466 const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'fixture-artifact-name-' ) ) ;
370467 t . after ( ( ) => fs . rmSync ( tempRoot , { force : true , recursive : true } ) ) ;
0 commit comments