diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index f17c7e8f1d7..ae695b8e3eb 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -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 @@ -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: diff --git a/config/scripts/mobile-typecheck-workflow.test.mjs b/config/scripts/mobile-typecheck-workflow.test.mjs new file mode 100644 index 00000000000..0ab57ad858f --- /dev/null +++ b/config/scripts/mobile-typecheck-workflow.test.mjs @@ -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) + }) +}) diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs index caec90ee086..2e8b77fd97c 100644 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ b/config/scripts/pr-e2e-gate-contract.test.mjs @@ -1,4 +1,5 @@ import { DEDICATED_E2E_SPECS } from './ci-e2e-job-selection.mjs' +import { linuxInstallPackageList } from './pr-e2e-linux-packages.test-fixture.mjs' import { existsSync, readdirSync, readFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { parse as parseJsonc } from 'jsonc-parser' @@ -20,6 +21,7 @@ import { const projectDir = resolve(import.meta.dirname, '../..') const prWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')) const e2eWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/e2e.yml'), 'utf8')) + const reliabilityManifest = parseJsonc( readFileSync(join(projectDir, 'config/reliability-gates.jsonc'), 'utf8') ) @@ -218,7 +220,7 @@ describe('PR E2E gate contract', () => { const installStep = e2eWorkflow.jobs[jobName].steps.find((step) => step.name.startsWith('Install native build') ) - expect(installStep.env.ORCA_E2E_APT_PACKAGES.split(/\s+/), jobName).toContain('zsh') + expect(linuxInstallPackageList(installStep, jobName), jobName).toMatch(/\bzsh\b/) } }) @@ -308,7 +310,7 @@ describe('PR E2E gate contract', () => { const changedInstall = e2eWorkflow.jobs['changed-e2e'].steps.find((step) => step.name.startsWith('Install native build') ) - expect(changedInstall.env.ORCA_E2E_APT_PACKAGES.split(/\s+/)).toContain('openssh-client') + expect(linuxInstallPackageList(changedInstall, 'changed-e2e')).toMatch(/\bopenssh-client\b/) }) it('routes direct-SSH workspace and tab restore from its unnamed source seams', () => { diff --git a/config/scripts/pr-e2e-linux-packages.test-fixture.mjs b/config/scripts/pr-e2e-linux-packages.test-fixture.mjs new file mode 100644 index 00000000000..dfbb2d4b798 --- /dev/null +++ b/config/scripts/pr-e2e-linux-packages.test-fixture.mjs @@ -0,0 +1,10 @@ +import { expect } from 'vitest' + +export function linuxInstallPackageList(step, jobName) { + const packages = step.env?.ORCA_E2E_APT_PACKAGES + if (packages !== undefined) { + expect(step.run, jobName).toContain('read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES"') + expect(step.run, jobName).toContain('sudo apt-get install -y "${packages[@]}"') + } + return packages ?? step.run +}