From e6523da2a12f0eaebc96ba95650efeec2436ab4d Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 29 Jul 2026 22:17:15 -0300 Subject: [PATCH] =?UTF-8?q?fix(ci):=20unblock=20the=20npm=20publish=20?= =?UTF-8?q?=E2=80=94=20dynamic=20runner=20+=20reuse=20CI's=20next-build=20?= =?UTF-8?q?(#8941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v3.8.49 publish failed: `Build CLI bundle` runs `npm run build:cli`, which shells out to a full `next build` when `.build/next/standalone/server.js` is missing. That build's working set outgrew the 16 GB GitHub-hosted runner during this cycle, so the job died at 6m20s with Creating an optimized production build ... ##[error]The runner has received a shutdown signal. and every step after it — artifact validation, SBOM, tarball boot-smoke, the staged publish — was skipped. The same job had passed in 16 minutes for v3.8.48, so this is the build growing past the runner, not the runner degrading. ci.yml already routes its heavy jobs around this via the USE_VPS_RUNNER expression; npm-publish.yml had `runs-on: ubuntu-latest` hardcoded and no way out. Two changes, one for the fast path and one for the fallback: - `runs-on` now uses the SAME expression as ci.yml's build/test-unit, so the operator can send the publish to the 32-core self-hosted runner. Kept verbatim (including the fork-safety clause that is always true here, since this workflow never runs on pull_request) so the expression stays greppable across workflows. - A best-effort step restores the `next-build` artifact that CI already produced for the SAME commit, matching on `head_sha` — same commit, same tree, so no equality gate is needed beyond the match itself. `build:cli` then finds the standalone tree and skips the build entirely, turning the heaviest step into a download. Retention is one day, so every miss (no successful CI run, expired artifact, bad extract) logs a notice and falls through to the build — which is exactly why the dynamic runner above is the necessary backstop rather than a nice-to-have. Needs `actions: read` to look up the run and download the artifact. All inputs reach the script through `env:`, never interpolated into the shell body. No new zizmor findings: the step uses the `gh` CLI rather than a new `uses:`, so the unpinned-uses count is unchanged (189 measured locally == the devbox side of the 190 baseline). Co-authored-by: diegosouzapw --- .github/workflows/npm-publish.yml | 44 ++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 692e6bd26c..f82ff1b669 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -56,8 +56,15 @@ env: jobs: publish: - runs-on: ubuntu-latest + # Same dynamic-runner rule as ci.yml's `build`/`test-unit`: `build:cli` falls back to a + # full `next build`, whose working set outgrew the 16 GB hosted runner during the + # v3.8.49 cycle — the publish died with "The runner has received a shutdown signal" + # mid-"Creating an optimized production build" while v3.8.48 had still fit in 16min. + # This job never runs on `pull_request`, so the fork-safety clause is always true here; + # it is kept verbatim so the expression stays greppable against ci.yml. + 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' }} permissions: + actions: read # find + download the CI run's next-build artifact for this SHA contents: write # gh release upload (attach SBOM to the GitHub Release) id-token: write # npm provenance packages: write # publish to npm.pkg.github.com @@ -145,6 +152,41 @@ jobs: run: | npm version "$VERSION" --no-git-tag-version --allow-same-version + # Fast path: CI already built the standalone tree for THIS commit and uploaded it as + # `next-build`. `build:cli` (scripts/build/prepublish.ts) only shells out to a full + # `next build` when `.build/next/standalone/server.js` is missing — restoring the + # artifact turns the heaviest step of the publish into a download. Matching on + # `head_sha` is the tree-equality guarantee: same commit, same tree. + # Best-effort by design (retention is 1 day): every miss falls through to the build + # step below, which is why the dynamic runner above matters as the backstop. + - name: Reuse CI's next-build artifact (skips the heavy rebuild) + if: steps.resolve.outputs.skip != 'true' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.sha }} + REPO: ${{ github.repository }} + run: | + set -uo pipefail + RUN=$(gh api "repos/$REPO/actions/runs?head_sha=$HEAD_SHA&per_page=100" \ + --jq '[.workflow_runs[] | select(.name == "CI" and .conclusion == "success")] | .[0].id // empty') || RUN="" + if [ -z "$RUN" ]; then + echo "::notice::no successful CI run for $HEAD_SHA — falling back to a full build" + exit 0 + fi + if ! gh run download "$RUN" --repo "$REPO" --name next-build --dir /tmp/next-build; then + echo "::notice::next-build artifact unavailable for run $RUN (expired?) — falling back to a full build" + exit 0 + fi + tar -xzf /tmp/next-build/e2e-build.tar.gz -C . + rm -rf /tmp/next-build + if [ -f .build/next/standalone/server.js ]; then + echo "✅ standalone tree restored from CI run $RUN — build:cli will skip next build" + else + echo "::notice::extract did not yield .build/next/standalone — falling back to a full build" + rm -rf .build + fi + - name: Build CLI bundle (standalone app) if: steps.resolve.outputs.skip != 'true' env: