Skip to content

Publish Smoke

Publish Smoke #1262

Workflow file for this run

# 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