mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +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>
426 lines
24 KiB
Cheetah
426 lines
24 KiB
Cheetah
---
|
|
name: gstack-upgrade
|
|
version: 1.1.0
|
|
description: |
|
|
Upgrade gstack to the latest version. Detects global vs vendored install,
|
|
runs the upgrade, and shows what's new. Use when asked to "upgrade gstack",
|
|
"update gstack", or "get latest version".
|
|
voice-triggers:
|
|
- "upgrade the tools"
|
|
- "update the tools"
|
|
- "gee stack upgrade"
|
|
- "g stack upgrade"
|
|
triggers:
|
|
- upgrade gstack
|
|
- update gstack version
|
|
- get latest gstack
|
|
allowed-tools:
|
|
- Bash
|
|
- Read
|
|
- Write
|
|
- AskUserQuestion
|
|
---
|
|
|
|
# /gstack-upgrade
|
|
|
|
Upgrade gstack to the latest version and show what's new.
|
|
|
|
## Inline upgrade flow
|
|
|
|
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
|
|
|
### Step 1: Ask the user (or auto-upgrade)
|
|
|
|
First, check if auto-upgrade is enabled:
|
|
```bash
|
|
_AUTO=""
|
|
[ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
|
|
[ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true)
|
|
echo "AUTO_UPGRADE=$_AUTO"
|
|
```
|
|
|
|
**If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. On setup failure, follow Step 4's install-specific recovery: vendored installs restore their backup; git installs stop with the pre-upgrade commit recorded, without a destructive reset. Never claim restoration unless it actually succeeded.
|
|
|
|
**Otherwise**, use AskUserQuestion:
|
|
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now?"
|
|
Four fields (`UPGRADE_AVAILABLE <old> <old>+<n> <sha>`, main moved): ask "gstack main is **{n} commit(s)** past v{old} (at {sha}). Upgrade now?"
|
|
- Options: ["Yes, upgrade now", "Always keep me up to date", "Not now", "Never ask again"]
|
|
|
|
**If "Yes, upgrade now":** Proceed to Step 2.
|
|
|
|
**If "Always keep me up to date":**
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-config set auto_upgrade true
|
|
```
|
|
Tell user: "Auto-upgrade enabled. Future updates will install automatically." Then proceed to Step 2.
|
|
|
|
**If "Not now":** Write snooze state with escalating backoff (first snooze = 24h, second = 48h, third+ = 1 week), then continue with the current skill. Do not mention the upgrade again.
|
|
```bash
|
|
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
|
_SNOOZE_FILE="$GSTACK_STATE_ROOT/update-snoozed"
|
|
_REMOTE_VER="{new}"
|
|
_CUR_LEVEL=0
|
|
if [ -f "$_SNOOZE_FILE" ]; then
|
|
_SNOOZED_VER=$(awk '{print $(1)}' "$_SNOOZE_FILE")
|
|
if [ "$_SNOOZED_VER" = "$_REMOTE_VER" ]; then
|
|
_CUR_LEVEL=$(awk '{print $(2)}' "$_SNOOZE_FILE")
|
|
case "$_CUR_LEVEL" in *[!0-9]*) _CUR_LEVEL=0 ;; esac
|
|
fi
|
|
fi
|
|
_NEW_LEVEL=$((_CUR_LEVEL + 1))
|
|
[ "$_NEW_LEVEL" -gt 3 ] && _NEW_LEVEL=3
|
|
echo "$_REMOTE_VER $_NEW_LEVEL $(date +%s)" > "$_SNOOZE_FILE"
|
|
```
|
|
Note: `{new}` is the remote version from the `UPGRADE_AVAILABLE` output — substitute it from the update check result. On a four-field line use the SHA (fourth field) as `{new}`, so the snooze ends when main moves.
|
|
|
|
Tell user the snooze duration: "Next reminder in 24h" (or 48h or 1 week, depending on level). Tip: "Set `auto_upgrade: true` in `~/.gstack/config.yaml` for automatic upgrades."
|
|
|
|
**If "Never ask again":**
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-config set update_check false
|
|
```
|
|
Tell user: "Update checks disabled. Run `~/.claude/skills/gstack/bin/gstack-config set update_check true` to re-enable."
|
|
Continue with the current skill.
|
|
|
|
### Step 2: Detect install type
|
|
|
|
```bash
|
|
if [ -d "$HOME/.claude/skills/gstack/.git" ]; then
|
|
INSTALL_TYPE="global-git"
|
|
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
|
elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then
|
|
INSTALL_TYPE="global-git"
|
|
INSTALL_DIR="$HOME/.gstack/repos/gstack"
|
|
elif [ -d ".claude/skills/gstack/.git" ]; then
|
|
INSTALL_TYPE="local-git"
|
|
INSTALL_DIR=".claude/skills/gstack"
|
|
elif [ -d ".agents/skills/gstack/.git" ]; then
|
|
INSTALL_TYPE="local-git"
|
|
INSTALL_DIR=".agents/skills/gstack"
|
|
elif [ -d ".claude/skills/gstack" ]; then
|
|
INSTALL_TYPE="vendored"
|
|
INSTALL_DIR=".claude/skills/gstack"
|
|
elif [ -d "$HOME/.claude/skills/gstack" ]; then
|
|
INSTALL_TYPE="vendored-global"
|
|
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
|
elif _SRC=$({{BIN_DIR}}/gstack-paths --get GSTACK_STATE_ROOT 2>/dev/null) && _SRC=$(awk -F '\t' '$(1) == "{{HOST_ID}}" && $(6) != "-" { print $(6); exit }' "$_SRC/installs.tsv" 2>/dev/null) && [ -d "$_SRC/.git" ]; then
|
|
# The install registry names the checkout setup activated for this host (e.g. a ~/gstack clone).
|
|
INSTALL_TYPE="global-git"
|
|
INSTALL_DIR="$_SRC"
|
|
else
|
|
echo "ERROR: gstack not found"
|
|
exit 1
|
|
fi
|
|
INSTALL_DIR=$(cd -- "$INSTALL_DIR" && pwd -P) || { echo "ERROR: cannot enter the gstack install directory" >&2; exit 1; }
|
|
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
|
|
```
|
|
|
|
The install type and the absolute directory printed above are used in all subsequent steps.
|
|
Carry `INSTALL_TYPE`, `INSTALL_DIR`, `OLD_VERSION`, and later `UPSTREAM_REF` and `NEW_VERSION` forward explicitly: tool calls may run in fresh shells, so start each later block by assigning them from the captured output (for example `INSTALL_DIR=/abs/path/printed/above`). Do not rely on a prior call's working directory or shell variables. Every block that changes files first checks `INSTALL_DIR` and stops before any git command when it is unset, empty, missing, unreadable, or not gstack's own checkout.
|
|
|
|
### Step 3: Save old version
|
|
|
|
Use the install directory from Step 2's output below:
|
|
|
|
```bash
|
|
OLD_VERSION=$(cat "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/VERSION" 2>/dev/null || echo "unknown")
|
|
echo "OLD_VERSION=$OLD_VERSION"
|
|
```
|
|
|
|
### Step 4: Upgrade
|
|
|
|
Use the install type and directory detected in Step 2:
|
|
|
|
**For git installs** (global-git, local-git):
|
|
|
|
Fast-forward first, through `bin/gstack-ff-update`, the same code the
|
|
session-update auto-upgrade uses: local edits that merge cleanly ride over the
|
|
update, and an edit that conflicts (or a staged change, or an unmerged file)
|
|
is saved in a rescue ref instead of being left as conflict markers:
|
|
```bash
|
|
cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1
|
|
{ [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ] && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $INSTALL_DIR is not a gstack checkout; nothing was changed. Re-run Step 2." >&2; exit 1; }
|
|
git fetch origin
|
|
# Channel ref (docs/releases.md): origin/stable for a team install, else origin/main
|
|
_CH=$(bash bin/gstack-update-channel.sh --fetch-ref 2>&1) || _CH=origin/main
|
|
echo "$_CH"; UPSTREAM_REF=$(printf '%s\n' "$_CH" | tail -1)
|
|
echo "UPSTREAM_REF=${UPSTREAM_REF:=origin/main}"
|
|
# Same pre-advance check as the auto-updater: the incoming release's Bun floor,
|
|
# judged by bash whatever this tool shell is (zsh does not word-split).
|
|
HOLD=$(UPSTREAM_REF="$UPSTREAM_REF" bash -c '. bin/gstack-bun-version.sh && gstack_bun_incoming_hold . "$UPSTREAM_REF"' 2>/dev/null) && { case "$HOLD" in bun-version-unreadable*) echo "BUN_VERSION_UNREADABLE: $HOLD; nothing was changed" >&2 ;; *) echo "BUN_TOO_OLD: $HOLD; nothing was changed" >&2 ;; esac; exit 1; }
|
|
PRE_UPGRADE_COMMIT=$(git rev-parse HEAD)
|
|
echo "PRE_UPGRADE_COMMIT=$PRE_UPGRADE_COMMIT"
|
|
# Past the release (by ancestry, or by VERSION in a depth-1 clone): wait for stable; nothing moves back.
|
|
if [ "$UPSTREAM_REF" = origin/stable ] && [ "$(git rev-parse origin/stable)" != "$PRE_UPGRADE_COMMIT" ] && ! git merge-base --is-ancestor HEAD origin/stable 2>/dev/null \
|
|
&& { git merge-base --is-ancestor origin/stable HEAD 2>/dev/null || [ "$(printf '%s\n%s\n' "$(git show origin/stable:VERSION)" "$(cat VERSION)" | sort -V | tail -1)" = "$(cat VERSION)" ]; }; then echo "AHEAD_OF_STABLE"; exit 0; fi
|
|
FF=$(bash bin/gstack-ff-update . "$UPSTREAM_REF"); FF_RC=$?
|
|
echo "$FF"
|
|
case "$FF_RC" in
|
|
0) if {{SETUP_COMMAND}} --refresh-registered; then echo "FF_OK"; else echo "SETUP_FAILED: git update succeeded; stop and inspect setup output (previous commit: $PRE_UPGRADE_COMMIT)" >&2; exit 1; fi ;;
|
|
4) echo "FF_REFUSED" ;;
|
|
*) echo "FF_STOPPED: $FF" >&2; exit 1 ;;
|
|
esac
|
|
```
|
|
|
|
Setup's `--refresh-registered` refreshes every install this checkout
|
|
registered (each host, global and this project's), prints the source first,
|
|
and ends with an `Upgrade summary` of one row per install. Relay those rows
|
|
as printed. A `failed` row means that host kept its previous install; give
|
|
its retry command and never say every host was refreshed. A `skipped` row
|
|
belongs to another checkout or project; pass on its command, do not run it.
|
|
|
|
If the output ends with `FF_OK`, the upgrade is done — skip the fallback
|
|
below entirely. When the line before it reads
|
|
`rescued <old> <new> refs/gstack/rescue/<oid> unmerged=<0|1>`, tell the user
|
|
their conflicting local edits were saved there and relay the `Inspect`,
|
|
`Export` and `Delete` commands the script printed (export into a scratch
|
|
checkout with `git apply -3`, never `stash apply` in the install;
|
|
`unmerged=1` means the rescue needs manual resolution).
|
|
On `FF_STOPPED`, STOP and relay the reason: nothing was changed.
|
|
On `AHEAD_OF_STABLE`, STOP: the checkout is already past the weekly release, so nothing changes until the next cut (`gstack-config set update_channel main` follows every merge). On a `fetching origin/stable failed` line above, STOP and relay it; nothing was changed.
|
|
`ff-busy` means another gstack update is running (retry in a minute);
|
|
`autostash-unrecoverable` names what is in the way (an untracked file the
|
|
release adds, or another git command); `index-unreadable` means the checkout's
|
|
index is damaged.
|
|
On `BUN_TOO_OLD`, STOP: tell the user to run `bun upgrade`, then /gstack-upgrade again.
|
|
On `BUN_VERSION_UNREADABLE`, STOP: `bun --version` did not print a version gstack can read, so `bun upgrade` may not run either; tell the user to reinstall Bun from https://bun.sh (`curl -fsSL https://bun.sh/install | bash` in Git Bash or a macOS/Linux terminal), check that `bun --version` prints a version such as 1.4.2, then run /gstack-upgrade again (docs/troubleshooting.md#bun-version-unreadable).
|
|
On `SETUP_FAILED`, STOP; keep user changes and report the recovery commit. There is no `.bak` on the git path. Do not enter the divergence fallback merely because setup failed. Enter it only on `FF_REFUSED` (local commits or a diverged history); network/auth failures stop for repair, not reset.
|
|
|
|
**Fallback (ff-only refused — local commits or divergence).** `git reset
|
|
--hard` DESTROYS things: a clean tree with unpushed local commits still loses
|
|
those commits. Gate it:
|
|
|
|
1. Run `git status --porcelain` and `git rev-list "${UPSTREAM_REF:?}"..HEAD --oneline`
|
|
in `$INSTALL_DIR`.
|
|
2. If BOTH are empty, the reset is provably safe — run the fallback block
|
|
below without asking.
|
|
3. Otherwise ask via AskUserQuestion (one-way door — destructive), listing
|
|
exactly what will be discarded: each dirty file and each unpushed commit
|
|
by hash + subject. Options: **A)** Discard them and upgrade (reset) —
|
|
requires the explicit letter; **B)** Abort the upgrade so the user can
|
|
rescue their work first (recommended when local commits exist). Never
|
|
proceed on a vague reply.
|
|
|
|
```bash
|
|
cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1
|
|
{ [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ] && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $INSTALL_DIR is not a gstack checkout; nothing was changed. Re-run Step 2." >&2; exit 1; }
|
|
# Regenerable render-footprint dirt (setup re-renders it) never goes into the stash.
|
|
git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' 2>/dev/null || true
|
|
STASH_OUTPUT=$(git stash 2>&1)
|
|
git reset --hard "${UPSTREAM_REF:?UPSTREAM_REF is not set: substitute the value Step 4 printed}"
|
|
{{SETUP_COMMAND}} --refresh-registered
|
|
```
|
|
If `$STASH_OUTPUT` contains "Saved working directory", warn the user: "Note: local changes were stashed (any modified generated SKILL.md/sections files were discarded first — they regenerate on setup). Run `git stash pop` in the skill directory to restore your own changes."
|
|
|
|
**For vendored installs** (vendored, vendored-global):
|
|
```bash
|
|
(cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: INSTALL_DIR=${INSTALL_DIR:-} is not a gstack install; nothing was changed. Re-run Step 2." >&2; exit 1; }
|
|
# A stale .bak from a previously crashed upgrade would make the mv below NEST
|
|
# the live install inside it and the failure-restore arm would "restore" the
|
|
# stale backup. It may also be the only good copy from that crashed run —
|
|
# abort and let the human inspect, never delete it silently.
|
|
[ -e "$INSTALL_DIR.bak" ] && { echo "ERROR: stale backup exists at $INSTALL_DIR.bak (from a previous failed upgrade?) — inspect it, salvage/remove it, then re-run." >&2; exit 1; }
|
|
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX") || { echo "ERROR: mktemp failed — aborting upgrade (install untouched)." >&2; exit 1; }
|
|
git clone --depth 1 https://github.com/garrytan/gstack.git "$TMP_DIR/gstack" || { echo "ERROR: clone failed — aborting upgrade (install untouched)." >&2; rm -rf "$TMP_DIR"; exit 1; }
|
|
mv "$INSTALL_DIR" "$INSTALL_DIR.bak" || { rm -rf "$TMP_DIR"; exit 1; }
|
|
if mv "$TMP_DIR/gstack" "$INSTALL_DIR"; then
|
|
if (cd "$INSTALL_DIR" && {{SETUP_COMMAND}} --refresh-registered); then
|
|
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
|
else
|
|
rm -rf "$INSTALL_DIR"
|
|
mv "$INSTALL_DIR.bak" "$INSTALL_DIR" || { echo "ERROR: restore failed; backup retained." >&2; exit 1; }
|
|
rm -rf "$TMP_DIR"
|
|
echo "ERROR: setup failed; previous install restored." >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
mv "$INSTALL_DIR.bak" "$INSTALL_DIR"
|
|
echo "ERROR: swap failed — previous install restored; upgrade aborted." >&2
|
|
rm -rf "$TMP_DIR"
|
|
exit 1
|
|
fi
|
|
```
|
|
|
|
### Step 4.5: Handle local vendored copy
|
|
|
|
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
|
|
|
|
```bash
|
|
_RESOLVED_PRIMARY=$(cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && pwd -P) || exit 1
|
|
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
LOCAL_GSTACK=""
|
|
if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
|
|
_RESOLVED_LOCAL=$(cd "$_ROOT/.claude/skills/gstack" && pwd -P)
|
|
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
|
|
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
|
fi
|
|
fi
|
|
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
|
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
|
echo "TEAM_MODE=$_TEAM_MODE"
|
|
```
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
|
|
|
|
```bash
|
|
(cd -- "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the detection block above and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: LOCAL_GSTACK=${LOCAL_GSTACK:-} is not a gstack copy; nothing was changed." >&2; exit 1; }
|
|
_ROOT=${LOCAL_GSTACK%/.claude/skills/gstack}
|
|
{ [ "$_ROOT" != "$LOCAL_GSTACK" ] && cd -- "${_ROOT:?}" && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $LOCAL_GSTACK is not the vendored copy at the root of a git repository; nothing was changed." >&2; exit 1; }
|
|
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
|
|
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
|
|
echo '.claude/skills/gstack/' >> .gitignore
|
|
fi
|
|
rm -rf "$LOCAL_GSTACK"
|
|
```
|
|
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
|
```bash
|
|
(cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: INSTALL_DIR=${INSTALL_DIR:-} is not a gstack install; nothing was changed." >&2; exit 1; }
|
|
(cd -- "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the detection block above and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: LOCAL_GSTACK=${LOCAL_GSTACK:-} is not a gstack copy; nothing was changed." >&2; exit 1; }
|
|
[ -e "$LOCAL_GSTACK.bak" ] && { echo "ERROR: stale vendored backup; inspect it before retrying." >&2; exit 1; }
|
|
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak" || exit 1
|
|
if cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK" && rm -rf "$LOCAL_GSTACK/.git" && (cd "$LOCAL_GSTACK" && {{SETUP_COMMAND}} --refresh-registered); then
|
|
rm -rf "$LOCAL_GSTACK.bak"
|
|
echo "LOCAL_SYNC_OK"
|
|
else
|
|
rm -rf "$LOCAL_GSTACK"
|
|
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK" || { echo "ERROR: restore failed; backup retained." >&2; exit 1; }
|
|
echo "ERROR: sync failed; previous vendored copy restored." >&2
|
|
exit 1
|
|
fi
|
|
```
|
|
Only on `LOCAL_SYNC_OK`, tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready." Otherwise stop and report the recovery outcome; do not continue migrations or announce success.
|
|
|
|
### Step 4.75: Run version migrations
|
|
|
|
After `./setup`, run the migrations in `gstack-upgrade/migrations/` (idempotent
|
|
`v{VERSION}.sh` scripts; CONTRIBUTING.md says how to add one) newer than the old
|
|
version: they fix state `./setup` alone cannot (stale config, orphaned files).
|
|
|
|
```bash
|
|
MIGRATIONS_DIR="${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/gstack-upgrade/migrations"
|
|
: "${OLD_VERSION:?OLD_VERSION is not set: substitute the value Step 3 printed}"
|
|
if [ -d "$MIGRATIONS_DIR" ]; then
|
|
for migration in $(find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V); do
|
|
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
|
|
if [ "$OLD_VERSION" != "unknown" ] && [ "$(printf '%s\n%s' "$OLD_VERSION" "$m_ver" | sort -V | head -1)" = "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$m_ver" ]; then
|
|
echo "Running migration $m_ver..."
|
|
# Without GSTACK_INSTALL_DIR a migration that cleans the install assumes ~/.claude/skills/gstack.
|
|
GSTACK_INSTALL_DIR="$INSTALL_DIR" bash "$migration" || echo " Warning: migration $m_ver had errors (non-fatal)"
|
|
fi
|
|
done
|
|
fi
|
|
```
|
|
|
|
### Step 4.8: Stop any stale daemon (unconditional)
|
|
|
|
A browse daemon started before the upgrade keeps running the old binary (the
|
|
reset and `./setup` do not stop it), so always run this step with the install
|
|
directory from Step 2.
|
|
|
|
```bash
|
|
INSTALL_DIR_PLACEHOLDER="<install dir from Step 2>"
|
|
NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "")
|
|
_STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}"
|
|
if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then
|
|
echo "DAEMON_CHECK=none (no state file or no fresh build hash)"
|
|
else
|
|
DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
|
|
DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
|
|
OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1)
|
|
if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then
|
|
echo "DAEMON_CHECK=dead (no live daemon to stop)"
|
|
elif [ "$OLD_HASH" = "$NEW_HASH" ]; then
|
|
echo "DAEMON_CHECK=current (daemon already runs the new binary)"
|
|
elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then
|
|
echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
|
|
"$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes"
|
|
else
|
|
echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
|
|
fi
|
|
fi
|
|
```
|
|
|
|
Substitute the install directory from Step 2, then act on `DAEMON_CHECK`:
|
|
|
|
1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the
|
|
old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command
|
|
starts a fresh daemon on the new binary."
|
|
2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to
|
|
it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon
|
|
is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is
|
|
busy right now. When it finishes, stop it with `browse stop` — or force it
|
|
immediately with `browse --force-restart stop` (loses that session's
|
|
tabs/cookies/logins)."
|
|
3. **`none` / `dead` / `current`:** say nothing.
|
|
|
|
### Step 5: Write marker + clear cache
|
|
|
|
```bash
|
|
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
|
mkdir -p "$GSTACK_STATE_ROOT"
|
|
echo "$OLD_VERSION" > "$GSTACK_STATE_ROOT"/just-upgraded-from
|
|
rm -f "$GSTACK_STATE_ROOT"/last-update-check
|
|
rm -f "$GSTACK_STATE_ROOT"/update-snoozed
|
|
```
|
|
|
|
### Step 6: Show What's New
|
|
|
|
Read `$INSTALL_DIR/CHANGELOG.md`. Find all version entries between the old version and the new version. Summarize as 5-7 bullets grouped by theme, user-facing changes first.
|
|
|
|
Four fields: summarize `git -C "$INSTALL_DIR" log --oneline <PRE_UPGRADE_COMMIT>..HEAD` (≤7 bullets) as "gstack v{old}, main at {sha}".
|
|
|
|
Format:
|
|
```
|
|
gstack v{new} — upgraded from v{old}!
|
|
|
|
What's new:
|
|
- [bullet 1]
|
|
- [bullet 2]
|
|
- ...
|
|
|
|
Happy shipping!
|
|
```
|
|
|
|
### Step 7: Continue
|
|
|
|
Continue with the skill the user originally invoked; the upgrade is done.
|
|
|
|
---
|
|
|
|
## Standalone usage
|
|
|
|
When invoked directly as `/gstack-upgrade` (not from a preamble):
|
|
|
|
1. Force a fresh update check (bypass cache):
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
|
|
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
|
|
```
|
|
|
|
2. If `UPGRADE_AVAILABLE` (three or four fields): follow Steps 2-6 above.
|
|
|
|
If `CHECK_FAILED ...`: the remote version could not be read, so the update
|
|
status is unknown. Show the line (it names the URL that failed) and tell the
|
|
user the check failed; never report "already on the latest version". Stop.
|
|
|
|
3. If no output (primary is up to date): check for a stale local vendored copy.
|
|
|
|
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
|
|
|
|
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
|
|
```bash
|
|
PRIMARY_VER=$(cat "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/VERSION" 2>/dev/null || echo "unknown")
|
|
LOCAL_VER=$(cat "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the Step 4.5 detection block}/VERSION" 2>/dev/null || echo "unknown")
|
|
echo "PRIMARY=$PRIMARY_VER LOCAL=$LOCAL_VER"
|
|
```
|
|
|
|
**If versions differ:** follow the Step 4.5 sync bash block above to update the local copy from the primary. Tell user: "Global v{PRIMARY_VER} is up to date. Updated local vendored copy from v{LOCAL_VER} → v{PRIMARY_VER}. Commit `.claude/skills/gstack/` when you're ready."
|
|
|
|
**If versions match:** tell the user "You're on the latest version (v{PRIMARY_VER}). Global and local vendored copy are both up to date."
|