Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .github/actions/setup-apple-runner-build/action.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
57 changes: 45 additions & 12 deletions scripts/__tests__/apple-ci-impact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]!),
Expand Down Expand Up @@ -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([]);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const uncovered = (text: string) =>
cacheInputs(text)
.filter((input) => !input.startsWith('!'))
Expand All @@ -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<string, string>;
if?: string;
run?: string;
with?: Record<string, string>;
};

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<string, string>; 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();

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This refactored test pins the cache-schema half of the restore key but never asserts the key contains steps.source-hash.outputs.value. The PR's core guarantee (commit 840486d: a native source change must produce a cold build) depends entirely on that key segment; if it were accidentally dropped from action.yml, this test would still pass while stale native products get restored. Add an assertion on the same key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/__tests__/apple-ci-impact.test.ts, line 98:

<comment>This refactored test pins the cache-schema half of the restore key but never asserts the key contains `steps.source-hash.outputs.value`. The PR's core guarantee (commit 840486d8: a native source change must produce a cold build) depends entirely on that key segment; if it were accidentally dropped from `action.yml`, this test would still pass while stale native products get restored. Add an assertion on the same key.</comment>

<file context>
@@ -82,14 +83,19 @@ function appleRunnerBuildAction(): { text: string; steps: AppleRunnerBuildStep[]
+  ]);
   expect(steps[restoreIndex]?.with?.key).toContain('steps.cache-schema.outputs.value');
-  expect(steps[restoreIndex]?.with?.['restore-keys']).toContain('steps.cache-schema.outputs.value');
+  expect(steps[restoreIndex]?.with?.['restore-keys']).toBeUndefined();
   expect(cacheInputs(text)).not.toContain('scripts/patch-xcuitest-runner-icon.ts');
 });
</file context>
Fix with cubic

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',
);
Expand Down
Loading