Files
gstack/bin/gstack-update-channel.sh
T
Garry Tanandcapy-ai[bot] 75badf306c v1.91.234.0 feat(releases): weekly tagged releases and a stable update channel for team mode (GSTA-98) (#3429)
* 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>
2026-10-26 07:22:44 +00:00

89 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-update-channel.sh — which branch of garrytan/gstack an install follows.
#
# Sourced by bin/gstack-session-update, bin/gstack-update-check and
# bin/gstack-upgrade; runnable by the /gstack-upgrade skill. Bash 3.2 plus git.
#
# Channels (docs/releases.md):
# main every merge to main (a version per merge, a dozen a day)
# stable the weekly tagged release: refs/heads/stable on origin, moved only
# forward along main's first-parent history by the release workflow
#
# The config key update_channel (bin/gstack-config) is auto | stable | main.
# auto, the default, is stable when team_mode is true and main otherwise, so a
# team install follows releases and a contributor install follows main with
# nothing to migrate. GSTACK_UPDATE_CHANNEL overrides the config (tests, a
# one-off command). Any other value counts as auto.
#
# Functions (never fail; a broken config means main, the behaviour before
# channels existed):
# gstack_update_channel <install dir> prints stable | main
# gstack_update_channel_refspec <channel> prints the fetch refspec for
# stable, "" for main (callers
# keep their @{upstream} /
# default-branch path)
# gstack_update_channel_missing <git stderr> true when a fetch failed only
# because origin has no such ref
# (no release cut yet): callers
# fall back to main
#
# CLI (the /gstack-upgrade skill): `bash bin/gstack-update-channel.sh [--fetch-ref] [<install dir>]`
# prints the channel; --fetch-ref fetches the channel ref from origin and
# prints the revision to fast-forward to (origin/stable or origin/main),
# falling back to origin/main only when origin has no stable yet. Any other
# fetch failure keeps origin/stable (the fast-forward then stops or finds
# nothing new); a team install never moves to main because the network
# blinked.
gstack_update_channel() {
_uc_dir=${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}
_uc_value=${GSTACK_UPDATE_CHANNEL:-}
if [ -z "$_uc_value" ] && [ -x "$_uc_dir/bin/gstack-config" ]; then
_uc_value=$("$_uc_dir/bin/gstack-config" get update_channel 2>/dev/null) || _uc_value=""
fi
case "$_uc_value" in
stable|main) printf '%s\n' "$_uc_value"; return 0 ;;
esac
_uc_team=""
[ -x "$_uc_dir/bin/gstack-config" ] && { _uc_team=$("$_uc_dir/bin/gstack-config" get team_mode 2>/dev/null) || _uc_team=""; }
if [ "$_uc_team" = true ]; then printf 'stable\n'; else printf 'main\n'; fi
}
gstack_update_channel_refspec() {
case "${1:-}" in stable) printf '+refs/heads/stable:refs/remotes/origin/stable\n' ;; *) printf '\n' ;; esac
}
gstack_update_channel_missing() {
printf '%s' "${1:-}" | grep -qiE "couldn't find remote ref|no such ref was fetched|invalid refspec" 2>/dev/null
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
_uc_fetch=0 _uc_arg=""
for _uc_a in "$@"; do
case "$_uc_a" in
--fetch-ref) _uc_fetch=1 ;;
-h|--help) sed -n '2,36p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) _uc_arg=$_uc_a ;;
esac
done
_uc_root=${_uc_arg:-$(cd "$(dirname "$0")/.." && pwd)}
_uc_ch=$(gstack_update_channel "$_uc_root")
if [ "$_uc_fetch" -eq 0 ]; then printf '%s\n' "$_uc_ch"; exit 0; fi
# Egress receipts for the fetches below, fail-open like /gstack-upgrade's own.
. "$(dirname "$0")/gstack-egress-lib.sh" 2>/dev/null || _receipted_git() { shift 5; "$@"; }
_uc_host=$(git -C "$_uc_root" remote get-url origin 2>/dev/null); _uc_host=${_uc_host#*://}; _uc_host=${_uc_host#*@}; _uc_host=${_uc_host%%[/:]*}
export GIT_TERMINAL_PROMPT=0
if [ "$_uc_ch" = stable ]; then
_uc_err=$(_receipted_git open gstack-upgrade "${_uc_host:-unknown}" gstack-self-update-pull "user ran /gstack-upgrade" git -C "$_uc_root" fetch -q origin "$(gstack_update_channel_refspec stable)" 2>&1 >/dev/null) \
&& { printf 'origin/stable\n'; exit 0; }
if gstack_update_channel_missing "$_uc_err"; then
echo "gstack-update-channel: origin has no stable release yet; following origin/main until the first weekly release is cut" >&2
else
echo "gstack-update-channel: fetching origin/stable failed ($(printf '%s' "$_uc_err" | head -1 | head -c 200)); staying on origin/stable as last fetched" >&2
printf 'origin/stable\n'; exit 0
fi
fi
_receipted_git open gstack-upgrade "${_uc_host:-unknown}" gstack-self-update-pull "user ran /gstack-upgrade" git -C "$_uc_root" fetch -q origin 2>/dev/null || true
printf 'origin/main\n'
fi