mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
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>
This commit is contained in:
1 parent
73e278d4a6
commit
964481a42e
7 files changed
+98
-46
No files matched your search
+12
-11
@@ -82,9 +82,9 @@ VERSION_FILE="$GSTACK_DIR/VERSION"
|
||||
. "$GSTACK_DIR/bin/gstack-update-channel.sh" 2>/dev/null || gstack_update_channel() { echo main; }
|
||||
CHANNEL=$(gstack_update_channel "$GSTACK_DIR" 2>/dev/null) || CHANNEL=main
|
||||
case "$CHANNEL" in stable|main) ;; *) CHANNEL=main ;; esac
|
||||
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/$CHANNEL/VERSION}"
|
||||
REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/garrytan/gstack.git}"
|
||||
REMOTE_RAW_BASE="${GSTACK_REMOTE_RAW_BASE:-https://raw.githubusercontent.com/garrytan/gstack}"
|
||||
REMOTE_URL="${GSTACK_REMOTE_URL:-$REMOTE_RAW_BASE/$CHANNEL/VERSION}"
|
||||
REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/garrytan/gstack.git}"
|
||||
COMPARE_API="${GSTACK_COMPARE_API:-https://api.github.com/repos/garrytan/gstack/compare}"
|
||||
|
||||
# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
|
||||
@@ -319,19 +319,20 @@ if [ -z "${GSTACK_REMOTE_URL:-}" ]; then
|
||||
# Disable credential prompts and apply a 5-second low-speed timeout so a
|
||||
# flaky network or captive portal can't hang every skill preamble.
|
||||
_REPO_HOST="${REMOTE_REPO#*://}"; _REPO_HOST="${_REPO_HOST#*@}"; _REPO_HOST="${_REPO_HOST%%[/:]*}"
|
||||
_ls_remote_branch() { # <branch> — the tip line of refs/heads/<branch> on REMOTE_REPO, or nothing
|
||||
_ls_remote_branch() { # <branch> — the tip line of refs/heads/<branch> on REMOTE_REPO; exit 0 and nothing when origin has no such branch
|
||||
GSTACK_HOME="$STATE_DIR" _receipted_git open update-check "${_REPO_HOST:-unknown}" version-ls-remote "update_check!=false" \
|
||||
bash -c 'GIT_TERMINAL_PROMPT=0 GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 \
|
||||
git ls-remote "$1" "refs/heads/$2" 2>/dev/null' _ "$REMOTE_REPO" "$1" || true
|
||||
git ls-remote "$1" "refs/heads/$2" 2>/dev/null' _ "$REMOTE_REPO" "$1"
|
||||
}
|
||||
_LSR_LINE="$(_ls_remote_branch "$CHANNEL")"
|
||||
if [ "$CHANNEL" = stable ] && [ -z "$_LSR_LINE" ]; then
|
||||
# Before the first release origin has no stable: this check follows main,
|
||||
# as bin/gstack-session-update (CHANNEL_FALLBACK) and bin/gstack-upgrade do.
|
||||
_LSR_LINE="$(_ls_remote_branch "$CHANNEL")" || _LSR_LINE=""
|
||||
if [ "$CHANNEL" = stable ] && [ -z "$_LSR_LINE" ] && _ls_remote_branch stable >/dev/null 2>&1; then
|
||||
# Before the first release origin has no stable (ls-remote answered, empty):
|
||||
# this check follows main, as bin/gstack-session-update (CHANNEL_FALLBACK)
|
||||
# and bin/gstack-upgrade do. The cache identity keeps #stable, so the
|
||||
# outcome replays within its TTL like any other.
|
||||
CHANNEL=main
|
||||
REMOTE_URL="https://raw.githubusercontent.com/garrytan/gstack/main/VERSION"
|
||||
UPSTREAM="${UPSTREAM%#stable}"
|
||||
_LSR_LINE="$(_ls_remote_branch main)"
|
||||
REMOTE_URL="$REMOTE_RAW_BASE/main/VERSION"
|
||||
_LSR_LINE="$(_ls_remote_branch main)" || _LSR_LINE=""
|
||||
fi
|
||||
_REMOTE_SHA="$(echo "$_LSR_LINE" | awk '{print $1}')"
|
||||
if echo "$_REMOTE_SHA" | grep -qE '^[0-9a-f]{40}$'; then
|
||||
|
||||
+3
-1
@@ -29,7 +29,9 @@ 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 commit with a check run still going is skipped and the next
|
||||
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
|
||||
|
||||
+14
-15
@@ -135,16 +135,18 @@ is saved in a rescue ref instead of being left as conflict markers:
|
||||
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.
|
||||
UPSTREAM_REF=$(bash bin/gstack-update-channel.sh --fetch-ref 2>/dev/null) || UPSTREAM_REF=origin/main
|
||||
# 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"
|
||||
# A team install past the release waits for stable; nothing moves back.
|
||||
if [ "$UPSTREAM_REF" = origin/stable ] && [ "$(git rev-parse origin/stable)" != "$PRE_UPGRADE_COMMIT" ] && git merge-base --is-ancestor origin/stable HEAD; then echo "AHEAD_OF_STABLE"; exit 0; fi
|
||||
# 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
|
||||
@@ -169,7 +171,7 @@ their conflicting local edits were saved there and relay the `Inspect`,
|
||||
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 `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
|
||||
@@ -307,10 +309,9 @@ fi
|
||||
|
||||
### Step 4.8: Stop any stale daemon (unconditional)
|
||||
|
||||
A browse daemon started before the upgrade keeps serving the OLD binary's code
|
||||
until it is stopped — it survives `git reset --hard` and `./setup` because the
|
||||
running process holds the old executable. Always run this step, using
|
||||
the install directory detected in Step 2.
|
||||
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>"
|
||||
@@ -335,8 +336,7 @@ else
|
||||
fi
|
||||
```
|
||||
|
||||
Replace `<install dir from Step 2>` with the actual install directory before
|
||||
running. Interpret the `DAEMON_CHECK` result:
|
||||
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
|
||||
@@ -347,7 +347,7 @@ running. Interpret the `DAEMON_CHECK` result:
|
||||
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`:** Nothing to do — say nothing.
|
||||
3. **`none` / `dead` / `current`:** say nothing.
|
||||
|
||||
### Step 5: Write marker + clear cache
|
||||
|
||||
@@ -361,7 +361,7 @@ 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. Don't overwhelm — focus on user-facing changes. Skip internal refactors unless they're significant.
|
||||
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}".
|
||||
|
||||
@@ -379,7 +379,7 @@ Happy shipping!
|
||||
|
||||
### Step 7: Continue
|
||||
|
||||
After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
|
||||
Continue with the skill the user originally invoked; the upgrade is done.
|
||||
|
||||
---
|
||||
|
||||
@@ -392,7 +392,6 @@ When invoked directly as `/gstack-upgrade` (not from a preamble):
|
||||
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
|
||||
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
|
||||
```
|
||||
Use the output to determine if an upgrade is available.
|
||||
|
||||
2. If `UPGRADE_AVAILABLE` (three or four fields): follow Steps 2-6 above.
|
||||
|
||||
|
||||
@@ -141,16 +141,18 @@ is saved in a rescue ref instead of being left as conflict markers:
|
||||
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.
|
||||
UPSTREAM_REF=$(bash bin/gstack-update-channel.sh --fetch-ref 2>/dev/null) || UPSTREAM_REF=origin/main
|
||||
# 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"
|
||||
# A team install past the release waits for stable; nothing moves back.
|
||||
if [ "$UPSTREAM_REF" = origin/stable ] && [ "$(git rev-parse origin/stable)" != "$PRE_UPGRADE_COMMIT" ] && git merge-base --is-ancestor origin/stable HEAD; then echo "AHEAD_OF_STABLE"; exit 0; fi
|
||||
# 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
|
||||
@@ -175,7 +177,7 @@ their conflicting local edits were saved there and relay the `Inspect`,
|
||||
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 `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
|
||||
@@ -313,10 +315,9 @@ fi
|
||||
|
||||
### Step 4.8: Stop any stale daemon (unconditional)
|
||||
|
||||
A browse daemon started before the upgrade keeps serving the OLD binary's code
|
||||
until it is stopped — it survives `git reset --hard` and `./setup` because the
|
||||
running process holds the old executable. Always run this step, using
|
||||
the install directory detected in Step 2.
|
||||
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>"
|
||||
@@ -341,8 +342,7 @@ else
|
||||
fi
|
||||
```
|
||||
|
||||
Replace `<install dir from Step 2>` with the actual install directory before
|
||||
running. Interpret the `DAEMON_CHECK` result:
|
||||
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
|
||||
@@ -353,7 +353,7 @@ running. Interpret the `DAEMON_CHECK` result:
|
||||
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`:** Nothing to do — say nothing.
|
||||
3. **`none` / `dead` / `current`:** say nothing.
|
||||
|
||||
### Step 5: Write marker + clear cache
|
||||
|
||||
@@ -367,7 +367,7 @@ 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. Don't overwhelm — focus on user-facing changes. Skip internal refactors unless they're significant.
|
||||
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}".
|
||||
|
||||
@@ -385,7 +385,7 @@ Happy shipping!
|
||||
|
||||
### Step 7: Continue
|
||||
|
||||
After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
|
||||
Continue with the skill the user originally invoked; the upgrade is done.
|
||||
|
||||
---
|
||||
|
||||
@@ -398,7 +398,6 @@ When invoked directly as `/gstack-upgrade` (not from a preamble):
|
||||
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
|
||||
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
|
||||
```
|
||||
Use the output to determine if an upgrade is available.
|
||||
|
||||
2. If `UPGRADE_AVAILABLE` (three or four fields): follow Steps 2-6 above.
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const REQUIRED_CHECK = 'free-tests';
|
||||
export const BACKSTOP_CHECK = 'main-backstop';
|
||||
export const NOTES_REF = 'refs/notes/gstack-trains';
|
||||
const NOTES_LOCAL = 'refs/notes/gstack-trains-release';
|
||||
const RED = new Set(['failure', 'timed_out', 'action_required', 'startup_failure', 'stale']);
|
||||
const RED = new Set(['failure', 'timed_out', 'cancelled', 'action_required', 'startup_failure', 'stale']);
|
||||
export const WINDOW_DAYS = 7;
|
||||
export const WINDOW_COMMITS = 200;
|
||||
export const NOTES_CAP = 60_000;
|
||||
@@ -223,11 +223,13 @@ export function planRelease(o: PlanOptions): PlanResult {
|
||||
let before = prevStable;
|
||||
if (prevStable === sha) {
|
||||
const prior = git(['describe', '--tags', '--abbrev=0', '--match', 'v[0-9]*', '--first-parent', sha + '^']);
|
||||
before = prior.ok && prior.out ? git(['rev-parse', '--verify', '-q', prior.out + '^{commit}']).out || null : null;
|
||||
const resolved = prior.ok && prior.out ? git(['rev-parse', '--verify', '-q', prior.out + '^{commit}']) : null;
|
||||
before = resolved?.ok ? resolved.out : null;
|
||||
}
|
||||
let prevVersion: string | null = null, prevTag: string | null = null;
|
||||
if (before) {
|
||||
prevVersion = git(['show', before + ':VERSION']).out.trim() || null;
|
||||
const shown = git(['show', before + ':VERSION']);
|
||||
prevVersion = shown.ok ? shown.out.trim() || null : null;
|
||||
if (prevVersion && git(['rev-parse', '--verify', '-q', 'refs/tags/v' + prevVersion + '^{commit}']).ok) prevTag = 'v' + prevVersion;
|
||||
} else {
|
||||
const date = git(['show', '-s', '--format=%cI', sha]).out;
|
||||
|
||||
@@ -157,6 +157,10 @@ describe('release-cut: planning', () => {
|
||||
expect(gateVerdict(paged, 'o/r', 'a'.repeat(40))).toEqual({ green: true });
|
||||
const pagedRed = checks([{ check_runs: GREEN }, { check_runs: [{ name: 'windows-free-tests', status: 'completed', conclusion: 'timed_out' }] }]);
|
||||
expect(gateVerdict(pagedRed, 'o/r', 'a'.repeat(40))).toEqual({ green: false, why: 'red check runs: windows-free-tests', pending: false });
|
||||
const cancelled = checks([{ check_runs: [...GREEN, { name: 'quality', status: 'completed', conclusion: 'cancelled' }, { name: 'docs', status: 'completed', conclusion: 'skipped' }] }]);
|
||||
expect(gateVerdict(cancelled, 'o/r', 'a'.repeat(40))).toEqual({ green: false, why: 'red check runs: quality', pending: false });
|
||||
const skipped = checks([{ check_runs: [...GREEN, { name: 'docs', status: 'completed', conclusion: 'skipped' }] }]);
|
||||
expect(gateVerdict(skipped, 'o/r', 'a'.repeat(40))).toEqual({ green: true });
|
||||
});
|
||||
|
||||
test('an active freeze in refs/notes/gstack-trains refuses the cut; a cleared one does not', () => {
|
||||
|
||||
@@ -241,6 +241,40 @@ describe.skipIf(process.platform === 'win32')('gstack-update-channel.sh as the /
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('the /gstack-upgrade Step 4 fence on the stable channel', () => {
|
||||
const fence = (() => {
|
||||
const tmpl = fs.readFileSync(path.join(ROOT, 'gstack-upgrade', 'SKILL.md.tmpl'), 'utf8');
|
||||
const hits = [...tmpl.matchAll(/```bash\n([\s\S]*?)```/g)].map(m => m[1]!).filter(f => f.includes('bash bin/gstack-ff-update . "$UPSTREAM_REF"'));
|
||||
if (hits.length !== 1) throw new Error('expected one Step 4 fence, found ' + hits.length);
|
||||
return hits[0]!.replace(/\{\{SETUP_COMMAND\}\}/g, './setup');
|
||||
})();
|
||||
const run = (w: World) => spawnSync('bash', ['-c', fence], { cwd: w.base, encoding: 'utf8', timeout: 60_000, env: { ...w.env, INSTALL_DIR: w.install } });
|
||||
|
||||
// Value: protects=a team install past the release stops with AHEAD_OF_STABLE and moves nothing, including the README's depth-1 clone where stable's history
|
||||
// never connects to HEAD (ancestry says nothing; the VERSION order decides), and an install behind the release does not take that exit; fails_when=the shortcut
|
||||
// tests ancestry alone (depth-1 installs made since the last cut get FF_REFUSED and an offer to reset backwards) or fires for an install stable is ahead of;
|
||||
// why_new=the template is covered by string pins only; seam=none; no_claim=the FF_OK arm (bin/gstack-ff-update and setup are not part of this world)
|
||||
test('a depth-1 team install past the release prints AHEAD_OF_STABLE and exits 0 untouched; one behind the release goes on to the fast-forward', () => {
|
||||
const ahead = world({ installAt: 'C' });
|
||||
const r = run(ahead);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain('UPSTREAM_REF=origin/stable');
|
||||
expect(r.stdout.trim().split('\n').at(-1)).toBe('AHEAD_OF_STABLE');
|
||||
expect(ahead.head()).toBe(ahead.shas.C);
|
||||
expect(git(ahead.install, 'rev-parse', 'refs/remotes/origin/stable')).toBe(ahead.shas.B);
|
||||
// The depth-1 histories never connect, so ancestry said nothing and the VERSION order decided.
|
||||
expect(spawnSync('git', ['-C', ahead.install, 'merge-base', '--is-ancestor', 'origin/stable', 'HEAD'], { encoding: 'utf8', timeout: 10_000 }).status).toBe(1);
|
||||
expect(fs.existsSync(path.join(ahead.base, 'setup-calls'))).toBe(false);
|
||||
|
||||
const behind = world({ installAt: 'A' });
|
||||
const b = run(behind);
|
||||
expect(b.stdout).toContain('UPSTREAM_REF=origin/stable');
|
||||
expect(b.stdout).not.toContain('AHEAD_OF_STABLE');
|
||||
expect(b.status).not.toBe(0);
|
||||
expect(behind.head()).toBe(behind.shas.A);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('gstack-upgrade on the stable channel', () => {
|
||||
const run = (w: World, args: string[] = []) => spawnSync('bash', [path.join(w.install, 'bin', 'gstack-upgrade'), ...args], { cwd: w.base, encoding: 'utf8', env: w.env, timeout: 60_000 });
|
||||
|
||||
@@ -324,6 +358,17 @@ describe.skipIf(process.platform === 'win32')('gstack-update-check on the stable
|
||||
expect(noRelease.stdout.trim()).toBe('UPGRADE_AVAILABLE 1.0.0 1.0.1');
|
||||
expect(fs.readFileSync(gitLog, 'utf8')).toContain('refs/heads/stable');
|
||||
expect(fs.readFileSync(gitLog, 'utf8')).toContain('refs/heads/main');
|
||||
expect(fs.readFileSync(path.join(state, 'last-update-check'), 'utf8')).toBe('UPGRADE_AVAILABLE 1.0.0 1.0.1\nIDENTITY head=' + local + ' remote=' + main + ' upstream=' + origin + '\n');
|
||||
expect(fs.readFileSync(path.join(state, 'last-update-check'), 'utf8')).toBe('UPGRADE_AVAILABLE 1.0.0 1.0.1\nIDENTITY head=' + local + ' remote=' + main + ' upstream=' + origin + '#stable\n');
|
||||
// The outcome replays from the cache within its TTL: no second pair of ls-remote calls per skill start.
|
||||
fs.writeFileSync(gitLog, '');
|
||||
expect(run({ GSTACK_UPDATE_CHANNEL: 'stable' }).stdout.trim()).toBe('UPGRADE_AVAILABLE 1.0.0 1.0.1');
|
||||
expect(fs.readFileSync(gitLog, 'utf8')).toBe('');
|
||||
// A failed ls-remote (origin unreachable) is not "no stable yet": the check does not flip to main.
|
||||
fs.writeFileSync(gitLog, '');
|
||||
fs.rmSync(path.join(state, 'last-update-check'), { force: true });
|
||||
const unreachable = run({ GSTACK_UPDATE_CHANNEL: 'stable', GSTACK_REMOTE_REPO: path.join(base, 'gone.git') });
|
||||
expect(unreachable.stdout.trim()).toBe('');
|
||||
expect(fs.readFileSync(gitLog, 'utf8')).not.toContain('refs/heads/main');
|
||||
expect(fs.readFileSync(path.join(state, 'last-update-check'), 'utf8')).toMatch(/^CHECK_FAILED 1\.0\.0 /);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user