diff --git a/.github/workflows/ci-cache-warmup-platform-jobs.yml b/.github/workflows/ci-cache-warmup-platform-jobs.yml index 07fb93918b8..f623fb74a1c 100644 --- a/.github/workflows/ci-cache-warmup-platform-jobs.yml +++ b/.github/workflows/ci-cache-warmup-platform-jobs.yml @@ -8,6 +8,7 @@ permissions: jobs: warm-linux-arm: + if: github.event_name != 'pull_request' concurrency: group: ci-cache-warmup-linux-arm-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name != 'schedule' }} @@ -85,6 +86,7 @@ jobs: arches: arm64 warm-linux-package-fixtures: + if: github.event_name != 'pull_request' concurrency: group: ci-cache-warmup-linux-package-fixtures-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name != 'schedule' }} diff --git a/.github/workflows/ci-cache-warmup-platforms.yml b/.github/workflows/ci-cache-warmup-platforms.yml index e8f37a09c78..857fbfaa21e 100644 --- a/.github/workflows/ci-cache-warmup-platforms.yml +++ b/.github/workflows/ci-cache-warmup-platforms.yml @@ -45,7 +45,6 @@ on: - 'resources/build/icon.ico' pull_request: paths: - - '.github/workflows/ci-cache-warmup.yml' - '.github/workflows/ci-cache-warmup-platforms.yml' - '.github/workflows/ci-cache-warmup-platform-jobs.yml' diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index 8487d95ef34..882dfac0bab 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -50,8 +50,6 @@ on: pull_request: paths: - '.github/workflows/ci-cache-warmup.yml' - - '.github/workflows/ci-cache-warmup-platform-jobs.yml' - - '.github/workflows/ci-cache-warmup-platforms.yml' - '.github/actions/prepare-headless-compiler/**' - 'config/scripts/headless-detector-compiler-cache.mjs' - '.github/actions/prepare-wayland-clipboard/**' diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index 8db466b4637..0bcfefa5edb 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs' import { runInNewContext } from 'node:vm' import { describe, expect, it } from 'vitest' import { parse } from 'yaml' +import { classifyPrJobs } from './pr-code-change-scope.mjs' const read = (path) => parse(readFileSync(path, 'utf8')) const workflow = (name) => read(`.github/workflows/${name}.yml`) @@ -300,6 +301,116 @@ describe('CI dependency download caches', () => { } }) + it('keeps PR warmer validation within the workflow family that runs it', () => { + const shared = workflow('ci-cache-warmup') + const platforms = workflow('ci-cache-warmup-platforms') + expect(shared.on.pull_request.paths).toEqual([ + '.github/workflows/ci-cache-warmup.yml', + '.github/actions/prepare-headless-compiler/**', + 'config/scripts/headless-detector-compiler-cache.mjs', + '.github/actions/prepare-wayland-clipboard/**' + ]) + expect(platforms.on.pull_request.paths).toEqual([ + '.github/workflows/ci-cache-warmup-platforms.yml', + '.github/workflows/ci-cache-warmup-platform-jobs.yml' + ]) + expect(shared.jobs['warm-platforms'].uses).toBe(platforms.jobs['warm-platforms'].uses) + expect(shared.on.schedule).toEqual([{ cron: '41 */6 * * *' }]) + expect(shared.on).toHaveProperty('workflow_dispatch') + for (const producer of [shared, platforms]) { + expect(producer.on.push.branches).toEqual(['main']) + expect(producer.on.push.paths).toEqual( + expect.arrayContaining([ + '.github/workflows/ci-cache-warmup.yml', + '.github/workflows/ci-cache-warmup-platforms.yml', + '.github/workflows/ci-cache-warmup-platform-jobs.yml', + '.github/actions/install-node-dependencies/**', + '.github/actions/prepare-native-runtime/**', + 'pnpm-lock.yaml' + ]) + ) + } + }) + + it.each([ + ['pull_request', false, false], + ['push', true, false], + ['schedule', true, true], + ['workflow_dispatch', true, true] + ])('retains cache producers and Windows validation on %s', (event, armRuns, sharedPlatforms) => { + const shared = workflow('ci-cache-warmup') + const platforms = read(workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses) + const context = { github: { event_name: event } } + expect(runInNewContext(platforms.jobs['warm-linux-arm'].if, context)).toBe(armRuns) + expect(runInNewContext(shared.jobs['warm-platforms'].if, context)).toBe(sharedPlatforms) + expect(shared.jobs.warm.if).toBeUndefined() + expect(platforms.jobs['warm-windows'].if).toBeUndefined() + expect(platforms.jobs['warm-windows'].strategy.matrix.os).toEqual([ + 'windows-2022', + 'windows-11-arm' + ]) + }) + + it('validates the omitted ARM warmer preparation in the real PR consumers', () => { + const platforms = read(workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses) + const warm = platforms.jobs['warm-linux-arm'] + const pr = workflow('pr') + const units = workflow('unit-tests').jobs.test + const installer = './.github/actions/install-node-dependencies' + const warmInstall = warm.steps.find((step) => step.uses === installer) + const unitInstall = units.steps.find((step) => step.uses === installer) + expect(warm['runs-on']).toBe('ubuntu-24.04-arm') + expect(pr.jobs.preflight['runs-on']).toBe(warm['runs-on']) + expect(pr.jobs.test.with.runner).toBe(warm['runs-on']) + expect(units['runs-on']).toBe('${{ inputs.runner }}') + expect(JSON.parse(pr.jobs.test.with.node_versions)).toEqual(['24']) + expect(warmInstall.if).toBeUndefined() + expect(unitInstall.if).toBeUndefined() + expect(warmInstall.with['native-runtime']).toBe('node') + expect(unitInstall.with['native-runtime']).toBe(warmInstall.with['native-runtime']) + expect(warmInstall.with['node-version']).toBe('24') + expect(unitInstall.with['node-version']).toBe('${{ matrix.node }}') + expect(unitInstall.with['cache-electron-package']).toBe( + warmInstall.with['cache-electron-package'] + ) + const preflightInstalls = pr.jobs.preflight.steps.filter((step) => step.uses === installer) + expect(preflightInstalls.map((step) => step.if)).toEqual([ + "needs.code_paths.outputs.mobile_dependencies != 'true'", + "needs.code_paths.outputs.mobile_dependencies == 'true'" + ]) + for (const step of preflightInstalls) { + expect(step.with['native-runtime']).toBe('node') + expect(step.with['node-version']).toBe('24') + } + const populate = 'node config/scripts/install-electron-package-binary.mjs' + const unitPopulate = units.steps.find((step) => step.run === populate) + expect(unitPopulate.if).toBeUndefined() + expect(warm.steps.map((step) => step.uses ?? step.run)).toEqual([ + 'actions/checkout@v6', + installer, + populate, + 'node config/scripts/ensure-native-runtime.mjs --check-only' + ]) + const native = read('.github/actions/prepare-native-runtime/action.yml') + const identity = native.runs.steps.find((step) => step.id === 'native-cache-scope') + const inputs = [...identity.env.NATIVE_SOURCE_HASH.matchAll(/'([^']+)'/g)].map( + (match) => match[1] + ) + for (const path of [ + ...inputs, + '.github/actions/install-node-dependencies/action.yml', + ...workflow('ci-cache-warmup-platforms').on.pull_request.paths, + '.github/workflows/ci-cache-warmup.yml' + ]) { + expect(classifyPrJobs([path], { allowMobileCiTools: true }), path).toMatchObject({ + static_analysis: true, + typecheck: true, + test: true, + mobile_ci_tools: false + }) + } + }) + it('restores Windows packaging downloads from the release cache without a PR upload', () => { const packaging = workflow('pr').jobs.package_windows const restore = packaging.steps.find((step) => step.name === 'Cache electron-builder downloads') diff --git a/config/scripts/linux-package-fixture-cache.test.mjs b/config/scripts/linux-package-fixture-cache.test.mjs index 6c677f0abc8..81465a38a29 100644 --- a/config/scripts/linux-package-fixture-cache.test.mjs +++ b/config/scripts/linux-package-fixture-cache.test.mjs @@ -1,9 +1,11 @@ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' +import { runInNewContext } from 'node:vm' import { delimiter, join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { parse } from 'yaml' import { runProcessSync } from '@orca/process-host' +import { classifyPrJobs } from './pr-code-change-scope.mjs' const action = parse( readFileSync('.github/actions/prepare-linux-package-fixture/action.yml', 'utf8') @@ -132,3 +134,45 @@ it('uses exact context/platform keys and the same restore-only action in package .every((step) => step.with['save-cache'] === "${{ github.event_name != 'pull_request' }}") ).toBe(true) }) + +it.each([ + ['pull_request', false], + ['push', true], + ['schedule', true], + ['workflow_dispatch', true] +])('keeps fixture seeding available without a redundant PR runner on %s', (event, runs) => { + const source = workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses + const warm = parse(readFileSync(source, 'utf8')).jobs['warm-linux-package-fixtures'] + const context = { github: { event_name: event } } + expect(warm['runs-on']).toBe('ubuntu-latest') + expect(warm.steps.map((step) => step.uses)).toEqual([ + 'actions/checkout@v6', + './.github/actions/prepare-linux-package-fixture', + './.github/actions/prepare-linux-package-fixture', + './.github/actions/prepare-linux-package-fixture' + ]) + expect(runInNewContext(warm.if, context)).toBe(runs) + for (const step of warm.steps.slice(1)) { + expect(runInNewContext(step.with['save-cache'].slice(3, -2), context)).toBe(runs) + } + const consumers = workflow('pr').jobs.package.steps.filter((step) => + step.uses?.endsWith('/prepare-linux-package-fixture') + ) + expect(consumers.every((step) => step.with['save-cache'] === undefined)).toBe(true) + expect(consumers.map((step) => step.with.fixture)).toEqual( + warm.steps.slice(1).map((step) => step.with.fixture) + ) +}) + +it.each([ + '.github/actions/prepare-linux-package-fixture/action.yml', + 'config/docker/daemon-shutdown-descendants/Dockerfile', + 'config/docker/headless-serve-shutdown/Dockerfile', + 'config/docker/cli-launch-contract/Dockerfile' +])('validates a changed fixture in Linux packaging: %s', (file) => { + expect(classifyPrJobs([file])).toMatchObject({ + test: true, + package: true, + package_windows: false + }) +})