Skip to content

Commit d7f6efd

Browse files
committed
fix(ci): skip release instead of erroring when both fixtures are cached (#2034)
map(select(.build)) yields an empty include list when both the iOS and Android fingerprints already have a trusted artifact, and GitHub Actions rejects an empty strategy.matrix at the workflow level -- so release was never created and the run was marked failure on every push since #1996 merged. Publish has-work alongside matrix and gate release on it, so the both-cached steady state now completes with release skipped instead of erroring the whole workflow.
1 parent c77bc40 commit d7f6efd

2 files changed

Lines changed: 102 additions & 2 deletions

File tree

.github/workflows/test-app-build-cache.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
timeout-minutes: 10
3434
outputs:
3535
matrix: ${{ steps.fingerprint.outputs.matrix }}
36+
has-work: ${{ steps.fingerprint.outputs.has-work }}
3637
steps:
3738
- name: Checkout
3839
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -94,10 +95,12 @@ jobs:
9495
}
9596
] | { include: map(select(.build)) }')"
9697
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
98+
echo "has-work=$(jq -r '.include | length > 0' <<<"$MATRIX")" >> "$GITHUB_OUTPUT"
9799
98100
release:
99101
name: ${{ matrix.name }}
100102
needs: fingerprint
103+
if: needs.fingerprint.outputs.has-work == 'true'
101104
runs-on: ${{ matrix.runsOn }}
102105
timeout-minutes: 60
103106
concurrency:

test/ci/trusted-fixture-artifact.test.mjs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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, /^matrix=/);
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+
368465
test('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

Comments
 (0)