A-g: run the quick free tier on GSTACK_BUN_DEFAULT in CI; drift test pins it

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-15 00:38:48 +00:00
1 parent d008757e87
commit db9fdbd0e6
3 files changed
+90 -8

No files matched your search

+55 -2
View File
@@ -227,6 +227,57 @@ jobs:
- name: CSO source formatting
run: bun run format:cso:check
# Capy machines install GSTACK_BUN_DEFAULT (bin/gstack-bun-version.sh) when
# no project pin decides, above the 1.4.2 minimum every other lane pins, so
# the quick tier also runs on it. Its pin equals GSTACK_BUN_DEFAULT;
# test/bun-version-drift.test.ts checks this job exists and agrees.
quick-bun-default:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.3
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Configure the bundled Chromium sandbox helper
run: |
set -euo pipefail
chrome=$(bun -e 'import { chromium } from "playwright"; import { realpathSync } from "node:fs"; console.log(realpathSync(chromium.executablePath()))')
case "$chrome" in
"$HOME"/.cache/ms-playwright/chromium-*/chrome-linux*/chrome) ;;
*) echo "Unexpected Chromium installation path" >&2; exit 1 ;;
esac
helper="${chrome%/*}/chrome_sandbox"
installed="${chrome%/*}/chrome-sandbox"
test -f "$helper" && test ! -L "$helper"
sudo install -T -o root -g root -m 4755 "$helper" "$installed"
- name: Install Xvfb + X11 utilities + gate tools
run: sudo apt-get install -y --no-install-recommends xvfb x11-utils poppler-utils fonts-noto-color-emoji zsh
- name: Configure git identity (tests init temp repos)
run: |
git config --global user.email "free-tests-ci@gstack.test"
git config --global user.name "Free Tests CI"
git config --global init.defaultBranch main
git config --global --add safe.directory '*'
- name: Generate host SKILL.md outputs (.agents, .factory)
run: bun run gen:skill-docs --host all
- name: Vendor xterm assets into the extension
run: bun run vendor:xterm
- name: Build server-node bundle (loaded by browse cli imports)
run: bash browse/scripts/build-node-server.sh
- name: Build gate binaries (make-pdf e2e gates)
run: bun run build:gates
- name: Build trusted CSO helper used by free contract tests
run: bun run build:cso
- name: Run the quick free tier on the install-default Bun
run: xvfb-run -a bun run test:quick
free-suite:
needs: free-plan
runs-on: ubicloud-standard-8
@@ -396,14 +447,15 @@ jobs:
# gate is merge-blocking without a separate branch-protection migration.
free-tests:
if: always()
needs: [free-suite, typecheck, cso-macos-launcher, macos-named-regressions, cso-windows-launcher, cso-docker-integration]
needs: [free-suite, typecheck, quick-bun-default, cso-macos-launcher, macos-named-regressions, cso-windows-launcher, cso-docker-integration]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require the free suite, typecheck, macOS regressions, and every CSO platform gate
- name: Require the free suite, typecheck, the quick tier on the default Bun, macOS regressions, and every CSO platform gate
env:
FREE_SUITE_RESULT: ${{ needs.free-suite.result }}
TYPECHECK_RESULT: ${{ needs.typecheck.result }}
QUICK_BUN_DEFAULT_RESULT: ${{ needs.quick-bun-default.result }}
CSO_MACOS_RESULT: ${{ needs.cso-macos-launcher.result }}
MACOS_REGRESSIONS_RESULT: ${{ needs.macos-named-regressions.result }}
CSO_WINDOWS_RESULT: ${{ needs.cso-windows-launcher.result }}
@@ -412,6 +464,7 @@ jobs:
set -eu
test "$FREE_SUITE_RESULT" = success
test "$TYPECHECK_RESULT" = success
test "$QUICK_BUN_DEFAULT_RESULT" = success
test "$CSO_MACOS_RESULT" = success
test "$MACOS_REGRESSIONS_RESULT" = success
test "$CSO_WINDOWS_RESULT" = success
+34 -5
View File
@@ -28,8 +28,13 @@ const WORKFLOWS_DIR = path.join(ROOT, '.github', 'workflows');
interface Pin {
surface: string;
version: string;
/** The workflow job the pin sits in (empty outside a workflow). */
job?: string;
}
/** The one job that runs on GSTACK_BUN_DEFAULT instead of the minimum (plan A-g). */
const DEFAULT_BUN_JOB = 'quick-bun-default';
function collectPins(): Pin[] {
const pins: Pin[] = [];
@@ -37,7 +42,9 @@ function collectPins(): Pin[] {
if (!/\.ya?ml$/.test(name)) continue;
const source = fs.readFileSync(path.join(WORKFLOWS_DIR, name), 'utf-8');
const lines = source.split('\n');
let job = '';
for (let i = 0; i < lines.length; i++) {
job = lines[i].match(/^ {2}([A-Za-z0-9_-]+):\s*$/)?.[1] ?? job;
if (!/uses:\s*oven-sh\/setup-bun@/.test(lines[i])) continue;
// A pinned stanza is `with:` + `bun-version: <v>` within the next few
// lines; an unpinned setup-bun is itself drift (installs latest).
@@ -46,6 +53,7 @@ function collectPins(): Pin[] {
pins.push({
surface: `${name}:${i + 1}`,
version: m ? m[1] : '<unpinned setup-bun — installs latest>',
job,
});
}
}
@@ -69,8 +77,8 @@ function collectPins(): Pin[] {
}
describe('bun version pins', () => {
test('every CI surface pins the same bun version', () => {
const pins = collectPins();
test('every CI surface pins the same bun version (the install-default job aside)', () => {
const pins = collectPins().filter(p => p.job !== DEFAULT_BUN_JOB);
// Sanity: the scan found the known surfaces (a regex rot that finds
// nothing must fail loudly, not vacuously pass).
expect(pins.length).toBeGreaterThanOrEqual(6);
@@ -96,19 +104,40 @@ describe('bun version pins', () => {
});
});
function bunVersionsFile(): { floor: string; tested: string } {
function bunVersionsFile(): { floor: string; tested: string; installDefault: string; verified: string[] } {
const src = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-bun-version.sh'), 'utf-8');
return {
floor: src.match(/^GSTACK_BUN_FLOOR="([^"]+)"$/m)?.[1] ?? '<no GSTACK_BUN_FLOOR>',
tested: src.match(/^GSTACK_BUN_TESTED="([^"]+)"$/m)?.[1] ?? '<no GSTACK_BUN_TESTED>',
installDefault: src.match(/^GSTACK_BUN_DEFAULT="([^"]+)"$/m)?.[1] ?? '<no GSTACK_BUN_DEFAULT>',
verified: (src.match(/^GSTACK_BUN_VERIFIED="([^"]+)"$/m)?.[1] ?? '').split(/\s+/).filter(Boolean),
};
}
// Plan A2/A-g: the Capy install default is separate from the floor. It must be
// a verified archive at or above the minimum, and exactly one CI job (the
// quick free tier) runs on it so the default Bun is never untested.
describe('install default Bun (A2, A-g)', () => {
const { tested, installDefault, verified } = bunVersionsFile();
test('GSTACK_BUN_DEFAULT is a verified archive and not below the supported minimum', () => {
expect(verified).toContain(installDefault);
expect(verified).toContain(tested);
expect(Bun.semver.order(installDefault, tested)).toBeGreaterThanOrEqual(0);
});
test('one CI job runs the quick free tier on GSTACK_BUN_DEFAULT, and the free-tests aggregate requires it', () => {
const onDefault = collectPins().filter(p => p.job === DEFAULT_BUN_JOB);
expect(onDefault.map(p => p.version)).toEqual([installDefault]);
const workflow = Bun.YAML.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'free-tests.yml'), 'utf-8')) as { jobs: Record<string, { needs?: string[]; steps: { run?: string }[] }> };
expect(workflow.jobs[DEFAULT_BUN_JOB].steps.some(step => step.run?.includes('bun run test:quick'))).toBe(true);
expect(workflow.jobs['free-tests'].needs).toContain(DEFAULT_BUN_JOB);
});
});
describe('Bun requirement surfaces agree (E1)', () => {
const { floor, tested } = bunVersionsFile();
test('tested version == every CI pin == engines.bun lower bound; floor sits below it', () => {
const ciVersions = [...new Set(collectPins().map((p) => p.version))];
test('tested version == every CI pin (the install-default job aside) == engines.bun lower bound; floor sits below it', () => {
const ciVersions = [...new Set(collectPins().filter((p) => p.job !== DEFAULT_BUN_JOB).map((p) => p.version))];
expect(ciVersions).toEqual([tested]);
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
expect(pkg.engines?.bun).toBe(`>=${tested}`);
+1 -1
View File
@@ -388,7 +388,7 @@ describe('CSO runtime staging gates', () => {
expect(gate['continue-on-error']).not.toBe(true);
const required = workflow.jobs['free-tests'];
expect(required.if).toBe('always()');
expect(required.needs).toEqual(['free-suite', 'typecheck', 'cso-macos-launcher', 'macos-named-regressions', 'cso-windows-launcher', 'cso-docker-integration']);
expect(required.needs).toEqual(['free-suite', 'typecheck', 'quick-bun-default', 'cso-macos-launcher', 'macos-named-regressions', 'cso-windows-launcher', 'cso-docker-integration']);
expect(required.steps[0].run).toContain('test "$CSO_DOCKER_RESULT" = success');
for (const current of Object.values(workflow.jobs) as any[]) for (const step of current.steps) {
if (step.uses?.startsWith('oven-sh/setup-bun')) expect(step.uses).toBe('oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6');