mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
E1: /gstack-upgrade checks the incoming Bun floor before pulling
gstack_bun_incoming_hold (bin/gstack-bun-version.sh) is now the one pre-advance check: the auto-updater calls it before its fast-forward and /gstack-upgrade's git path calls it after fetching. Below the incoming release's floor the upgrade stops with BUN_TOO_OLD and the checkout, VERSION and installs stay untouched. Vendored installs were already safe: setup refuses before writing and the swap restores the previous install.
This commit is contained in:
1 parent
ee8736f656
commit
10ab8559b9
4 files changed
+93
-9
No files matched your search
@@ -1,8 +1,9 @@
|
||||
# shellcheck shell=bash
|
||||
# gstack-bun-version.sh — the one source for the Bun versions gstack needs.
|
||||
# Sourced, never executed: ./setup refuses or warns from it before writing
|
||||
# anything, and bin/gstack-session-update reads the INCOMING release's copy
|
||||
# (git show) before advancing a live checkout. test/bun-version-drift.test.ts
|
||||
# anything, and bin/gstack-session-update and /gstack-upgrade read the
|
||||
# INCOMING release's floor (gstack_bun_incoming_hold) before advancing a
|
||||
# live checkout. test/bun-version-drift.test.ts
|
||||
# keeps it equal to package.json engines.bun, every CI pin, setup's install
|
||||
# hint and README. Bash 3.2 builtins only.
|
||||
#
|
||||
@@ -56,3 +57,19 @@ gstack_bun_status() {
|
||||
else echo ok
|
||||
fi
|
||||
}
|
||||
|
||||
# gstack_bun_incoming_hold <git-dir> <rev> — the pre-advance check shared by
|
||||
# the auto-updater and /gstack-upgrade. Reads the floor from <rev>'s copy of
|
||||
# this file and checks the `bun` setup will run (first on PATH). Below that
|
||||
# floor it prints the held reason and returns 0; otherwise it prints nothing
|
||||
# and returns 1 (no bun, no floor in <rev>, or an unparseable version, which
|
||||
# setup only warns on). The comparison rule is this release's.
|
||||
gstack_bun_incoming_hold() {
|
||||
command -v bun >/dev/null 2>&1 || return 1
|
||||
_gb_floor=$(git -C "$1" show "$2:bin/gstack-bun-version.sh" 2>/dev/null | sed -n 's/^GSTACK_BUN_FLOOR="\([0-9A-Za-z.+-]*\)".*/\1/p' | head -1)
|
||||
[ -n "$_gb_floor" ] || return 1
|
||||
_gb_found=$(bun --version 2>/dev/null | head -1)
|
||||
[ "$(gstack_bun_status "$_gb_found" "$_gb_floor")" = too-old ] || return 1
|
||||
_gb_ver=$(git -C "$1" show "$2:VERSION" 2>/dev/null | head -1)
|
||||
echo "bun-too-old: found Bun $_gb_found at $(command -v bun); gstack ${_gb_ver:-update} needs $_gb_floor or newer"
|
||||
}
|
||||
@@ -192,13 +192,8 @@ fi
|
||||
if [ "$PULL_EXIT" -eq 0 ]; then
|
||||
INCOMING=$(git -C "$GSTACK_DIR" rev-parse --verify '@{upstream}^{commit}' 2>"${PULL_ERR_FILE:-/dev/null}") || PULL_EXIT=1
|
||||
fi
|
||||
if [ "$PULL_EXIT" -eq 0 ] && [ "$INCOMING" != "$OLD_HEAD" ] && command -v bun >/dev/null 2>&1; then
|
||||
_IN_FLOOR=$(git -C "$GSTACK_DIR" show "$INCOMING:bin/gstack-bun-version.sh" 2>/dev/null | sed -n 's/^GSTACK_BUN_FLOOR="\([0-9A-Za-z.+-]*\)".*/\1/p' | head -1)
|
||||
_BUN_FOUND=$(bun --version 2>/dev/null | head -1)
|
||||
if [ -n "$_IN_FLOOR" ] && [ "$(gstack_bun_status "$_BUN_FOUND" "$_IN_FLOOR" 2>/dev/null)" = too-old ]; then
|
||||
_IN_VERSION=$(git -C "$GSTACK_DIR" show "$INCOMING:VERSION" 2>/dev/null | head -1)
|
||||
BUN_HELD="bun-too-old: found Bun $_BUN_FOUND at $(command -v bun); gstack ${_IN_VERSION:-update} needs $_IN_FLOOR or newer"
|
||||
fi
|
||||
if [ "$PULL_EXIT" -eq 0 ] && [ "$INCOMING" != "$OLD_HEAD" ]; then
|
||||
BUN_HELD=$(gstack_bun_incoming_hold "$GSTACK_DIR" "$INCOMING" 2>/dev/null) || BUN_HELD=""
|
||||
fi
|
||||
if [ "$PULL_EXIT" -eq 0 ] && [ -z "$BUN_HELD" ]; then
|
||||
git -C "$GSTACK_DIR" merge --ff-only --autostash -q "$INCOMING" >/dev/null 2>"${PULL_ERR_FILE:-/dev/null}"
|
||||
|
||||
@@ -143,6 +143,8 @@ cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the p
|
||||
# re-renders to ~/.gstack/render), so discarding is lossless.
|
||||
git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' 2>/dev/null || true
|
||||
git fetch origin
|
||||
# Same pre-advance check as the auto-updater: the incoming release's Bun floor.
|
||||
HOLD=$(. bin/gstack-bun-version.sh 2>/dev/null && gstack_bun_incoming_hold . origin/main) && { echo "BUN_TOO_OLD: $HOLD; nothing was changed" >&2; exit 1; }
|
||||
PRE_UPGRADE_COMMIT=$(git rev-parse HEAD)
|
||||
echo "PRE_UPGRADE_COMMIT=$PRE_UPGRADE_COMMIT"
|
||||
if git pull --ff-only --autostash origin main; then
|
||||
@@ -161,6 +163,7 @@ 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.
|
||||
On `BUN_TOO_OLD`, STOP: tell the user to run `bun upgrade`, then /gstack-upgrade again.
|
||||
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`, after inspecting the pull error; network/auth failures stop for repair, not reset.
|
||||
|
||||
**Fallback (ff-only refused — local commits or divergence).** `git reset
|
||||
|
||||
@@ -68,3 +68,72 @@ describe.skipIf(process.platform === 'win32')('upgrade setup recovery (real shel
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// /gstack-upgrade checks the incoming release's Bun floor before pulling, with
|
||||
// the auto-updater's helper: below it the checkout and installs stay untouched.
|
||||
describe.skipIf(process.platform === 'win32')('upgrade Bun floor (real git)', () => {
|
||||
const git = (cwd: string, ...args: string[]) =>
|
||||
spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 30_000 }).stdout.trim();
|
||||
|
||||
function fixture(floor: string, bunVersion: string) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'upgrade-bun-floor-'));
|
||||
const origin = join(root, 'origin.git');
|
||||
const seed = join(root, 'seed');
|
||||
const install = join(root, 'install');
|
||||
const stub = join(root, 'stub');
|
||||
for (const dir of [join(seed, 'bin'), stub]) mkdirSync(dir, { recursive: true });
|
||||
spawnSync('git', ['init', '-q', '--bare', '-b', 'main', origin], { timeout: 30_000 });
|
||||
const helper = readFileSync(join(import.meta.dir, '../bin/gstack-bun-version.sh'), 'utf8');
|
||||
writeFileSync(join(seed, 'bin', 'gstack-bun-version.sh'), helper);
|
||||
writeFileSync(join(seed, 'bin', 'gstack-config'), '#!/bin/sh\n', { mode: 0o755 });
|
||||
writeFileSync(join(seed, 'VERSION'), '1.0.0\n');
|
||||
writeFileSync(join(seed, 'setup'), '#!/bin/sh\necho ran >> "$SETUP_LOG"\n', { mode: 0o755 });
|
||||
git(seed, 'init', '-q', '-b', 'main');
|
||||
git(seed, 'add', '-A');
|
||||
git(seed, 'commit', '-qm', 'seed');
|
||||
git(seed, 'remote', 'add', 'origin', origin);
|
||||
git(seed, 'push', '-q', 'origin', 'main');
|
||||
spawnSync('git', ['clone', '-q', origin, install], { timeout: 30_000 });
|
||||
writeFileSync(join(seed, 'bin', 'gstack-bun-version.sh'),
|
||||
helper.replace(/^GSTACK_BUN_FLOOR="[^"]*"/m, `GSTACK_BUN_FLOOR="${floor}"`));
|
||||
writeFileSync(join(seed, 'VERSION'), '1.1.0\n');
|
||||
git(seed, 'commit', '-aqm', 'release 1.1.0');
|
||||
git(seed, 'push', '-q', 'origin', 'main');
|
||||
writeFileSync(join(stub, 'bun'), `#!/bin/sh\necho ${bunVersion}\n`, { mode: 0o755 });
|
||||
const before = git(install, 'rev-parse', 'HEAD');
|
||||
const result = spawnSync('bash', ['-c', blockAfter('**For git installs**')], {
|
||||
cwd: root, encoding: 'utf8', timeout: 30_000,
|
||||
env: { ...process.env, PATH: `${stub}:${process.env.PATH}`, INSTALL_DIR: install, SETUP_LOG: join(root, 'setup.log') },
|
||||
});
|
||||
return { root, install, stub, before, result, after: git(install, 'rev-parse', 'HEAD'),
|
||||
setupRan: existsSync(join(root, 'setup.log')), version: readFileSync(join(install, 'VERSION'), 'utf8') };
|
||||
}
|
||||
|
||||
test('below the incoming floor: stops before the pull with the held reason', () => {
|
||||
const fx = fixture('9.0.0', '1.4.0');
|
||||
try {
|
||||
expect(fx.result.status).toBe(1);
|
||||
expect(fx.result.stderr).toContain(
|
||||
`BUN_TOO_OLD: bun-too-old: found Bun 1.4.0 at ${join(fx.stub, 'bun')}; gstack 1.1.0 needs 9.0.0 or newer; nothing was changed`);
|
||||
expect(fx.result.stdout).not.toContain('FF_OK');
|
||||
expect(fx.after).toBe(fx.before);
|
||||
expect(fx.version).toBe('1.0.0\n');
|
||||
expect(fx.setupRan).toBe(false);
|
||||
} finally {
|
||||
rmSync(fx.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('at or above the incoming floor: the fast-forward and setup run', () => {
|
||||
const fx = fixture('1.3.3', '1.4.0');
|
||||
try {
|
||||
expect(fx.result.status, fx.result.stderr).toBe(0);
|
||||
expect(fx.result.stdout).toContain('FF_OK');
|
||||
expect(fx.after).not.toBe(fx.before);
|
||||
expect(fx.version).toBe('1.1.0\n');
|
||||
expect(fx.setupRan).toBe(true);
|
||||
} finally {
|
||||
rmSync(fx.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user