mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
* fix(release-cut,ship-queue): the weekly release accepts a train-gated commit, and five lane frictions seen in operation scripts/release-cut.ts gates a main commit either by a successful backstop check run or by a green/merging/merged train record in refs/notes/gstack-trains whose merged[] names its sha or tree (CANDIDATE: ... gated by train <nonce>); a red check, a running run, the required check and the freeze are judged as before. --required-check, --backstop-check and --notes-ref make the names configurable. lib/issue-links.ts reads (name #N[, #M]), (name PR #N) and (name issue #N) as cross-repo references. lib/gstack-post.ts allowlists an 11-digit run id after a run word or chained after one, and pr-body merges by ## section into the PR's current body (--replace overwrites). gstack-pregate mirrors each record under <state root>/pregate/by-tree/<tree>.json and ready reads the mirror for its tree when the checkout's record is missing or for another tree. docs/troubleshooting.md gains #redact-run-id-as-phone, #gate-error-no-base and #gstack-post-pr-body-merge. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (pass 1) Fence-length-aware section splitting in gstack-post pr-body (an unclosed receipt fence or a four-backtick block no longer exposes a heading to the merge), the run-id allowlist drops generic words (id, job, attempt, workflow), ready accepts a by-tree pregate mirror only from a complete run against origin/<base> under a reviewed policy and prints no home path, release-cut skips non-object notes and never substitutes train evidence for the required check when both names are equal, issue-links treats closing/cf./e.g./v1.2 as words rather than repositories. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (pass 2) An unclosed receipt fence makes the body from that line on one section the merge never matches, so posted leading prose cannot replace it and a closed receipt before it stays a receipt; the by-tree pregate mirror must hold the tests stage (what --stage tests --remote writes) rather than every check; empty sha/tree strings in a train note are not evidence; (v1.2.3-rc1 #301) is a version word, not a repository. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CANDIDATE line names its gate; issue-links reference documents the (name #N) form Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-queue): a PREGATE_ENVIRONMENT local record is final; no mirror stands in for it Merging origin/mainb004c06(#3480) into this branch created a semantic conflict between two individually correct parents: #3480 made verifyPregate return PREGATE_ENVIRONMENT (instead of PREGATE_INCOMPLETE) when every incomplete check was caused by the environment, and this branch's by-tree mirror reader in ready() treated only PREGATE_LANE_FAILED and PREGATE_INCOMPLETE as final. Merged, a local record whose secrets scan never finished fell through to a same-tree tests-only mirror and ready passed without a secrets scan, against the rule this branch documents ("a record whose own checks failed or stayed incomplete is never overridden"). ready() now keeps a PREGATE_ENVIRONMENT record final as well. Regression in test/gstack-ship-queue.test.ts ("one READY block ..."): an environment-incomplete secrets record beside a passing same-tree mirror is refused with (PREGATE_ENVIRONMENT) and no "from the state root" suffix. Red on the bare merge28b64d7(expected exit 3, received 0), green with this fix. Found by the pass-4 delta review of the merge. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.262.0 stamp Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
163 lines
8.3 KiB
Markdown
163 lines
8.3 KiB
Markdown
# Releases and update channels
|
|
|
|
gstack stamps a version at every merge to main, about a dozen a day. Since
|
|
late October 2026 there is also a weekly tagged release, and team-mode
|
|
installs follow it instead of every merge.
|
|
|
|
## What a release is
|
|
|
|
Every Monday at 07:00 Pacific (06:00 in winter) the **Weekly release**
|
|
workflow (`.github/workflows/weekly-release.yml`, which runs
|
|
`scripts/release-cut.ts`) takes the newest green commit on main and:
|
|
|
|
1. tags it `v<VERSION>` (the VERSION file at that commit, an annotated tag);
|
|
2. publishes a GitHub Release whose notes are one line per CHANGELOG version
|
|
since the previous release, with a compare link and a link to the full
|
|
CHANGELOG at that tag;
|
|
3. fast-forwards the `stable` branch to it.
|
|
|
|
A tag and its release never change. `stable` is a moving pointer: always a
|
|
commit on main's first-parent history, moved only by this workflow, only
|
|
forward.
|
|
|
|
**Green** means, on that exact commit: the `free-tests` check run succeeded
|
|
(the status main requires), the commit was gated (below), every check run on
|
|
it has completed and none failed, and `refs/notes/gstack-trains` holds no
|
|
active freeze. A cancelled check run counts as red. A commit with a check run
|
|
still going is skipped and the next older one is considered, back through 7
|
|
days or 200 first-parent commits. The workflow runs no tests and no paid evals
|
|
of its own; it only reads what the trains and the backstop already recorded.
|
|
With nothing green in the window it fails and lists why each commit was
|
|
skipped.
|
|
|
|
**Gated** means one of two pieces of evidence. The first is the
|
|
`main-backstop` check run: the Main backstop gate workflow publishes it on
|
|
each push to main from a job of its own, success when a green train record
|
|
covers the tree or the backstop gated it green, neutral when main moved past
|
|
the push before it was gated (superseded) or a freeze's own backstop run owns
|
|
it (deferred), failure when it froze the queue or could not finish. The second
|
|
is a train record in `refs/notes/gstack-trains` (kind `train`, state
|
|
`green`, `merging` or `merged`) whose `merged[]` lists the commit's sha or
|
|
its tree: those entries are the main commits `gstack-ship-queue verify
|
|
--train` confirmed, so a commit a green train merged is gated even when trains
|
|
merged faster than the backstop could settle and its own backstop run ended
|
|
neutral or never ran. The `CANDIDATE` line says which: `gated by
|
|
main-backstop` or `gated by train <nonce>`. Train evidence relaxes nothing
|
|
else: a red backstop run, a failed or missing `free-tests` run, any other red
|
|
check run or a run still going keeps the commit out, and a record in any other
|
|
state (`red`, `superseded`, `free-green`, …) is not evidence. A commit with
|
|
neither a successful backstop run nor a train record is not released: a
|
|
dependabot push, a policy without a train block, or a tree that was never
|
|
gated.
|
|
|
|
Another repository runs the same script with its own names:
|
|
`--required-check <name>` (default `free-tests`), `--backstop-check <name>`
|
|
(default `main-backstop`) and `--notes-ref <ref>` (default
|
|
`refs/notes/gstack-trains`). `weekly-release.yml` passes none of them.
|
|
|
|
A cut that stopped half-way (tag and `stable` pushed, release not created) is
|
|
finished by the next run, by cron or by hand: `stable` itself stays a
|
|
candidate until its release exists, and its notes compare against the release
|
|
tag before it.
|
|
|
|
## Channels
|
|
|
|
| Channel | Follows | Who |
|
|
|---|---|---|
|
|
| `stable` | `refs/heads/stable`: the weekly release, plus any hotfix cut off-cycle | team mode (`./setup --team`) by default |
|
|
| `main` | every merge to main | plain `./setup`, contributors, Capy crew machines |
|
|
|
|
The config key is `update_channel` (`bin/gstack-config`):
|
|
|
|
```bash
|
|
gstack-config get update_channel # auto (default) | stable | main
|
|
gstack-config set update_channel main # a team install that wants every merge
|
|
gstack-config set update_channel stable # a solo install that wants weekly releases
|
|
gstack-config unset update_channel # back to auto
|
|
```
|
|
|
|
`auto` resolves to `stable` when `team_mode` is true and to `main` otherwise,
|
|
so existing team installs moved to weekly releases with nothing to migrate and
|
|
contributor installs did not move at all. `GSTACK_UPDATE_CHANNEL=stable|main`
|
|
overrides the key for one command. The resolution lives in one helper,
|
|
`bin/gstack-update-channel.sh`, used by:
|
|
|
|
- `bin/gstack-session-update`, the team-mode session-start hook: it fetches
|
|
the channel ref and fast-forwards to it. Its log
|
|
(`<state root>/analytics/session-update.log`) carries `CHANNEL stable target=<sha>`
|
|
when it follows a release.
|
|
- `bin/gstack-update-check`, the "update available" line in every skill
|
|
preamble: it reads `<channel>/VERSION` and compares with `refs/heads/<channel>`,
|
|
so a stable install is not told every hour that main moved.
|
|
- `bin/gstack-upgrade` and `/gstack-upgrade`: they fast-forward to the channel
|
|
ref (`UPSTREAM_REF=origin/stable` in the skill's output) and say which channel
|
|
they followed.
|
|
|
|
Two edge cases are handled on purpose:
|
|
|
|
- **Before the first release exists** origin has no `stable`. Every caller then
|
|
follows main and says so (`CHANNEL_FALLBACK stable->main` in the hook log,
|
|
`channel stable has no release on origin yet` from `gstack-upgrade`).
|
|
- **An install already past the release** (it followed main until now, or a
|
|
hotfix was cut below it) stays where it is until stable passes it. The
|
|
updater never moves a checkout backwards. A depth-1 clone cannot see that an
|
|
older release is its ancestor, so the VERSIONs decide: main stamps a higher
|
|
version at every merge and stable is always a main commit.
|
|
|
|
The local branch stays `main` either way. A stable install is a `main`
|
|
checkout parked at the last released commit, which is why fast-forward-only
|
|
still holds and nothing about version stamping changed.
|
|
|
|
## Cutting a release by hand (hotfixes)
|
|
|
|
A hotfix is an ordinary PR: it merges to main through a train and gets its
|
|
version like any other change. To put it on stable before Monday, run the
|
|
workflow by hand with the merge commit's SHA:
|
|
|
|
```bash
|
|
gh workflow run weekly-release.yml -f ref=<full sha on main> -f dry_run=false
|
|
```
|
|
|
|
or Actions → **Weekly release** → Run workflow, on main (the cut job runs
|
|
only from `refs/heads/main`, with main's tooling; `ref` must be the full
|
|
40-hex SHA). The same green checks apply. The commit is tagged with its VERSION, the release notes say "hotfix release"
|
|
and list every version since the previous release (everything merged in
|
|
between ships with it), and stable fast-forwards. Team installs pick it up at
|
|
their next hourly check.
|
|
|
|
There is no release branch and no cherry-picking: stable only ever names main
|
|
commits. If main carries a change that must not ship yet, revert it on main
|
|
first, then promote.
|
|
|
|
## Dry runs
|
|
|
|
Run the workflow with `dry_run=true` (the default for a manual run) to see the
|
|
plan without changing anything: the candidate, its VERSION and tag, the stable
|
|
move, how many version lines the notes would carry, every skipped commit with
|
|
its reason, and the exact commands a real cut would run. The scheduled Monday
|
|
run is never a dry run.
|
|
|
|
```bash
|
|
gh workflow run weekly-release.yml -f dry_run=true
|
|
gh run watch
|
|
```
|
|
|
|
The step summary carries the same plan plus the full release notes.
|
|
|
|
A pull request that touches `.github/workflows/weekly-release.yml` or
|
|
`scripts/release-cut.ts` runs the same dry run against main as its own job
|
|
(`Dry run against main`, read-only token), so a change to the release tooling
|
|
is exercised against real check runs before it merges.
|
|
|
|
## Outcomes
|
|
|
|
| Line | Meaning |
|
|
|---|---|
|
|
| `CANDIDATE: <sha> (<date>) gated by main-backstop|train <nonce>` | the commit that will be (or was) released, and which evidence gated it (`[hotfix: --ref]` before `gated by` on a `--ref` cut) |
|
|
| `NOTHING_NEW: origin/main <sha> is already released as v<X>` | nothing merged since the last release; exit 0 |
|
|
| `NO_GREEN_COMMIT: ...` with `SKIPPED:` lines | nothing in the window passed the gate; exit 1 (a dry run exits 0: the skips are its answer) |
|
|
| `REFUSED: non-fast-forward: ...` | the candidate is behind stable; stable only moves forward |
|
|
| `REFUSED: queue frozen since train ...` | the main backstop froze the queue; clear it first (docs/ship-policy.md) |
|
|
| `REFUSED: tag v<X> already names <other sha>` | a version was stamped twice; needs a look before anything is cut |
|
|
| `TAG:`, `STABLE:`, `RELEASE:` | what the cut did; a re-run reports each step as already done and finishes a step an earlier run did not reach |
|