diff --git a/.github/actions/setup-apple-runner-build/action.yml b/.github/actions/setup-apple-runner-build/action.yml index acb36d6910..fedd064796 100644 --- a/.github/actions/setup-apple-runner-build/action.yml +++ b/.github/actions/setup-apple-runner-build/action.yml @@ -1,5 +1,5 @@ name: 'Set Up Apple Runner Build' -description: 'Restore an exact Apple runner build or build and cache it on a miss' +description: 'Restore an exact Apple runner build and verify it with Xcode' inputs: derived-path: @@ -44,7 +44,13 @@ runs: id: source-hash run: | set -euo pipefail - echo "value=${{ hashFiles('apple/runner/**', 'apple/snapshot-presentation/**', 'scripts/build-xcuitest-apple.sh', 'scripts/runner-isolation-diagnostics.ts', 'scripts/swift-toolchain-tmpdir.ts', 'scripts/write-xcuitest-cache-metadata.mjs', '.github/actions/setup-apple-runner-build/action.yml') }}" >> "$GITHUB_OUTPUT" + echo "value=${{ hashFiles('apple/runner/**', 'apple/snapshot-presentation/**') }}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Resolve Apple runner cache schema + id: cache-schema + run: | + echo "value=${{ hashFiles('.github/actions/setup-apple-runner-build/action.yml', 'scripts/build-xcuitest-apple.sh') }}" >> "$GITHUB_OUTPUT" shell: bash - name: Resolve Apple runner build variant @@ -79,10 +85,9 @@ runs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: path: ${{ inputs.derived-path }} - key: ${{ inputs.cache-key-prefix }}-${{ steps.xcode.outputs.key }}${{ inputs.cache-key-suffix }}-${{ steps.build-variant.outputs.key }}-${{ steps.source-hash.outputs.value }} + key: ${{ inputs.cache-key-prefix }}-${{ steps.xcode.outputs.key }}${{ inputs.cache-key-suffix }}-${{ steps.build-variant.outputs.key }}-${{ steps.cache-schema.outputs.value }}-${{ steps.source-hash.outputs.value }} - - name: Build Apple runner artifacts on cache miss - if: steps.restore-runner-build.outputs.cache-hit != 'true' + - name: Verify Apple runner artifacts with Xcode uses: ./.github/actions/run-gate with: gate: ${{ inputs.gate }} @@ -111,7 +116,7 @@ runs: run: | set -euo pipefail if [ "$CACHE_HIT" = 'true' ]; then - RESULT='restored native build; compilation skipped; icon patch applied' + RESULT='restored exact native build; verified incrementally with Xcode; icon patch applied' else RESULT='cache miss; built and cached native runner; icon patch applied' fi diff --git a/scripts/__tests__/apple-ci-impact.test.ts b/scripts/__tests__/apple-ci-impact.test.ts index 31b79a3d86..e6a00d9dce 100644 --- a/scripts/__tests__/apple-ci-impact.test.ts +++ b/scripts/__tests__/apple-ci-impact.test.ts @@ -9,10 +9,10 @@ import { selectChecks } from '../check-affected/model.ts'; const repoRoot = path.resolve(import.meta.dirname, '../..'); -function cacheInputs(action: string): string[] { +function cacheInputs(action: string, stepId = 'source-hash'): string[] { const doc = parse(action) as { runs?: { steps?: Array<{ id?: string; run?: string }> } }; - const sourceHash = doc.runs?.steps?.find((step) => step.id === 'source-hash')?.run ?? ''; - const expressions = [...sourceHash.matchAll(/hashFiles\(([\s\S]*?)\)/g)]; + const hashStep = doc.runs?.steps?.find((step) => step.id === stepId)?.run ?? ''; + const expressions = [...hashStep.matchAll(/hashFiles\(([\s\S]*?)\)/g)]; expect(expressions.length).toBeGreaterThan(0); return expressions.flatMap((expression) => [...expression[1]!.matchAll(/'([^']+)'/g)].map((match) => match[1]!), @@ -40,6 +40,7 @@ test('native runner build-cache inputs trigger the PR XCTest lane', () => { 'utf8', ); expect(cacheInputs(action).filter((input) => input.startsWith('packages/'))).toEqual([]); + expect(cacheInputs(action).filter((input) => input.endsWith('.ts'))).toEqual([]); const uncovered = (text: string) => cacheInputs(text) .filter((input) => !input.startsWith('!')) @@ -62,26 +63,58 @@ test('native runner build-cache inputs trigger the PR XCTest lane', () => { ).toEqual(['packages/platform-apple/src/foldable/**']); }); -test('the cache stores native products before applying the current runner icon patch', () => { +type AppleRunnerBuildStep = { + id?: string; + name?: string; + env?: Record; + if?: string; + run?: string; + with?: Record; +}; + +function appleRunnerBuildAction(): { text: string; steps: AppleRunnerBuildStep[] } { const action = fs.readFileSync( path.join(repoRoot, '.github/actions/setup-apple-runner-build/action.yml'), 'utf8', ); const doc = parse(action) as { - runs: { - steps: Array<{ name?: string; env?: Record; if?: string; run?: string }>; - }; + runs: { steps: AppleRunnerBuildStep[] }; }; - const steps = doc.runs.steps; - const build = steps.find((step) => step.name === 'Build Apple runner artifacts on cache miss'); + return { text: action, steps: doc.runs.steps }; +} + +test('Apple runner build cache uses only declared source and schema hashes', () => { + const { text, steps } = appleRunnerBuildAction(); + const restoreIndex = steps.findIndex((step) => step.name === 'Restore Apple runner build cache'); + expect(steps.filter((step) => step.run?.includes('hashFiles(')).map((step) => step.id)).toEqual([ + 'source-hash', + 'cache-schema', + ]); + expect(cacheInputs(text, 'cache-schema')).toEqual([ + '.github/actions/setup-apple-runner-build/action.yml', + 'scripts/build-xcuitest-apple.sh', + ]); + expect(steps[restoreIndex]?.with?.key).toContain('steps.cache-schema.outputs.value'); + expect(steps[restoreIndex]?.with?.['restore-keys']).toBeUndefined(); + expect(cacheInputs(text)).not.toContain('scripts/patch-xcuitest-runner-icon.ts'); +}); + +test('restored native products are rebuilt before caching and icon patching', () => { + const { steps } = appleRunnerBuildAction(); + const restoreIndex = steps.findIndex((step) => step.name === 'Restore Apple runner build cache'); + const buildIndex = steps.findIndex( + (step) => step.name === 'Verify Apple runner artifacts with Xcode', + ); const saveIndex = steps.findIndex((step) => step.name === 'Save Apple runner build cache'); const patchIndex = steps.findIndex((step) => step.name === 'Patch XCTest runner icon'); - expect(build?.env?.AGENT_DEVICE_XCUITEST_SKIP_ICON_PATCH).toBe('1'); - expect(saveIndex).toBeGreaterThan(-1); + expect(buildIndex).toBeGreaterThan(restoreIndex); + expect(steps[buildIndex]?.if).toBeUndefined(); + expect(steps[buildIndex]?.env?.AGENT_DEVICE_XCUITEST_SKIP_ICON_PATCH).toBe('1'); + expect(saveIndex).toBeGreaterThan(buildIndex); + expect(steps[saveIndex]?.if).toContain("cache-hit != 'true'"); expect(patchIndex).toBeGreaterThan(saveIndex); expect(steps[patchIndex]?.if).toBeUndefined(); expect(steps[patchIndex]?.run).toContain('scripts/patch-xcuitest-runner-icon.ts'); - expect(cacheInputs(action)).not.toContain('scripts/patch-xcuitest-runner-icon.ts'); expect(fs.readFileSync(path.join(repoRoot, 'scripts/build-xcuitest-apple.sh'), 'utf8')).toContain( 'if ! is_truthy "${AGENT_DEVICE_XCUITEST_SKIP_ICON_PATCH:-}"; then', );