Start fresh backends at Pearl heights and retire the legacy agent-registry alias #3355
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| branches: [main, dev/v2] | |
| push: | |
| branches: [main, dev/v2] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| backend: | |
| name: Backend (Go) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| defaults: | |
| run: | |
| working-directory: backend | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: 'backend/go.mod' | |
| cache-dependency-path: backend/go.sum | |
| - name: Build | |
| run: go build ./... | |
| - name: Test with coverage | |
| run: go test -race -count=1 -coverprofile=coverage.out -covermode=atomic ./... | |
| - name: Check coverage threshold | |
| run: | | |
| COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%') | |
| echo "Backend coverage: ${COVERAGE}%" | |
| if (( $(echo "$COVERAGE < 20" | bc -l) )); then | |
| echo "::error::Backend coverage ${COVERAGE}% is below 20% threshold" | |
| exit 1 | |
| fi | |
| if (( $(echo "$COVERAGE < 50" | bc -l) )); then | |
| echo "::warning::Backend coverage ${COVERAGE}% is below 50% target — improve gradually" | |
| fi | |
| - name: Upload coverage | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: backend-coverage | |
| path: backend/coverage.out | |
| retention-days: 14 | |
| - name: Vulnerability check | |
| run: go install golang.org/x/vuln/cmd/govulncheck@v1.3.0 && $(go env GOPATH)/bin/govulncheck ./... | |
| # Must be the EXACT same invocation as deploy-backend.yml's Backend CI Gate | |
| # Lint step. The unversioned module path installs golangci-lint v1, whose | |
| # default exclusions suppress errcheck on Close() — so a PR passed here, | |
| # then the v2 deploy gate failed post-merge and main was undeployable | |
| # (#710). Pinned so a new v2.x release changing default linters/exclusions | |
| # can't redden both gates with no code change. | |
| - name: Lint | |
| run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 && golangci-lint run ./... | |
| frontend: | |
| name: Frontend (React · Node ${{ matrix.node-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node-version: ['20', '22'] | |
| defaults: | |
| run: | |
| working-directory: frontend | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install | |
| run: npm ci | |
| - name: "Determinism gate: arcade verify-worker bundle is fresh" | |
| run: | | |
| # The attester's verify worker (backend/internal/arcade/worker) is ONE | |
| # esbuild bundle of every game's frontend sim (BARRICADE + Space | |
| # Invaders), committed and go:embed'd. Rebuild it and fail on any diff | |
| # — a stale bundle could attest a result the current frontend sims no | |
| # longer produce. Rebuild locally with: | |
| # node backend/internal/arcade/worker/build.mjs | |
| # NOTE: a sim BEHAVIOR change surfaces here as a bundle diff (and a | |
| # fixtures diff). Regenerating those is not enough on a LIVE season | |
| # — it must be paired with that game's sim-version bump (barricade | |
| # SIM_VERSION, invaders INVADERS_SIM_VERSION) so old attestations stay | |
| # partitioned by their frozen build. That decision is the owner-gated | |
| # season-cutover ceremony (both versions are still unreleased). | |
| node ../backend/internal/arcade/worker/build.mjs | |
| if ! git diff --exit-code -- ../backend/internal/arcade/worker/bundle/verify-worker.cjs; then | |
| echo "::error::verify-worker.cjs is stale. Run 'node backend/internal/arcade/worker/build.mjs' and commit the result." | |
| exit 1 | |
| fi | |
| echo "✅ verify-worker.cjs matches its source" | |
| - name: Type check | |
| run: npx tsc --noEmit | |
| - name: Lint | |
| run: npm run lint | |
| - name: "Safety gate: feature-flag template hygiene (.env.example)" | |
| working-directory: . | |
| run: | | |
| # Template hygiene only — keeps .env.example from suggesting a fund-gated | |
| # flag be enabled. The AUTHORITATIVE gate is now build-time: vite.config.ts | |
| # (safeFlagsPlugin → src/lib/safeFlags.ts) fails `npm run build` — in this CI | |
| # Build step AND the Netlify build — if a gated flag resolves to "true" from | |
| # the real env (.env / process.env / Netlify dashboard), which this grep never | |
| # saw. To enable a flag: remove it from SAFETY_GATED_FLAGS AND this list, and | |
| # pass code review. | |
| FAIL=0 | |
| for FLAG in VITE_ENABLE_TREASURY_SPEND VITE_ENABLE_AGENT_CREDITS; do | |
| VALUE=$(grep "^${FLAG}=" .env.example | cut -d= -f2 || true) | |
| if [ "$VALUE" = "true" ]; then | |
| echo "::error::SAFETY GATE FAILED — ${FLAG}=true in .env.example. This flag gates a feature with incomplete on-chain enforcement. See MEMBA_AAA_IMPLEMENTATION_PLAN.md." | |
| FAIL=1 | |
| fi | |
| done | |
| if [ "$FAIL" = "1" ]; then exit 1; fi | |
| echo "✅ All safety-gated feature flags are disabled in .env.example" | |
| - name: "Safety gate: light-theme text colors (§13 — no hardcoded color: in component CSS)" | |
| run: | | |
| # §13: light-theme contrast is enforced through the theme-aware | |
| # --color-k-*-text tokens. A hardcoded color: (hex/hsl/rgb) as TEXT in a | |
| # component CSS file is invisible/low-contrast on one theme. Fail on any. | |
| # Allowed: the token definitions (tokens.css / index.css), the dark-only | |
| # files (hacker-mode, validators-hacker), gated features (nft/marketplace/ | |
| # studio/freelance), near-black/near-white on a bright fill, and | |
| # non-text color properties (border/background/outline/etc). | |
| VIOL=$(grep -rnHE "color: ?(#[0-9a-fA-F]{3,8}|hsl\(|rgb\()" src --include='*.css' \ | |
| | grep -viE "(tokens|index)\.css|hacker-mode|validators-hacker|/nft|marketplace|/studio/|freelance" \ | |
| | grep -viE "(border|background|outline|text-decoration|caret|column-rule)-color" \ | |
| | grep -viE "#fff\b|#ffffff|#000\b|#000000|#0a0a14|#1a1a1a|#0a0a0a|#0a1210|#16161d|#1a1200" || true) | |
| if [ -n "$VIOL" ]; then | |
| echo "::error::Hardcoded text color in component CSS — use a --color-k-*-text token (see docs/DESIGN_SYSTEM.md §13)." | |
| echo "$VIOL" | |
| exit 1 | |
| fi | |
| echo "✅ No hardcoded text colors in component CSS (light-theme contrast preserved)" | |
| - name: Build | |
| run: npm run build | |
| # Coverage instrumentation runs only on the Node 22 leg (the one that | |
| # uploads it). Node 20 runs the same suite for version-compat WITHOUT the | |
| # ~coverage overhead + duplicate artifact — the results are identical, so | |
| # instrumenting both legs was wasted compute. | |
| - name: Unit tests (version-compat) | |
| if: matrix.node-version == '20' | |
| run: npm test | |
| - name: Unit tests with coverage | |
| if: matrix.node-version == '22' | |
| run: npm test -- --coverage | |
| - name: Upload frontend coverage | |
| if: always() && matrix.node-version == '22' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: frontend-coverage-node${{ matrix.node-version }} | |
| path: frontend/coverage/ | |
| retention-days: 14 | |
| - name: Check bundle size | |
| run: | | |
| # Measure the main entry chunk (index-*.js), not all lazy-loaded chunks | |
| MAIN_CHUNK=$(find dist/assets -name 'index-*.js' ! -name '*.map' -exec wc -c {} + | tail -1 | awk '{print $1}') | |
| MAIN_KB=$((MAIN_CHUNK / 1024)) | |
| TOTAL_SIZE=$(find dist/assets -name '*.js' ! -name '*.map' -exec wc -c {} + | tail -1 | awk '{print $1}') | |
| TOTAL_KB=$((TOTAL_SIZE / 1024)) | |
| echo "Main chunk: ${MAIN_KB}KB | Total JS (all chunks): ${TOTAL_KB}KB" | |
| # Fail if main entry chunk exceeds 600KB (uncompressed) | |
| if [ "$MAIN_KB" -gt 600 ]; then | |
| echo "::error::Main chunk ${MAIN_KB}KB exceeds 600KB budget" | |
| exit 1 | |
| fi | |
| # Warn if total JS exceeds 3MB | |
| if [ "$TOTAL_KB" -gt 3072 ]; then | |
| echo "::warning::Total JS ${TOTAL_KB}KB exceeds 3MB — review lazy loading" | |
| fi | |
| - name: "Bundle gate: BARRICADE 3D chunk isolated + precache-excluded" | |
| # Fails the build if the three / react-three-fiber / postprocessing stack | |
| # ever leaks out of its lazy async chunk — into the eager entry graph or | |
| # the Workbox precache manifest. That is the cost firewall behind | |
| # VITE_ENABLE_BARRICADE_3D (0 bytes for the main app / 2D-mode users). | |
| # Inert-but-passing until the 3D renderer actually imports three (PR-0c). | |
| run: npm run check:bundle | |
| # Lighthouse is gated to the Node 22 leg only (it exercises the built | |
| # bundle in a static preview; the Node *runner* version is irrelevant). | |
| # The full Playwright E2E used to run here too — it has been moved to its | |
| # own parallel job (`frontend-e2e-full`) so its ~6.5min no longer stacks | |
| # onto this job's critical path. IMPORTANT: because E2E left this job, the | |
| # `Frontend (React · Node 22)` required check no longer covers E2E — add | |
| # `Frontend E2E (chromium)` to branch protection's required checks. | |
| # Dependency audit is Node-version-independent — run once (Node 22), not | |
| # on both matrix legs. Uses the audit:ci gate (scripts/audit-ci.mjs): a | |
| # prod `npm audit --audit-level=high` minus a small, justified allowlist of | |
| # advisories that don't apply to Memba (e.g. React Router's RSC-mode CSRF — | |
| # Memba is a Vite SPA). Fails on any un-allowlisted high/critical. | |
| - name: Security audit | |
| if: matrix.node-version == '22' | |
| run: npm run audit:ci | |
| # Second lane: the same gate WITHOUT --omit=dev, i.e. the build tree. | |
| # | |
| # `--omit=dev` is the conventional boundary but not a safety boundary: | |
| # anything running during compilation can alter the emitted bundle, so | |
| # build tooling is a real supply-chain surface. Three high advisories | |
| # (js-yaml GHSA-5p4m-2wfm-xmqj, undici GHSA-4cwx-7wf7-3272, nanoid | |
| # GHSA-2v37-7h3g-55p8) sat open in 2026-08 and were invisible to the step | |
| # above BY CONSTRUCTION — they reached us only via Dependabot alerts. | |
| # | |
| # Separate step, not a flag on the one above, so a failure names which tree | |
| # is affected. Separate allowlist too (DEV_ALLOWLIST) — a "dev-only, never | |
| # shipped" waiver must not leak into the production lane. | |
| - name: Security audit (build/dev tree) | |
| if: matrix.node-version == '22' | |
| run: npm run audit:ci:dev | |
| - name: Lighthouse CI | |
| if: matrix.node-version == '22' | |
| run: | | |
| npm install -g @lhci/cli | |
| lhci autorun --config=lighthouserc.json || echo "::warning::Lighthouse budget exceeded — check artifacts" | |
| - name: Upload Lighthouse report | |
| if: always() && matrix.node-version == '22' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: lighthouse-report-node${{ matrix.node-version }} | |
| path: frontend/.lighthouseci/ | |
| retention-days: 14 | |
| # Full desktop (chromium) Playwright suite — split out of the Frontend (Node | |
| # 22) job so it runs in PARALLEL instead of stacking ~6.5min onto that job's | |
| # critical path (CI wall-clock ~12.5min → ~7-8min). The webServer is the vite | |
| # dev server (playwright.config), so no build step is needed here. | |
| # OWNER ACTION: add "Frontend E2E (chromium)" to the branch-protection | |
| # required checks so E2E keeps gating merges (it left the Node-22 job). | |
| frontend-e2e-full: | |
| name: Frontend E2E (chromium) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: frontend | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install | |
| run: npm ci | |
| - name: Install Playwright | |
| run: npx playwright install chromium --with-deps | |
| - name: E2E tests (chromium) | |
| run: npx playwright test --project=chromium --reporter=list | |
| - name: Upload E2E artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report-chromium | |
| path: | | |
| frontend/test-results/ | |
| frontend/playwright-report/ | |
| retention-days: 7 | |
| frontend-e2e: | |
| name: Frontend E2E guardrails | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: frontend | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install | |
| run: npm ci | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium webkit | |
| - name: Run E2E guardrails (desktop-intact + mobile shell) | |
| run: npm run test:e2e:guardrails | |
| workspaces: | |
| # The pnpm workspaces (packages/gno-rpc, mcp-server, mcp-server-dao-analyst) | |
| # were never compiled or tested in CI — main stayed green for months while | |
| # mcp-server-dao-analyst had a TS build break and a failing vitest test. | |
| # NOT a required check: main's branch protection lists exact check names, | |
| # so this job reports on PRs without blocking merges until it's added there. | |
| name: pnpm workspaces (gno-rpc · MCP servers) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Install | |
| run: pnpm install --frozen-lockfile | |
| # `pnpm -r` runs in topological order: @samouraiworld/gno-rpc builds | |
| # before the MCP servers that depend on it via workspace:*. Packages | |
| # without a `test` script (mcp-server) are skipped by `pnpm -r run`. | |
| - name: Build all workspaces | |
| run: pnpm -r build | |
| - name: Test all workspaces | |
| run: pnpm -r test | |
| workspaces-audit: | |
| # Nothing in CI had ever audited pnpm-lock.yaml: every audit gate in the | |
| # repo (this workflow's frontend job, security.yml, deploy-frontend.yml) | |
| # points at frontend/package-lock.json via `npm audit`. The workspace | |
| # lockfile drifted unwatched until 8 advisories had accumulated in it — two | |
| # high (ip-address GHSA-mwp4-54f8-5fhr, nanoid GHSA-2v37-7h3g-55p8, the | |
| # latter never surfaced by dependabot at all). | |
| # | |
| # Deliberately a SEPARATE job from `workspaces` above, not another step in | |
| # it: a vulnerable dependency and a broken build are different failures | |
| # wanting different owners, and a distinct check name is what branch | |
| # protection can require. It also needs no `pnpm install` — `pnpm audit` | |
| # resolves straight from the committed lockfile — so it stays a ~20s job | |
| # that reports independently of whether the build is green. | |
| # | |
| # NOT yet a required check: main's branch protection lists exact check | |
| # names and this one isn't among them, so it reports on PRs without | |
| # blocking. OWNER ACTION: add "pnpm audit (workspaces)" to the required | |
| # checks — until then this reports and does not gate. Note the trade-off | |
| # that comes with it: the gate fails CLOSED, so a registry outage blocks | |
| # merges until re-run. That is the same trade-off already accepted for the | |
| # frontend audit inside the required `Frontend (React · Node 22)` check. | |
| name: pnpm audit (workspaces) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10 | |
| # No `cache: 'pnpm'` — this job never installs, so there is no store to | |
| # restore and a cache miss/hit would only add wall-clock. | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| # The gate's own decision logic, run before the gate itself so a | |
| # regression in it surfaces as a test failure rather than as a silently | |
| # permissive audit. Node's built-in runner — no dependencies, no install. | |
| - name: Gate self-test (fail-closed logic) | |
| run: node --test scripts/pnpm-audit-ci.test.mjs | |
| # Fails on any high/critical advisory in pnpm-lock.yaml that isn't in the | |
| # documented ALLOWLIST, and fails CLOSED if the audit can't be read — a | |
| # registry or network error must never read as "no vulns". Notably does | |
| # NOT use `pnpm audit --ignore-registry-errors`, which exits 0 and prints | |
| # a bare non-JSON line when the registry is unreachable. | |
| - name: pnpm audit (high/critical, fail-closed) | |
| run: node scripts/pnpm-audit-ci.mjs | |
| proto: | |
| name: Proto (Buf) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: bufbuild/buf-action@v1 | |
| with: | |
| lint: true | |
| breaking: true | |
| breaking_against: 'https://github.com/samouraiworld/memba.git#branch=main' | |
| # Dependabot PRs run with a READ-ONLY GITHUB_TOKEN (GitHub forces this | |
| # for the pull_request event from dependabot[bot], and workflow-level | |
| # `permissions:` can't grant it back). buf-action's default | |
| # `pr_comment: true` then fails with "Resource not accessible by | |
| # integration" AFTER lint/breaking pass — turning this required check | |
| # red on every dependabot PR for a comment it can't post. Skip the | |
| # comment for dependabot; the lint/breaking gates still run and gate. | |
| pr_comment: ${{ github.actor != 'dependabot[bot]' }} | |
| docker: | |
| name: Docker Build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| # Only needs `backend`, not `frontend`. This job is a build-smoke test that | |
| # compiles the images from source — it does NOT consume the frontend job's | |
| # test results, so gating it behind the ~12min frontend leg just pushed it | |
| # onto the critical-path tail. It still runs as a required check (a broken | |
| # Dockerfile blocks merge); a frontend *test* failure is independently caught | |
| # by the required `Frontend (React · Node 22)` check. | |
| needs: [backend] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: docker/setup-buildx-action@v4 | |
| - name: Build backend image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: backend | |
| push: false | |
| cache-from: type=gha,scope=backend | |
| cache-to: type=gha,mode=max,scope=backend | |
| - name: Build frontend image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| # W6.1: context is the repo root — the vite build imports the | |
| # repo-root CHANGELOG.md (?raw) for the /changelogs page, which a | |
| # frontend-only context cannot see. | |
| context: . | |
| file: frontend/Dockerfile | |
| push: false | |
| cache-from: type=gha,scope=frontend | |
| cache-to: type=gha,mode=max,scope=frontend |