Publish Smoke #1283
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Publish Smoke — the dynamic half of the #3091 prevention. | |
| # | |
| # 15.1.0 shipped with every fresh project's auth endpoints returning 500: | |
| # the workspace's pnpm overrides (better-auth pinned to 1.7.0-rc.1) made all | |
| # in-repo CI green, but overrides do NOT ship with published packages, so | |
| # downstream installs resolved a dependency mix that was never tested here. | |
| # The static gate is scripts/check-override-consistency.mjs (#3085 — override | |
| # targets must be reflected in published manifests). This workflow is the | |
| # dynamic gate: install the exact bits a user would get and drive the | |
| # first-run flow (auth sign-up/sign-in/get-session + REST CRUD) for real. | |
| # | |
| # Two jobs, one driver script (scripts/publish-smoke.sh): | |
| # | |
| # pack-smoke SMOKE_MODE=pack — `pnpm pack` every publishable package | |
| # (pack applies the same manifest rewrites as publish), | |
| # scaffold a fresh project OUTSIDE the workspace, pin | |
| # @objectstack/* to the tarballs via the project's own pnpm | |
| # overrides, and smoke it. This is "what 15.1.0 would have | |
| # failed": the release-candidate combination, no workspace | |
| # overrides in sight. | |
| # | |
| # registry-canary SMOKE_MODE=registry — weekly `npx create-objectstack@latest` | |
| # against the real npm registry. Catches ^-range drift in the | |
| # ecosystem (a transitive release) breaking ALREADY-published | |
| # versions after the fact. Opens/refreshes an issue on failure. | |
| # | |
| # Trigger notes: the changesets release PR (changeset-release/main) is pushed | |
| # with GITHUB_TOKEN, and GITHUB_TOKEN events never trigger other workflows — | |
| # a plain `on: pull_request` / `on: push` would silently never run (the | |
| # release PR has NO Actions checks today). So pack-smoke runs on | |
| # `workflow_run` after each Release run completes (that's the moment | |
| # changesets creates/updates the release branch), checks out the release | |
| # branch if an open release PR exists, and reports the verdict back as a | |
| # commit status on the branch head so it IS visible on the release PR. | |
| # Not wired into normal PR CI on purpose: full build + pack + clean install | |
| # is far too slow for the inner loop. | |
| # | |
| # That reporting path is also why there is a third job, resolve-guard (#5343): | |
| # every verdict this workflow can produce reaches a human through that commit | |
| # status, so a `resolve` that FAILS writes nothing at all — the release PR then | |
| # looks exactly like "there was nothing to smoke". The status is not a required | |
| # check (branch protection on main requires TypeScript Type Check, Build Core, | |
| # Test Core and Dogfood Regression Gate), so an absent one blocks nothing and is | |
| # simply invisible. resolve-guard turns that silence into an explicit red. | |
| name: Publish Smoke | |
| on: | |
| workflow_run: | |
| workflows: [Release] | |
| types: [completed] | |
| schedule: | |
| - cron: '47 4 * * 1' # weekly registry canary (Mon 04:47 UTC) | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: publish-smoke-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| resolve: | |
| name: Resolve target | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| run: ${{ steps.target.outputs.run }} | |
| ref: ${{ steps.target.outputs.ref }} | |
| report-sha: ${{ steps.target.outputs.report-sha }} | |
| steps: | |
| - name: Pick the ref to smoke | |
| id: target | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| # Manual run: smoke whatever ref the run was dispatched on. | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| echo "ref=${{ github.ref }}" >> "$GITHUB_OUTPUT" | |
| echo "report-sha=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # workflow_run (a Release run finished): smoke the release branch | |
| # iff an open changesets release PR exists; otherwise skip. | |
| pr=$(gh pr list --repo "$GITHUB_REPOSITORY" \ | |
| --head changeset-release/main --state open \ | |
| --json headRefOid -q '.[0].headRefOid // empty') | |
| if [ -n "$pr" ]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| # Checkout the exact SHA the status is reported against, so a | |
| # concurrent force-push of the release branch can't skew the two. | |
| echo "ref=$pr" >> "$GITHUB_OUTPUT" | |
| echo "report-sha=$pr" >> "$GITHUB_OUTPUT" | |
| echo "Release PR open at $pr — smoking changeset-release/main" | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| echo "No open release PR — nothing to smoke" | |
| fi | |
| # resolve failing is NOT "nothing to smoke" — it is "we do not know". Say so | |
| # out loud (#5343). Without this job the two outcomes are indistinguishable | |
| # everywhere it matters: pack-smoke is skipped in both, and a skipped job reads | |
| # as green; and since this workflow runs on `workflow_run`, its own red run is | |
| # filed against the default branch, nowhere near the release PR a human is | |
| # about to merge. | |
| # | |
| # The alternative shape — #4928's `!cancelled() && ... != 'false'`, i.e. "when | |
| # in doubt, run everything" — is wrong HERE, and that is why the decision was | |
| # taken per workflow rather than copied. ci.yml's filter job defaults its | |
| # outputs to "run it" (`|| 'true'`); resolve does not, and it also computes the | |
| # `ref` to check out. Running on doubt would check out an EMPTY ref (the | |
| # workflow_run default), spend 45 minutes smoking whatever that resolves to, | |
| # and report the verdict as if it were about the release candidate. A false | |
| # green on release integrity is worse than no answer. | |
| resolve-guard: | |
| name: Resolve target failed — the candidate was NOT smoked | |
| needs: resolve | |
| if: always() && needs.resolve.result == 'failure' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| statuses: write | |
| steps: | |
| - name: Report the gap on the release PR, then fail | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Best-effort, and deliberately a SECOND lookup rather than a reuse of | |
| # resolve's outputs: resolve failed, so its outputs are empty — that is | |
| # the whole problem. Same query as resolve's (`--head | |
| # changeset-release/main`); keep the two in step if that convention | |
| # ever changes. Whatever this lookup does, the job still fails below, | |
| # so the fallback can never turn into a silent pass. | |
| release_head=$(gh pr list --repo "$GITHUB_REPOSITORY" \ | |
| --head changeset-release/main --state open \ | |
| --json headRefOid -q '.[0].headRefOid // empty') || release_head='' | |
| if [ -n "$release_head" ]; then | |
| gh api "repos/$GITHUB_REPOSITORY/statuses/$release_head" \ | |
| -f state=failure -f context='publish-smoke / packed-tarballs' \ | |
| -f description='Target resolution failed — the candidate was never smoked' \ | |
| -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| || echo "::warning::could not write the commit status on $release_head" | |
| else | |
| echo "::warning::no open changeset-release/main PR found — the failure is reported here only" | |
| fi | |
| echo "::error title=Publish smoke did not run::resolve failed, so the release candidate was never smoked. This is NOT the same as 'nothing to smoke' — re-run the workflow, or check the release PR by hand before merging." | |
| exit 1 | |
| pack-smoke: | |
| name: Packed-tarball smoke (release candidate) | |
| needs: resolve | |
| # success() is spelled out rather than left to GitHub's implicit wrapper | |
| # (#5343): an `if:` naming no status function is silently wrapped in one, so | |
| # "resolve failed" and "resolve said don't run" both arrive here as the same | |
| # skip. The behaviour is unchanged — smoke only a resolve that SUCCEEDED and | |
| # said run — but it is now written down, and resolve-guard above states the | |
| # failure case that this condition deliberately does not cover. | |
| if: success() && needs.resolve.outputs.run == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| statuses: write | |
| steps: | |
| - name: Mark pending on the release PR head | |
| if: needs.resolve.outputs.report-sha != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api "repos/$GITHUB_REPOSITORY/statuses/${{ needs.resolve.outputs.report-sha }}" \ | |
| -f state=pending -f context='publish-smoke / packed-tarballs' \ | |
| -f description='Installing the release candidate into a fresh project…' \ | |
| -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.resolve.outputs.ref }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| - name: Enable Corepack | |
| run: corepack enable | |
| - name: Get pnpm store directory | |
| shell: bash | |
| run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | |
| - name: Setup pnpm cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: ${{ env.STORE_PATH }} | |
| key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm-store-v3- | |
| - name: Setup turbo cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: .turbo/cache | |
| key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-turbo-${{ github.job }}- | |
| ${{ runner.os }}-turbo- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build | |
| run: pnpm run build | |
| - name: Publish smoke (packed tarballs) | |
| run: bash scripts/publish-smoke.sh | |
| - name: Report verdict to the release PR head | |
| if: always() && needs.resolve.outputs.report-sha != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ "${{ job.status }}" = "success" ]; then | |
| state=success; desc='Fresh install of the release candidate: auth + CRUD green' | |
| else | |
| state=failure; desc='Release candidate fails a fresh install — see the run log' | |
| fi | |
| gh api "repos/$GITHUB_REPOSITORY/statuses/${{ needs.resolve.outputs.report-sha }}" \ | |
| -f state="$state" -f context='publish-smoke / packed-tarballs' \ | |
| -f description="$desc" \ | |
| -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" | |
| registry-canary: | |
| name: Registry canary (published latest) | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| - name: Publish smoke (npm registry) | |
| run: SMOKE_MODE=registry bash scripts/publish-smoke.sh | |
| - name: Open or refresh the canary issue | |
| # Scheduled runs have no human watching — surface the breakage as an | |
| # issue (deduped: one open issue, refreshed with a comment per failure). | |
| if: failure() && github.event_name == 'schedule' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" | |
| title="Registry canary failed: fresh npx create-objectstack install is broken" | |
| existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ | |
| --search "\"$title\" in:title" --json number -q '.[0].number // empty') | |
| body=$(printf 'The weekly publish-smoke registry canary failed: a fresh `npx create-objectstack@latest` project no longer passes first-run auth + CRUD against the npm registry.\n\nThis is the #3091 failure class hitting ALREADY-published versions (e.g. a transitive dependency released into a ^ range). See the run log for the failing probe: %s\n' "$run_url") | |
| if [ -n "$existing" ]; then | |
| gh issue comment "$existing" --repo "$GITHUB_REPOSITORY" \ | |
| --body "Still failing as of $run_url" | |
| else | |
| gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$body" | |
| fi |