Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 25 additions & 21 deletions .github/workflows/spec-main-shape-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,18 @@ name: Spec Main Shape Gate
# request of that lane arms the queue while the red stands.
# - `pull_request` posts the same verdict as an advisory check. A human reads
# it; nothing waits for it.
# - `merge_group` stays subscribed, deliberately. The ruled order is merge
# first, ruleset edit second, and until that edit the queue still requires
# this name: a workflow that stopped reporting on `merge_group` would stall
# every queue build at the ruleset's 60-minute timeout with nothing red to
# point at (objectui#3523 -- subscribe first, enrol second, and the mirror
# image on the way out). It also keeps the context REQUIRABLE, so
# re-enrolling it stays a settings edit.
# - `merge_group` is NOT subscribed (objectui#12252), the other half of the
# same ruling: the context is not in the queue's required set, so a
# verdict on a queue build would be read by nobody while every merge group
# paid a runner for it. (Whether the ruleset says so is
# `scripts/check-required-check-set.mjs`'s reading, ⛔ not this comment's.)
# ⛔ Re-subscribe `merge_group` BEFORE re-enrolling the context, never
# after: a required context that never reports on a queue build stalls
# it at the ruleset's 60-minute timeout with nothing red to point at
# (objectui#3523 -- subscribe first, enrol second). `merge-queue-reporting`
# derives that floor from `REQUIRED_CONTEXTS`, and
# `scripts/__tests__/spec-main-shape-gate.test.ts` pins the absence, so
# re-enrolling edits both on purpose rather than by accident.
#
# ⚠️ Requiredness is repository SETTINGS (GitHub ruleset 11776024), which no
# file in this tree can set, so "advisory" is not a mechanism in this file.
Expand Down Expand Up @@ -101,13 +106,13 @@ name: Spec Main Shape Gate
# before, so `inject` still reads the dependencies the spec resolves.
#
# Only an artifact from a `main` run of THIS repository is reused. A pull
# request or a queue build runs the workflow file of its own branch, so an
# artifact it uploaded is a file anybody with a branch could have shaped; a
# run on `main` executes reviewed YAML. The lookup is a READ (`actions: read`);
# nothing here writes through the token, and the upload uses the runner's own
# artifact credential. A lookup that fails, a download that fails, or an
# artifact whose recorded sha is not the one resolved above all mean BUILD --
# never a verdict about a spec this job did not hold.
# request (or a manual dispatch on a branch) runs the workflow file of its own
# branch, so an artifact it uploaded is a file anybody with a branch could have
# shaped; a run on `main` executes reviewed YAML. The lookup is a READ
# (`actions: read`); nothing here writes through the token, and the upload uses
# the runner's own artifact credential. A lookup that fails, a download that
# fails, or an artifact whose recorded sha is not the one resolved above all
# mean BUILD -- never a verdict about a spec this job did not hold.
#
# ⚠️ What this buys is the build and the pack, not the compile: the forced
# typecheck below is the bulk of this job and is deliberately never cached.
Expand All @@ -126,8 +131,6 @@ name: Spec Main Shape Gate
on:
pull_request:
branches: [main, develop]
merge_group:
types: [checks_requested]
workflow_dispatch: {}
# The full battery's hourly run on `main` (objectui#12214). Off the hour on
# purpose, and on a different minute from `ci.yml`'s.
Expand All @@ -144,9 +147,10 @@ permissions:

concurrency:
group: spec-main-shape-gate-${{ github.ref }}
# Superseded pull-request runs are cancelled; a QUEUE build never is. A
# cancelled check is not `success`, and on a queue build that is a dequeue
# rather than a saved runner minute.
# Superseded pull-request runs are cancelled; a run on `main` (the hourly
# `schedule`, or a dispatch) never is. A cancelled check is not `success`, and
# the scheduled run is the one place this gate's verdict is waited on, so a
# cancellation there is a missing reading rather than a saved runner minute.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
Expand All @@ -156,8 +160,8 @@ jobs:
# Sized against measurements, not taste: the spec's own build and this
# repository's forced (uncached) build + typecheck each cost minutes, and the
# runner is slower than the box they were measured on. A ceiling this job can
# reach turns a verdict into `cancelled`, which the merge queue cannot tell
# from `failure` and which reports nothing a reader can act on.
# reach turns a verdict into `cancelled`, which reads as a red on a pull
# request or on `main` and reports nothing a reader can act on.
timeout-minutes: 45

steps:
Expand Down
28 changes: 15 additions & 13 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ one has its own section below.
| `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 |
| `repo-gates.yml` | Repo Gates | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — fifteen gates as steps of one job ([#12214](https://github.com/objectstack-ai/objectui/issues/12214)); the job is red when any step is, and `scripts/dependabot-merge-gate.mjs` declares the context in `REQUIRED_CONTEXTS` |
| `line-citation-gate.yml` | Line Citation Gate | PR to `main`, `develop` — **no path filter**; manual | No — **report-only** while it beds in; it exits 0 whatever it finds, and exits 1 only when one of its own synthetic controls fails. It declares no `merge_group` trigger, so it cannot be a required context in its current state |
| `spec-main-shape-gate.yml` | Spec Main Shape Gate | PR to `main`, `develop` — **no path filter**; merge-queue builds; manual; hourly `schedule` on `main` | No — **advisory** since [#12240](https://github.com/objectstack-ai/objectui/issues/12240): the maintainer's ruling took the context out of the required set, and `scripts/dependabot-merge-gate.mjs` classifies it `NOT_A_GATE`. Its verdict is unchanged — a shape break is a real red naming the objectui file and the objectstack commit ([#9860](https://github.com/objectstack-ai/objectui/issues/9860)) — and a red on the hourly `schedule` run on `main` is a main-red for the `domain:devx` lane |
| `spec-main-shape-gate.yml` | Spec Main Shape Gate | PR to `main`, `develop` — **no path filter**; manual; hourly `schedule` on `main` | No — **advisory** since [#12240](https://github.com/objectstack-ai/objectui/issues/12240): the maintainer's ruling took the context out of the required set, and `scripts/dependabot-merge-gate.mjs` classifies it `NOT_A_GATE`. Its verdict is unchanged — a shape break is a real red naming the objectui file and the objectstack commit ([#9860](https://github.com/objectstack-ai/objectui/issues/9860)) — and a red on the hourly `schedule` run on `main` is a main-red for the `domain:devx` lane |
| `governed-surface-guard.yml` | Governed Surface Queue Guard | PR to `main`, `develop` (incl. `ready_for_review`) — **no path filter**; merge-queue builds | **Yes on a queue build only** — a governed-surface diff with no authorized approval record (on any commit) is refused there, and so is any merge group whose queued pull requests still carry `needs:contract-review`; on the pull request itself it is deliberately green and prints an early warning |
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
| `lockfile-integrity.yml` | Lockfile Integrity Check | PR to `main`, `develop` touching `pnpm-lock.yaml` or the gate's own two files; manual | No — **deliberately not a blocking context** ([#8326](https://github.com/objectstack-ai/objectui/issues/8326)); it names the packages and the Dependabot merge gate classifies it `NOT_A_GATE` |
Expand Down Expand Up @@ -2096,7 +2096,7 @@ Uses [Lychee](https://github.com/lycheeverse/lychee) with configuration from `ly

## Spec Main Shape Gate (`spec-main-shape-gate.yml`)

**Trigger:** PR to `main`, `develop` — **no path filter**; merge-queue builds; manual; hourly `schedule` on `main`.
**Trigger:** PR to `main`, `develop` — **no path filter**; manual; hourly `schedule` on `main`.
**Appears as:** Spec Main Shape Gate.

It builds `@objectstack/spec` from objectstack `main` at a git sha and compiles this repository
Expand Down Expand Up @@ -2144,11 +2144,11 @@ of it the job needs.
`objectstack-spec-<sha>`, with the sha it was packed at, kept one day. Every other run first looks
for that artifact at the sha it just resolved and injects it instead of building again; the clone
and the spec's own install still run, because the injection reads the dependencies the spec's
build resolved. Only an artifact from a `main` run of this repository is reused: a pull request or
a queue build runs its own branch's workflow file, so what it could upload is not evidence about a
spec. A failed lookup or download, or an artifact recording another sha, means *build*. What this
saves is the build and the pack — the forced compile below is most of the job and is never cached;
the measured split is on #12214's pull request, not restated here.
build resolved. Only an artifact from a `main` run of this repository is reused: a pull request
(or a manual dispatch on a branch) runs its own branch's workflow file, so what it could upload is
not evidence about a spec. A failed lookup or download, or an artifact recording another sha, means
*build*. What this saves is the build and the pack — the forced compile below is most of the job and
is never cached; the measured split is on #12214's pull request, not restated here.

**The compile is `TURBO_FORCE=true pnpm type-check --continue`, and every word of it is
load-bearing.** That task dependsOn `^build`, so the job builds every workspace package against
Expand All @@ -2174,8 +2174,8 @@ every line locally, and on a GitHub Actions runner a `##[group]<package>:<task>`
output emitted bare beneath it — with the *failing* task announced by a bare colourised header
and no group at all. Only the second ever reaches this job, and a prefix-only reading has nothing
to match in it. `scripts/__tests__/spec-main-shape-gate.test.ts` pins the bypass, the completeness
flag, the unfiltered trigger, the queue subscription and both attribution carriers, each with the
firing control that proves the assertion is not satisfied by something else.
flag, the unfiltered trigger, the absence of a queue subscription and both attribution carriers,
each with the firing control that proves the assertion is not satisfied by something else.

**How it became a required check, and how it left the set.** Requiredness is a GitHub ruleset —
repository settings — which `scripts/check-required-check-set.mjs` can read and nothing in this tree
Expand All @@ -2202,10 +2202,12 @@ what changed is who waits for its verdict.
hide the verdict and change nothing about who waits for it.
- The name moved back to `NOT_A_GATE` in `scripts/dependabot-merge-gate.mjs`, so a Dependabot bump
does not wait for it either, and `scripts/check-required-check-set.mjs` no longer watches it.
- `merge_group` stays subscribed. The maintainer removes the context from the ruleset in the same
sitting as that pull request's merge, and until that edit the queue still requires the name: a
workflow that stopped reporting on queue builds would stall each of them at the ruleset's
60-minute timeout. The subscription also keeps the context requirable.
- It does not run on queue builds. [#12252](https://github.com/objectstack-ai/objectui/issues/12252)
dropped the `merge_group` trigger once the context was out of the queue's required set: a verdict
on a merge group is read by nobody, and every merge group paid a runner for it. The way back is
two edits in this order — re-subscribe `merge_group`, then enrol the context — because a required
context that never reports on a queue build stalls it at the ruleset's 60-minute timeout. The
test file pins the absence, so re-enrolling edits both on purpose.

Whether the context is enrolled today is not answered here; the patrol's reading answers it.

Expand Down
31 changes: 24 additions & 7 deletions scripts/__tests__/spec-main-shape-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,27 @@ describe('the gate exists and is wired the way a requirable check has to be', ()
expect(trigger.filtered, 'a path filter would make this context unrequirable').toBe(false);
});

it('subscribes merge_group, so a maintainer can enrol it without stalling the queue', () => {
// The #3523 ordering: subscribe first (a pure addition), enrol second. The
// reverse order is the state in which every queued pull request burns the
// ruleset's 60-minute timeout with nothing red to point at.
expect(subscribesMergeGroup(workflow as NonNullable<typeof workflow>)).toBe(true);
it('does not subscribe merge_group — re-subscribe merge_group before re-enrolling the context (objectui#12252)', () => {
// The #3523 ordering, on the way out. The context left the queue's required
// set (objectui#12240), so a queue build's verdict on this gate is read by
// nobody while every merge group pays a runner for it; the hourly run on
// `main` and the advisory run on a pull request are where it is read.
//
// This is the ruling's in-tree lock, not a ban on the trigger. The way back
// is two edits in this order — subscribe first (a pure addition), enrol
// second — and the second is the case the derived floor in
// `merge-queue-reporting.test.ts` refuses on its own: a name in
// `REQUIRED_CONTEXTS` whose workflow lacks the trigger. The reverse order is
// the state in which every queued pull request burns the ruleset's
// 60-minute timeout with nothing red to point at.
expect(
subscribesMergeGroup(workflow as NonNullable<typeof workflow>),
`${WORKFLOW} subscribes \`merge_group\` again, so every merge group runs a gate whose ` +
`verdict nothing waits for (objectui#12252). If the context is being RE-ENROLLED on a ` +
`ruling, subscribing first is the right order (objectui#3523): replace this pin with the ` +
`subscription it then owes, and enrol second — the in-tree half is \`REQUIRED_CONTEXTS\` ` +
`in scripts/dependabot-merge-gate.mjs. Otherwise drop the trigger.`,
).toBe(false);
});

it('runs both halves of the gate script', () => {
Expand Down Expand Up @@ -120,8 +136,9 @@ describe('the spec is built only when objectstack main moved (objectui#12214)',
});

it('reuses only an artifact a main run of THIS repository built, named by the resolved sha', () => {
// A pull request or queue build runs its own branch's YAML, so what it
// uploads is not evidence about a spec; a main run executes reviewed YAML.
// A pull request (or a manual dispatch on a branch) runs its own branch's
// YAML, so what it uploads is not evidence about a spec; a main run
// executes reviewed YAML.
const lookup = step('Look for the spec a main run already built at that commit');
expect(lookup).toContain('actions/artifacts?name=objectstack-spec-$UPSTREAM_SHA');
expect(lookup).toContain('.workflow_run.head_branch == "main"');
Expand Down
Loading