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
18 changes: 18 additions & 0 deletions .changeset/quiet-goats-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@cosyte/cli': patch
---

Caller workflow and branch protection only, with no runtime impact: the pre-publish check now blocks a merge here instead of merely showing a red X.

The shared pipeline grew a `prepublish` job on 2026-08-05, so this repo started emitting a
`ci / prepublish` context on every pull request without a commit landing here. The branch ruleset
did not name it, which meant a red pre-publish result reported a failure and merged anyway. That
job is the offline manifest lint plus the pack-and-install probe, and this package is the reason
both exist: two published versions carry local-path dependency specifiers and are permanently
uninstallable. The context was read off a real check run first and then added to the ruleset, in
that order, because requiring a context nothing emits strands every pull request instead of
failing it.

The banner on the caller now records the hazard it did not previously cover: a context can arrive
in this repo with no commit in this repo, because the `uses:` reference is unpinned, and it always
arrives unrequired.
49 changes: 39 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,45 @@ permissions:
contents: read

jobs:
# ┌─────────────────────────────────────────────────────────────────────────────────────────┐
# │ A REQUIRED JOB GATES ALL OF ITS STEPS. The `ci-required-checks` ruleset on this repo │
# │ requires the CONTEXTS `ci / verify (22, ubuntu-latest)`, `ci / verify (24, │
# │ ubuntu-latest)` and `ci / actionlint`. Every gate the shared pipeline runs (typecheck, │
# │ lint, format:check, PHI scan, test, gating coverage, build, attw, dual ESM/CJS smoke) is │
# │ a STEP inside `verify`, and is required only because that job is. Promote any one of │
# │ them to its own job and it is silently NO LONGER REQUIRED: the ruleset names contexts, │
# │ the new context is not in it, and nothing anywhere errors or warns. If you split a step │
# │ out, add its context to the ruleset in the same change. │
# └─────────────────────────────────────────────────────────────────────────────────────────┘
# ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
# │ A REQUIRED JOB GATES ALL OF ITS STEPS. The `ci-required-checks` ruleset on this repo requires │
# │ all four contexts this caller emits: `ci / verify (22, ubuntu-latest)`, `ci / verify (24, │
# │ ubuntu-latest)`, `ci / prepublish` and `ci / actionlint`. Every gate the shared pipeline runs │
# │ (typecheck, lint, format:check, PHI scan, test, gating coverage, build, attw, dual ESM/CJS │
# │ smoke) is a STEP inside `verify`, and is required only because that job is. Promote any one of │
# │ them to its own job and it is silently NO LONGER REQUIRED: the ruleset names contexts, the new │
# │ context is not in it, and nothing anywhere errors or warns. If you split a step out, add its │
# │ context to the ruleset in the same change. │
# │ │
# │ AND THE HAZARD IS NOT ONLY A LOCAL SPLIT: THE CALLED WORKFLOW CAN GROW A JOB UPSTREAM. The │
# │ `uses:` below is pinned to `@main`, so a job added over there starts emitting a new `ci / <job>` │
# │ context here with NO COMMIT IN THIS REPO, and it arrives NOT REQUIRED: a red X that does not │
# │ block a merge. That is not hypothetical. `prepublish` arrived exactly that way on 2026-08-05 and │
# │ was unrequired here until it was measured off a real check run and added. A `ci / *` context you │
# │ do not see in the list above is that same event happening again. Decide it, and write the │
# │ decision down: require it, or record here why it must not be. │
# └──────────────────────────────────────────────────────────────────────────────────────────────────┘
#
# `prepublish` is the pre-publish gate: it refuses a dependency specifier no registry can resolve
# (offline manifest lint) and then packs this tree and installs the tarball into a clean directory.
# Both layers are ON by default upstream and this caller passes neither toggle, so the defaults are
# the whole story for whether it runs. It is the only place a consumer's install question gets
# asked while the answer can still prevent a permanent publish: `@cosyte/cli@0.0.1` and `0.0.2` are
# on the registry uninstallable forever, and this job is what would have refused them.
#
# ONE DEFAULT IS PERMANENTLY STALE HERE, AND THE OBVIOUS "FIX" FOR IT IS FORBIDDEN. The upstream
# default for `expect-unpublished-deps` is `@cosyte/fhir=blocked`. Be precise about why that is
# stale, because the short version is false: `@cosyte/fhir` IS declared in this manifest, as
# `devDependencies: {"@cosyte/fhir": "file:vendor/cosyte-fhir-0.0.0.tgz"}`, which is what lets this
# repo's own FHIR tests run. The operative fact is that `prepublish-check.mjs` does not read
# `devDependencies` at all (a `file:` specifier is fine there, since no consumer installs them), so
# the allowance names a package that is not a CONSUMER dependency of this one and is reported stale
# on every run. That notice never fails the job.
#
# It must NOT be silenced by promoting `@cosyte/fhir` into `dependencies` or `optionalDependencies`:
# declaring it in either, alongside `@cosyte/transform`, fails the whole install with `ERESOLVE`,
# which is the measured reason it is not there. See CLAUDE.md. If the notice is ever worth
# silencing, the way is to pass an empty `expect-unpublished-deps` from this caller.
#
# Universal gates from the shared pipeline (typecheck, lint, format, test, coverage, build, attw,
# dual ESM/CJS smoke, actionlint) on the standard Node matrix. Repo-specific extra jobs (e.g. an
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,36 @@ still do. Each entry was assigned to the release whose tag first contains it, re

### Fixed

- **A red pre-publish gate showed a red X and merged anyway, because `ci / prepublish` was a
required check nowhere (CI-REQUIRED-CHECKS).** The shared pipeline this repo calls grew a
`prepublish` job on 2026-08-05 (`cosyte/.github#35`, `6142ac4`; its second layer defaulted on in
`#36`, `90936ea`), so `ci.yml` began emitting a `ci / prepublish` context on every pull request
here **with no commit landing in this repo**. Repository ruleset `19907924` did not name it, so
the job could fail and the merge still landed on `main`, which is the branch that publishes.
`ci / prepublish` is now a required context, `integration_id`-pinned like the other six.
- **What it gates is this package's own worst shipped defect.** The job is an offline manifest
lint that refuses a dependency specifier no registry can resolve, plus a probe that packs this
tree and installs the tarball into a clean directory. `@cosyte/cli@0.0.1` and `0.0.2` reached
the registry carrying `file:vendor/*.tgz` specifiers and are permanently uninstallable; the
manifest lint would have refused both. A gate that catches that and then cannot block the merge
reintroducing it is documentation.
- **The order was load-bearing and is recorded because it is easy to get backwards.** The context
name was read off a real `pull_request` check run before the ruleset was written, never off the
workflow's `name:` field. Requiring a context nothing emits does not fail a pull request, it
leaves it pending and unmergeable forever. A census of the eight most recent head shas here
(`#27` through `#34`) finds `ci / prepublish` **zero** times, because the newest of them merged
before the upstream job existed, so it could not have been required any earlier.
- **`ci.yml`'s banner now covers the hazard that actually bit.** Every previous wording of it was
scoped to splitting a step out of `verify` locally. The `uses:` reference is unpinned, so a job
added upstream emits a new `ci / <job>` context here with no commit in this repo, and it always
arrives unrequired.
- **Deliberately NOT in this change: `release.yml`'s version-PR trap note.** It carries a stale
required-context count and a claim about the "Version Packages" PR arriving with zero check runs
whose truth depends on whether `RELEASE_PR_TOKEN` is authoring that PR. Two attempts to correct
it inside this change each produced a fresh false claim: the first a false operational premise,
the second a false claim about the note's own history. So it is cut out to its own change rather
than rewritten a third time. Nothing about it is made worse here; it is left exactly as it was.

- **The shipped documentation sidebar was off the canonical IA spine, and it was holding up the
docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).** `docs-content/sidebars.json` declared two
top-level categories that are not on the spine, **"MCP server"** and **"Reference"**. The docs
Expand Down
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,21 @@ Full ruleset, the required-context table and the per-check reasoning:
[agent-notes § Branch protection](documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim).

- `main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`). Before it
existed every check here was advisory, on the branch that publishes. Six required contexts, each
pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy it.
existed every check here was advisory, on the branch that publishes. **Seven** required contexts,
each pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy
it. **Do not quote that count without re-deriving it** (`gh api repos/cosyte/cli/rulesets/19907924`):
it was `6` until `ci / prepublish` was added, and it moves when the called workflow does.
- **Read a required context off a REAL check run, never off a workflow's `name:` field.** Requiring a
context nothing emits does not fail a PR: it leaves it **pending and unmergeable forever**, with no
error and no warning.
- **▶ A `ci / *` CONTEXT CAN APPEAR HERE WITH NO COMMIT IN THIS REPO, AND IT ARRIVES NOT REQUIRED.**
`ci.yml` calls `cosyte/.github/.github/workflows/ci.yml@main` unpinned, so a job added upstream
starts emitting a context here that the ruleset does not name: **a red X that does not block a
merge**, which is the failure this repo's whole protection claim exists to prevent. `ci / prepublish`
arrived that way on 2026-08-05 and was unrequired until measured and added. **Census `ci / *` against
a real check run whenever `.github` moves**, then require it or write down why not, in `ci.yml`'s
banner. Detail:
[agent-notes § Branch protection](documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim).
- **`no-internal-refs` and `no-emdash` are bare JOB IDS**, not `<workflow> / <job>`, because they are
ordinary jobs in this repo's own workflows. **Renaming the job silently detaches the required
check.** Rename the job and the ruleset together, or neither.
Expand Down
Loading
Loading