mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
* ci: cancel superseded runs + per-job timeouts on test.yml & e2e.yml Ports the GH-Actions hygiene gbrain already uses in heavy-tests.yml to the two hot-path workflows. concurrency cancels a superseded run (keyed on PR number for pull_request events — fork-safe — with github.ref fallback for push/scheduled); frees runners and stops a stale-SHA run reporting a flaky failure on an obsolete commit. Per-job timeout-minutes (test matrix 15, verify 12, serial 15, slow-* 12, e2e tier1 20 / tier2 30, trivial jobs 5-10) convert a wedged job from a 6-hour zombie (GitHub's default) into a fast legible fail. fail-fast:false already set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add actionlint workflow (rhysd/actionlint v1.7.11, SHA-pinned) Lints workflow YAML on .github/workflows/** changes so a malformed workflow / bad action ref / missing-permission bug is caught before it ships a broken pipeline. gbrain edits these workflows often; cheap preventive guard. Mirrors GStack's actionlint job, SHA-pinned to gbrain's convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(e2e): scrub operator/agent env before E2E (hermetic runner) A dev or Conductor shell exports CONDUCTOR_*/MCP_*/OPENCLAW_*/GBRAIN_* overrides that silently change test behavior, making hermetic E2E non-hermetic and its failures unreproducible across machines. run-e2e.sh drops those prefixes before bun starts (denylist — PATH/HOME/TMPDIR/DATABASE_URL survive; GBRAIN_HOME kept for the existing HOME isolation). Adapts GStack's buildHermeticEnv to gbrain's shell runner. Verified: a 78-test e2e file passes with DATABASE_URL surviving + a planted GBRAIN_BRAIN_ID scrubbed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * v0.42.50.0 ci: reliability hardening — cancel-superseded + per-job timeouts + actionlint + hermetic E2E env Ports GStack's CI-reliability hygiene to gbrain's hot-path workflows: concurrency cancel-in-progress (PR-number keyed), per-job timeout-minutes (no more 6-hour zombie jobs), an actionlint workflow, and an operator-env scrub in run-e2e.sh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
287 lines
14 KiB
YAML
287 lines
14 KiB
YAML
name: Test
|
||
|
||
on:
|
||
push:
|
||
branches: [master]
|
||
pull_request:
|
||
branches: [master]
|
||
# Manual dispatch lets a local dev/agent offload the suite to GitHub's
|
||
# on-demand runners from ANY branch (see scripts/ship-remote-tests.sh).
|
||
# Frees a load-saturated local machine (e.g. many Conductor agents running
|
||
# their own bun-test suites at once — load avg 120 on 16 cores).
|
||
workflow_dispatch:
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
# Cancel a superseded run when a newer commit lands on the same PR/branch.
|
||
# Keyed on the PR number for pull_request events (unique per PR, so two PRs
|
||
# from forks sharing a branch name don't cancel each other) and falls back to
|
||
# github.ref for push/scheduled runs. Mirrors heavy-tests.yml; frees runners
|
||
# and stops a stale-SHA run from reporting a flaky failure on an obsolete commit.
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
# cache-check: runs first, computes the content hash of every tracked
|
||
# file EXCEPT the deny-list (CHANGELOG.md, README.md, docs/**/*.md, etc.
|
||
# — see scripts/ci-cache-hash.sh for the full list). Looks up
|
||
# `ci-pass-<hash>` in actions/cache; if hit, the test matrix + verify
|
||
# + serial jobs all skip and test-status reports green immediately.
|
||
# If miss, the full suite runs and cache-write seals it on success.
|
||
#
|
||
# Hit rate covers re-pushes (same SHA twice), branch rebases that
|
||
# don't touch tracked code, and any branch update that touches only
|
||
# the deny-listed doc files.
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
cache-check:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
outputs:
|
||
hit: ${{ steps.lookup.outputs.cache-hit }}
|
||
hash: ${{ steps.compute.outputs.hash }}
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
- name: Compute content hash
|
||
id: compute
|
||
run: |
|
||
# --verbose writes the "X/Y files in hash" diagnostic to stderr;
|
||
# stdout carries the 16-char hash. Capture both.
|
||
HASH=$(bash scripts/ci-cache-hash.sh --verbose 2>/tmp/cache-diag)
|
||
cat /tmp/cache-diag
|
||
echo "Computed cache hash: $HASH"
|
||
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
|
||
- name: Lookup actions/cache for ci-pass-<hash>
|
||
id: lookup
|
||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
key: ci-pass-${{ steps.compute.outputs.hash }}
|
||
path: .ci-cache-marker
|
||
# `lookup-only: true` means we only probe whether the cache
|
||
# entry exists — we don't download it (the marker contents
|
||
# don't matter, only the key match does). `cache-hit` returns
|
||
# true only on EXACT key match (per actions/cache docs); a
|
||
# restore-keys prefix fallback would set cache-hit=false, so
|
||
# it's deliberately omitted here. Cross-branch scoping works
|
||
# naturally: PR branches can read default-branch (master)
|
||
# cache entries via exact key match when the content hash
|
||
# matches, which happens whenever the tree is doc-only
|
||
# different from a green master run.
|
||
lookup-only: true
|
||
- name: Cache status
|
||
run: |
|
||
if [ "${{ steps.lookup.outputs.cache-hit }}" = "true" ]; then
|
||
echo "✓ cache HIT for hash ${{ steps.compute.outputs.hash }} — test jobs will skip"
|
||
else
|
||
echo "✗ cache MISS for hash ${{ steps.compute.outputs.hash }} — full suite will run"
|
||
fi
|
||
|
||
gitleaks:
|
||
needs: cache-check
|
||
if: needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
with:
|
||
fetch-depth: 0
|
||
- uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
verify:
|
||
# Pre-test gates: privacy/jsonb/source-id/etc + typecheck + admin-build.
|
||
# Lives in its own runner so the matrix shards aren't carrying ~2-3min
|
||
# of verify work in addition to their test files (the old shape stuffed
|
||
# this into `test (1)` via `if: matrix.shard == 1`, which made shard 1
|
||
# the slowest matrix worker). scripts/run-verify-parallel.sh fans out
|
||
# the 20 checks via & + wait (~5s vs ~15-25s sequential).
|
||
needs: cache-check
|
||
if: needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 12
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.13
|
||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
path: ~/.bun/install/cache
|
||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||
- run: bun install
|
||
- run: bun run verify
|
||
|
||
serial-tests:
|
||
# *.serial.test.ts at --max-concurrency=1. Lives in its own runner so
|
||
# the matrix shards aren't carrying the serial-pass tail (the old shape
|
||
# stuffed this into `test (1)` after the matrix work, which compounded
|
||
# shard 1's overload).
|
||
needs: cache-check
|
||
if: needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.13
|
||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
path: ~/.bun/install/cache
|
||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||
- run: bun install
|
||
- run: bun run test:serial
|
||
|
||
slow-eval-longmemeval:
|
||
# Dedicated runner for the LongMemEval end-to-end test file. The file
|
||
# was originally 359s. TODO #1 (engine-sharing in runEvalLongMemEval
|
||
# via RunOpts.engine) cut it to ~200s by amortizing PGLite cold-create
|
||
# across all 13 runEvalLongMemEval calls in one beforeAll-shared brain.
|
||
# Pulled out of the matrix (see scripts/test-shard.sh) so a single 200s
|
||
# atom doesn't dominate a shard's wallclock. Companion file
|
||
# test/eval-longmemeval.slow.test.ts (the pure-bucket half) stays in
|
||
# the matrix because it's light (~42s).
|
||
needs: cache-check
|
||
if: needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 12
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.13
|
||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
path: ~/.bun/install/cache
|
||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||
- run: bun install
|
||
- run: bun test test/eval-longmemeval-e2e.slow.test.ts --timeout=60000
|
||
|
||
slow-entity-resolve-perf:
|
||
# Dedicated runner for the entity-resolve perf test (~159s, single perf
|
||
# describe with one test that builds 5000+ pages and asserts the NEW
|
||
# tryPrefixExpansion shape is 5x faster than the OLD shape — not
|
||
# subdivisible without weakening the perf guarantee). Pulled out of the
|
||
# matrix (see scripts/test-shard.sh) so a single 159s atom doesn't
|
||
# dominate a shard's wallclock. Runs in parallel with the matrix.
|
||
needs: cache-check
|
||
if: needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 12
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.13
|
||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
path: ~/.bun/install/cache
|
||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||
- run: bun install
|
||
- run: bun test test/entity-resolve-perf.slow.test.ts --timeout=300000
|
||
|
||
test:
|
||
# Pure matrix shard — no verify, no serial. Each shard runs its slice
|
||
# of the unit test set under one `bun test` invocation.
|
||
#
|
||
# 10 shards (was 6) drops per-shard total from 532s → 287s. With the two
|
||
# dedicated jobs (slow-eval-longmemeval, slow-entity-resolve-perf) also
|
||
# pulled out, the matrix is bounded by ~287s ≈ 4.8 min. Total CI ≈ max
|
||
# of matrix + slow-eval (~3.3 min after engine-sharing in TODO #1) +
|
||
# slow-entity-resolve-perf (~2.6 min) ≈ 4.8 min.
|
||
#
|
||
# Concurrency budget: 10 shards + verify + serial + slow-eval +
|
||
# slow-entity-resolve-perf + gitleaks + cache-check + cache-write +
|
||
# test-status = ~18 jobs × 2 concurrent PRs = 36. GitHub free-tier
|
||
# caps at ~20 concurrent jobs, so multi-PR days will see some queue
|
||
# pressure. Single-PR runs are unaffected.
|
||
#
|
||
# Partition policy is weight-aware LPT bin-packing via scripts/sharding.ts
|
||
# (replaces FNV-1a path hash). Weights live in scripts/test-weights.json,
|
||
# mined from real CI logs via scripts/mine-shard-weights.ts. Missing
|
||
# weights fall back to corpus median — new test files work immediately.
|
||
needs: cache-check
|
||
if: needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||
steps:
|
||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.13
|
||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
path: ~/.bun/install/cache
|
||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||
- run: bun install
|
||
- name: Run test shard ${{ matrix.shard }}/10
|
||
run: scripts/test-shard.sh ${{ matrix.shard }} 10
|
||
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
# cache-write: ONLY runs when every gated job succeeded. Writes the
|
||
# cache entry under `ci-pass-<hash>` so future runs at the same hash
|
||
# hit cache. Codex's load-bearing correctness point: writing the
|
||
# cache before the matrix completes would permanently bless bad states
|
||
# (a future run at the same hash would skip tests because of a cache
|
||
# entry written when tests hadn't actually passed).
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
cache-write:
|
||
needs: [cache-check, gitleaks, verify, serial-tests, slow-eval-longmemeval, slow-entity-resolve-perf, test]
|
||
if: success() && needs.cache-check.outputs.hit != 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
steps:
|
||
- name: Create cache marker
|
||
run: |
|
||
mkdir -p .ci-cache-marker
|
||
echo "${{ needs.cache-check.outputs.hash }}" > .ci-cache-marker/hash
|
||
echo "$GITHUB_SHA" > .ci-cache-marker/sha
|
||
echo "$GITHUB_REF" > .ci-cache-marker/ref
|
||
- uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||
with:
|
||
key: ci-pass-${{ needs.cache-check.outputs.hash }}
|
||
path: .ci-cache-marker
|
||
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
# test-status: the single user-visible "did CI pass?" check.
|
||
# Runs always (if: always()), succeeds when EITHER cache-check.hit==true
|
||
# OR all gated jobs (gitleaks, verify, serial-tests, test) succeeded.
|
||
# Branch protection (when configured) gates on this single job name.
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
test-status:
|
||
needs: [cache-check, gitleaks, verify, serial-tests, slow-eval-longmemeval, slow-entity-resolve-perf, test]
|
||
if: always()
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
steps:
|
||
- name: Aggregate result
|
||
run: |
|
||
HIT="${{ needs.cache-check.outputs.hit }}"
|
||
GITLEAKS="${{ needs.gitleaks.result }}"
|
||
VERIFY="${{ needs.verify.result }}"
|
||
SERIAL="${{ needs.serial-tests.result }}"
|
||
SLOW_EVAL="${{ needs.slow-eval-longmemeval.result }}"
|
||
SLOW_PERF="${{ needs.slow-entity-resolve-perf.result }}"
|
||
TEST="${{ needs.test.result }}"
|
||
echo "cache-check.hit=$HIT"
|
||
echo "gitleaks=$GITLEAKS verify=$VERIFY serial-tests=$SERIAL slow-eval-longmemeval=$SLOW_EVAL slow-entity-resolve-perf=$SLOW_PERF test=$TEST"
|
||
if [ "$HIT" = "true" ]; then
|
||
echo "✓ cache HIT for hash ${{ needs.cache-check.outputs.hash }} — CI green"
|
||
exit 0
|
||
fi
|
||
# Cache miss: every gated job must have succeeded.
|
||
for r in "$GITLEAKS" "$VERIFY" "$SERIAL" "$SLOW_EVAL" "$SLOW_PERF" "$TEST"; do
|
||
if [ "$r" != "success" ]; then
|
||
echo "✗ gated job did not succeed (got $r) — CI fail"
|
||
exit 1
|
||
fi
|
||
done
|
||
echo "✓ all gated jobs succeeded — CI green"
|