v1.91.118.0 test: held-install recovery fixture no longer assumes the checkout has no local main (#3237)

* test: held-install recovery fixture no longer assumes the checkout has no local main

actions/checkout leaves push runs on a local main branch, so the fixture's
clone already had main and 'checkout --orphan main' failed. Build the root
commit on an orphan held-fixture branch and push HEAD:refs/heads/main; add a
regression covering a source on main and a detached source with no branch.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.118.0 test: held-install recovery fixture no longer assumes the checkout has no local main

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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-17 04:55:37 +00:00
1 parent 82530ac143
commit a6eb647328
5 files changed
+36 -8

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
# Changelog
## [1.91.118.0] - 2026-10-17
**Free Tests is green on pushes to main again.** Since v1.91.106.0, every push to main failed `test/held-install-recovery.test.ts`, while pull request runs passed.
### Fixed
- **The held-install recovery fixture no longer assumes the checkout has no `main` branch.** On a push, `actions/checkout` leaves the checkout on a local `main`, so the fixture's clone already had `main` and `git checkout --orphan main` failed with `a branch named 'main' already exists`. Pull request runs check out a detached merge ref and never hit it. The fixture now builds on its own orphan branch and pushes `HEAD:refs/heads/main`, and a new test builds it from a source on `main` and from a detached source with no branch.
### If you…
- **…write a test fixture that clones this repository:** don't create a branch with a fixed name such as `main` in the clone. Push runs and pull request runs leave different local branches behind.
## [1.91.117.0] - 2026-10-17
**Pregate understands bats repositories and says why a fingerprint failed, `gstack-doctor` lists rescued edits and pasted skill lists, oversized SKILL.md files can no longer grow, and hosts without plan mode drop the plan-mode preamble.** The last PR of the 2026-10-15 fix wave (`docs/designs/FIX_WAVE_2026_10_15.md`, PR 5), built after #3190.
+1 -1
View File
@@ -1 +1 @@
1.91.117.0
1.91.118.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.117.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.118.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.117",
"version": "1.91.118",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+23 -5
View File
@@ -36,15 +36,16 @@ const CONFLICT_LINE = 'THROTTLE_SECONDS=3600 # 1 hour';
* install sits one commit behind it, at the same tree without
* bin/gstack-ff-update (an install that predates it), with VERSION 1.91.101.0.
*/
function makeFixture() {
function makeFixture(source = ROOT) {
const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-held-')));
bases.push(base);
const origin = path.join(base, 'origin.git');
const work = path.join(base, 'work');
execFileSync('git', ['clone', '-q', '--shared', ROOT, work], { timeout: 60_000 });
execFileSync('git', ['clone', '-q', '--shared', source, work], { timeout: 60_000 });
// A root commit of this tree: the checkout may be shallow, and a bare
// origin refuses a shallow push.
git(work, 'checkout', '-q', '--orphan', 'main');
// origin refuses a shallow push. The clone already has a local main when
// the source is on main (actions/checkout runs `checkout -B main` on a push).
git(work, 'checkout', '-q', '--orphan', 'held-fixture');
fs.writeFileSync(path.join(work, 'VERSION'), '1.91.101.0\n');
fs.rmSync(path.join(work, 'bin', 'gstack-ff-update'), { force: true });
git(work, 'add', '-A');
@@ -59,7 +60,7 @@ function makeFixture() {
git(work, 'commit', '-q', '-m', 'the fixed release');
const fixed = git(work, 'rev-parse', 'HEAD');
execFileSync('git', ['init', '-q', '--bare', '-b', 'main', origin], { timeout: 60_000 });
git(work, 'push', '-q', origin, 'main');
git(work, 'push', '-q', origin, 'HEAD:refs/heads/main');
return { base, origin, held, fixed };
}
type Fx = ReturnType<typeof makeFixture>;
@@ -126,6 +127,23 @@ describe.skipIf(process.platform === 'win32')('held-install recovery (Git Bash f
expect(form('recovery-powershell')).toContain('d=$(mktemp -d)');
});
for (const checkout of ['on main', 'detached with no branch'] as const) {
test(`the fixture builds from a source checkout ${checkout}`, () => {
const src = path.join(fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-held-src-'))), 'src');
bases.push(path.dirname(src));
execFileSync('git', ['clone', '-q', '--shared', ROOT, src], { timeout: 60_000 });
if (checkout === 'on main') git(src, 'checkout', '-q', '-B', 'main');
else {
git(src, 'checkout', '-q', '--detach');
for (const b of git(src, 'for-each-ref', '--format=%(refname:short)', 'refs/heads').split('\n').filter(Boolean)) git(src, 'branch', '-q', '-D', b);
}
const fx = makeFixture(src);
expect(git(fx.origin, 'rev-parse', 'refs/heads/main')).toBe(fx.fixed);
expect(git(fx.origin, 'rev-parse', `${fx.fixed}^`)).toBe(fx.held);
expect(git(fx.origin, 'rev-list', '--max-parents=0', fx.fixed)).toBe(fx.held);
}, 60_000);
}
for (const state of ['clean', 'dirty', 'unmerged'] as const) {
test(`a ${state} prefix-mode install reaches the fixed release; a planted symlink is never followed`, async () => {
const fx = makeFixture();