Files
treg/.github/workflows/ci.yml
T
UncleCode 3ee1948c64 Merge origin/main into dev/hub: the tool hub, behind TREG_HUB_ENABLED
Brings main's 86 commits (dashboard boot and loading, legacy dashboard removal, test pruning,
overflow and routing fixes) together with the tool hub branch.

Conflicts, both sides kept unless noted:
- the legacy dashboard stays deleted, as on main;
- App.vue and the dashboard state: main's search page and loading states plus the hub pages;
- ci.yml: main's Postgres job, with the hub tests added to its list;
- dev-local.sh: main's server environment plus the hub flag passthrough;
- test_call_application_contract.py, test_marketplace_call.py: main's pruned files plus the
  hub branch's sync `settle: usage` test.

Not conflicts: main and the hub branch fixed the same CompanyEnrich empty-page billing; main's
rule runs first, so the hub branch's copy and its test are dropped. The Listing-tab test reads
the Vue source instead of the deleted legacy page.
2026-09-26 07:32:53 +08:00

226 lines
9.5 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
# A new push supersedes the running build of the same branch or PR: the outdated run is cancelled
# instead of holding a runner and queueing the new one behind it (two overlapped runs were live
# the day this landed).
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Least-privilege: CI only reads the repo (checkout + tests + secret scan).
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
# A throttled shared runner once turned the 5-minute suite into 20+ (log showed 68s before the
# first test started). Die loudly instead of eating half an hour; a re-run usually lands on a
# healthier machine.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # the docs-only check diffs HEAD against its parent
- name: Detect a docs-only push
id: docs
# Direct pushes that change only docs/ and *.md skip the suite — half of one week's runs
# bought nothing. PULL REQUESTS ALWAYS RUN: the branch-protection check must report, and a
# skipped required check would block the merge forever.
run: |
ONLY=false
if [ "${{ github.event_name }}" = "push" ]; then
CHANGED=$(git diff --name-only HEAD^ HEAD || true)
if [ -n "$CHANGED" ] && ! echo "$CHANGED" | grep -qvE '^docs/|\.md$'; then
ONLY=true
fi
fi
echo "docs-only: $ONLY"
echo "only=$ONLY" >> "$GITHUB_OUTPUT"
- uses: astral-sh/setup-uv@v7
if: steps.docs.outputs.only != 'true'
with:
python-version: "3.13"
- uses: actions/setup-node@v4
if: steps.docs.outputs.only != 'true'
with:
node-version: '22'
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Build dashboard
if: steps.docs.outputs.only != 'true'
run: bash scripts/build-dashboard.sh
- name: Install dependencies
if: steps.docs.outputs.only != 'true'
run: uv sync --locked
- name: Enforce import boundaries
if: steps.docs.outputs.only != 'true'
run: uv run --locked lint-imports
- name: Run tests
if: steps.docs.outputs.only != 'true'
# -n auto: the suite is 2,700+ small independent tests — serial execution is what let a
# throttled runner turn minutes into tens of minutes. xdist arrives via --with rather than
# the lockfile on purpose (local daily default uses the same --with).
run: uv run --with pytest-xdist pytest -n auto -v -o faulthandler_timeout=300
test-postgres:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: treg
POSTGRES_PASSWORD: treg
POSTGRES_DB: treg_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U treg -d treg_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
TREG_TEST_DB_URL: postgresql+asyncpg://treg:treg@127.0.0.1:5432/treg_test
PYTHONUNBUFFERED: '1'
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Build dashboard
run: bash scripts/build-dashboard.sh
- name: Install dependencies
run: uv sync --locked
- name: Run Postgres tests
# Each xdist worker gets its own database (treg_test_gw0, ...; see tests/conftest.py), so
# reset_db() in one worker never empties another's tables.
# -v + faulthandler: this job has twice been killed at its time limit with zero output on
# this runner while the identical list passes locally. Per-test lines locate the stall;
# the fault handler dumps every thread's stack after 300s of one test.
run: >-
uv run --frozen --with pytest-xdist python -m pytest -n auto -v -o faulthandler_timeout=300
tests/callmatrix
tests/test_team_limit.py
tests/test_ledger.py
tests/test_ledger_claim_review.py
tests/test_asynctasks.py
tests/test_alembic_baseline.py
tests/test_enrich_arena.py
tests/test_arena_insights.py
tests/test_arena_verification_insights.py
tests/test_feedback.py
tests/test_archive.py
tests/test_cache_result_admission.py
tests/test_archive_r2.py
tests/test_call_cancellation.py
tests/test_call_pool_discipline.py
tests/test_api_keys.py
tests/test_postgres_reset.py
tests/test_read_replica.py
tests/test_marketplace_call.py
tests/test_capacity_overflow_spend.py
tests/test_billing.py
tests/test_agent_capture.py
tests/test_adsconv.py
tests/test_health.py
tests/test_localrun.py
tests/test_orgs.py
tests/test_orgs_isolation.py
tests/test_orgs_mgmt.py
tests/test_hub.py
tests/test_hub_sandbox.py
tests/test_mcp.py
- name: Dump Postgres activity on failure
# Three CI-only hangs at archive drains were undiagnosable from Python stacks alone; this
# shows the DATABASE's view: every session, its state (idle in transaction = a leaked or
# blocked one), what it waits on, and ungranted locks with their holders' queries.
# Delete once the hang is understood and fixed.
if: failure()
env:
PGPASSWORD: treg
run: |
psql -h 127.0.0.1 -U treg -d treg_test -x -c "select datname, pid, state, wait_event_type, wait_event, xact_start, state_change, left(query,140) as query from pg_stat_activity where datname like 'treg_test%' order by xact_start nulls last;" || true
psql -h 127.0.0.1 -U treg -d treg_test -c "select l.pid, l.locktype, l.mode, l.granted, a.state, left(a.query,90) as query from pg_locks l join pg_stat_activity a using(pid) where not l.granted;" || true
# RESTORED 2026-08-24. This job is a REQUIRED check on main, and the CI rewrite in 528383d
# dropped it while keeping the requirement — so for three weeks every pull request waited on a
# check that could never report, and only an admin bypass moved anything. It never skips (see
# the docs-only note in `test`: a skipped required check blocks a merge forever), and it scans
# the FULL history, because a secret removed from the tip still lives in the commit that added
# it. `.gitleaks.toml` allowlists the deliberately-fake placeholders in the tests and the demo
# sandbox; everything else is a real finding.
gitleaks:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # scan the full history of the push/PR, not just the tip
- name: Install gitleaks
env:
GITLEAKS_VERSION: "8.21.2"
run: |
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz gitleaks
sudo mv gitleaks /usr/local/bin/
- name: Scan for secrets
# HEAD is the PR merge commit (both parents) or the pushed main commit. Scan all
# reachable history, including deleted secrets, without unrelated fetched branch tips.
run: gitleaks detect --source . --config .gitleaks.toml --log-opts="HEAD" --verbose --redact
frontend:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: uv sync --locked
- run: bash scripts/build-dashboard.sh
- run: npm --prefix frontend test
- name: Install browser
working-directory: frontend
run: npx playwright install --with-deps chromium
- run: npm --prefix frontend run test:e2e
- name: Check distributable assets
run: |
uv build
python - <<'PYCODE'
from pathlib import Path
from tarfile import open as open_tar
from zipfile import ZipFile
import re
with ZipFile(next(Path('dist').glob('*.whl'))) as wheel:
assert not [name for name in wheel.namelist() if name.endswith('.mp4')]
html = wheel.read('treg/web/dashboard/index.html').decode()
assets = re.findall(r'/app/ui/(assets/[^"\s]+)', html)
assert assets, 'Dashboard entry must reference compiled assets'
for asset in assets:
assert wheel.read('treg/web/dashboard/' + asset)
assert not [name for name in wheel.namelist() if 'dashboard-legacy' in name]
arena = wheel.read('treg/web/enrich-arena.html').decode()
for asset in re.findall(r'src="(/vendor/vue-[^"\s]+)"', arena):
assert wheel.read('treg/web' + asset)
assert wheel.read('treg/web/vendor/LICENSE')
with open_tar(next(Path('dist').glob('*.tar.gz'))) as source:
assert not [member.name for member in source.getmembers()
if member.name.endswith('.mp4')]
PYCODE