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
96 changes: 96 additions & 0 deletions .agents/docs/architecture/release-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Release channels

What each GHCR image tag, npm publish and GitHub release means, and which run moves it. The policy
these implement, written for deployers, is `docs/en/5-reference/5.10-versioning.md`; cutting a release
is `.agents/docs/playbooks/cut-a-release.md`. This file is the model both act on.

> **A version tag is immutable, and `latest` and `<major>` move only with a release.** Deployments
> pull `<major>` by default (`RELEASE_CHANNEL` in `.env.template`). Anything that re-pushes a released
> tag changes what every one of them runs on its next pull, so only a run that also creates the
> `v<version>` git tag may push `<version>`, `<major>` or `latest`.

## The decision is `planRelease`, and it reads only the repository

`.github/scripts/release.cjs` exports `planRelease({ ref, tags, version })`. `release.yaml`'s
`configure` job calls it with the pushed or dispatched ref, every `v*` git tag and the root
`package.json` version, and every later job acts on its outputs. It never reads GHCR or npm: a deleted
image or package must not be able to wedge the next release, and the GHCR lookup an earlier version
made did exactly that.

| Output | Meaning |
| --------- | ------------------------------------------------------------------------- |
| `release` | `v<version>` is not a tag yet, so this run publishes it |
| `latest` | `release` on `main`: moves `latest` on GHCR and GitHub, and publishes npm |
| `edge` | the ref is `main` |
| `major` | `<major>` of the version, the image tag every release of that major moves |

**One Release runs at a time, across every ref.** The plan is only as fresh as its tag snapshot, so
`release.yaml` uses one concurrency group with no ref in it and `cancel-in-progress: false`. GitHub
keeps one waiting run per group, so a third run cancels the waiting one. That loses nothing on `main`:
the next push finds the version still untagged and releases it. A full re-run plans again from fresh
tags; `gh run rerun --failed` reuses the stale plan, so it is never used on this workflow.

**A release is the newest of its line.** On `main`, `planRelease` refuses an untagged version below
any tag, so `latest` and npm never move back. On a release branch it refuses one below a tag of the
same major, so a late retry cannot move `<major>` back.

## What each run publishes

| Run | GHCR image tags | npm | GitHub release |
| ------------------------------------------ | --------------------------------------------------- | ---------------- | --------------------------- |
| push to `main`, version already tagged | `edge`, `sha-<7>` | nothing | nothing |
| push to `main`, new version | `edge`, `sha-<7>`, `<version>`, `<major>`, `latest` | every package | `v<version>`, marked latest |
| dispatch on `release/<major>`, new version | `<version>`, `<major>` | skipped, noticed | `v<version>`, not latest |
| dispatch on `release/<major>`, otherwise | — `configure` fails | — | — |
| any other ref | — `configure` fails | — | — |

**npm publishes from `main` only, so every npm publish is a `latest` publish.** There is no other
dist-tag. A release from a branch skips `publish-npm`, and `configure` says so in a notice. The
`release` job's `if` lets it run past that skip but not past a failed publish.

**The GitHub tag goes on the commit the run built.** `Create Release` sets
`target_commitish: ${{ github.sha }}`; without it `softprops/action-gh-release` creates a new tag on
the default branch, which for a release branch is a different commit.

**`edge` builds carry the last version.** `RELEASE_VERSION` must be `<major>.<minor>.<patch>`
(`packages/release-info/AGENTS.md`), so an `edge` image reports the root `package.json` version even
though it is newer code. The `org.opencontainers.image.revision` label is the exact commit.

## Release branches

**A release branch is `release/<major>`, and it exists only for a superseded major.** `planRelease`
refuses a dispatch on `release/<major>` until a higher major is tagged, so a branch release can never
plan `latest`, and refuses a version from another major. It is cut from the last release of that
major once the next major ships from `main`, and released only by
`gh workflow run Release --ref release/<major>`. A push to it publishes nothing. Changes land on it by a
pull request, which `ci.yaml` gates as it gates one to `main`.

**A release branch takes fixes only.** `scripts/increment-version.ts` asks `release.cjs`'s
`isSupersededMajor` whether the current major is superseded; if it is, it offers and accepts a patch
bump only, and refuses a breaking or `feat` commit in range outright. On `main`, once a breaking
commit is in range, it accepts a major bump only.

After a release from a branch, merge the branch back into `main`. That carries its `CHANGELOG.md`
section and makes its tag reachable, so `scripts/changelog.ts` on `main` starts the next range at it.

**What holds a branch back is hosted configuration, not this file.** The `release` environment allows
deployments from `main` only, each npm trusted publisher names `release.yaml` and that environment,
and the `Release tags` ruleset lets only admins move or delete a `v*` tag. GHCR is not covered: any
workflow on any branch can push an image with `GITHUB_TOKEN`.

## A major bump moves two more files

`increment-version.ts` on a major bump rewrites `RELEASE_CHANNEL` in `.env.template` to the new major,
and edits the table between `<!-- support-table:start -->` and `<!-- support-table:end -->` in
`docs/en/5-reference/5.10-versioning.md`: a row for the new major, and the previous major's superseded
month and end of support, 18 months on (`SUPPORT_WINDOW_MONTHS`). It dates both by the month of the
bump, which is the month the release ships only if the PR merges that month.

## Base images

**`main` floats; a release branch pins.** On `main` every Dockerfile builds `FROM node:lts-krypton`,
so each `edge` build and each release from `main` picks up the newest Node 24 patch with no commit. A
release branch replaces that line with the exact version, `FROM node:<major>.<minor>.<patch>`, in a
`fix:` commit before its first bump, so a re-run of an old release builds the same runtime. A Node
security release for a supported major is therefore a `fix:` commit on its branch that moves the pin,
then a patch bump; nothing updates a branch's pin for you.
13 changes: 7 additions & 6 deletions .agents/docs/architecture/testing-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,21 @@ only what it ran against the whole coverage scope, so add `--coverage.thresholds

## A test nothing collects passes silently

Root `vitest.config.ts` collects only `apps/*/vitest.config.ts`, `packages/*/vitest.config.ts` and
`runtime/*/vitest.config.ts`. A test file in a workspace with no config of its own is collected by
Root `vitest.config.ts` collects only `.github/vitest.config.ts`, `apps/*/vitest.config.ts`,
`packages/*/vitest.config.ts` and `runtime/*/vitest.config.ts`. A test file in a workspace with no config of its own is collected by
nothing, reported by nothing, and passes CI green. A green run prints counts, not file names;
`pnpm exec vitest list --filesOnly [--project <name>]` prints every collected file prefixed with its
project. Before trusting a new test, see it listed and watch it fail on purpose.

## Vitest projects

Twenty-one, each named after its directory except `runtime/v1` (`runtime-v1`). Quirks of each live in
Twenty-two, each named after its directory except `runtime/v1` (`runtime-v1`) and `.github` (`github`). Quirks of each live in
that package's `AGENTS.md` §Tests. Three run under happy-dom; the rest run in node, where one file
opts into a DOM with a first-line `// @vitest-environment happy-dom` docblock.

| Project | Package | Notes |
| ------------------------ | --------------------------------- | -------------------------------------------------------- |
| `github` | `.github` | not a workspace; tests `scripts/release.cjs` |
| `api` | `apps/api` | libnest SWC plugin, confined to `apps/api` sources |
| `gateway` | `apps/gateway` | |
| `playground` | `apps/playground` | |
Expand Down Expand Up @@ -117,15 +118,15 @@ counts their functions twice in the merged report.

## What CI gates

`.github/workflows/ci.yaml`, one job, on `pull_request` to `main` and `workflow_dispatch` only — a
push alone runs nothing. In order: `./scripts/generate-env.sh`, `pnpm install --frozen-lockfile`,
`.github/workflows/ci.yaml`, one job, on `pull_request` to `main` or a `release/**` branch and
`workflow_dispatch` only — a push alone runs nothing. In order: `./scripts/generate-env.sh`, `pnpm install --frozen-lockfile`,
`pnpm lint`, `pnpm test:coverage`, Playwright install (chromium + firefox, capped at 10 minutes, after
lint and unit tests so a hung download cannot hide them), `pnpm test:e2e`.

Not gated: knip (`pnpm knip` exists), and formatting — prettier runs only in the `.husky/pre-commit`
hook, which formats rather than verifies.

`.github/workflows/release.yaml` (push to `main`) validates with `pnpm lint` only — **no unit or e2e
`.github/workflows/release.yaml` (push to `main`, dispatch on `release/<major>`) validates with `pnpm lint` only — **no unit or e2e
tests before a release build.** `.github/workflows/commitlint.yaml` (`pull_request` to any branch)
lints the commits the PR adds against `commitlint.config.ts`.

Expand Down
1 change: 1 addition & 0 deletions .agents/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ isn't one.
| The six similarly-named `runtime-*` things, and `vendor/` | `.agents/docs/architecture/runtime-and-vendor.md` |
| CASL, `@RouteAccess`, and group scoping | `.agents/docs/architecture/auth-and-permissions.md` |
| What is tested where, and what CI gates | `.agents/docs/architecture/testing-strategy.md` |
| Image tags, npm, `latest`, release branches and the bump rules | `.agents/docs/architecture/release-channels.md` |

## Step-by-step playbooks

Expand Down
Loading
Loading