mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
fix(ci): bounded Playwright install with one infra retry, PR runs get main's local branch, Bun install default 1.4.4 (GSTA-68)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
00100f1598
commit
f2557581e5
8 files changed
+177
-68
No files matched your search
@@ -238,12 +238,43 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Push runs check out main with `checkout -B main`, so the clone holds a
|
||||
# local main; PR runs are a detached merge ref and train dispatches sit on
|
||||
# train/<nonce>. A fixture that made its own main passed its PR and then
|
||||
# failed every push to main (#3237, run 38999168852). Give every run the
|
||||
# push layout so a PR run catches what the main push would.
|
||||
- name: Match main's local branch layout on PR and dispatch runs
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: git checkout -q -B main
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.4.3
|
||||
bun-version: 1.4.4
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Read the Playwright version for the browser cache key
|
||||
id: playwright
|
||||
run: echo "version=$(node -p 'require("playwright-core/package.json").version')" >> "$GITHUB_OUTPUT"
|
||||
# Keyed on the Playwright version, which fixes the Chromium revision: a
|
||||
# lockfile bump that keeps Playwright reuses the browser, and a Playwright
|
||||
# bump never restores a stale revision.
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: linux-playwright-chromium-${{ steps.playwright.outputs.version }}
|
||||
# The cache restores the browser; --with-deps still runs apt for system
|
||||
# libraries. On 2026-10-21 apt stalled on an archive.ubuntu.com fetch and
|
||||
# held free-suite (12) for the whole 20-minute job timeout (run
|
||||
# 39641712129). Each attempt gets 5 minutes (a warm install takes about
|
||||
# 15 s) and the install alone is retried once. This retries infrastructure,
|
||||
# never a test.
|
||||
- name: Install Playwright Chromium
|
||||
id: playwright_install
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Retry the Playwright Chromium install once
|
||||
if: steps.playwright_install.outcome == 'failure'
|
||||
timeout-minutes: 5
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Configure the bundled Chromium sandbox helper
|
||||
run: |
|
||||
@@ -294,6 +325,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Push runs check out main with `checkout -B main`, so the clone holds a
|
||||
# local main; PR runs are a detached merge ref and train dispatches sit on
|
||||
# train/<nonce>. A fixture that made its own main passed its PR and then
|
||||
# failed every push to main (#3237, run 38999168852). Give every run the
|
||||
# push layout so a PR run catches what the main push would.
|
||||
- name: Match main's local branch layout on PR and dispatch runs
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: git checkout -q -B main
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
@@ -314,16 +354,33 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Read the Playwright version for the browser cache key
|
||||
id: playwright
|
||||
run: echo "version=$(node -p 'require("playwright-core/package.json").version')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Keyed on the Playwright version, which fixes the Chromium revision: a
|
||||
# lockfile bump that keeps Playwright reuses the browser, and a Playwright
|
||||
# bump never restores a stale revision.
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: linux-playwright-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
linux-playwright-
|
||||
key: linux-playwright-chromium-${{ steps.playwright.outputs.version }}
|
||||
|
||||
# Cache restores browser binaries; install is still required for system
|
||||
# deps and is a fast no-op for already-present browsers.
|
||||
# The cache restores the browser; --with-deps still runs apt for system
|
||||
# libraries. On 2026-10-21 apt stalled on an archive.ubuntu.com fetch and
|
||||
# held free-suite (12) for the whole 20-minute job timeout (run
|
||||
# 39641712129). Each attempt gets 5 minutes (a warm install takes about
|
||||
# 15 s) and the install alone is retried once. This retries infrastructure,
|
||||
# never a test.
|
||||
- name: Install Playwright Chromium
|
||||
id: playwright_install
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Retry the Playwright Chromium install once
|
||||
if: steps.playwright_install.outcome == 'failure'
|
||||
timeout-minutes: 5
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Configure the bundled Chromium sandbox helper
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
**A stalled apt mirror can no longer hold a Free Tests shard for 20 minutes, PR runs now see main's branch layout, and gstack's install default moves to Bun 1.4.4.** On 2026-10-21, free-suite (12) in run 39641712129 sat in "Install Playwright Chromium" until the job timeout cancelled it. The browser had already come from cache: `--with-deps` was waiting on apt's fetch of noble-security from archive.ubuntu.com. No tests ran, and a whole merge train had to be rebuilt. Separately, a push to main checks out a local `main` while PR runs check out a detached merge ref, so a fixture that created its own `main` passed its PR and then failed every main push until #3237 (run 38999168852). And Capy images and gbrain now run Bun 1.4.4, which no CI lane tested, while the installer still downloaded 1.4.3.
|
||||
|
||||
### Fixed
|
||||
- **`.github/workflows/free-tests.yml`**: in free-suite and quick-bun-default, the Playwright install gets its own 5-minute timeout (a warm install takes about 15 s), and that install step alone gets one bounded retry, also 5 minutes. This retries infrastructure, never a test: a second failure fails the job. The browser cache is keyed on the Playwright version instead of `bun.lock`, with no prefix fallback, so a lockfile bump keeps the browser and a Playwright bump never restores a stale revision. quick-bun-default, which had no browser cache, now has one.
|
||||
- **`.github/workflows/free-tests.yml`**: free-suite and quick-bun-default run `git checkout -q -B main` right after checkout on every run whose ref is not main (PRs and train dispatches). Their clone then has the same local `main` a push run gets, so a PR run catches what the main push would.
|
||||
- **`bin/gstack-bun-version.sh`**: `GSTACK_BUN_DEFAULT` is 1.4.4, the Bun that Capy images ship, that gbrain pins, and that carries the CVE-2026-111497 fix. quick-bun-default runs the quick tier on it. The 1.4.2 minimum, engines.bun, Dockerfile.ci, every other CI lane and the verified archives (1.4.2, 1.4.3, 1.4.4) are unchanged. `docs/capy-install.md` and `docs/troubleshooting.md` name the new default.
|
||||
|
||||
### Tests
|
||||
- `test/free-tests-workflow-wiring.test.ts` requires the version-keyed cache, the 5-minute install, its single retry as the only `continue-on-error` step in each job, and the local-main step. Both new cases fail against the previous workflow.
|
||||
- `test/gstack-capy-install.test.ts` reads the install default from `GSTACK_BUN_DEFAULT` wherever it meant "the install default", so the next bump doesn't need a literal sweep. `test/bun-resolve.test.ts` moves its gbrain-like project pin from 1.4.3 to 1.4.4, gbrain's real pin.
|
||||
|
||||
### Gate integrity
|
||||
- One gate edit to review: the Playwright install step in the required Free Tests lane is `continue-on-error`, and a retry step that is not marked that way repeats it. Verdict: not weakening. A shard still fails unless one of the two installs succeeds, and every test, threshold, timeout and trial count stays the same. The local-main step strengthens PR runs. No test assertion was removed.
|
||||
|
||||
## [1.91.186.0] - 2026-10-24
|
||||
|
||||
**Approving an autoplan run no longer fails one time in ten because its run id looks like a card number.** Today's daily flaky-fix pass found `test/capy-captain-flow.test.ts` failing once and passing on the serial retry in 7 of the 57 Free Tests runs from 2026-10-21 to 2026-10-23, one of them a push to main (39878113535). Every failure had the same assertion: `gstack-approvals import` refused the approval row with "field ask: pii.cc". The default autoplan run id is `run-YYYYMMDDHHMMSS`, the import writes it into the row's ask and source, and the credit-card rule saw those 14 digits as a card number whenever they passed the Luhn check, which a timestamp does about one time in ten. A real user's approval import failed the same way.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# at or above the minimum, and one CI job runs the quick tier on it.
|
||||
GSTACK_BUN_FLOOR="1.3.3"
|
||||
GSTACK_BUN_TESTED="1.4.2"
|
||||
GSTACK_BUN_DEFAULT="1.4.3"
|
||||
GSTACK_BUN_DEFAULT="1.4.4"
|
||||
|
||||
# gstack_bun_archive_sha256 TARGET [VERSION] — the SHA-256 of
|
||||
# https://github.com/oven-sh/bun/releases/download/bun-v$VERSION/bun-TARGET.zip
|
||||
|
||||
@@ -141,7 +141,7 @@ session holds no lease) and a reclaim keeps the lease. `bin/gstack-lease list` s
|
||||
packageManager, `.tool-versions`, `.bun-version` and CI `bun-version:` lanes
|
||||
for this platform). CI exact pins for this platform are alternatives: a
|
||||
repository may test several Bun versions (gstack's own CI pins 1.4.2 and
|
||||
runs one job on 1.4.3), so a Bun equal to any one of them satisfies CI;
|
||||
runs one job on 1.4.4), so a Bun equal to any one of them satisfies CI;
|
||||
CI ranges and every non-CI pin stay required. One resolver picks the Bun (`gstack_bun_resolve` in
|
||||
`bin/gstack-bun-version.sh`; `resolveBun` in `lib/pregate/runtime.ts` is
|
||||
its TypeScript twin), and the installer, `--check`, the doctor, the bun
|
||||
@@ -160,7 +160,7 @@ session holds no lease) and a reclaim keeps the lease. `bin/gstack-lease list` s
|
||||
exact pin when gstack carries a verified archive for it
|
||||
(`GSTACK_BUN_VERIFIED`: 1.4.2, 1.4.3, 1.4.4); with only CI exact pins, the install
|
||||
default when it is one of them, else the highest verified one; else the
|
||||
install default `GSTACK_BUN_DEFAULT` (1.4.3, above the 1.4.2 minimum; one
|
||||
install default `GSTACK_BUN_DEFAULT` (1.4.4, above the 1.4.2 minimum; one
|
||||
CI job runs the quick free tier on it).
|
||||
`GSTACK_BUN_INSTALL_VERSION=<v>` overrides the default; a version without a
|
||||
verified archive or below the minimum stops the install
|
||||
|
||||
@@ -1590,7 +1590,7 @@ next `--check` passes.
|
||||
### `gstack-capy-install: GSTACK_BUN_INSTALL_VERSION=<v> is not a verified Bun at or above gstack's supported minimum ...; fix: ... (BUN_INSTALL_VERSION_INVALID)`
|
||||
|
||||
**Meaning.** `GSTACK_BUN_INSTALL_VERSION` overrides the Bun the installer
|
||||
downloads when no exact project pin decides (`GSTACK_BUN_DEFAULT`, 1.4.3). It
|
||||
downloads when no exact project pin decides (`GSTACK_BUN_DEFAULT`, 1.4.4). It
|
||||
must be listed in `GSTACK_BUN_VERIFIED` (a SHA-256-verified archive) and sit at
|
||||
or above the supported minimum (`GSTACK_BUN_TESTED`). Nothing was installed.
|
||||
|
||||
|
||||
+15
-15
@@ -3,7 +3,7 @@
|
||||
* bin/gstack-bun-version.sh and `resolveBun` / `launchRuntime` in
|
||||
* lib/pregate/runtime.ts must pick the same binary (by realpath) for the
|
||||
* GBRA-60 layout: the image's old Bun first on PATH, Bun 1.4.2 in
|
||||
* $BUN_INSTALL/bin, Bun 1.4.3 elsewhere, a project pinning 1.4.3 only through
|
||||
* $BUN_INSTALL/bin, Bun 1.4.4 elsewhere, a project pinning 1.4.4 only through
|
||||
* a CI lane, an unpinned project and a directory outside any project. A file
|
||||
* carrying the shim marker is never returned, and a candidate is chosen only
|
||||
* when it satisfies every applicable pin.
|
||||
@@ -41,7 +41,7 @@ function layout(opts: { storeVersions?: string[]; homeVersion?: string } = {}) {
|
||||
const pinned = path.join(base, 'gbrain');
|
||||
fs.mkdirSync(path.join(pinned, '.github/workflows'), { recursive: true });
|
||||
fs.writeFileSync(path.join(pinned, 'package.json'), '{ "name": "gbrain", "engines": { "bun": ">=1.4.0" } }\n');
|
||||
fs.writeFileSync(path.join(pinned, '.github/workflows/ci.yml'), 'jobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: oven-sh/setup-bun@v2\n with:\n bun-version: 1.4.3\n');
|
||||
fs.writeFileSync(path.join(pinned, '.github/workflows/ci.yml'), 'jobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: oven-sh/setup-bun@v2\n with:\n bun-version: 1.4.4\n');
|
||||
const unpinned = path.join(base, 'plain');
|
||||
fs.mkdirSync(unpinned);
|
||||
fs.writeFileSync(path.join(unpinned, 'package.json'), '{ "name": "plain" }\n');
|
||||
@@ -74,24 +74,24 @@ function tsResolve(l: ReturnType<typeof layout>, project: string | null): string
|
||||
}
|
||||
|
||||
describe('one Bun resolution contract, two twins', () => {
|
||||
test('GBRA-60: 1.4.3 in the store, 1.4.2 in $BUN_INSTALL, a CI-lane pin of 1.4.3 — both twins pick the store 1.4.3 for the project and for an unpinned or outside directory', () => {
|
||||
const l = layout({ storeVersions: ['1.4.3'] });
|
||||
const store143 = path.join(bunStore(l.state), '1.4.3/bun');
|
||||
for (const [project, want] of [[l.pinned, `${store143} 1.4.3 pin`], [l.unpinned, `${store143} 1.4.3 default`], [null, `${store143} 1.4.3 default`]] as const) {
|
||||
test('GBRA-60: 1.4.4 in the store, 1.4.2 in $BUN_INSTALL, a CI-lane pin of 1.4.4 — both twins pick the store 1.4.4 for the project and for an unpinned or outside directory', () => {
|
||||
const l = layout({ storeVersions: ['1.4.4'] });
|
||||
const store144 = path.join(bunStore(l.state), '1.4.4/bun');
|
||||
for (const [project, want] of [[l.pinned, `${store144} 1.4.4 pin`], [l.unpinned, `${store144} 1.4.4 default`], [null, `${store144} 1.4.4 default`]] as const) {
|
||||
expect(bashResolve(l, project)).toBe(want);
|
||||
expect(tsResolve(l, project)).toBe(want);
|
||||
}
|
||||
const lanes = launchRuntime(l.pinned, l.env);
|
||||
expect(path.join(lanes.dir!, 'bun')).toBe(store143);
|
||||
expect(path.join(lanes.dir!, 'bun')).toBe(store144);
|
||||
expect(lanes.verdict).toBe('skipped');
|
||||
expect(lanes.label).toContain('install verdict: not checked');
|
||||
});
|
||||
|
||||
test('1.4.2 in $BUN_INSTALL and 1.4.3 in ~/.bun/bin, nothing in the store: the pinned project skips the ineligible 1.4.2; unpinned directories take $BUN_INSTALL first', () => {
|
||||
const l = layout({ homeVersion: '1.4.3' });
|
||||
const home143 = path.join(l.home, '.bun/bin/bun');
|
||||
test('1.4.2 in $BUN_INSTALL and 1.4.4 in ~/.bun/bin, nothing in the store: the pinned project skips the ineligible 1.4.2; unpinned directories take $BUN_INSTALL first', () => {
|
||||
const l = layout({ homeVersion: '1.4.4' });
|
||||
const home144 = path.join(l.home, '.bun/bin/bun');
|
||||
const bi142 = path.join(l.bunInstall, 'bin/bun');
|
||||
for (const [project, want] of [[l.pinned, `${home143} 1.4.3 home`], [l.unpinned, `${bi142} 1.4.2 BUN_INSTALL`], [null, `${bi142} 1.4.2 BUN_INSTALL`]] as const) {
|
||||
for (const [project, want] of [[l.pinned, `${home144} 1.4.4 home`], [l.unpinned, `${bi142} 1.4.2 BUN_INSTALL`], [null, `${bi142} 1.4.2 BUN_INSTALL`]] as const) {
|
||||
expect(bashResolve(l, project)).toBe(want);
|
||||
expect(tsResolve(l, project)).toBe(want);
|
||||
}
|
||||
@@ -106,20 +106,20 @@ describe('one Bun resolution contract, two twins', () => {
|
||||
});
|
||||
|
||||
test('a symlinked candidate resolves to its target realpath in both twins (portable readlink loop, no realpath(1))', () => {
|
||||
const l = layout({ storeVersions: ['1.4.3'] });
|
||||
const l = layout({ storeVersions: ['1.4.4'] });
|
||||
const linkDir = path.join(l.base, 'links');
|
||||
fs.mkdirSync(linkDir);
|
||||
fs.symlinkSync('../home/.gstack/runtimes/bun/1.4.3/bun', path.join(linkDir, 'bun'));
|
||||
fs.symlinkSync('../home/.gstack/runtimes/bun/1.4.4/bun', path.join(linkDir, 'bun'));
|
||||
fs.rmSync(path.join(l.bunInstall, 'bin/bun'));
|
||||
const env = { ...l.env, GSTACK_STATE_ROOT: path.join(l.base, 'elsewhere'), PATH: `${linkDir}:${l.env.PATH}` };
|
||||
const want = `${path.join(bunStore(l.state), '1.4.3/bun')} 1.4.3 PATH`;
|
||||
const want = `${path.join(bunStore(l.state), '1.4.4/bun')} 1.4.4 PATH`;
|
||||
expect(bashResolve({ ...l, env, state: env.GSTACK_STATE_ROOT }, null)).toBe(want);
|
||||
expect(tsResolve({ ...l, env, state: env.GSTACK_STATE_ROOT }, null)).toBe(want);
|
||||
});
|
||||
|
||||
// Integration item 8: the gstack checkout itself binds the 'none' verdict.
|
||||
test('launchRuntime on the gstack checkout reads the none verdict and launches its recorded Bun', () => {
|
||||
const l = layout({ storeVersions: ['1.4.3'] });
|
||||
const l = layout({ storeVersions: ['1.4.4'] });
|
||||
const { installVerdictPath } = require('../lib/install-verdict') as typeof import('../lib/install-verdict');
|
||||
const checkout = fs.realpathSync(ROOT);
|
||||
const file = installVerdictPath(checkout, l.state, null);
|
||||
|
||||
@@ -131,6 +131,41 @@ describe('free-tests workflow wiring', () => {
|
||||
} finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('every Linux Playwright install has a version-keyed browser cache, 5-minute attempts and one retry of the install alone (run 39641712129)', () => {
|
||||
const jobs = (Bun.YAML.parse(source) as any).jobs;
|
||||
const installing = Object.keys(jobs).filter(name => jobs[name].steps.some((step: any) => step.run?.includes('playwright install --with-deps')));
|
||||
expect(installing.sort()).toEqual(['free-suite', 'quick-bun-default']);
|
||||
for (const name of installing) {
|
||||
const steps = jobs[name].steps;
|
||||
const version = steps.findIndex((step: any) => step.id === 'playwright');
|
||||
const cache = steps.findIndex((step: any) => step.with?.path === '~/.cache/ms-playwright');
|
||||
const install = steps.findIndex((step: any) => step.id === 'playwright_install');
|
||||
const retry = steps.findIndex((step: any) => step.if === "steps.playwright_install.outcome == 'failure'");
|
||||
expect(steps[version].run, name).toContain('require("playwright-core/package.json").version');
|
||||
expect(steps[cache].with.key, name).toBe('linux-playwright-chromium-${{ steps.playwright.outputs.version }}');
|
||||
expect(steps[cache].with['restore-keys'], name).toBeUndefined();
|
||||
expect(version, name).toBeLessThan(cache);
|
||||
expect(cache, name).toBeLessThan(install);
|
||||
expect(retry, name).toBe(install + 1);
|
||||
for (const step of [steps[install], steps[retry]]) {
|
||||
expect(step['timeout-minutes'], name).toBe(5);
|
||||
expect(step.run, name).toBe('npx playwright install --with-deps chromium');
|
||||
}
|
||||
expect(steps[install]['continue-on-error'], name).toBe(true);
|
||||
expect(steps.filter((step: any) => step['continue-on-error']), name).toEqual([steps[install]]);
|
||||
}
|
||||
});
|
||||
|
||||
test('test jobs give PR and dispatch runs the local main that push runs get from checkout (#3237)', () => {
|
||||
const jobs = (Bun.YAML.parse(source) as any).jobs;
|
||||
for (const name of ['free-suite', 'quick-bun-default']) {
|
||||
const [checkout, guard] = jobs[name].steps;
|
||||
expect(checkout.uses, name).toStartWith('actions/checkout@');
|
||||
expect(guard.if, name).toBe("github.ref != 'refs/heads/main'");
|
||||
expect(guard.run, name).toBe('git checkout -q -B main');
|
||||
}
|
||||
});
|
||||
|
||||
test('least-privilege token: contents read-only, credentials not persisted', () => {
|
||||
// The job executes PR-controlled code (install lifecycle scripts + the
|
||||
// suite itself). A default-grant GITHUB_TOKEN persisted into .git/config
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
const REPO = path.resolve(import.meta.dir, '..');
|
||||
const BUN_DEFAULT = fs.readFileSync(path.join(REPO, 'bin/gstack-bun-version.sh'), 'utf8').match(/^GSTACK_BUN_DEFAULT="([^"]+)"$/m)![1];
|
||||
const bases: string[] = [];
|
||||
afterEach(() => { for (const b of bases.splice(0)) fs.rmSync(b, { recursive: true, force: true }); });
|
||||
|
||||
@@ -211,17 +212,17 @@ describe('gstack-capy-install', () => {
|
||||
const f = fixture({ bun: '1.3.14' });
|
||||
const zip = path.join(f.base, 'bad.zip');
|
||||
fs.mkdirSync(path.join(f.base, 'z/bun-linux-x64'), { recursive: true });
|
||||
script(path.join(f.base, 'z/bun-linux-x64/bun'), ['echo 1.4.3']);
|
||||
script(path.join(f.base, 'z/bun-linux-x64/bun'), [`echo ${BUN_DEFAULT}`]);
|
||||
expect(sh(`cd "${f.base}/z" && zip -qr "${zip}" bun-linux-x64`).status).toBe(0);
|
||||
const r = run(f, [], { STUB_CURL_ZIP: zip });
|
||||
expect(r.status).toBe(3);
|
||||
expect(r.stderr).toContain('SHA-256 mismatch');
|
||||
expect(r.stderr).toContain('nothing installed');
|
||||
expect(f.calls().filter(l => l.startsWith('curl '))).toHaveLength(1);
|
||||
expect(f.calls()[0]).toContain('https://github.com/oven-sh/bun/releases/download/bun-v1.4.3/bun-linux-x64');
|
||||
expect(f.calls()[0]).toContain(`https://github.com/oven-sh/bun/releases/download/bun-v${BUN_DEFAULT}/bun-linux-x64`);
|
||||
expect(fs.existsSync(path.join(f.home, '.bun/bin/bun'))).toBe(false);
|
||||
expect(fs.existsSync(store(f, '1.4.3'))).toBe(false);
|
||||
expect(fs.readdirSync(path.dirname(store(f, '1.4.3')))).toEqual([]);
|
||||
expect(fs.existsSync(store(f, BUN_DEFAULT))).toBe(false);
|
||||
expect(fs.readdirSync(path.dirname(store(f, BUN_DEFAULT)))).toEqual([]);
|
||||
expect(f.calls().some(l => l.startsWith('setup '))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -230,16 +231,16 @@ describe('gstack-capy-install', () => {
|
||||
const target = sh('grep -q avx2 /proc/cpuinfo && echo linux-x64 || echo linux-x64-baseline').stdout.trim();
|
||||
const zip = path.join(f.base, 'good.zip');
|
||||
fs.mkdirSync(path.join(f.base, `z/bun-${target}`), { recursive: true });
|
||||
script(path.join(f.base, `z/bun-${target}/bun`), ['echo 1.4.3']);
|
||||
script(path.join(f.base, `z/bun-${target}/bun`), [`echo ${BUN_DEFAULT}`]);
|
||||
expect(sh(`cd "${f.base}/z" && zip -qr "${zip}" bun-${target}`).status).toBe(0);
|
||||
trustZip(f.root, '1.4.3', target, zip);
|
||||
trustZip(f.root, BUN_DEFAULT, target, zip);
|
||||
const r = run(f, ['--for', 'autoplan'], { STUB_CURL_ZIP: zip });
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain('installing bun 1.4.3 from its release archive');
|
||||
expect(r.stdout).toContain(`bun 1.4.3 installed at ${store(f, '1.4.3')}/bun`);
|
||||
expect(r.stdout).toContain(`bun 1.4.3 at ${store(f, '1.4.3')}/bun (gstack's install default, from gstack's runtime store;`);
|
||||
expect(r.stdout).toContain(`installing bun ${BUN_DEFAULT} from its release archive`);
|
||||
expect(r.stdout).toContain(`bun ${BUN_DEFAULT} installed at ${store(f, BUN_DEFAULT)}/bun`);
|
||||
expect(r.stdout).toContain(`bun ${BUN_DEFAULT} at ${store(f, BUN_DEFAULT)}/bun (gstack's install default, from gstack's runtime store;`);
|
||||
expect(fs.existsSync(path.join(f.home, '.bun/bin/bun'))).toBe(false);
|
||||
expect(f.calls().filter(l => l.startsWith('env '))).toEqual(Array(2).fill('env GSTACK_SKIP_PLAYWRIGHT= GSTACK_SKIP_FONTS= bun=1.4.3'));
|
||||
expect(f.calls().filter(l => l.startsWith('env '))).toEqual(Array(2).fill(`env GSTACK_SKIP_PLAYWRIGHT= GSTACK_SKIP_FONTS= bun=${BUN_DEFAULT}`));
|
||||
expect(lastLine(r.stdout)).toBe('gstack: ok 1.2.3 for=autoplan');
|
||||
});
|
||||
|
||||
@@ -333,15 +334,15 @@ describe('gstack-capy-install: launch PATH, Bun reuse, CI pins, setup skip, verd
|
||||
|
||||
test.skipIf(process.platform !== 'linux')('after installing Bun the launch line carries its PATH; a second run reuses it (one download) and skips setup on the same HEAD', () => {
|
||||
const f = fixture({ bun: '1.3.14' });
|
||||
const zip = zipBun(f, '1.4.3');
|
||||
const bunDir = store(f, '1.4.3');
|
||||
const zip = zipBun(f, BUN_DEFAULT);
|
||||
const bunDir = store(f, BUN_DEFAULT);
|
||||
const first = run(f, ['--for', 'autoplan'], { STUB_CURL_ZIP: zip });
|
||||
expect(first.status, first.stdout + first.stderr).toBe(0);
|
||||
expect(first.stdout).toContain(`GSTACK_LAUNCH: PATH=${bunDir}:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=${path.join(f.home, '.gstack-state')} GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=`);
|
||||
expect(first.stdout).toContain(`GSTACK_LAUNCH_PATH: ${bunDir}`);
|
||||
const second = run(f, ['--for', 'autoplan'], { STUB_CURL_ZIP: zip });
|
||||
expect(second.status, second.stdout + second.stderr).toBe(0);
|
||||
expect(second.stdout).toContain(`bun 1.4.3 at ${bunDir}/bun`);
|
||||
expect(second.stdout).toContain(`bun ${BUN_DEFAULT} at ${bunDir}/bun`);
|
||||
expect(second.stdout).toContain(`GSTACK_LAUNCH: PATH=${bunDir}:$PATH `);
|
||||
expect(f.calls().filter(l => l.startsWith('curl '))).toHaveLength(1);
|
||||
expect(f.calls().filter(l => l.startsWith('setup '))).toHaveLength(2);
|
||||
@@ -477,19 +478,19 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: one resoluti
|
||||
expectLaunch(outside, `${bi}/bin/bun`);
|
||||
});
|
||||
|
||||
// Integration item 1: gstack's own CI pins 1.4.2 on every lane and 1.4.3 on
|
||||
// the install-default job. They are alternatives, so the real checkout as
|
||||
// --project certifies with either Bun; a repo whose CI pins only 1.4.3 still
|
||||
// refuses 1.4.2.
|
||||
test('the real gstack checkout as --project with a separate global install certifies with ~/.bun 1.4.2 and with 1.4.3; nothing is downloaded', () => {
|
||||
for (const version of ['1.4.2', '1.4.3']) {
|
||||
// Integration item 1: gstack's own CI pins 1.4.2 on every lane and the
|
||||
// install default on the install-default job. They are alternatives, so the
|
||||
// real checkout as --project certifies with either Bun; a repo whose CI pins
|
||||
// only 1.4.3 still refuses 1.4.2.
|
||||
test('the real gstack checkout as --project with a separate global install certifies with ~/.bun 1.4.2 and with the install default; nothing is downloaded', () => {
|
||||
for (const version of ['1.4.2', BUN_DEFAULT]) {
|
||||
const f = fixture({ bun: '1.3.14' });
|
||||
withPins(f);
|
||||
realBun(path.join(f.home, '.bun/bin/bun'), version);
|
||||
const r = run(f, ['--for', 'review', '--project', REPO], {}, f.home);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stderr).not.toContain('RUNTIME_PIN_CONFLICT');
|
||||
expect(r.stdout).toContain(`bun ${version} at ${f.home}/.bun/bin/bun ($BUN_INSTALL/bin; satisfies gstack's supported minimum 1.4.2 and the project's CI pins (alternatives: 1.4.2 1.4.3))`);
|
||||
expect(r.stdout).toContain(`bun ${version} at ${f.home}/.bun/bin/bun ($BUN_INSTALL/bin; satisfies gstack's supported minimum 1.4.2 and the project's CI pins (alternatives: 1.4.2 ${BUN_DEFAULT}))`);
|
||||
expect(f.calls().some(l => l.startsWith('curl '))).toBe(false);
|
||||
expect(verdictFor(f, r)).toMatchObject({ project: REPO, bun_version: version, bun_rule: 'BUN_INSTALL' });
|
||||
expect(run(f, ['--check', '--for', 'review', '--project', REPO], {}, f.home).status).toBe(0);
|
||||
@@ -499,9 +500,9 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: one resoluti
|
||||
test('CI alternatives with nothing installed: the install default is published when it is one of them; a gbrain-like repo (CI 1.4.3 only) refuses ~/.bun 1.4.2 and installs 1.4.3', () => {
|
||||
const f = fixture({ bun: '1.3.14' });
|
||||
withPins(f);
|
||||
const r = run(f, ['--for', 'review', '--project', REPO], { STUB_CURL_ZIP: zipFor(f, '1.4.3') }, f.home);
|
||||
const r = run(f, ['--for', 'review', '--project', REPO], { STUB_CURL_ZIP: zipFor(f, BUN_DEFAULT) }, f.home);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain(`bun 1.4.3 at ${store(f, '1.4.3')}/bun (`);
|
||||
expect(r.stdout).toContain(`bun ${BUN_DEFAULT} at ${store(f, BUN_DEFAULT)}/bun (`);
|
||||
const g = fixture({ bun: '1.3.14' });
|
||||
withPins(g);
|
||||
realBun(path.join(g.home, '.bun/bin/bun'), '1.4.2');
|
||||
@@ -546,13 +547,13 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: one resoluti
|
||||
test('a broken store entry is repaired in place: the version directory is never moved aside, bun is renamed over its predecessor and bunx is published', () => {
|
||||
const f = fixture({ bun: '1.3.14' });
|
||||
withPins(f);
|
||||
const dir = store(f, '1.4.3');
|
||||
const dir = store(f, BUN_DEFAULT);
|
||||
realBun(path.join(dir, 'bun'), '1.4.0');
|
||||
const inode = fs.statSync(dir).ino;
|
||||
const r = run(f, ['--for', 'autoplan'], { BUN_INSTALL: '', STUB_CURL_ZIP: zipFor(f, '1.4.3') }, f.base);
|
||||
const r = run(f, ['--for', 'autoplan'], { BUN_INSTALL: '', STUB_CURL_ZIP: zipFor(f, BUN_DEFAULT) }, f.base);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(fs.statSync(dir).ino).toBe(inode);
|
||||
expect(sh(`"${dir}/bun" --version`).stdout.trim()).toBe('1.4.3');
|
||||
expect(sh(`"${dir}/bun" --version`).stdout.trim()).toBe(BUN_DEFAULT);
|
||||
expect(fs.readlinkSync(path.join(dir, 'bunx'))).toBe('bun');
|
||||
expect(fs.readdirSync(path.dirname(dir)).filter(n => n.startsWith('.tmp'))).toEqual([]);
|
||||
});
|
||||
@@ -588,10 +589,10 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: one resoluti
|
||||
const unset = run(f, ['--check', '--for', 'autoplan'], { BUN_INSTALL: '' });
|
||||
expect(unset.status, unset.stdout + unset.stderr).toBe(0);
|
||||
expectLaunch(unset, `${bi}/bin/bun`);
|
||||
realBun(path.join(store(f, '1.4.3'), 'bun'), '1.4.3');
|
||||
realBun(path.join(store(f, BUN_DEFAULT), 'bun'), BUN_DEFAULT);
|
||||
const newer = run(f, ['--check', '--for', 'autoplan'], { BUN_INSTALL: bi });
|
||||
expect(newer.status, newer.stdout + newer.stderr).toBe(0);
|
||||
expect(newer.stdout).toContain(`note: the recorded bun 1.4.2 at ${bi}/bin/bun stays certified; bun 1.4.3 at ${store(f, '1.4.3')}/bun (default) would now resolve first; run ${f.root}/bin/gstack-capy-install --for autoplan to record the current resolution`);
|
||||
expect(newer.stdout).toContain(`note: the recorded bun 1.4.2 at ${bi}/bin/bun stays certified; bun ${BUN_DEFAULT} at ${store(f, BUN_DEFAULT)}/bun (default) would now resolve first; run ${f.root}/bin/gstack-capy-install --for autoplan to record the current resolution`);
|
||||
expectLaunch(newer, `${bi}/bin/bun`);
|
||||
expect(newer.stdout).not.toContain('BUN_RESOLUTION_DRIFT');
|
||||
expect(run(f, ['--for', 'autoplan'], { BUN_INSTALL: bi }).status).toBe(0);
|
||||
@@ -622,7 +623,7 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: one resoluti
|
||||
expect(f.calls().some(l => l.startsWith('curl ') || l.startsWith('setup '))).toBe(false);
|
||||
const r = run(f, ['--for', 'autoplan'], { GSTACK_BUN_INSTALL_VERSION: '1.4.2', STUB_CURL_ZIP: zipFor(f, '1.4.2') });
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain('install version 1.4.2 from GSTACK_BUN_INSTALL_VERSION (default 1.4.3)');
|
||||
expect(r.stdout).toContain(`install version 1.4.2 from GSTACK_BUN_INSTALL_VERSION (default ${BUN_DEFAULT})`);
|
||||
expect(r.stdout).toContain(`bun 1.4.2 at ${store(f, '1.4.2')}/bun (gstack's install default, from gstack's runtime store;`);
|
||||
});
|
||||
|
||||
@@ -662,8 +663,8 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
const state = path.join(f.base, 'custom-state');
|
||||
const storeDir = path.join(state, 'runtimes/bun');
|
||||
realBun(path.join(storeDir, '1.4.2/bun'), '1.4.2');
|
||||
realBun(path.join(storeDir, '1.4.3/bun'), '1.4.3');
|
||||
const a = repo(f, 'a', '1.4.2'), b = repo(f, 'b', '1.4.3'), c = repo(f, 'c');
|
||||
realBun(path.join(storeDir, `${BUN_DEFAULT}/bun`), BUN_DEFAULT);
|
||||
const a = repo(f, 'a', '1.4.2'), b = repo(f, 'b', BUN_DEFAULT), c = repo(f, 'c');
|
||||
for (const dir of [a, b, c]) {
|
||||
const r = run(f, ['--for', 'review'], { GSTACK_STATE_ROOT: state }, dir);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
@@ -673,14 +674,14 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
expect(shim).toContain(`store='${storeDir}'`);
|
||||
const outside = path.join(f.base, 'outside');
|
||||
fs.mkdirSync(outside);
|
||||
for (const [dir, want] of [[a, '1.4.2'], [b, '1.4.3'], [a, '1.4.2'], [c, '1.4.3'], [outside, '1.4.3'], [b, '1.4.3']] as const) {
|
||||
for (const [dir, want] of [[a, '1.4.2'], [b, BUN_DEFAULT], [a, '1.4.2'], [c, BUN_DEFAULT], [outside, BUN_DEFAULT], [b, BUN_DEFAULT]] as const) {
|
||||
const r = fresh(f, dir, 'bun --version');
|
||||
expect(r.stdout.trim(), `${dir}\n${r.stderr}`).toBe(want);
|
||||
expect(r.stderr).toBe('');
|
||||
}
|
||||
const explained = fresh(f, path.join(a), 'mkdir -p sub && cd sub && bun --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' });
|
||||
expect(explained.stderr).toMatch(new RegExp(`^gstack bun shim: project=${a} record=${state}/install-verdicts/[0-9a-f]{64}/[0-9a-f]{64}\\.json chose=${storeDir}/1\\.4\\.2/bun rule=record\\n$`));
|
||||
expect(fresh(f, outside, 'bun --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' }).stderr).toBe(`gstack bun shim: project=none record=none chose=${storeDir}/1.4.3/bun rule=default\n`);
|
||||
expect(fresh(f, outside, 'bun --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' }).stderr).toBe(`gstack bun shim: project=none record=none chose=${storeDir}/${BUN_DEFAULT}/bun rule=default\n`);
|
||||
const bunx = fresh(f, a, 'bunx --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' });
|
||||
expect(bunx.stderr).toContain(`chose=${storeDir}/1.4.2/bun rule=record`);
|
||||
const unrecordedPinned = repo(f, 'd', '1.4.2');
|
||||
@@ -690,7 +691,7 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
// Integration item 6: an unrecorded, unpinned repo falls through quietly;
|
||||
// only GSTACK_BUN_SHIM_EXPLAIN=1 names the missing record.
|
||||
const e = fresh(f, unrecorded, 'bun --version');
|
||||
expect([e.stdout.trim(), e.stderr]).toEqual(['1.4.3', '']);
|
||||
expect([e.stdout.trim(), e.stderr]).toEqual([BUN_DEFAULT, '']);
|
||||
expect(fresh(f, unrecorded, 'bun --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' }).stderr).toContain(`gstack bun shim: ${unrecorded} has no recorded Bun and no exact pin; run ${f.root}/bin/gstack-capy-install --check here to record one\n`);
|
||||
});
|
||||
|
||||
@@ -700,7 +701,7 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
fs.mkdirSync(path.join(f.home, '.capy/system'), { recursive: true });
|
||||
const state = path.join(f.base, 'state');
|
||||
const storeDir = path.join(state, 'runtimes/bun');
|
||||
realBun(path.join(storeDir, '1.4.3/bun'), '1.4.3');
|
||||
realBun(path.join(storeDir, `${BUN_DEFAULT}/bun`), BUN_DEFAULT);
|
||||
expect(run(f, ['--for', 'autoplan'], { GSTACK_STATE_ROOT: state }, f.base).status).toBe(0);
|
||||
const pwned = path.join(f.base, 'pwned');
|
||||
const payload = (dir: string) => script(path.join(dir, 'bun'), [`touch "${pwned}"`, 'echo payload']);
|
||||
@@ -709,15 +710,15 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
write(path.join(evilRepo, '.bun-version'), '../../../repo/payload\n');
|
||||
expect(fs.existsSync(path.join(storeDir, '../../../repo/payload/bun'))).toBe(true);
|
||||
const t = fresh(f, evilRepo, 'bun --version');
|
||||
expect(t.stdout.trim()).toBe('1.4.3');
|
||||
expect(t.stdout.trim()).toBe(BUN_DEFAULT);
|
||||
expect(t.stderr).toContain(`${evilRepo} pins bun ../../../repo/payload, which is not in gstack's runtime store`);
|
||||
payload(path.join(f.base, 'evil'));
|
||||
fs.symlinkSync(path.join(f.base, 'evil'), path.join(storeDir, '1.4.9'));
|
||||
const escape = fresh(f, repo(f, 'escape', '1.4.9'), 'bun --version');
|
||||
expect(escape.stdout.trim()).toBe('1.4.3');
|
||||
expect(escape.stdout.trim()).toBe(BUN_DEFAULT);
|
||||
write(path.join(storeDir, '1.4.8/bun'), '#!/bin/sh\n# gstack-bun-shim: gstack-owned; delete this file to remove it\necho loop\n', 0o755);
|
||||
const marked = fresh(f, repo(f, 'marked', '1.4.8'), 'bun --version');
|
||||
expect(marked.stdout.trim()).toBe('1.4.3');
|
||||
expect(marked.stdout.trim()).toBe(BUN_DEFAULT);
|
||||
expect(fs.existsSync(pwned)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -732,7 +733,7 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
const r = run(f, ['--for', 'autoplan'], { BUN_INSTALL: '' }, outside);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain(`bun 1.4.2 at ${f.home}/.bun/bin/bun (~/.bun/bin;`);
|
||||
expect(fs.existsSync(path.join(f.home, '.gstack-state/runtimes/bun/1.4.3'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(f.home, `.gstack-state/runtimes/bun/${BUN_DEFAULT}`))).toBe(false);
|
||||
for (const dir of [outside, f.root]) {
|
||||
const e = fresh(f, dir, 'bun --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' });
|
||||
expect(e.stdout.trim(), e.stderr).toBe('1.4.2');
|
||||
@@ -752,13 +753,13 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
const f = fixture({ bun: '1.3.14' });
|
||||
fs.mkdirSync(path.join(f.home, '.capy/system'), { recursive: true });
|
||||
const state = path.join(f.base, "it's state");
|
||||
realBun(path.join(state, 'runtimes/bun/1.4.3/bun'), '1.4.3');
|
||||
realBun(path.join(state, `runtimes/bun/${BUN_DEFAULT}/bun`), BUN_DEFAULT);
|
||||
expect(run(f, ['--for', 'autoplan'], { GSTACK_STATE_ROOT: state }, f.base).status).toBe(0);
|
||||
expect(sh(`sh -n "${f.home}/.local/bin/bun"`).status).toBe(0);
|
||||
const r = fresh(f, f.base, 'bun --version && bunx --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.stdout.trim().split('\n')[0]).toBe('1.4.3');
|
||||
expect(r.stderr.split(`chose=${state}/runtimes/bun/1.4.3/bun rule=record`), r.stderr).toHaveLength(3);
|
||||
expect(r.stdout.trim().split('\n')[0]).toBe(BUN_DEFAULT);
|
||||
expect(r.stderr.split(`chose=${state}/runtimes/bun/${BUN_DEFAULT}/bun rule=record`), r.stderr).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('a foreign ~/.local/bin/bun or a dangling link is never touched; GSTACK_BUN_SHIM=0 writes nothing; a rerun rewrites only gstack\'s own file', () => {
|
||||
@@ -789,8 +790,8 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
test('the shim adds little over the Bun it runs (fixture timing bound)', () => {
|
||||
const f = fixture();
|
||||
fs.mkdirSync(path.join(f.home, '.capy/system'), { recursive: true });
|
||||
const storeBun = path.join(f.home, '.gstack-state/runtimes/bun/1.4.3/bun');
|
||||
realBun(storeBun, '1.4.3');
|
||||
const storeBun = path.join(f.home, `.gstack-state/runtimes/bun/${BUN_DEFAULT}/bun`);
|
||||
realBun(storeBun, BUN_DEFAULT);
|
||||
expect(run(f).status).toBe(0);
|
||||
const time = (cmd: string) => {
|
||||
const samples = Array.from({ length: 7 }, () => { const t = performance.now(); fresh(f, f.base, cmd); return performance.now() - t; }).sort((x, y) => x - y);
|
||||
|
||||
Reference in new issue
Block a user