diff --git a/.changeset/6321-changeset-guard-self-coverage.md b/.changeset/6321-changeset-guard-self-coverage.md new file mode 100644 index 000000000..25123a3f2 --- /dev/null +++ b/.changeset/6321-changeset-guard-self-coverage.md @@ -0,0 +1,7 @@ +--- +--- + +Internal CI-only change: `changeset-guard.yml`'s `paths:` filter now includes its own +YAML and `scripts/check-changeset-no-major.mjs`, so a change to the gate is exercised by +the PR that makes it instead of by the next unrelated `.changeset/**` PR (objectui#6321). +`content/docs/guide/ci-cd-pipeline.md` is updated to match. No published package changed. diff --git a/.github/workflows/changeset-guard.yml b/.github/workflows/changeset-guard.yml index b64d800a5..814098831 100644 --- a/.github/workflows/changeset-guard.yml +++ b/.github/workflows/changeset-guard.yml @@ -25,16 +25,55 @@ name: Changeset Guard # # It needs no install and no build — a checkout plus one `node` call, a few # seconds — so keep it that way if you add checks to it. +# +# ── Self-coverage (objectui#6321) ──────────────────────────────────────────── +# +# The `.changeset/**` entry above is the inverse trigger this file exists for; +# the two entries below it are a DIFFERENT thing — ordinary self-coverage, so a +# change to the gate itself is exercised by the PR that makes it rather than by +# the next unrelated PR that happens to touch `.changeset/**`. Before this, +# neither this YAML nor the script it runs was in the filter at all: measured +# against the other six path-filtered workflows in this repo, 5 of 7 +# self-include and this was one of the two that did not (the other, +# `performance-budget.yml`, was closed in #6315). +# +# The convention those five follow is "list what the `run:` step below actually +# executes" — own YAML plus the invoked script — not the transitive import +# graph. `half-state-patrol.yml` additionally lists `scripts/invoked-as.mjs`, +# but that is a documented one-off (its own header's divergence #2, tied to +# objectui#5791's port from objectstack) and NOT the pattern the other three +# self-including, script-running gates follow: `node-esm-load-gate.yml`, +# `published-dist-gate.yml` and `spec-range-floors.yml` all import +# `scripts/invoked-as.mjs` too (same `isEntrypoint` helper) and none of them +# lists it — it is a widely shared utility (40+ importers under `scripts/`), +# and a change to it is already caught by `check-entry-guard.mjs` / +# `entry-guard-wiring.test.ts` in the ordinary vitest suite, not by re-running +# every gate that happens to import it. So this filter matches the +# three-of-four majority and does not add it either. +# +# Also deliberately NOT listed: `scripts/__tests__/check-changeset-no-major.test.ts`. +# That file matches the `~ partial` pattern `published-dist-gate.yml` and +# `spec-range-floors.yml` already set for their own gate scripts' `__tests__` +# files — the test lives under `scripts/**`, which the `Decide whether this +# change needs a full run` step in `ci.yml`/`lint.yml` does NOT exclude, so any +# PR that edits it (or the script) already gets a real run of the root vitest +# suite that executes it. Adding it here would duplicate coverage `pnpm test` +# already provides on exactly the PRs where it matters, for no PR it would add +# it on. on: pull_request: branches: [main, develop] paths: - '.changeset/**' + - '.github/workflows/changeset-guard.yml' + - 'scripts/check-changeset-no-major.mjs' push: branches: [main] paths: - '.changeset/**' + - '.github/workflows/changeset-guard.yml' + - 'scripts/check-changeset-no-major.mjs' concurrency: group: changeset-guard-${{ github.event.pull_request.number || github.ref }} diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 946b062ee..708913b39 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -25,7 +25,7 @@ one has its own section below. |---|---|---|---| | `ci.yml` | CI | Push / PR to `main`, `develop`; merge-queue builds | **Yes** — every job but the two coverage-lane jobs (`test-coverage` and `coverage-report`, push only) runs on PRs and on queue builds | | `lint.yml` | Lint | Push / PR to `main`, `develop`; merge-queue builds; manual | **Yes** — ESLint **errors** only | -| `changeset-guard.yml` | Changeset Bump Policy | PR / push touching `.changeset/**` | **Yes** | +| `changeset-guard.yml` | Changeset Bump Policy | PR / push touching `.changeset/**` or the gate itself | **Yes** | | `changeset-presence.yml` | Changeset Declaration | PR to `main`, `develop` — **no path filter**; merge-queue builds | **Yes** — when a released package's `src/` changed and no changeset was added | | `control-bytes.yml` | Control Byte Scan | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** | | `docs-links.yml` | Internal Docs Link Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** | @@ -65,8 +65,10 @@ The path filters explain most "why did nothing run on my PR?" questions: the whole point: a check that is never *created* cannot be a required check — it leaves the PR pending rather than failing it — so while the filter sat on the trigger, none of these could be required at all. -- `changeset-guard.yml` carries the inverse filter — it runs *only* when `.changeset/**` changes, - which is precisely why it is a separate workflow instead of a job inside `ci.yml`. +- `changeset-guard.yml` carries the inverse filter — it runs *only* when `.changeset/**` changes + (plus its own YAML and `scripts/check-changeset-no-major.mjs`, so a change to the gate itself is + exercised by the PR that makes it — objectui#6321), which is precisely why it is a separate + workflow instead of a job inside `ci.yml`. - `changeset-presence.yml` is that guard's mirror image and the reason there are two: a PR which *forgot* its changeset does not touch `.changeset/**`, so the inverse filter guarantees the one check that could notice never runs. It therefore carries **no** filter and decides from the diff @@ -134,8 +136,9 @@ Two things follow for anyone editing this directory: reading; they are examples rather than a census, so a further workflow carrying any of these shapes is just as unrequirable without appearing here. - **Changeset Bump Policy** (`changeset-guard.yml`) — an **inverse** path filter: its - `pull_request` trigger declares `paths: ['.changeset/**']`, so on a PR that touches nothing - under `.changeset/**` the context is never created at all. + `pull_request` trigger declares + `paths: ['.changeset/**', '.github/workflows/changeset-guard.yml', 'scripts/check-changeset-no-major.mjs']`, + so on a PR that touches none of those three the context is never created at all. - **Bundle Analysis** (`performance-budget.yml`) — an ordinary path filter on the same trigger, with the same consequence for every PR that matches none of its paths. - **Live E2E (informational)** (`live-e2e.yml`) — the job carries `continue-on-error: true`, @@ -1262,6 +1265,18 @@ changeset started nothing at all. Since [#3523](https://github.com/objectstack-a job in them short-circuits, because `.changeset/**` is still on the in-job ignore list. The check that has to read the changeset therefore still lives here. +The same `paths:` list also carries the gate's own YAML and +`scripts/check-changeset-no-major.mjs` ([#6321](https://github.com/objectstack-ai/objectui/issues/6321)) +— self-coverage, not the inverse trigger above: without it, a PR that edits the gate is not the PR +that runs it, and the first real execution lands on someone else's unrelated `.changeset/**` PR. +Deliberately not listed: `scripts/invoked-as.mjs` (a dependency the gate script imports, but a +widely shared one — 40+ importers under `scripts/` — that `published-dist-gate.yml`, +`spec-range-floors.yml` and `node-esm-load-gate.yml` also import without listing; only +`half-state-patrol.yml` lists it, as a documented one-off) and the script's own +`__tests__/check-changeset-no-major.test.ts` (it already runs in the root vitest suite on any PR +that touches `scripts/**`, the same `~ partial` reasoning `published-dist-gate.yml` and +`spec-range-floors.yml` apply to their own gate scripts' `__tests__` files). + Runs `scripts/check-changeset-no-major.mjs`, which fails if any pending changeset declares a `major` bump. Every publishable package is in one `fixed` group (39 packages), so a single `major` publishes all of them as the next major — and objectui's major is pinned to the