diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml index 916baf446b7..e2b13ac21c0 100644 --- a/.github/actions/install-node-dependencies/action.yml +++ b/.github/actions/install-node-dependencies/action.yml @@ -2,6 +2,10 @@ name: Install Node dependencies description: Installs the Node toolchain and repository dependencies for CI jobs, with optional Electron archive caching. inputs: + cache-pnpm-store: + description: Restore or save the pnpm download store; verification and native caches are independent. + required: false + default: 'true' cache-pnpm-verification: description: Restore pnpm's policy-checked lockfile verification record. required: false @@ -69,7 +73,7 @@ runs: uses: actions/setup-node@v6 with: node-version-file: package.json - cache: ${{ github.event_name != 'pull_request' && 'pnpm' || '' }} + cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }} cache-dependency-path: ${{ inputs.cache-dependency-path }} package-manager-cache: false @@ -79,7 +83,7 @@ runs: uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} - cache: ${{ github.event_name != 'pull_request' && 'pnpm' || '' }} + cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }} cache-dependency-path: ${{ inputs.cache-dependency-path }} package-manager-cache: false @@ -87,8 +91,10 @@ runs: - name: Resolve pnpm download store id: pnpm-store if: >- - github.event_name == 'pull_request' && - (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) + github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && + (runner.os != 'Windows' || + !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && + !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) shell: bash env: LOCKFILE_HASH: ${{ hashFiles(inputs.cache-dependency-path) }} @@ -100,11 +106,13 @@ runs: printf 'arch=%s\n' "$(node -p 'require("node:os").arch()')" >> "$GITHUB_OUTPUT" # Match setup-node's key and path so existing default-branch stores remain reusable. - # Hosted Windows x64 mixed installs cost less than restoring their root/mobile store. + # Measured Windows installs cost less than unpacking their download store. - name: Restore pnpm download store without saving if: >- - github.event_name == 'pull_request' && - (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) + github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && + (runner.os != 'Windows' || + !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && + !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) uses: actions/cache/restore@v5 with: path: ${{ steps.pnpm-store.outputs.path }} diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index 081f22d4046..dad53d33f47 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -154,6 +154,7 @@ jobs: - uses: ./.github/actions/install-node-dependencies with: native-runtime: ${{ runner.os == 'Windows' && 'node' || 'none' }} + cache-pnpm-store: ${{ runner.os != 'Windows' }} # Linux release slots come from the floor and Alpine lanes; this slot serves local tests. - uses: ./.github/actions/prepare-orcad-prebuilds id: orcad-prebuild diff --git a/.github/workflows/ssh-windows-hosts.yml b/.github/workflows/ssh-windows-hosts.yml index cd77ee87b67..8afae35ca12 100644 --- a/.github/workflows/ssh-windows-hosts.yml +++ b/.github/workflows/ssh-windows-hosts.yml @@ -109,6 +109,7 @@ jobs: id: dependencies with: native-runtime: node + cache-pnpm-store: 'false' # The deploy materializes rung A from this template; only this runner's slot exists here. # The process-tree addon carries the launcher that starts the relay outside sshd's job; the # orcad slot and the relay both stage it, and a standard-user host has no other launch route. diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index 584c37f6740..6f2ec4f4d05 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -11,7 +11,9 @@ describe('CI dependency download caches', () => { it('scopes desktop stores to the root lockfile and lets mixed installs opt in', () => { expect(action.inputs['cache-dependency-path'].default).toBe('pnpm-lock.yaml') for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) { - expect(step.with.cache).toBe("${{ github.event_name != 'pull_request' && 'pnpm' || '' }}") + expect(step.with.cache).toBe( + "${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }}" + ) expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}') expect(step.with['package-manager-cache']).toBe(false) } @@ -30,13 +32,13 @@ describe('CI dependency download caches', () => { ]) }) - it('restores PR stores except measured Windows mixed installs, without a post-job save', () => { + it('restores PR stores except measured Windows installs, without a post-job save', () => { const resolve = action.runs.steps.find((step) => step.id === 'pnpm-store') const restore = action.runs.steps.find( (step) => step.name === 'Restore pnpm download store without saving' ) expect(resolve.if).toBe( - "github.event_name == 'pull_request' && (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml'))" + "github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))" ) expect(restore.if).toBe(resolve.if) expect(restore.uses).toBe('actions/cache/restore@v5') @@ -69,36 +71,111 @@ describe('CI dependency download caches', () => { ['Windows x64 mixed PR', 'pull_request', 'Windows', 'X64', true, false, ''], ['Windows ARM64 mixed PR', 'pull_request', 'Windows', 'ARM64', true, true, ''], ['Windows x86 mixed PR', 'pull_request', 'Windows', 'X86', true, true, ''], - ['Windows x64 root-only PR', 'pull_request', 'Windows', 'X64', false, true, ''], + ['Windows x64 root-only PR', 'pull_request', 'Windows', 'X64', false, false, ''], + ['Windows ARM64 root-only PR', 'pull_request', 'Windows', 'ARM64', false, false, ''], + ['Windows x86 root-only PR', 'pull_request', 'Windows', 'X86', false, true, ''], + ['Windows x64 custom PR', 'pull_request', 'Windows', 'X64', 'cloud/pnpm-lock.yaml', true, ''], + [ + 'Windows ARM64 custom PR', + 'pull_request', + 'Windows', + 'ARM64', + 'cloud/pnpm-lock.yaml', + true, + '' + ], + ['Windows ARM64 root-only push', 'push', 'Windows', 'ARM64', false, false, 'pnpm'], + [ + 'Windows ARM64 root-only manual run', + 'workflow_dispatch', + 'Windows', + 'ARM64', + false, + false, + 'pnpm' + ], + ['Explicit Linux PR opt-out', 'pull_request', 'Linux', 'X64', false, false, '', 'false'], + ['Explicit Windows push opt-out', 'push', 'Windows', 'ARM64', false, false, '', 'false'], + [ + 'Explicit Windows manual opt-out', + 'workflow_dispatch', + 'Windows', + 'X64', + false, + false, + '', + 'false' + ], + [ + 'Explicit custom-store opt-out', + 'pull_request', + 'Windows', + 'X64', + 'cloud/pnpm-lock.yaml', + false, + '', + 'false' + ], + ['Windows x64 root-only push', 'push', 'Windows', 'X64', false, false, 'pnpm'], ['Linux x64 mixed PR', 'pull_request', 'Linux', 'X64', true, true, ''], ['Linux ARM64 mixed PR', 'pull_request', 'Linux', 'ARM64', true, true, ''], ['macOS ARM64 mixed PR', 'pull_request', 'macOS', 'ARM64', true, true, ''], ['Windows x64 mixed push', 'push', 'Windows', 'X64', true, false, 'pnpm'], ['Windows x64 mixed manual run', 'workflow_dispatch', 'Windows', 'X64', true, false, 'pnpm'] - ])('%s keeps its scoped store policy', (_name, event, os, arch, mixed, restore, cache) => { - const context = { - github: { event_name: event }, - runner: { os, arch }, - inputs: { - 'cache-dependency-path': mixed ? 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' : 'pnpm-lock.yaml' - }, - contains: (value, search) => value.toLowerCase().includes(search.toLowerCase()) - } - const evaluate = (expression) => - runInNewContext( - expression.replaceAll('inputs.cache-dependency-path', 'inputs["cache-dependency-path"]'), - context - ) - for (const step of action.runs.steps.filter( - (step) => - step.id === 'pnpm-store' || step.name === 'Restore pnpm download store without saving' - )) { - expect(evaluate(step.if)).toBe(restore) - } - for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) { - expect(evaluate(step.with.cache.slice(3, -2))).toBe(cache) - expect(step.with['package-manager-cache']).toBe(false) + ])( + '%s keeps its scoped store policy', + (_name, event, os, arch, mixed, restore, cache, storeCache = 'true') => { + const context = { + github: { event_name: event }, + runner: { os, arch }, + inputs: { + 'cache-pnpm-store': storeCache, + 'cache-dependency-path': + typeof mixed === 'string' + ? mixed + : mixed + ? 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' + : 'pnpm-lock.yaml' + }, + contains: (value, search) => value.toLowerCase().includes(search.toLowerCase()) + } + const evaluate = (expression) => + runInNewContext( + expression + .replaceAll('inputs.cache-dependency-path', 'inputs["cache-dependency-path"]') + .replaceAll('inputs.cache-pnpm-store', 'inputs["cache-pnpm-store"]'), + context + ) + for (const step of action.runs.steps.filter( + (step) => + step.id === 'pnpm-store' || step.name === 'Restore pnpm download store without saving' + )) { + expect(evaluate(step.if)).toBe(restore) + } + for (const step of action.runs.steps.filter( + (step) => step.uses === 'actions/setup-node@v6' + )) { + expect(evaluate(step.with.cache.slice(3, -2))).toBe(cache) + expect(step.with['package-manager-cache']).toBe(false) + } } + ) + + it('opts Windows server consumers out while preserving the main warmer store writer', () => { + const installer = './.github/actions/install-node-dependencies' + const persistence = workflow('node-server-tests').jobs.persistence.steps.find( + (step) => step.uses === installer + ) + const ssh = workflow('ssh-windows-hosts').jobs.hosts.steps.find( + (step) => step.uses === installer + ) + const warmer = workflow('ci-cache-warmup').jobs['warm-windows'].steps.find( + (step) => step.uses === installer + ) + expect(action.inputs['cache-pnpm-store'].default).toBe('true') + expect(persistence.with['cache-pnpm-store']).toBe("${{ runner.os != 'Windows' }}") + expect(ssh.with['cache-pnpm-store']).toBe('false') + expect(warmer.with['cache-pnpm-store']).toBeUndefined() }) it('restores Windows packaging downloads from the release cache without a PR upload', () => { diff --git a/config/scripts/mobile-release-shell-switch-workflow.test.mjs b/config/scripts/mobile-release-shell-switch-workflow.test.mjs index f6f05c779eb..aa6917acb3c 100644 --- a/config/scripts/mobile-release-shell-switch-workflow.test.mjs +++ b/config/scripts/mobile-release-shell-switch-workflow.test.mjs @@ -122,7 +122,7 @@ const REVIEWED_COMPUTED_PATHS = [ '${{ steps.pnpm-store.outputs.path }}', // Only pnpm's lockfile-verified.jsonl record, never Metro transforms. '${{ steps.verification-cache.outputs.path }}', - "${{ github.event_name != 'pull_request' && 'pnpm' || '' }} store" + "${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }} store" ] /** Every step a workflow runs, descending into the repository's own composite actions. */ diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs index 611dd974b79..43e8784f185 100644 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ b/config/scripts/pr-workflow-parallelism.test.mjs @@ -289,7 +289,8 @@ describe('PR workflow parallelism', () => { expect(steps[pnpmIndex].uses).toBe('pnpm/setup@v2') expect(steps[pnpmIndex].with.version).toBeUndefined() expect(steps[pnpmIndex].with.install).toBe(false) - const saveOutsidePrs = "${{ github.event_name != 'pull_request' && 'pnpm' || '' }}" + const saveOutsidePrs = + "${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }}" expect(steps[nodeIndex].with.cache).toBe(saveOutsidePrs) expect(steps[nodeIndex].if).toBe("inputs.node-version == ''") expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''") @@ -304,7 +305,7 @@ describe('PR workflow parallelism', () => { ) expect(steps[restoreIndex].uses).toBe('actions/cache/restore@v5') expect(steps[restoreIndex].if).toBe( - "github.event_name == 'pull_request' && (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml'))" + "github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))" ) }) diff --git a/docs/reference/ci-runner-efficiency.md b/docs/reference/ci-runner-efficiency.md index 77b6479804b..f72fa3f2d26 100644 --- a/docs/reference/ci-runner-efficiency.md +++ b/docs/reference/ci-runner-efficiency.md @@ -76,6 +76,41 @@ request reuse only following an exact prepared native-cache hit; manual SSH and all release builders retain fresh compilation. Subsequent staging checks still run. Hosted validation and the reuse interval remain to be measured. +## Windows root download stores: registry installs finish sooner + +Three paired samples on each Windows architecture compared the existing exact +main download-store restore with a fresh registry install. Each treatment used a +fresh dependency tree, store and pnpm metadata, with registry-first ordering in +sample 2. Both restored the same policy-checked verification record before timing. +All six pairs used Node 24.21.0 and pnpm 12.8.1; manifest digests and installed +lockfile digests matched, and both retained frozen, script-free installation. + +| Runner | Cached totals (seconds) | Registry totals (seconds) | Paired median saving | +| ------------- | --------------------------- | --------------------------- | -------------------- | +| Windows x64 | 26.820 / 28.885 / 27.751 | 13.644 / 14.126 / 12.908 | 14.759 seconds | +| Windows ARM64 | 216.540 / 288.492 / 189.342 | 119.856 / 238.562 / 115.611 | 73.731 seconds | + +The x64 samples are the three successful Windows 2022 jobs in +[run 37064549378](https://github.com/stablyai/orca/actions/runs/37064549378). +Its ARM cleanup guard rejected pnpm's setup-owned store path before measurement; +those incomplete ARM jobs are excluded. The corrected +[ARM-only run](https://github.com/stablyai/orca/actions/runs/37065220916) passed all +three samples. Earlier rejected measurements also stopped before installation +because an optional config file was absent; none count toward these timings. + +Intervals include actual store lookup/restore, inter-step overhead and root +installation. Checkout, toolchain setup, tree/store reset, verification-record +restoration and native preparation are excluded. ARM variation is substantial; +these samples do not measure whole-workflow, queue or billing savings. + +Root-only Windows x64/ARM64 PR installs now skip the download-store restore. +The existing x64 mixed-install exception remains. An explicit store opt-out also +lets Windows headless persistence and SSH jobs avoid the archive on main or +manual runs. Frozen installs, verification records, native caches and every +qualification check remain. Other lockfile sets and platforms keep their +existing policy. Default non-PR writers, including the warmer, still seed stores +for direct setup-node consumers and workflows that run package scripts. + ## October 1 Windows and dependency cache follow-up [PR #24355](https://github.com/stablyai/orca/pull/24355) merged at `197ea3a3`.