diff --git a/.github/workflows/pack-smoke-optin.yml b/.github/workflows/pack-smoke-optin.yml new file mode 100644 index 0000000000..7fc687e686 --- /dev/null +++ b/.github/workflows/pack-smoke-optin.yml @@ -0,0 +1,154 @@ +# Opt-in pre-merge pack smoke (#14214, derived from the #14000 ruling). +# +# ## The gap this closes +# +# `publish-smoke.yml` owns the pack smoke, and it runs on `workflow_run` AFTER a +# Release run — it resolves the changesets release branch and reports its verdict +# as a commit status on that branch head. That is the right place for a release +# verdict and the wrong place for a PR author: #11767 (audience default flipped +# to `invite_only`, a breaking auth change) merged with ZERO packed-install +# coverage on its own PR, and the smoke then sat red on the release candidate for +# ~7 days because the only surface carrying the verdict was one no PR author +# looks at (measured in #14000). +# +# This workflow adds the missing TRIGGER — and only the trigger. The driver, the +# probes and every assertion stay exactly as `scripts/publish-smoke.sh` defines +# them; re-pinning those to the declared first-run contract was #14000's own +# deliverable (PR #14255) and is deliberately untouched here. What is new is +# WHEN the smoke runs and WHERE its verdict lands: on the pull request, before +# the merge, as an ordinary check run the author already reads. +# +# ## Opt-in, by ruling — and what that buys and costs +# +# Maintainer ruling 2026-09-01 (director batch #23, 「同意」) approved the +# LABEL-GATED OPT-IN shape: the cost stays in opt-in. A full build + pack + +# clean install is ~45 minutes, so putting it in the default inner loop would +# tax every PR in the repo to cover the handful that can break a fresh install. +# +# ⛔ Therefore this workflow must never grow a `paths:` filter that runs it by +# default, and must never be added to `.github/labeler.yml`. Auto-applying the +# label from changed paths would be a DIFFERENT design (auto-detect), not the +# one that was ruled, and it would reintroduce the default-inner-loop cost the +# ruling removed. +# +# The residual risk is stated rather than hidden: self-declaration only covers +# the author who RECOGNISES their change as breaking. An author who does not +# realise they widened the unauthenticated surface still gets no pre-merge pack +# coverage — the release-time leg in `publish-smoke.yml` remains the backstop for +# that case. Closing that gap would require a detection rule, which is a +# different card and a different ruling. +# +# ## When to apply the label +# +# Apply `needs:pack-smoke` to your own PR when it changes the auth/audience +# DEFAULTS or the accept/reject behaviour of the unauthenticated surface. The +# same criterion is written where PR authors meet it, in CONTRIBUTING.md. +# +# ## Wiring decisions +# +# `types:` restates GitHub's three defaults alongside `labeled`, because naming +# `types:` REPLACES the default set rather than extending it (#8304) — dropping +# one produces no run on that activity, which is an absence rather than a skip. +# `opened` is in the list so a PR created via the API already carrying the label +# is smoked; `synchronize` is the load-bearing one, because the thing under test +# is the MERGE PREVIEW and every push changes it. +# +# The guard has two limbs and both are needed: +# +# 1. the PR must CURRENTLY carry the label — this is what makes an unlabelled +# PR cost nothing; +# 2. on a `labeled` event the label just added must be OURS. Without this limb +# every unrelated label the size labeler adds (`size/l`, `ci/cd`, …) to an +# already-labelled PR would start another 45-minute smoke of a tree that +# has not changed. +# +# `concurrency` sits at JOB level, not workflow level, and that is deliberate. +# At workflow level the group is taken by every run this workflow starts, +# including the runs whose job then skips — so an unrelated `labeled` event +# would CANCEL a smoke that was still running and then skip, leaving the PR with +# no verdict at all. That is the #14000 silence shape (a missing answer reading +# as a green one) rebuilt inside its own fix. A skipped job never enters a +# job-level group, so only a real smoke can supersede a real smoke. +# +# ⛔ No `merge_group:` trigger, and this is not the oversight it resembles. A +# `merge_group` event carries no `pull_request` context, so neither limb of the +# guard above can be evaluated there; and the ruling puts this cost pre-merge and +# opt-in, not in the queue. This job is correspondingly NOT a required context +# (`scripts/check-required-contexts.mjs` holds that registry) — it is advisory by +# design, and a PR that never opts in simply never produces it. + +name: Pack Smoke (opt-in) + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened, labeled] + +permissions: + contents: read + +jobs: + pack-smoke: + # ~45 minutes of build + pack + clean install. Opt-in only — see the header. + name: Packed-tarball smoke (opt-in) + if: >- + contains(github.event.pull_request.labels.*.name, 'needs:pack-smoke') + && (github.event.action != 'labeled' || github.event.label.name == 'needs:pack-smoke') + runs-on: ubuntu-latest + timeout-minutes: 45 + concurrency: + group: pack-smoke-optin-${{ github.event.pull_request.number }} + cancel-in-progress: true + steps: + # No `ref:` — on `pull_request` the default checkout is `refs/pull/N/merge`, + # the MERGE PREVIEW, which is the tree the ruling names: what `main` will + # actually contain, not what the branch contains in isolation. The driver + # packs from the checkout root (`REPO_ROOT` in scripts/publish-smoke.sh), + # so the preview is what gets packed, installed and probed. + - name: Checkout the merge preview + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '22' + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - 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 + + # The driver's own prerequisite, asserted by the script itself: pack mode + # fails fast unless packages/cli/dist and the create-objectstack bin exist. + - name: Build + run: pnpm run build + + # SMOKE_MODE defaults to `pack`, so this is the same invocation + # publish-smoke.yml's pack-smoke job makes. ⛔ Do not add flags or env here + # to make a red go away: the assertions are #14000's deliverable and a + # refusal this script reports is a refusal a user would get. + - name: Publish smoke (packed tarballs) + run: bash scripts/publish-smoke.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 117ad6aeb8..1d533d0f2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -363,6 +363,9 @@ Provide both English and Chinese versions: - [ ] Naming conventions are followed - [ ] JSDoc comments are complete - [ ] No unrelated changes included +- [ ] If the PR changes the auth/audience **defaults** or the **accept/reject behaviour** of + the unauthenticated surface, label it `needs:pack-smoke` — that runs the packed-install + smoke on the merge preview before you merge, instead of finding out at release time. ### PR Checklist diff --git a/scripts/pm/ensure-pm-labels.sh b/scripts/pm/ensure-pm-labels.sh index b0274d42a1..453bbbe360 100644 --- a/scripts/pm/ensure-pm-labels.sh +++ b/scripts/pm/ensure-pm-labels.sh @@ -282,6 +282,34 @@ done # a script would be issuing the review verdict, which is 自查放行. gh label create needs:contract-review -R objectstack-ai/objectstack -c d93f0b -d "Clause-② enqueue gate: dispatched below contract-review tier — blocked until the review clears it" 2>/dev/null || true +# needs:pack-smoke — the opt-in pre-merge pack smoke (#14214, derived from the +# #14000 ruling; maintainer-approved 2026-09-01, director batch #23, 「同意」). +# Its named consumer is a GATE rather than a query: the job guard in +# `.github/workflows/pack-smoke-optin.yml`, which runs the packed-install smoke +# on the PR's merge preview only while this label is on the PR. +# +# ⚠️ It is the first label in this file that is NOT part of the PM state +# machine, and the difference matters to anyone extending it. Every pm:* row +# above is a state a PM seat writes; this one is written by the PR AUTHOR about +# their own diff — the ruling's word is 自声明 — and it names a property of the +# CHANGE, not a position on the board. So it is one-of with nothing, it blocks +# no dispatch, and no sweep in scripts/pm/ reads it. It is declared here anyway +# because this file is the repo's only declared home for a label OBJECT, and an +# undeclared label is the grey / empty-description drift the header describes: +# auto-created by its first application and unrepairable by any rerun. +# +# ⛔ Never auto-applied. Nothing in .github/labeler.yml may grow a rule for it +# (the workflow header argues why: path-derived application is a different +# design from the one that was ruled, and it puts the ~45-minute cost back into +# the default inner loop the ruling kept it out of). Like every other row here, +# this file only ensures the OBJECT — it never hangs the label on a PR. +# +# Main repo only, for the ordinary reason: the workflow that consumes it lives +# here. Colour 006b75 is deliberately outside every family above — this is not a +# lane, not a state, and not a routing seam, and borrowing one of their colours +# would assert a kinship the paragraph above spends its length denying. +gh label create needs:pack-smoke -R objectstack-ai/objectstack -c 006b75 -d "Opt-in pre-merge pack smoke: self-declared breaking auth/audience change (see CONTRIBUTING.md)" 2>/dev/null || true + # Routing labels exist only on the main backlog repo, and mark SEAM cards only # (file-at-destination ruling: pure sibling-repo fixes live in the target repo). #