mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
* fix(browse): direct mode launches Chromium with --proxy-server=direct:// so proxy env vars never win silently (#3440 item 8) BROWSE_PROXY_URL=direct only stopped gstack from configuring a proxy; the daemon inherits the environment and the launch passed no proxy switch, so Chromium read HTTPS_PROXY/HTTP_PROXY/ALL_PROXY itself and every goto still went through that proxy while browse reported a direct connection. Every launch path now adds --proxy-server=direct:// when no proxy is configured (chrome-headless-shell ignores --no-proxy-server), the daemon logs the effective proxy mode on launch, and BROWSER.md's proxy section says what Chromium actually gets. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Name the direct-connection switch once (DIRECT_PROXY_ARG in proxy-config.ts) so the launch argument and the daemon log line cannot drift apart. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(browse): force direct:// only where Chromium would pick its own proxy; review fixes Red team (ship Step 9.2): the first cut passed --proxy-server=direct:// on every platform, but Chromium reads HTTPS_PROXY/HTTP_PROXY/ALL_PROXY itself only on Linux. On macOS and Windows it follows the system proxy settings (PAC/WPAD), which GStack Browser.app users behind a corporate proxy depend on, and BROWSE_PROXY_URL cannot name a PAC. forcesDirect(env, platform) now gates the switch: Linux always, elsewhere only with BROWSE_PROXY_URL=direct. The daemon log says what is really ignored per platform, or that the system settings apply. Review pass 2: the CLI e2e asserts on the daemon-log slice its own invocation wrote (the log is append-mode and shared across cases); connectProxy moves to browse/test/helpers/connect-proxy.ts for both e2e files; the CHANGELOG Tests bullets cover the generated cases. Adds the coverage-audit cases (unparseable HTTPS_PROXY; extension, headed and handoff paths under Xvfb). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(browse): assert the direct switch on the launched Chromium command line per platform (#3440 item 8) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Ship review pass 3 (red team, GStack Minder's decision B, 2026-10-26): on macOS and Windows an explicit BROWSE_PROXY_URL=direct is the only thing that makes the daemon bypass the system proxy settings, so explicitDirect() puts it in the daemon config hash, server.ts records it as proxyDirect in the state file, and buildRestartEnv re-emits it on a crash auto-restart. A reused daemon started on the system proxy is now refused (the refusal names BROWSE_PROXY_URL and offers browse stop or unsetting it) instead of silently keeping the system proxy while the CLI reports direct. Linux hashes as before. BROWSER.md says the daemon log records the mode each time the daemon starts, not on every launch. The Linux desktop (GNOME/KDE) system-proxy question is filed as #3450. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(browse): daemon-state and refusal e2e for the explicit direct opt-out; headed env and BROWSER.md contract checks (#3440 item 8) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Ship rerun review pass: proxy-direct-launch-args.test.ts joins the macOS named-regressions job so its omission arm (no switch without the opt-out) runs in CI; serverProxyEnv takes the platform so the state-file predicate is pinned per platform instead of mirroring process.platform; buildHeadedServerEnv's parameter type requires explicitDirect so a narrowed caller cannot drop the opt-out silently; BROWSER.md's precedence item 6 and the reuse sentence say what each platform does (a connect daemon gets the generic flags refusal); CHANGELOG notes the one-time refusal against a pre-release daemon. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Ship adversarial pass: a crash restart re-emits BROWSE_PROXY_URL=direct from the daemon state only when the invocation has no flags, so the env it hands the new daemon never contradicts the config hash it also hands over; an explicit-direct session is refused against a daemon with no recorded config hash, like --proxy and --headed already were; the unparseable-variable hint says that the opt-out also bypasses the system proxy settings on macOS and Windows; BROWSER.md notes that a managed Chromium proxy policy still overrides the switch; CHANGELOG names both files that joined the macOS named-regressions job. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(browse): read scheme-less proxy variables as http:// so the explicit direct launch keeps those sessions proxied (#3440 item 8) A standard proxy variable without a scheme (curl-style host:port or user:pass@host:port) was rejected by browse's parser with a "connects directly" warning while Chromium on Linux read the variable itself, so the session was proxied anyway. With the launch now explicit (--proxy-server=direct://) that environment went genuinely direct. resolveProxy now reads such values as http://, the way curl and Chromium's own environment reader take them; --proxy and BROWSE_PROXY_URL still require the scheme, and a value that does not parse even as http:// still warns and connects directly. Also drops buildRestartEnv's persisted-state re-emit branch, which production never reached (_globalFlags is set before any restart), names the four BrowserManager launch sites in the CHANGELOG and excludes the cookie-import worker, and carries the docs audit's one-line BROWSER.md project-tree note. Decision: GStack Minder (2026-10-26), option A. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes The lenient scheme-less proxy read is now gated on the plain [user:pass@]host[:port] shape (SCHEMELESS_PROXY_RE, trimmed): a scheme'd value with surrounding whitespace, a one-slash typo and a bare number keep the behaviour they had, and a credential that would spill past the userinfo (?, #, /) is not rewritten, so nothing can land outside the redacted part. Credentials that are not valid percent-encoding are a ProxyConfigError for every source instead of a URIError crash. buildRestartEnv's docblock, BROWSER.md and the CHANGELOG describe the parse (port 80 default, macOS/Windows now treat such a value as a configured proxy). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes A backslash is excluded from the scheme-less proxy shape and parseProxyConfig refuses any proxy URL with a path, query or fragment, for every source: WHATWG parsing ends an http authority at a backslash, so a DOMAIN\user login or a / in a password landed in the path, which the redactor does not mask, and was printed in the clear on the start line, in the daemon log and in the state file. Such values now warn (standard variables) or error (--proxy, BROWSE_PROXY_URL) without echoing the credential. Docs updated. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(browse): keep a slash-less scheme spelling, refuse a bare ? or #, and tighten the proxy docs A standard proxy variable whose userinfo opens with a known scheme (https:bob:pw@host:3128, which the URL parser reads as https://) is no longer taken as scheme-less credentials, so the protocol it names is kept. parseProxyConfig refuses a literal ? or # anywhere in the value, not only a non-empty query or fragment. The unparseable-variable test now puts the credential in every looped value, so its no-echo assertion is real; new cases pin the slash-less scheme spelling, the bare ?/#, an env-sourced trailing slash, and that mixed credential sources stay a hard error for the scheme-less spelling as for the http:// one (the cred-mixing refusal predates this branch and its test on main pins it, so it is documented rather than softened). CHANGELOG and BROWSER.md: single backslashes inside code spans, "a path other than a bare /", the socks5 bridge log lines in the effective-mode list, and the character list applies anywhere in the value. Decision: GStack Minder (2026-10-26), option B. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(browse): keep a basic-auth URL out of the scheme-guard comment so the pregate secret scan stays clean Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(browse): pin the mismatch remedy per platform so the Windows subset does not take process.platform proxyMismatchLines defaulted to process.platform in the env-proxy-meets-direct-daemon case, so on the windows-free-tests lane the remedy read 'or unset HTTPS_PROXY' and the assertion failed. The case now passes 'linux' explicitly and pins the darwin/win32 remedy beside it. Test only; source unchanged since1f6c85af. Decision: GStack Minder (2026-10-26), option A. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(browse): the one-slash http:/host:3128 spelling was never a diagnostic; say what the parser does with it The CHANGELOG sentence and the SCHEMELESS_PROXY_RE docblock claimed a one-slash scheme 'keeps its diagnostic'; the URL parser has always read it as http://host:3128/ and browse/test/proxy-env.test.ts:83 pins that. Wording only, found by the Step 14.5 docs audit on1f6c85af. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(browse): percent-encoding advice per scheme, and state the credential-free-path narrowing and the new scheme-less cred-mix refusal BROWSER.md said to percent-encode / \ ? # @ in http(s) credentials; ? and # are refused for every scheme, @ needs no encoding, and only the backslash is http(s)-specific. The CHANGELOG now states the narrowing that a credential-free standard-variable value with a path (http://proxy.corp:3128/pac) used to be read with the path dropped and now warns and goes direct, and that the cred-mix refusal is new for the scheme-less spelling. Wording only. Decision: GStack Minder (2026-10-26), option A. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(browse): only host:port was Chromium's own reading; user:pass@host:port is newly proxied Chromium's proxy spec parser (net/base/proxy_string_util.cc, ProxySchemeHostAndPortToProxyServer) returns an invalid server for any userinfo, so a scheme-less user:pass@host:port in HTTPS_PROXY was never honoured by Chromium on Linux and went direct; reading it as http:// follows curl and is new behaviour. BROWSER.md, the CHANGELOG, the resolveProxy docblock and the Linux e2e test comment now say so, and the scheme-less spill test is named for what it proves (those values never reach the path refusal; the http:// cases in the next test do). Wording only. 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>
558 lines
26 KiB
YAML
558 lines
26 KiB
YAML
name: Free Tests
|
|
|
|
# A single duration-balanced plan covers every free test exactly once. Each
|
|
# shard runs serially on its own machine; the aggregate requires every receipt
|
|
# and strict outcome. Tree-mutating files, when present, get a separate machine.
|
|
#
|
|
# Deliberately SECRETLESS: free tests make no API calls, so this lane gets no
|
|
# provider keys at all — least privilege, and fork PRs get real test signal
|
|
# here (the eval matrix skips fork PRs because repository secrets can't reach
|
|
# them). test/free-tests-workflow-wiring.test.ts fails CI if a secret sneaks in.
|
|
#
|
|
# This is a REQUIRED check from day one (branch protection lists it). If it's
|
|
# red, fix or quarantine-with-issue — don't make it advisory; an advisory lane
|
|
# is permanent false comfort.
|
|
#
|
|
# Local `bun run test` still uses the existing bounded process pool.
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
# Also on main pushes: two individually-green PRs can merge into a red
|
|
# main; without this nothing runs the free suite on main until the next PR.
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
# Keyed on the PR number, not head_ref: a bare branch name carries no fork
|
|
# prefix, so same-name branches from two forks would share one group and a
|
|
# push to fork B's PR would cancel fork A's in-flight REQUIRED check.
|
|
concurrency:
|
|
group: free-tests-${{ github.event.pull_request.number || github.run_id }}
|
|
cancel-in-progress: true
|
|
|
|
# Least privilege: this job executes PR-controlled code (install lifecycle
|
|
# scripts + the test suite), so the GITHUB_TOKEN gets read-only contents and
|
|
# the checkout doesn't persist it into .git/config.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
free-plan:
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
outputs:
|
|
matrix: ${{ steps.plan.outputs.matrix }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
# Full history: the seed ratchet compares the allowlist with its
|
|
# merge-base copy, which a depth-1 checkout cannot read.
|
|
fetch-depth: 0
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- id: plan
|
|
name: Inventory and balance the complete free suite
|
|
# Warns in the job summary when more than 5 files are unseeded.
|
|
run: |
|
|
matrix=$(bun run scripts/test-free-shards.ts --ci-plan "$RUNNER_TEMP/free-plan.json" --shards 20)
|
|
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
|
- name: Seed growth ratchet (60 s ceiling, shrink-only allowlist)
|
|
run: |
|
|
GSTACK_FREE_SEED_BASE="$(git merge-base HEAD origin/main)" bun test test/free-seed-ratchet.test.ts
|
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: free-plan
|
|
path: ${{ runner.temp }}/free-plan.json
|
|
if-no-files-found: error
|
|
|
|
cso-macos-launcher:
|
|
runs-on: macos-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- name: Install helper dependencies
|
|
run: bun install --frozen-lockfile --ignore-scripts
|
|
- name: Build and sign the native macOS startup boundary
|
|
run: bun run build:cso
|
|
- name: Exercise hardened-runtime startup and environment scrubbing
|
|
run: bun run test:cso:macos
|
|
env:
|
|
GSTACK_CSO_MACOS_TESTS: "1"
|
|
- name: Build browser lifecycle prerequisites
|
|
env:
|
|
GSTACK_STRICT_BUILD: '1'
|
|
run: |
|
|
bunx playwright install chromium
|
|
bun run build
|
|
- name: Exercise native agent ownership and linked settings
|
|
env:
|
|
TMPDIR: /tmp
|
|
run: |
|
|
files=(browse/test/terminal-agent-lifecycle.test.ts browse/test/terminal-agent-native-observation.test.ts browse/test/terminal-agent-watchdog.test.ts browse/test/server-embedder-terminal-port.test.ts browse/test/server-factory.test.ts test/gstack-settings-hook-symlink.test.ts test/gstack-settings-hook-schema-aware.test.ts)
|
|
for file in "${files[@]}"; do test -f "$file"; done
|
|
bun test "${files[@]}"
|
|
|
|
# Named regressions that only reproduce on macOS's default temp root:
|
|
# $TMPDIR sits under /var/folders and /var is a symlink to /private/var, so
|
|
# code that compares a realpath with an unresolved path passes on Linux's
|
|
# /tmp and fails here (#2982). TMPDIR is deliberately left at the runner's
|
|
# default, and the job fails if that layout is not symlinked. It also sources
|
|
# the Codex probe from zsh, macOS's default shell (#3024; no paid call).
|
|
macos-named-regressions:
|
|
runs-on: macos-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile --ignore-scripts
|
|
- name: Build browser prerequisites
|
|
env:
|
|
GSTACK_STRICT_BUILD: '1'
|
|
run: |
|
|
bunx playwright install chromium
|
|
bun run build
|
|
- 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
|
|
- name: Codex probe selects a model from zsh, run as a command and sourced (macOS default shell, no paid call)
|
|
working-directory: ${{ runner.temp }}
|
|
run: |
|
|
set -euo pipefail
|
|
run="$(zsh -c '"$GITHUB_WORKSPACE/bin/gstack-codex-probe" select-model exec' 2>&1)"
|
|
printf '%s\n' "$run"
|
|
printf '%s\n' "$run" | grep -E '^CODEX_SEL: [A-Za-z0-9._:/-]+$'
|
|
out="$(zsh -c 'source "$GITHUB_WORKSPACE/bin/gstack-codex-probe" && _gstack_codex_select_model exec' 2>&1)"
|
|
printf '%s\n' "$out"
|
|
printf '%s\n' "$out" | grep -E '^CODEX_MODEL: [A-Za-z0-9._:/-]+ \(exec; source: '
|
|
- name: Run the named macOS regressions on the default temp root
|
|
run: |
|
|
set -euo pipefail
|
|
tmp="${TMPDIR:-/tmp}"
|
|
resolved="$(realpath "$tmp")"
|
|
echo "temp root: $tmp -> $resolved"
|
|
test "$resolved" != "${tmp%/}"
|
|
files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts browse/test/proxy-env-daemon.test.ts browse/test/proxy-direct-launch-args.test.ts)
|
|
for file in "${files[@]}"; do test -f "$file"; done
|
|
bun test "${files[@]}"
|
|
|
|
cso-windows-launcher:
|
|
runs-on: windows-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- name: Install helper dependencies
|
|
run: bun install --frozen-lockfile --ignore-scripts
|
|
- name: Build the native Windows startup boundary from Git Bash
|
|
shell: bash
|
|
run: bun run build:cso
|
|
- name: Exercise native startup, environment, and argument forwarding
|
|
run: bun run test:cso:windows
|
|
env:
|
|
GSTACK_CSO_WINDOWS_TESTS: "1"
|
|
- name: Upload #2894 lease diagnostics
|
|
if: failure()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: cso-lease-diagnostics-a${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/cso-lease-diagnostics
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
cso-docker-integration:
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- name: Require local Docker containment prerequisites
|
|
# A missing daemon is a failed gate, never a skipped integration test.
|
|
run: |
|
|
command -v docker
|
|
docker --host unix:///var/run/docker.sock info
|
|
command -v cc
|
|
- name: Install helper dependencies
|
|
run: bun install --frozen-lockfile --ignore-scripts
|
|
- name: Compile trusted CSO helper and watchdog
|
|
run: bun run build:cso
|
|
# Only the file that can run here: the node-lifecycle and stack-cold
|
|
# files need GSTACK_CSO_TEST_STACK images and run in the CSO runtime
|
|
# workflows (test:cso:docker), so in this job they always skipped.
|
|
- name: Run CSO Docker integration gate
|
|
run: |
|
|
bun test --max-concurrency 1 test/cso-docker-integration.test.ts
|
|
env:
|
|
GSTACK_CSO_DOCKER_TESTS: "1"
|
|
DOCKER_HOST: unix:///var/run/docker.sock
|
|
|
|
typecheck:
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile --ignore-scripts
|
|
- name: Typecheck product code (zero errors)
|
|
run: bun run typecheck
|
|
- name: Test-code type-debt ratchet
|
|
run: bun run typecheck:test
|
|
- 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: ubicloud-standard-8
|
|
timeout-minutes: 30
|
|
steps:
|
|
- 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.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: |
|
|
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
|
|
env:
|
|
GSTACK_FREE_RETRY_FLAKY: "1"
|
|
GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl
|
|
run: xvfb-run -a bun run test:quick
|
|
|
|
free-suite:
|
|
needs: free-plan
|
|
runs-on: ubicloud-standard-8
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
max-parallel: 20
|
|
matrix: ${{ fromJSON(needs.free-plan.outputs.matrix) }}
|
|
steps:
|
|
- 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.2
|
|
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
with:
|
|
name: free-plan
|
|
path: ${{ runner.temp }}
|
|
|
|
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
|
with:
|
|
path: ~/.bun/install/cache
|
|
key: linux-bun-${{ hashFiles('bun.lock') }}
|
|
# A lockfile bump starts from the previous cache instead of cold.
|
|
restore-keys: |
|
|
linux-bun-
|
|
|
|
- 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: |
|
|
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"
|
|
test "$(stat -c '%u:%a' "$installed")" = '0:4755'
|
|
cmp -s "$helper" "$installed"
|
|
|
|
# Headed-browser tests (handoff, extension sidepanel DOM) need a real
|
|
# DISPLAY — first Linux run failed with Playwright's "launched a headed
|
|
# browser without an XServer" banner. xvfb-run below provides it;
|
|
# x11-utils ships xdpyinfo for display probing. poppler-utils ships
|
|
# pdftotext/pdffonts/pdftoppm for the make-pdf e2e gates;
|
|
# fonts-noto-color-emoji is the emoji-gate's render font (playwright
|
|
# --with-deps usually installs it, but the gate must not depend on a
|
|
# transitive package list). Fonts must land BEFORE the first browse
|
|
# daemon launch — Chromium snapshots fontconfig at startup. zsh is not
|
|
# in the runner image; without it the bash+zsh portability arms (#2669)
|
|
# skip instead of running.
|
|
- 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
|
|
# Some tests run git against the checkout itself; CI checkouts can be
|
|
# owned by a different uid than the runner user.
|
|
git config --global --add safe.directory '*'
|
|
|
|
- name: Generate host SKILL.md outputs (.agents, .factory)
|
|
# Golden-file tests read generated host outputs that are gitignored.
|
|
run: bun run gen:skill-docs --host all
|
|
|
|
- name: Vendor xterm assets into the extension
|
|
# extension/lib/xterm* are gitignored (vendored from npm at build
|
|
# time). Without them the sidepanel's terminal script bails and the
|
|
# sidepanel DOM tests time out waiting on init that never happens.
|
|
run: bun run vendor:xterm
|
|
|
|
- name: Build server-node bundle (loaded by browse cli imports)
|
|
run: bash browse/scripts/build-node-server.sh
|
|
|
|
# Narrowed gate build: the make-pdf e2e gates probe make-pdf/dist/pdf,
|
|
# browse/dist/browse, and the diagram-render bundle, then self-skip when
|
|
# absent — which made them silently skip on Linux for their whole life
|
|
# (this lane never built binaries). Full `bun run build` compiles five
|
|
# binaries and would add ~60-90s to the ONLY required check; the gates
|
|
# need exactly these three artifacts.
|
|
- 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
|
|
|
|
# GSTACK_EXPECT_BINARIES=1 arms make-pdf/test/e2e/ci-prereqs.test.ts:
|
|
# if a future edit drops the gate build (or poppler), the lane FAILS
|
|
# instead of the gates silently self-skipping back to false green.
|
|
- name: Run free suite
|
|
run: xvfb-run -a bun run test:free --ci-run "$RUNNER_TEMP/free-plan.json" --shard ${{ matrix.shard }} --result "$RUNNER_TEMP/free-results/shard-${{ matrix.shard }}.json"
|
|
env:
|
|
GSTACK_EXPECT_BINARIES: "1"
|
|
# WS1 flake telemetry: a single timing flake must not red the only
|
|
# required lane — the runner's attribution-gated retry pass (cap 5,
|
|
# truncation veto) re-runs failing files once, serially, and a
|
|
# clean retry downgrades to a LOUD flaky-pass. Every flaky-pass is
|
|
# appended to the ledger (single writer: the parent runner) and
|
|
# uploaded below, so repeat offenders are an enumerable series —
|
|
# recorded and ranked, never masked. Pinned by
|
|
# test/free-tests-workflow-wiring.test.ts.
|
|
GSTACK_FREE_RETRY_FLAKY: "1"
|
|
GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl
|
|
|
|
- name: Upload strict shard result
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: free-result-${{ matrix.shard }}
|
|
path: ${{ runner.temp }}/free-results/*.json
|
|
if-no-files-found: error
|
|
|
|
# Uploaded unconditionally (not just on failure): a flaky-pass run is
|
|
# GREEN — that's the point — so its evidence must survive green runs.
|
|
- name: Upload flake ledger
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: flake-ledger-${{ matrix.shard }}
|
|
path: ${{ runner.temp }}/flake-ledger.jsonl
|
|
if-no-files-found: ignore
|
|
retention-days: 90
|
|
|
|
- name: Detect recovered failures for log retention
|
|
id: flake_spool
|
|
if: always()
|
|
run: |
|
|
if [ -s "$RUNNER_TEMP/flake-ledger.jsonl" ]; then
|
|
echo 'present=true' >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# The quiet console omits assertion details. Preserve the original spool
|
|
# after a recovered retry too, so a green job retains its first failure.
|
|
- name: Upload shard logs on failure or recovered retry
|
|
if: failure() || steps.flake_spool.outputs.present == 'true'
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: free-test-shard-logs-${{ matrix.shard }}
|
|
path: .context/free-test-logs/gstack-free-test-*.log
|
|
include-hidden-files: true
|
|
if-no-files-found: ignore
|
|
|
|
# Branch protection already requires the `free-tests` context. Keep that
|
|
# stable name as an always-running aggregate so every platform-specific CSO
|
|
# gate is merge-blocking without a separate branch-protection migration.
|
|
free-tests:
|
|
if: always()
|
|
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, 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 }}
|
|
CSO_DOCKER_RESULT: ${{ needs.cso-docker-integration.result }}
|
|
run: |
|
|
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
|
|
test "$CSO_DOCKER_RESULT" = success
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
with:
|
|
name: free-plan
|
|
path: ${{ runner.temp }}
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
with:
|
|
pattern: free-result-*
|
|
merge-multiple: true
|
|
path: ${{ runner.temp }}/free-results
|
|
# Per-shard ledgers land in their own subdirectories (all are named
|
|
# flake-ledger.jsonl). A green run is where a retried flake hides, so the
|
|
# summary runs before strict verification.
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
with:
|
|
pattern: flake-ledger-*
|
|
path: ${{ runner.temp }}/flake-ledgers
|
|
- name: Summarize flaky passes absorbed by the serial retry
|
|
run: bun run scripts/test-health-report.ts flake-summary "$RUNNER_TEMP/flake-ledgers" >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Require exact coverage and complete strict results
|
|
run: bun run scripts/test-free-shards.ts --ci-verify "$RUNNER_TEMP/free-plan.json" --results "$RUNNER_TEMP/free-results"
|