diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 7163c9c22b..dd34601323 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -60,13 +60,49 @@ jobs: build: name: Build (advisory) needs: changes - if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }} - # Dynamic runner — same fork-safe rule as ci.yml / fast-gates. - runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }} + # FORK PRs ONLY. build.yml's `Fast Production Build` triggers on `push: branches: ["**"]` + # and runs `build:release` — a superset of this job — so for an own-origin branch this job + # was building the same tree twice. A fork contributor pushes to THEIR repo, so that push + # never fires here, and this is the only pre-merge build signal they get. Measured + # 2026-08-14: 72 of the last 100 PRs into release/** came from forks, so the fork case is + # the majority of the traffic, not the exception — this job earns its place, it just should + # not duplicate build.yml for the own-origin 28%. + if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true' && github.event.pull_request.head.repo.full_name != github.repository) }} + # PINNED to hosted — this was the last job in THIS workflow still on the USE_VPS_RUNNER + # switch (ci.yml's Build, nightly-release-green and npm-publish keep it, so the variable + # stays meaningful), and with USE_VPS_RUNNER=true it produced NO signal at all here. + # Measured 2026-08-14 over the last 25 + # quality.yml runs: not one Build (advisory) reached a conclusion. Every sample was either + # queued on the self-hosted pool (2 runners, `omniroute-113-6/7`, both permanently busy — one + # job sat queued 2h+ and was still unclaimed) or, when it did land, killed mid-build by this + # workflow's own `cancel-in-progress` concurrency. 6/6 sampled "failures" are exit 143 / + # "The runner has received a shutdown signal" at ~3.5 min into `npm run build` — zero OOM, + # zero build errors. So the job burned a scarce runner that the gates actually need while + # reporting a permanent red on every PR. + # + # Gap 19 left USE_VPS_RUNNER governing build-like jobs on the premise that "the build needs + # the .113's RAM". That premise no longer holds: `Fast Production Build` (build.yml) runs + # `build:release` — a SUPERSET of this job's `npm run build`, plus the CLI bundle — on plain + # ubuntu-latest and passed 24/25 of its last runs in ~15 min. What it has and this job did + # not is memory PROVISIONING: a 10 GB swapfile plus a 12 GB V8 heap. That matters because + # --max-old-space-size only bounds V8's JS heap, never Turbopack's native (Rust) allocation + # (#6409) — swap is what absorbs the native peak. Both are mirrored below. + runs-on: ubuntu-latest # #7307: advisory for the first week of release-PR runs; remove # continue-on-error after the production-build signal is stable. continue-on-error: true steps: + # Mirrors build.yml: Turbopack's native peak is not bounded by --max-old-space-size, so + # the hosted runner needs swap headroom before the build starts. + - name: Expand virtual memory (10 GB swap) + run: | + sudo swapoff -a || true + sudo rm -f /mnt/swapfile /swapfile + sudo fallocate -l 10G /mnt/swapfile || sudo dd if=/dev/zero of=/mnt/swapfile bs=1M count=10240 + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + free -h - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -79,6 +115,10 @@ jobs: - run: npm run build env: OMNIROUTE_USE_TURBOPACK: "1" + # Same heap build.yml proves sufficient. build-next-isolated.mjs defaults to 8192 and + # honours OMNIROUTE_BUILD_MEMORY_MB; NODE_OPTIONS is set for parity with build.yml. + NODE_OPTIONS: "--max-old-space-size=12288" + OMNIROUTE_BUILD_MEMORY_MB: "12288" # No artifact upload here: the PR-to-release quality workflow has no # downstream package/e2e jobs that consume the Next.js build output. diff --git a/changelog.d/fixes/build-advisory-hosted-runner.md b/changelog.d/fixes/build-advisory-hosted-runner.md new file mode 100644 index 0000000000..4bc5ef5469 --- /dev/null +++ b/changelog.d/fixes/build-advisory-hosted-runner.md @@ -0,0 +1 @@ +- fix(ci): make `Build (advisory)` produce a signal again — pinned to a hosted runner with the swap/heap provisioning `Fast Production Build` proves sufficient, and scoped to fork PRs, which are the only ones `build.yml` cannot cover (72 of the last 100 PRs into `release/**`) diff --git a/docs/providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md b/docs/providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md index 62c9058da1..3bb3a592d5 100644 --- a/docs/providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md +++ b/docs/providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md @@ -32,7 +32,7 @@ different endpoint families, so all four products remain separate provider IDs. | Provider family | `global-sg` | `china-beijing` | Wire format | | ----------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | ----------- | | `alibaba` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | -| `bailian-coding-plan` | `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1` | `https://coding.dashscope.aliyuncs.com/apps/anthropic/v1` | Anthropic | +| `bailian-coding-plan` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1` | `https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic/v1` | Anthropic | | `qwen-cloud` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | | `qwen-cloud-token-plan` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1` | OpenAI | diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 7acc218d39..f2348b12d1 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -565,8 +565,8 @@ } }, "url": { - "nonStream": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", - "stream": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1" + "nonStream": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1", + "stream": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic/v1" } }, "baseten": { diff --git a/tests/unit/build/check-workflows.test.ts b/tests/unit/build/check-workflows.test.ts index 40eb377fd3..3018b68600 100644 --- a/tests/unit/build/check-workflows.test.ts +++ b/tests/unit/build/check-workflows.test.ts @@ -329,11 +329,35 @@ test("#7307 quality.yml adds an advisory production build for release PR code ch assert.match(buildJob[0], /needs\.changes\.outputs\.code == 'true'/); assert.match(buildJob[0], /github\.event\.pull_request\.draft == false/); assert.match(buildJob[0], /startsWith\(github\.head_ref, 'mergify\/merge-queue\/'\)/); + // FORK PRs ONLY (2026-08-14). build.yml's `Fast Production Build` fires on + // `push: branches: ["**"]` and runs the superset `build:release`, so own-origin branches + // were building twice; a fork's push never reaches this repo, making this their only + // pre-merge build signal — and forks are 72 of the last 100 PRs into release/**. assert.match( buildJob[0], - /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/ + /github\.event\.pull_request\.head\.repo\.full_name != github\.repository/ ); - assert.match(buildJob[0], /fromJSON\('\["self-hosted","omni-release"\]'\) \|\| 'ubuntu-latest'/); + // Runner PINNED to hosted. The self-hosted pool is 2 permanently-busy runners, where this + // job either queued for hours or was killed by cancel-in-progress — ~10-15% of runs ever + // reached a conclusion across 2026-08-13/14. It must NOT go back on the USE_VPS_RUNNER + // switch (other workflows keep that variable). + assert.match(buildJob[0], /\n {4}runs-on: ubuntu-latest\n/); + // Check the DIRECTIVES, not the prose: the comment above legitimately explains why the + // self-hosted pool was abandoned, so a naive /self-hosted/ scan over the whole block would + // match its own rationale. + const buildDirectives = buildJob[0] + .split("\n") + .filter((line) => !/^\s*#/.test(line)) + .join("\n"); + assert.doesNotMatch(buildDirectives, /self-hosted/); + assert.doesNotMatch(buildDirectives, /USE_VPS_RUNNER/); + // Memory provisioning mirrored from build.yml: --max-old-space-size bounds only V8's heap, + // never Turbopack's native Rust allocation (#6409), so the swapfile is the load-bearing + // half. Dropping either one puts the hosted build back at risk of an OOM. + assert.match(buildJob[0], /fallocate -l 10G \/mnt\/swapfile/); + assert.match(buildJob[0], /swapon \/mnt\/swapfile/); + assert.match(buildJob[0], /NODE_OPTIONS: "--max-old-space-size=12288"/); + assert.match(buildJob[0], /OMNIROUTE_BUILD_MEMORY_MB: "12288"/); assert.match(buildJob[0], /continue-on-error: true/); assert.match(buildJob[0], /uses: actions\/checkout@[0-9a-f]{40} # v7/); assert.match(buildJob[0], /uses: actions\/setup-node@[0-9a-f]{40} # v7/);