Skip to content

Transaction links point somewhere real; mobile marketplace spec stays offline #3368

Transaction links point somewhere real; mobile marketplace spec stays offline

Transaction links point somewhere real; mobile marketplace spec stays offline #3368

Workflow file for this run

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