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
42 changes: 32 additions & 10 deletions .github/actions/install-node-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ inputs:
required: false
default: 'true'
cache-pnpm-store-lookup-only:
description: On non-PR producers, refresh existing stores without downloading and save stores on a miss.
description: Auto uses measured hosted Node 24 root installs; true forces lookup, false retains archive restoration.
required: false
default: 'false'
default: auto
cache-pnpm-verification:
description: Restore pnpm's policy-checked lockfile verification record.
required: false
Expand Down Expand Up @@ -67,6 +67,30 @@ outputs:
runs:
using: composite
steps:
- name: Resolve pnpm store mode
id: pnpm-store-mode
if: >-
github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' &&
(inputs.cache-pnpm-store-lookup-only == 'true' ||
(inputs.cache-pnpm-store-lookup-only == 'auto' &&
inputs.cache-dependency-path == 'pnpm-lock.yaml' &&
runner.environment == 'github-hosted' && job.container.id == '' &&
(runner.os == 'Linux' || runner.os == 'macOS' || runner.os == 'Windows') &&
(runner.arch == 'X64' || runner.arch == 'ARM64') &&
(inputs.node-version == '' || inputs.node-version == '24')))
shell: bash
env:
LOOKUP_REQUEST: ${{ inputs.cache-pnpm-store-lookup-only }}
run: |
lookup_only=true
case "$LOOKUP_REQUEST" in
[aA][uU][tT][oO])
# Hosted runners have Node for this manifest-only check before toolchain setup.
lookup_only="$(node -p 'const p = require("./package.json"); p.engines?.node === "24" && typeof p.packageManager === "string" && p.packageManager.split("+")[0] === "pnpm@12.8.1"')"
;;
esac
printf 'lookup-only=%s\n' "$lookup_only" >> "$GITHUB_OUTPUT"

# setup-node needs pnpm on PATH to locate and restore its store.
- name: Setup pnpm
uses: pnpm/setup@v2
Expand All @@ -80,7 +104,7 @@ runs:
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && inputs.cache-pnpm-store-lookup-only != 'true' && 'pnpm' || '' }}
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: false

Expand All @@ -90,7 +114,7 @@ runs:
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && inputs.cache-pnpm-store-lookup-only != 'true' && 'pnpm' || '' }}
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: false

Expand All @@ -104,11 +128,11 @@ runs:
!(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')) ||
(github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' &&
inputs.cache-pnpm-store-lookup-only == 'true')
steps.pnpm-store-mode.outputs.lookup-only == 'true')
shell: bash
env:
LOCKFILE_HASH: ${{ hashFiles(inputs.cache-dependency-path) }}
STORE_LOOKUP_ONLY: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store-lookup-only == 'true' }}
STORE_LOOKUP_ONLY: ${{ steps.pnpm-store-mode.outputs.lookup-only == 'true' }}
run: |
test -n "$LOCKFILE_HASH"
cache_path="$(pnpm store path --silent)"
Expand Down Expand Up @@ -137,12 +161,10 @@ runs:
# Producers can refresh access and publish misses without downloading existing archives.
- name: Keep pnpm download store without restoring
id: pnpm-store-lookup
if: >-
github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' &&
inputs.cache-pnpm-store-lookup-only == 'true'
if: steps.pnpm-store-mode.outputs.lookup-only == 'true'
uses: actions/cache@v5
with:
# Post-job saves cannot resolve the composite's internal step outputs.
# Twice-nested composite cleanup loses internal step outputs.
path: ${{ env.ORCA_PNPM_STORE_CACHE_PATH }}
key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}
lookup-only: true
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/mobile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

# pnpm entrypoints can auto-install; finish each before the next mutates node_modules.
# Call installed tools so pnpm's dependency refresh cannot race between checks.
- name: Typecheck
id: production-types
background: true
run: pnpm typecheck
run: node node_modules/typescript/bin/tsc --noEmit

- wait: production-types

Expand All @@ -127,7 +127,7 @@ jobs:
# drifted. This fails when a test file that checks today stops checking, when a test leaves
# the program, and on @ts-nocheck; the baseline may only shrink.
- name: Typecheck tests (ratchet)
run: pnpm run check:tests-typecheck
run: node scripts/check-tests-typecheck-ratchet.mjs

# This includes the bridged replay of the whole recording corpus, which used to be a second
# step of its own behind RPC_FOUNDATION_BRIDGE=1. A gate nobody can forget to set is the point:
Expand Down
24 changes: 18 additions & 6 deletions config/scripts/ci-dependency-download-cache.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('CI dependency download caches', () => {
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' && inputs.cache-pnpm-store != 'false' && inputs.cache-pnpm-store-lookup-only != 'true' && 'pnpm' || '' }}"
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}"
)
expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}')
expect(step.with['package-manager-cache']).toBe(false)
Expand Down Expand Up @@ -41,7 +41,7 @@ describe('CI dependency download caches', () => {
"github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (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(resolve.if).toBe(
`${restore.if} || (github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && inputs.cache-pnpm-store-lookup-only == 'true')`
`${restore.if} || (github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only == 'true')`
)
expect(restore.uses).toBe('actions/cache/restore@v5')
expect(restore.with.path).toBe('${{ steps.pnpm-store.outputs.path }}')
Expand Down Expand Up @@ -72,11 +72,9 @@ describe('CI dependency download caches', () => {
it('keeps producer lookup optional and compatible with the existing store archive', () => {
const lookup = action.runs.steps.find((step) => step.id === 'pnpm-store-lookup')
const restore = action.runs.steps.find((step) => step.id === 'pnpm-store-restore')
expect(action.inputs['cache-pnpm-store-lookup-only'].default).toBe('false')
expect(action.inputs['cache-pnpm-store-lookup-only'].default).toBe('auto')
expect(lookup.uses).toBe('actions/cache@v5')
expect(lookup.if).toBe(
"github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && inputs.cache-pnpm-store-lookup-only == 'true'"
)
expect(lookup.if).toBe("steps.pnpm-store-mode.outputs.lookup-only == 'true'")
expect(lookup.with).toEqual({
path: '${{ env.ORCA_PNPM_STORE_CACHE_PATH }}',
key: restore.with.key,
Expand Down Expand Up @@ -203,6 +201,16 @@ describe('CI dependency download caches', () => {
const context = {
github: { event_name: event },
runner: { os, arch },
steps: {
'pnpm-store-mode': {
outputs: {
'lookup-only':
event !== 'pull_request' && storeCache !== 'false' && lookupOnly === 'true'
? 'true'
: ''
}
}
},
inputs: {
'cache-pnpm-store': storeCache,
'cache-pnpm-store-lookup-only': lookupOnly,
Expand All @@ -218,6 +226,10 @@ describe('CI dependency download caches', () => {
const evaluate = (expression) =>
runInNewContext(
expression
.replaceAll(
'steps.pnpm-store-mode.outputs.lookup-only',
'steps["pnpm-store-mode"].outputs["lookup-only"]'
)
.replaceAll(
'inputs.cache-pnpm-store-lookup-only',
'inputs["cache-pnpm-store-lookup-only"]'
Expand Down
150 changes: 150 additions & 0 deletions config/scripts/ci-pnpm-store-mode.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { runInNewContext } from 'node:vm'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
import { runProcessSync } from './script-child-process.mjs'

const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8'))
const mode = action.runs.steps.find((step) => step.id === 'pnpm-store-mode')
const defaultContext = {
github: { event_name: 'push' },
runner: { os: 'Linux', arch: 'X64', environment: 'github-hosted' },
job: { container: { id: '' } },
inputs: {
'cache-pnpm-store': 'true',
'cache-pnpm-store-lookup-only': 'auto',
'cache-dependency-path': 'pnpm-lock.yaml',
'node-version': ''
}
}
const expression = mode.if.replaceAll(/inputs\.([\w-]+)/g, 'inputs["$1"]')

function eligible(changes) {
const context = structuredClone(defaultContext)
for (const [name, fields] of Object.entries(changes)) {
Object.assign(context[name], fields)
}
return runInNewContext(expression, context)
}

function resolveMode(request, node, manager) {
const directory = mkdtempSync(join(tmpdir(), 'orca-store-mode-'))
const output = join(directory, 'output')
try {
writeFileSync(
join(directory, 'package.json'),
JSON.stringify({ engines: { node }, packageManager: manager })
)
const result = runProcessSync({
program: 'bash',
args: ['-e', '-o', 'pipefail', '-c', mode.run],
cwd: directory,
env: { ...process.env, LOOKUP_REQUEST: request, GITHUB_OUTPUT: output }
})
expect(result.code, result.stderr || result.stdout).toBe(0)
return readFileSync(output, 'utf8')
} finally {
rmSync(directory, { recursive: true, force: true })
}
}

describe('automatic pnpm store mode', () => {
it.each([
['auto', '24', 'pnpm@12.8.1', '', true],
['auto', '25', 'pnpm@12.8.1', 'pnpm', false],
['auto', '24', 'pnpm@13.0.0', 'pnpm', false],
['true', '25', 'pnpm@13.0.0', '', true]
])(
'routes resolved %s mode for Node %s / %s into both cache steps',
(request, node, manager, cache, lookup) => {
const context = structuredClone(defaultContext)
context.inputs['cache-pnpm-store-lookup-only'] = request
const resolved = resolveMode(request, node, manager).split('=')[1].trim()
const evaluate = (value) =>
runInNewContext(
value
.replaceAll(/inputs\.([\w-]+)/g, 'inputs["$1"]')
.replaceAll(
'steps.pnpm-store-mode.outputs.lookup-only',
'steps["pnpm-store-mode"].outputs["lookup-only"]'
),
{ ...context, steps: { 'pnpm-store-mode': { outputs: { 'lookup-only': resolved } } } }
)
const nodeSetup = action.runs.steps.find((step) => step.id === 'default-node')
expect(evaluate(nodeSetup.with.cache.slice(3, -2))).toBe(cache)
expect(evaluate(action.runs.steps.find((step) => step.id === 'pnpm-store-lookup').if)).toBe(
lookup
)
}
)

it.each(
['Linux', 'Windows', 'macOS'].flatMap((os) => ['X64', 'ARM64'].map((arch) => [os, arch]))
)('qualifies the measured %s/%s hosted root context', (os, arch) => {
expect(eligible({ runner: { os, arch } })).toBe(true)
})

it.each([
['PR', { github: { event_name: 'pull_request' } }],
['opted-out store', { inputs: { 'cache-pnpm-store': 'false' } }],
['opted-out lookup', { inputs: { 'cache-pnpm-store-lookup-only': 'false' } }],
['unknown request', { inputs: { 'cache-pnpm-store-lookup-only': 'other' } }],
[
'mixed lockfiles',
{ inputs: { 'cache-dependency-path': 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' } }
],
['custom lockfile', { inputs: { 'cache-dependency-path': 'cloud/pnpm-lock.yaml' } }],
['Node 25', { inputs: { 'node-version': '25' } }],
['job container', { job: { container: { id: 'container-id' } } }],
['self-hosted runner', { runner: { environment: 'self-hosted' } }],
['unknown host kind', { runner: { environment: '' } }],
['unmeasured architecture', { runner: { arch: 'X86' } }],
['unmeasured OS', { runner: { os: 'other' } }]
])('retains the legacy policy for %s', (_name, changes) => {
expect(eligible(changes)).toBe(false)
})

it('allows an explicit request to preserve the existing force-lookup contract', () => {
expect(
eligible({
inputs: {
'cache-pnpm-store-lookup-only': 'true',
'node-version': '25',
'cache-dependency-path': 'custom-lock.yaml'
},
runner: { environment: 'self-hosted' },
job: { container: { id: 'container-id' } }
})
).toBe(true)
expect(
eligible({
github: { event_name: 'pull_request' },
inputs: { 'cache-pnpm-store-lookup-only': 'true' }
})
).toBe(false)
})

it.each([
['24', 'pnpm@12.8.1', 'true'],
['24', 'pnpm@12.8.1+sha512.fixture', 'true'],
['25', 'pnpm@12.8.1', 'false'],
['24.x', 'pnpm@12.8.1', 'false'],
['24', 'pnpm@12.8.2', 'false'],
['24', 'pnpm@12.8.10', 'false'],
['24', undefined, 'false'],
[undefined, 'pnpm@12.8.1', 'false'],
['24', 12, 'false']
])('checks manifest Node %s and manager %s before choosing lookup', (node, manager, expected) => {
expect(resolveMode('auto', node, manager)).toBe(`lookup-only=${expected}\n`)
})

it('checks uppercase auto requests consistently with GitHub expression comparisons', () => {
expect(resolveMode('AUTO', '25', 'pnpm@12.8.1')).toBe('lookup-only=false\n')
})

it('does not constrain an explicit request to the automatic manifest profile', () => {
expect(resolveMode('true', '25', 'pnpm@13.0.0')).toBe('lookup-only=true\n')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const REVIEWED_COMPUTED_PATHS = [
'${{ env.ORCA_PNPM_STORE_CACHE_PATH }}',
// Only pnpm's lockfile-verified.jsonl record, never Metro transforms.
'${{ steps.verification-cache.outputs.path }}',
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && inputs.cache-pnpm-store-lookup-only != 'true' && 'pnpm' || '' }} store"
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} store"
]

/** Every step a workflow runs, descending into the repository's own composite actions. */
Expand Down
42 changes: 42 additions & 0 deletions config/scripts/mobile-typecheck-workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'

const workflow = parse(readFileSync('.github/workflows/mobile.yml', 'utf8'))
const packageJson = JSON.parse(readFileSync('mobile/package.json', 'utf8'))
const job = workflow.jobs.verify
const steps = job.steps

describe('mobile verification command ownership', () => {
it('runs the declared checks through installed tools without a script-time install', () => {
const production = steps.find((step) => step.name === 'Typecheck')
const tests = steps.find((step) => step.name === 'Typecheck tests (ratchet)')

expect(packageJson.scripts.typecheck).toMatch(/^tsc\b/)
expect(production.run).toBe(`node node_modules/typescript/bin/${packageJson.scripts.typecheck}`)
expect(tests.run).toBe(packageJson.scripts['check:tests-typecheck'])
expect(tests.run).toMatch(/^node\s/)
expect(job.defaults.run['working-directory']).toBe('mobile')
for (const name of ['typecheck', 'check:tests-typecheck']) {
expect(packageJson.scripts[`pre${name}`]).toBeUndefined()
expect(packageJson.scripts[`post${name}`]).toBeUndefined()
}
})

it('finishes installation and joins production types before checking test types', () => {
const installIndex = steps.findIndex((step) => step.name === 'Install dependencies')
const productionIndex = steps.findIndex((step) => step.name === 'Typecheck')
const ratchetIndex = steps.findIndex((step) => step.name === 'Typecheck tests (ratchet)')
const waitIndex = steps.findIndex((step) => step.wait === steps[productionIndex].id)
const testIndex = steps.findIndex((step) => step.name === 'Test')

expect(steps[installIndex].run).toBe('pnpm install --frozen-lockfile')
expect(steps[installIndex].background ?? false).toBe(false)
expect(installIndex).toBeLessThan(productionIndex)
expect(steps[productionIndex].background).toBe(true)
expect(productionIndex).toBeLessThan(ratchetIndex)
expect(waitIndex).toBeGreaterThan(productionIndex)
expect(waitIndex).toBeLessThan(ratchetIndex)
expect(ratchetIndex).toBeLessThan(testIndex)
})
})
Loading
Loading