ci: narrow pnpm cache keys and shallow development checkouts (#20370)

* ci: cache dependency downloads and shallow development checkouts

* ci: defer mobile caches after measuring restore overhead

* ci: retain existing release signing cache behavior
This commit is contained in:
Neil
2026-09-12 01:31:04 -07:00
committed by GitHub
parent 1b5092492f
commit 4aa9329e99
9 changed files with 123 additions and 30 deletions
@@ -10,6 +10,10 @@ inputs:
description: Node.js version override; defaults to the version declared in package.json.
required: false
default: ''
cache-dependency-path:
description: Lockfiles for the pnpm download store; include mobile/pnpm-lock.yaml only when the job installs mobile dependencies.
required: false
default: pnpm-lock.yaml
persist-native-cache:
description: Save restored native modules at job end. Set false when a later step overwrites the same path with a different ABI.
required: false
@@ -39,9 +43,7 @@ runs:
with:
install: false
# Why both lockfiles: setup-node keys the pnpm store on the root lockfile alone, so
# jobs that also install mobile restored a store with none of the React Native tree
# in it and re-downloaded the lot on every run.
# Desktop-only jobs should not miss their download cache when mobile dependencies change.
- name: Setup Node.js
id: default-node
if: inputs.node-version == ''
@@ -49,9 +51,7 @@ runs:
with:
node-version-file: package.json
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
mobile/pnpm-lock.yaml
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- name: Setup requested Node.js
id: requested-node
@@ -60,9 +60,7 @@ runs:
with:
node-version: ${{ inputs.node-version }}
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
mobile/pnpm-lock.yaml
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- name: Validate native runtime
shell: bash
+2 -4
View File
@@ -160,16 +160,14 @@ jobs:
- name: Checkout the requested ref
uses: actions/checkout@v6
env:
# Full-history checkout must also preserve case-twin branch and tag names.
GIT_DEFAULT_REF_FORMAT: reftable
with:
# Why an input at all rather than just github.ref: the whole point is to
# build code that has not landed, and the workflow definition itself
# always comes from the dispatch ref — naming the branch here instead
# applies main's current copy of this file to an arbitrary branch.
ref: ${{ steps.vetted.outputs.sha }}
fetch-depth: 0
# Version helpers only read HEAD; published versions come from the release API.
fetch-depth: 1
# This job only reads stablyai/orca and never pushes; every write goes
# to the adhoc repo through a minted App token passed by env. Not
# persisting the checkout credential shrinks the blast radius if a build
+2 -1
View File
@@ -90,7 +90,8 @@ jobs:
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
# Version helpers only read HEAD; published versions come from the release API.
fetch-depth: 1
# Why: this job only reads stablyai/orca and never pushes; every write
# goes to the daily repo through a minted App token passed by env.
# Not persisting the checkout credential shrinks the blast radius if a
+2 -1
View File
@@ -137,7 +137,8 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.head_sha }}
fetch-depth: 0
# Version helpers only read HEAD; published versions come from the release API.
fetch-depth: 1
# Why: this job only reads stablyai/orca and never pushes; every write
# goes to the hourly repo through a minted App token passed by env.
# Not persisting the checkout credential shrinks the blast radius if a
+4
View File
@@ -41,6 +41,10 @@ jobs:
uses: actions/checkout@v6
- uses: ./.github/actions/install-node-dependencies
with:
cache-dependency-path: |
pnpm-lock.yaml
mobile/pnpm-lock.yaml
# bundler-cache installs mobile/Gemfile.lock, so this job is also what
# proves the pinned fastlane the release workflow depends on still
+3
View File
@@ -126,6 +126,9 @@ jobs:
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
cache-dependency-path: |
pnpm-lock.yaml
mobile/pnpm-lock.yaml
- name: Lint
run: pnpm exec oxlint --format github
@@ -0,0 +1,30 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const read = (path) => parse(readFileSync(path, 'utf8'))
const workflow = (name) => read(`.github/workflows/${name}.yml`)
const action = read('.github/actions/install-node-dependencies/action.yml')
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('pnpm')
expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}')
}
const install = action.runs.steps.find((step) => step.name === 'Install dependencies')
expect(install.if).toBeUndefined()
expect(install.run).toContain('pnpm install --frozen-lockfile --ignore-scripts')
expect(install.run).toContain(
'diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml'
)
const mobile = workflow('mobile').jobs.verify.steps.find((step) =>
step.uses?.includes('install-node-dependencies')
)
expect(mobile.with['cache-dependency-path'].trim().split('\n')).toEqual([
'pnpm-lock.yaml',
'mobile/pnpm-lock.yaml'
])
})
})
@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import { runProcessSync } from '../../src/shared/child-process/run-process'
const projectDir = resolve(import.meta.dirname, '../..')
@@ -15,13 +18,74 @@ const REF_MIRRORS = [
]
describe('ref-mirroring vet steps', () => {
it('keeps the full-history adhoc checkout on the same case-safe backend', () => {
it.each(['daily', 'hourly', 'adhoc'])('%s builds only need the current commit', (channel) => {
const job = readWorkflow(`.github/workflows/${channel}-mac-build.yml`).jobs[
`build-${channel}-mac`
]
const checkout = job.steps.find((step) => step.uses === 'actions/checkout@v6')
expect(checkout.with['fetch-depth']).toBe(1)
expect(job.steps.some((step) => step.run?.includes('gh release list'))).toBe(true)
expect(
job.steps.some((step) => step.run?.includes('ORCA_PUBLISHED_VERSIONS="$published"'))
).toBe(true)
})
it('retains release-cut history for version reservation and retry ancestry', () => {
const checkout = readWorkflow('.github/workflows/release-cut.yml').jobs.cut.steps.find(
(step) => step.uses === 'actions/checkout@v6'
)
expect(checkout.with['fetch-depth']).toBe(0)
})
it('resolves identical dev identities in full and depth-one checkouts without local tags', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-checkout-identity-'))
const source = join(directory, 'source')
const shallow = join(directory, 'shallow')
const run = (program, args, cwd) => {
const result = runProcessSync({ program, args, cwd })
expect(result.code, result.stderr).toBe(0)
return result.stdout.trim()
}
const git = (args, cwd = directory) => run('git', args, cwd)
try {
git(['init', source])
git(['config', 'user.name', 'CI test'], source)
git(['config', 'user.email', 'ci@example.invalid'], source)
writeFileSync(join(source, 'package.json'), JSON.stringify({ version: '1.4.165-rc.0' }))
git(['add', 'package.json'], source)
git(['-c', 'commit.gpgsign=false', 'commit', '-m', 'initial'], source)
git(['tag', 'v1.4.167'], source)
git(['-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'head'], source)
git(['clone', '--depth=1', '--no-tags', pathToFileURL(source).href, shallow])
expect(git(['rev-list', '--count', 'HEAD'], shallow)).toBe('1')
expect(git(['tag', '--list'], shallow)).toBe('')
const script = `
const result = [];
for (const [channel, exported] of [['daily', 'Daily'], ['hourly', 'Hourly'], ['adhoc', 'Adhoc']]) {
const module = await import(${JSON.stringify(pathToFileURL(join(projectDir, 'config/scripts/')).href)} + channel + '-build-version.mjs');
const date = new Date('2026-09-12T00:00:00Z');
result.push(channel === 'adhoc'
? module.getAdhocBuildIdentity(date, 'branch', ['v1.4.167'])
: module['get' + exported + 'BuildIdentity'](date, { publishedVersions: ['v1.4.167'], releaseNames: [] }));
}
process.stdout.write(JSON.stringify(result));
`
const identities = (cwd) => run(process.execPath, ['--input-type=module', '-e', script], cwd)
expect(identities(shallow)).toBe(identities(source))
expect(
JSON.parse(identities(shallow)).every((identity) => identity.version.startsWith('1.4.168-'))
).toBe(true)
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
it('checks out only the vetted commit without remirroring refs', () => {
const steps = readWorkflow('.github/workflows/adhoc-mac-build.yml').jobs['build-adhoc-mac']
.steps
const checkout = steps.find((step) => step.name === 'Checkout the requested ref')
expect(checkout.env.GIT_DEFAULT_REF_FORMAT).toBe('reftable')
expect(checkout.with.ref).toBe('${{ steps.vetted.outputs.sha }}')
expect(checkout.with['fetch-depth']).toBe(0)
expect(checkout.with['fetch-depth']).toBe(1)
expect(checkout.with['persist-credentials']).toBe(false)
})
@@ -95,7 +95,7 @@ describe('release ref trust with case-twin names', () => {
expect(result.stdout).toContain('Refusing to build PR ref')
})
it('preserves both case variants in the subsequent full-history checkout', async () => {
it('checks out the vetted SHA shallowly without mirroring case-twin refs again', async () => {
const checkout = join(directory, 'checkout')
const env = { ...identity, ...macCheckout.env }
await git(['init', checkout], env)
@@ -105,21 +105,15 @@ describe('release ref trust with case-twin names', () => {
checkout,
'fetch',
'--no-tags',
`--depth=${macCheckout.with['fetch-depth']}`,
repository,
'+refs/heads/*:refs/remotes/origin/*',
'+refs/tags/*:refs/tags/*'
upper
],
env
)
await git(['-C', checkout, 'checkout', '--detach', upper], env)
for (const [ref, sha] of [
['refs/remotes/origin/Fix', upper],
['refs/remotes/origin/fix', lower],
['refs/tags/Release', upper],
['refs/tags/release', lower]
]) {
expect(await git(['-C', checkout, 'rev-parse', `${ref}^{commit}`], env)).toBe(sha)
}
expect(await git(['-C', checkout, 'rev-parse', 'HEAD'], env)).toBe(upper)
expect(await git(['-C', checkout, 'rev-list', '--count', 'HEAD'], env)).toBe('1')
expect(await git(['-C', checkout, 'for-each-ref', '--format=%(refname)'], env)).toBe('')
})
})