mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
* feat(releases): weekly tagged releases and a stable update channel for team mode (GSTA-98)
Every Monday morning .github/workflows/weekly-release.yml tags the newest green
commit on main as v<VERSION>, publishes a GitHub Release from the CHANGELOG
headlines and fast-forwards refs/heads/stable to it (scripts/release-cut.ts;
workflow_dispatch with dry_run, and ref for a hotfix). Team-mode installs
follow stable through the new update_channel key (auto: stable when
team_mode, else main) in bin/gstack-session-update, bin/gstack-update-check,
bin/gstack-upgrade and /gstack-upgrade. Per-merge stamping and the merge
train are unchanged. docs/releases.md explains it.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci(weekly-release): check out the dispatched ref so a dry run exercises that branch's script
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci(weekly-release): a PR touching the release tooling runs the dry run against main (read-only); the cut job checks out the dispatched ref
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(gstack-upgrade): no $1 in the rendered skill (Claude Code rewrites $N), trim Step 4 under the size budget, pin the new floor-check literal
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: leave setup untouched (its channel note moves to README and docs/releases.md); the release workflow and script are free-only PR files
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: record the pr-profile free-only classification as non-behavioral (HARNESS_VERSION 6)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(releases): gate the cut on a main-backstop check run, finish half-done cuts, keep team installs on their channel
/ship review fixes (GSTA-98, PR #3429):
- The weekly release read the Main backstop gate run's conclusion, but
scripts/train-backstop.ts exits 0 when main moved past the push before it
was gated (superseded), so a never-gated tree read as green. The backstop
workflow now publishes its verdict on the commit as the `main-backstop`
check run from a code-free checks:write job (success for covered|gated,
neutral for inert|superseded|deferred, failure for frozen|failed), fed by
the new `--outputs` flag; release-cut.ts requires that check run and
free-tests to succeed and holds a commit whose check runs are still going.
- A cut that pushed the tag and stable but failed to create the release can
be finished: stable itself stays a candidate until its release exists, the
notes compare against the release tag behind it, and a dry run with nothing
green exits 0 with the skips named (the real cut still fails).
- weekly-release.yml: the cut job runs only from refs/heads/main, `ref` must
be a full 40-hex SHA, and actions:read is dropped (no workflow-runs call).
- /gstack-upgrade: the destructive fallback requires the carried UPSTREAM_REF
instead of defaulting to origin/main in a fresh shell; a team install past
the release stops with AHEAD_OF_STABLE instead of FF_REFUSED and a reset
backwards. gstack-update-channel.sh --fetch-ref keeps origin/stable on any
fetch failure other than a missing release; dead gstack_update_channel_ref
removed. gstack-update-check follows main before the first release instead
of caching CHECK_FAILED; gstack-upgrade treats a failed ls-remote as a
fetch failure, not an empty answer.
- Tests: release-cut (check-run fixture, partial cut, neutral/pending),
weekly-release-workflow (new), evals-train-workflow and train-backstop
(backstop-check job, --outputs), update-channel (no-release fallback,
offline --fetch-ref), template pins.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(releases): the upgrade skill's ahead-of-stable exit works in depth-1 clones; update-check keeps its cache identity on the pre-release fallback
/ship second-pass review of 73e278d4 (GSTA-98, PR #3429):
- gstack-upgrade/SKILL.md.tmpl: README installs are depth-1 clones, where
origin/stable never connects to HEAD, so the AHEAD_OF_STABLE shortcut now
also fires when HEAD is not behind stable and stable's VERSION is not
higher than the installed one (the order bin/gstack-upgrade uses). The
fence prints the channel helper's warnings instead of discarding them, and
a 'fetching origin/stable failed' line is a STOP. Prose trimmed to stay
under the skill size budget. New behavioral test runs the Step 4 fence on
a depth-1 install past the release (AHEAD_OF_STABLE, nothing moved) and on
one behind it (goes on to the fast-forward).
- bin/gstack-update-check: the no-stable fallback keeps #stable in the
cache identity (so the outcome replays within its TTL instead of two
ls-remote calls per skill start), triggers only when ls-remote answered
with no such branch (a failed ls-remote stays CHECK_FAILED on stable), and
the branch-pinned URL derives from REMOTE_RAW_BASE so tests stay offline.
- scripts/release-cut.ts: a cancelled check run is red; the previous-release
lookups check .ok before using git output. docs/releases.md says which
pushes get no main-backstop check run at all.
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>
145 lines
7.2 KiB
Markdown
145 lines
7.2 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 `main-backstop` check run succeeded, every
|
|
check run on it has completed and none failed, and `refs/notes/gstack-trains`
|
|
holds no active freeze. The Main backstop gate workflow publishes
|
|
`main-backstop` 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. Neutral is never green: a commit whose own tree was never gated is
|
|
not released. A push that gets no backstop run at all (a dependabot push, a
|
|
policy without a train block) has no `main-backstop` check run and is not
|
|
green either; 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.
|
|
|
|
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>)` | the commit that will be (or was) released |
|
|
| `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 |
|