diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index 8fe84611686..2b0669a8421 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -136,10 +136,9 @@ jobs: fi persistence: - needs: changes + needs: [changes, mobile_web_bundle] permissions: contents: read - actions: read concurrency: group: node-server-persistence-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }} @@ -147,6 +146,7 @@ jobs: # Drafts re-trigger at readiness; inherited caller events must not depend on PR fields. if: >- ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && + (needs.changes.outputs.qualification == 'false' || needs.mobile_web_bundle.result == 'success') && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} strategy: fail-fast: false @@ -213,32 +213,6 @@ jobs: if: runner.os == 'Windows' with: arches: ${{ (inputs.build_template && matrix.os == 'windows-2022') && 'x64 arm64' || (runner.arch == 'ARM64' && 'arm64' || 'x64') }} - - name: Wait for the qualified mobile bundle - if: needs.changes.outputs.qualification != 'false' - uses: actions/github-script@v8 - with: &mobile-bundle-wait - script: | - const deadline = Date.now() + 15 * 60_000 - while (Date.now() < deadline) { - const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { - ...context.repo, run_id: context.runId, filter: 'latest', per_page: 100 - }) - const producers = jobs.filter(job => job.name === 'mobile_web_bundle' || - job.name.endsWith(' / mobile_web_bundle')) - if (producers.length > 1) throw new Error('Ambiguous mobile bundle producer') - const producer = producers[0] - if (producer?.status === 'completed') { - if (producer.conclusion !== 'success') { - throw new Error(`Mobile bundle production did not succeed: ${producer.conclusion}`) - } - const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { - ...context.repo, run_id: context.runId, per_page: 100 - }) - if (artifacts.some(artifact => artifact.name === 'orcad-mobile-web-bundle' && !artifact.expired)) return - } - await new Promise(resolve => setTimeout(resolve, 5_000)) - } - throw new Error('Timed out waiting for the qualified mobile bundle') - uses: actions/download-artifact@v8 if: needs.changes.outputs.qualification != 'false' with: @@ -379,10 +353,9 @@ jobs: overwrite: true linux_glibc_floor: - needs: changes + needs: [changes, mobile_web_bundle] permissions: contents: read - actions: read concurrency: group: node-server-linux_glibc_floor-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }} @@ -391,6 +364,7 @@ jobs: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && needs.changes.outputs.qualification != 'false' && + needs.mobile_web_bundle.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} strategy: fail-fast: false @@ -438,9 +412,6 @@ jobs: pnpm build:orcad-prebuilds pnpm build:orcad-prebuilds --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" pnpm build:orcad-prebuilds --smoke - - name: Wait for the qualified mobile bundle - uses: actions/github-script@v8 - with: *mobile-bundle-wait - uses: actions/download-artifact@v8 with: name: orcad-mobile-web-bundle @@ -515,10 +486,9 @@ jobs: overwrite: true linux_musl: - needs: changes + needs: [changes, mobile_web_bundle] permissions: contents: read - actions: read concurrency: group: node-server-linux_musl-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }} @@ -526,6 +496,7 @@ jobs: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && needs.changes.outputs.qualification != 'false' && + needs.mobile_web_bundle.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} strategy: fail-fast: false @@ -566,9 +537,6 @@ jobs: pnpm build:orcad-prebuilds --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" pnpm build:orcad-prebuilds --smoke NODE_SERVER_PREPARATION - - name: Wait for the qualified mobile bundle - uses: actions/github-script@v8 - with: *mobile-bundle-wait - uses: actions/download-artifact@v8 with: name: orcad-mobile-web-bundle diff --git a/config/scripts/node-server-change-scope.test.mjs b/config/scripts/node-server-change-scope.test.mjs index 0d93a253e20..fe6cd375fb2 100644 --- a/config/scripts/node-server-change-scope.test.mjs +++ b/config/scripts/node-server-change-scope.test.mjs @@ -571,11 +571,11 @@ it('keeps every platform job and runs them when detection is skipped or fails', expect(workflow.jobs.persistence.strategy.matrix.os).toContain('needs.changes.outputs.runners') for (const jobName of ['linux_glibc_floor', 'linux_musl']) { const job = workflow.jobs[jobName] - expect(job.needs).toBe('changes') + expect(job.needs).toEqual(['changes', 'mobile_web_bundle']) // The containers never install mobile/, and orcad refuses to build without the page. expect(job.steps.some((step) => step.with?.name === 'orcad-mobile-web-bundle')).toBe(true) expect(job.steps.some((step) => step.name === 'Wait for the qualified mobile bundle')).toBe( - true + false ) expect(job.if).not.toContain('needs.persistence') expect(job.if).toContain("needs.changes.outputs.qualification != 'false'") diff --git a/config/scripts/node-server-native-compat-scope.test.mjs b/config/scripts/node-server-native-compat-scope.test.mjs index 39f9bbe567b..7adc20ad16b 100644 --- a/config/scripts/node-server-native-compat-scope.test.mjs +++ b/config/scripts/node-server-native-compat-scope.test.mjs @@ -329,7 +329,9 @@ it('builds server glibc slots on glibc 2.28 and the compat slot on glibc 2.17 (d const compat = workflow.jobs.linux_glibc217_compat expect(compat.needs).toBe('changes') - expect(compat.if).toContain(floor.if.slice(0, -3)) + expect(compat.if).toContain( + floor.if.replace("needs.mobile_web_bundle.result == 'success' && ", '').slice(0, -3) + ) expect(compat.if).toContain("needs.changes.outputs.native_compatibility != 'false'") expect(compat.if).toContain( "github.event_name != 'pull_request' || inputs.build_template || inputs.ref != ''" diff --git a/config/scripts/node-server-release-parallelism.test.mjs b/config/scripts/node-server-release-parallelism.test.mjs index 775520df3d2..6546ed7bc31 100644 --- a/config/scripts/node-server-release-parallelism.test.mjs +++ b/config/scripts/node-server-release-parallelism.test.mjs @@ -21,17 +21,17 @@ function context({ qualification, producer = 'success', event = 'pull_request', it.each([ { qualification: 'true', producer: 'success', expected: true }, { qualification: '', producer: 'success', expected: true }, - { qualification: 'true', producer: 'failure', expected: true }, - { qualification: 'true', producer: 'cancelled', expected: true }, - { qualification: 'true', producer: 'skipped', expected: true }, - { qualification: '', producer: 'skipped', expected: true }, + { qualification: 'true', producer: 'failure', expected: false }, + { qualification: 'true', producer: 'cancelled', expected: false }, + { qualification: 'true', producer: 'skipped', expected: false }, + { qualification: '', producer: 'skipped', expected: false }, { qualification: 'false', producer: 'skipped', expected: true }, { qualification: 'false', producer: 'skipped', draft: true, expected: false }, { producer: 'success', event: 'workflow_dispatch', expected: true }, { producer: 'success', event: 'schedule', expected: true } -])('starts native setup independently while retaining test-only behavior: %j', (scenario) => { +])('gates qualification on the bundle while retaining test-only behavior: %j', (scenario) => { const ctx = context(scenario) - expect(persistence.needs).toBe('changes') + expect(persistence.needs).toEqual(['changes', 'mobile_web_bundle']) expect(shouldPersist(ctx).coerceString()).toBe(String(scenario.expected)) expect(shouldPersist(ctx, true).coerceString()).toBe('false') }) @@ -112,107 +112,36 @@ it('avoids template download-cache transfers while retaining native and verifica } }) -const mobileWait = persistence.steps.find( - (step) => step.name === 'Wait for the qualified mobile bundle' -) -const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor - -function waitForMobile(snapshots) { - let now = 0 - let index = 0 - const calls = [] - const github = { - rest: { actions: { listJobsForWorkflowRun: 'jobs', listWorkflowRunArtifacts: 'artifacts' } }, - async paginate(endpoint, input) { - calls.push({ endpoint, input }) - return snapshots[Math.min(index, snapshots.length - 1)][endpoint] ?? [] +it.each(['persistence', 'linux_glibc_floor', 'linux_musl'])( + '%s waits through the dependency graph without polling the Actions API', + (name) => { + const job = workflow.jobs[name] + expect(job.needs).toEqual(['changes', 'mobile_web_bundle']) + expect(job.steps.some((step) => step.uses?.includes('github-script'))).toBe(false) + const evaluate = compileAggregateExpression(job.if, ['needs', 'github', 'inputs']) + for (const producer of ['success', 'failure', 'cancelled', 'skipped', 'timed_out']) { + const ctx = { + ...context({ qualification: 'true', producer }), + inputs: { build_template: true } + } + expect(evaluate(ctx).coerceString()).toBe(String(producer === 'success')) } } - const execute = new AsyncFunction( - 'github', - 'context', - 'Date', - 'setTimeout', - mobileWait.with.script - ) - return { - calls, - result: execute( - github, - { repo: { owner: 'owner', repo: 'repo' }, runId: 42 }, - { now: () => now }, - (resolve, delay) => { - now += delay - index++ - resolve() - } - ) - } -} - -const mobileProducer = (conclusion, prefix = 'orcad-template / qualify / ') => ({ - name: `${prefix}mobile_web_bundle`, - status: conclusion ? 'completed' : 'in_progress', - conclusion -}) -const mobileArtifact = { name: 'orcad-mobile-web-bundle', expired: false } - -it.each(['', 'orcad-template / ', 'orcad-template / qualify / '])( - 'waits for the same-run producer to succeed before accepting its artifact (%s)', - async (prefix) => { - const { result, calls } = waitForMobile([ - { artifacts: [mobileArtifact] }, - { jobs: [mobileProducer(undefined, prefix)], artifacts: [mobileArtifact] }, - { - jobs: [mobileProducer('success', prefix)], - artifacts: [{ ...mobileArtifact, expired: true }] - }, - { jobs: [mobileProducer('success', prefix)], artifacts: [mobileArtifact] } - ]) - await result - expect(calls.filter((call) => call.endpoint === 'jobs')).toHaveLength(4) - expect(calls.filter((call) => call.endpoint === 'artifacts')).toHaveLength(2) - expect(calls.every((call) => call.input.run_id === 42)).toBe(true) - expect( - calls - .filter((call) => call.endpoint === 'jobs') - .every((call) => call.input.filter === 'latest') - ).toBe(true) - } ) -it.each(['failure', 'cancelled', 'skipped', 'timed_out', 'action_required', 'startup_failure'])( - 'rejects a %s producer even when an earlier attempt left an artifact', - async (conclusion) => { - await expect( - waitForMobile([{ jobs: [mobileProducer(conclusion)], artifacts: [mobileArtifact] }]).result - ).rejects.toThrow('Mobile bundle production did not succeed') - } -) - -it('fails closed when the producer is missing or ambiguous', async () => { - await expect(waitForMobile([{}]).result).rejects.toThrow('Timed out') - await expect( - waitForMobile([{ jobs: [mobileProducer('success'), mobileProducer('success', '')] }]).result - ).rejects.toThrow('Ambiguous') -}) - -it('prepares every native lane before waiting and builds with the shared page afterward', () => { +it('prepares every native lane and builds with the shared page', () => { for (const name of ['persistence', 'linux_glibc_floor', 'linux_musl']) { const job = workflow.jobs[name] - const wait = job.steps.findIndex((step) => step.name === mobileWait.name) - const download = job.steps.findIndex((step) => step.with?.name === mobileArtifact.name) + const download = job.steps.findIndex((step) => step.with?.name === 'orcad-mobile-web-bundle') const prepare = job.steps.findIndex( (step) => step.uses?.includes('prepare-orcad-prebuilds') || step.run?.includes('pnpm build:orcad-prebuilds') ) - expect(job.needs).toBe('changes') + expect(job.needs).toEqual(['changes', 'mobile_web_bundle']) expect(prepare).toBeGreaterThan(-1) - expect(prepare).toBeLessThan(wait) - expect(wait).toBeLessThan(download) - expect(job.steps[wait].with.script).toBe(mobileWait.with.script) - expect(job.permissions).toEqual({ contents: 'read', actions: 'read' }) + expect(prepare).toBeLessThan(download) + expect(job.permissions).toEqual({ contents: 'read' }) } const alpine = workflow.jobs.linux_musl.steps const prepare = alpine.find((step) => step.name === 'Prepare native Alpine artifact') diff --git a/config/scripts/release-cut-token-permissions.test.mjs b/config/scripts/release-cut-token-permissions.test.mjs index c527bb16d4d..498fd9b01ac 100644 --- a/config/scripts/release-cut-token-permissions.test.mjs +++ b/config/scripts/release-cut-token-permissions.test.mjs @@ -32,9 +32,7 @@ const EXPECTED_MATRIX = { 'persistence' ].map((job) => [ `.github/workflows/release-mac-build.yml#qualification-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#${job}`, - ['persistence', 'linux_glibc_floor', 'linux_musl'].includes(job) - ? { actions: 'read', contents: 'read' } - : { contents: 'read' } + { contents: 'read' } ]) ), ...Object.fromEntries( @@ -78,12 +76,10 @@ const EXPECTED_MATRIX = { }, [`${RELEASE_WORKFLOW}#orcad-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#linux_glibc_floor`]: { - actions: 'read', contents: 'read' }, [`${RELEASE_WORKFLOW}#orcad-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#linux_musl`]: { - actions: 'read', contents: 'read' }, [`${RELEASE_WORKFLOW}#orcad-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#mobile_web_bundle`]: @@ -92,7 +88,6 @@ const EXPECTED_MATRIX = { }, [`${RELEASE_WORKFLOW}#orcad-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#persistence`]: { - actions: 'read', contents: 'read' }, [`${RELEASE_WORKFLOW}#post-release-e2e`]: { actions: 'write' },