mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(release): avoid API rate limits waiting for the mobile bundle (#31431)
* fix(release): replace mobile bundle API polling with job dependencies * test(release): update qualification token permission expectations
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'")
|
||||
|
||||
@@ -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 != ''"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user