box#95 taught the family that a forgotten manual tag is the worst failure shape: silent, no red X, a release that simply doesn't happen. The ship decision already lives in the ceremony PR — the one whose whole diff is the version leaving -dev, carrying the reviews and the maintainer's merge — so tagging after it is transcription, and transcription belongs to the machine (box#96's design; this is cast's twin). release.yml now also triggers on pull_request closed against main, gated on merged == true AND the hand-set release label. The merge path asserts four facts in order, each fail-loud and creating nothing: the merged package.json version is non--dev (read via node, never regex — the pkg_version discipline); the version CHANGED in this PR (base vs merge — the -dev interlock, so a mislabeled ordinary PR fails loudly); the version's changelog section extracts non-empty via the existing release-notes.sh; and no tag or release exists yet (idempotent re-runs, and the loud answer to a manual-tag race). Then, in the same job, it tags the merge commit via the API and publishes. Same-job is load-bearing: a GITHUB_TOKEN-created tag triggers no workflows, so the tag-push path cannot fire on it and double-publish. Both trigger paths converge on literally the same steps — each entry step exports RELEASE_VERSION, and the notes extraction, the exact existing asset build (npm ci, npm run build, npm prune --omit=dev, staged as cast-X.Y.Z/), and the gh release create read only that — so the paths cannot drift and the installer keeps finding the one asset name it knows, cast-X.Y.Z.tgz. The tag-push path survives as the documented manual fallback and backfill, and it matters immediately: 0.1.0 never carried -dev (cast predates the ritual), so the interlock correctly does not fire for #110's ceremony — that one ships by manual tag, and the automation applies from 0.1.1 on. test/release.test.ts pins the new wiring in the house grep style, fail-closed: the merged+labeled gate, the four asserts strictly ordered ahead of tag/build/publish, the single job, the anti-recursion comment, and that no per-path asset name exists. CONTRIBUTING.md's Releasing now says it plainly: merge is the ship decision; the tag is the fallback. Fixes #111 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
5.9 KiB
Markdown
99 lines
5.9 KiB
Markdown
# Contributing
|
|
|
|
How change lands in this repo. The short version: PRs are born as drafts,
|
|
three reviewer bots take the first rounds, a human takes the last word — and
|
|
labels tell you where everything is without opening anything.
|
|
|
|
## The PR loop
|
|
|
|
1. **Fork and branch.** Contributors work from forks; upstream branches are
|
|
for maintainers. Title the PR conventionally (`feat:`, `fix:`, `docs:`).
|
|
2. **Open as a draft** while you build. Drafts are invisible to the reviewer
|
|
bots on purpose.
|
|
3. **When it's ready**: mark ready-for-review and request all three bots —
|
|
`claude-bot-andresmgsl`, `codex-bot-andresmgsl`, `grok-bot-andresmgsl`.
|
|
They poll roughly every 15 minutes.
|
|
4. **Rounds are answered whole.** Wait until all three have reviewed, then
|
|
answer the entire round in a **single reply**, push the fixes, and
|
|
re-request the bots that didn't approve. Prefer verification over
|
|
argument: a test settles what a comment thread can't.
|
|
5. **Reviews end in a verdict.** A reviewer — bot or human — either
|
|
**approves** or **requests changes**, never a bare comment. A
|
|
comment-only review is a non-verdict: it doesn't say whether the round
|
|
passed, and the state machine (and anyone scanning the board) has to
|
|
guess. The verdict carries *blockingness only*, the body carries the
|
|
feedback: non-blocking nits ride an **approval** and the author addresses
|
|
them at their discretion; anything blocking — including a question that
|
|
gates the verdict — is **request changes**, saying what unblocks it. The
|
|
reconciler treats a comment-only review as not-approved, so commenting
|
|
without a verdict only stalls the PR. The machine never reads review
|
|
bodies: when a comment-only reviewer's line is really an agreement, that
|
|
judgment belongs to the **author** — escalate by requesting the
|
|
maintainer's review (step 6), and the reconciler flips the label on that
|
|
request, because an explicit request is a fact it can trust.
|
|
6. **When the round passes, the author hands the PR to the maintainer** by
|
|
requesting their review — that request is what flips `state:needs-human`.
|
|
With three formal head-current approvals the labels workflow requests it
|
|
automatically; when part of the panel is comment-only, reading their
|
|
agreement is the author's judgment, so the author makes the request.
|
|
7. **Checks must be green**: `npm run check`, `npm run build`, and
|
|
`npm test` locally mirror what CI runs.
|
|
8. **Feature PRs land their changelog entry as part of the PR** (box's
|
|
convention): add it under `CHANGELOG.md`'s `## Unreleased` heading —
|
|
that section becomes the release notes verbatim when a release is cut.
|
|
|
|
## Releasing
|
|
|
|
A release is a PR, and merging it IS the release
|
|
([#111](https://github.com/heavy-duty/cast/issues/111); box#96's design,
|
|
on box#83's shape):
|
|
|
|
1. A small PR — `release: X.Y.Z`, labeled `release` — bumps `package.json`'s
|
|
`version` (and `package-lock.json`; `npm install --package-lock-only`
|
|
keeps them in step) and stamps `CHANGELOG.md`'s Unreleased section as
|
|
`## X.Y.Z — YYYY-MM-DD`. CI green on it, same loop as any PR.
|
|
2. **Merge. That's the ship decision — nothing else to do.**
|
|
[release.yml](.github/workflows/release.yml) fires on the merged,
|
|
`release`-labeled PR and asserts, in order, each fail-loud and creating
|
|
nothing: the merged version is non-`-dev`; the version *changed in this
|
|
PR* (the `-dev` transition is the interlock — a mislabeled ordinary PR
|
|
fails here); that version's changelog section extracts non-empty
|
|
([.github/scripts/release-notes.sh](.github/scripts/release-notes.sh));
|
|
and no tag or release exists for it yet. Then, in the same job, it tags
|
|
the merge commit bare `X.Y.Z` (no `v` prefix — box's tag scheme), builds
|
|
the package once (`npm ci && npm run build && npm prune --omit=dev`), and
|
|
publishes the release with the runnable tree — `bin/`, `dist/`,
|
|
production `node_modules/`, `package.json` — attached as
|
|
`cast-X.Y.Z.tgz`. That asset is what the installer's release channels
|
|
download: the build happens once, in CI, never on an operator's machine.
|
|
*Manual fallback and backfill:* push a bare `X.Y.Z` tag on the merge
|
|
commit yourself — the same workflow runs the same asserts, build, and
|
|
publish from the tag.
|
|
3. **Right after the release, a follow-up PR bumps `package.json` to
|
|
`X.Y.(Z+1)-dev`** (and `package-lock.json` with it) — box#90's step of
|
|
the family ritual. Installs are versioned by the tree's `package.json`
|
|
version, so a `CAST_REF=main` install between releases must land as
|
|
`versions/X.Y.(Z+1)-dev`, never as `versions/X.Y.Z` — main's tree must
|
|
not impersonate the release it merely descends from.
|
|
|
|
## Labels — who sets what
|
|
|
|
The full taxonomy lives in [LABELS.md](LABELS.md). What matters day to day is
|
|
who sets each kind — most of it is machinery, and hand-moving a
|
|
machine-owned label just gets corrected on the next pass:
|
|
|
|
| Labels | Set by |
|
|
|---|---|
|
|
| `state:*` | the labels workflow ([.github/workflows/labels.yml](.github/workflows/labels.yml)) — recomputed from GitHub's own facts every 15 minutes and on PR events. Never by hand. |
|
|
| `stale` | the same workflow — 48h without commits, comments, or reviews. `blocked` PRs are exempt: they are quiet legitimately. |
|
|
| `scope:*` on PRs | actions/labeler, from the changed paths ([.github/labeler.yml](.github/labeler.yml)). Additive — you may add more, the machine won't remove them. |
|
|
| `scope:*` on issues | you, when opening or triaging — issues have no paths to derive from. |
|
|
| `blocked`, `release` | you — automation never guesses intent. |
|
|
| `bug` / `enhancement` / `documentation` | you, on issues only — a PR's type already lives in its title. |
|
|
|
|
## Issues
|
|
|
|
Give issues the same care as PR titles: say the surface in the title, apply a
|
|
`scope:` label and a type label (`bug` / `enhancement` / `documentation`) when
|
|
you open one, and `blocked` when it waits on something — that is what keeps
|
|
the board navigable as the issue count grows.
|