mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-07 01:15:47 +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
@@ -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