Skip to content

test: stabilize Design tutorial interaction coverage #22154

test: stabilize Design tutorial interaction coverage

test: stabilize Design tutorial interaction coverage #22154

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
# Cancel an in-progress CI run when a new commit is pushed to the same PR.
# Without this, every push starts a fresh ~20-min matrix while the prior one
# keeps running, stacking 3-4 overlapping runs on a busy PR. Keyed on the PR
# number (falling back to the ref) so runs on different PRs never cancel each
# other.
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
change-scope:
name: Determine change scope
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
docs_only: ${{ steps.scope.outputs.docs_only }}
full: ${{ steps.scope.outputs.full }}
lint: ${{ steps.scope.outputs.lint }}
typecheck: ${{ steps.scope.outputs.typecheck }}
workspace_filters: ${{ steps.scope.outputs.workspace_filters }}
fast_tests: ${{ steps.scope.outputs.fast_tests }}
content: ${{ steps.scope.outputs.content }}
core_integration: ${{ steps.scope.outputs.core_integration }}
plan_e2e: ${{ steps.scope.outputs.plan_e2e }}
brain_evals: ${{ steps.scope.outputs.brain_evals }}
brain_privacy: ${{ steps.scope.outputs.brain_privacy }}
build: ${{ steps.scope.outputs.build }}
trusted_acceptance: ${{ steps.scope.outputs.trusted_acceptance }}
scaffold: ${{ steps.scope.outputs.scaffold }}
ssr_boot: ${{ steps.scope.outputs.ssr_boot }}
guards: ${{ steps.scope.outputs.guards }}
drizzle: ${{ steps.scope.outputs.drizzle }}
qa_static: ${{ steps.scope.outputs.qa_static }}
steps:
# A name-only diff reads no historical file contents, so history can skip
# them. Worth ~5s of the ~20s checkout — modest, but every job downstream
# of this gate waits on it.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
filter: blob:none
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
- name: Test change scope classifier
run: node --experimental-strip-types --test scripts/ci-change-scope.test.ts
- name: Classify changed paths
id: scope
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CI_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node --experimental-strip-types scripts/ci-change-scope.ts
lint:
name: Lint & format
needs: change-scope
if: needs.change-scope.outputs.lint == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
filter: blob:none
# Lint only needs oxfmt + oxlint from root devDependencies; no package
# builds are required, so skip the postinstall build chain entirely.
- uses: ./.github/actions/setup-pnpm
with:
install: no-scripts
- name: Check the full tree
if: needs.change-scope.outputs.full == 'true'
run: pnpm fmt:check && pnpm oxlint
- name: Check changed source files
if: needs.change-scope.outputs.full != 'true'
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CI_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
format_files=()
lint_files=()
while IFS= read -r -d '' file; do
[ -f "$file" ] || continue
case "$file" in
# oxfmt formats Markdown too, and `fmt:check` covers the whole
# tree. Leaving .md/.mdx out here let unformatted docs land on
# main and turn this job red on every unrelated PR afterwards.
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.mts|*.cts|*.json|*.jsonc|*.css|*.md|*.mdx)
format_files+=("$file")
;;
esac
case "$file" in
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.mts|*.cts)
lint_files+=("$file")
;;
esac
done < <(git diff --name-only -z "$CI_BASE_SHA...$CI_HEAD_SHA")
if ((${#format_files[@]} > 0)); then
pnpm exec oxfmt --check "${format_files[@]}"
fi
if ((${#lint_files[@]} > 0)); then
pnpm exec oxlint --config .oxlintrc.json --no-error-on-unmatched-pattern "${lint_files[@]}"
fi
typecheck:
name: Typecheck
needs: change-scope
if: needs.change-scope.outputs.typecheck == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Typecheck affected workspaces
env:
CI_FULL: ${{ needs.change-scope.outputs.full }}
CI_WORKSPACE_FILTERS: ${{ needs.change-scope.outputs.workspace_filters }}
run: |
if [ "$CI_FULL" = "true" ]; then
pnpm typecheck
else
filters=()
while IFS= read -r filter; do
filters+=(--filter "$filter")
done < <(node -e 'for (const filter of JSON.parse(process.env.CI_WORKSPACE_FILTERS)) console.log(filter)')
pnpm -r --no-bail --if-present "${filters[@]}" run typecheck
fi
# Full-fallback PRs run the complete fast-test suite - nothing is
# change-selected, so a test can never be silently skipped. Workspace-scoped
# PRs use test-targeted below, and docs-only PRs use the focused docs job.
# Both suites are split across the same parallel lanes by
# scripts/ci-test-lanes.ts. Core is divided into Vitest shards so its large
# suite cannot become one serial tail; every other package lands in exactly
# one lane. Adding a package auto-assigns it; dropping one fails discover-lanes.
discover-lanes:
name: Plan test lanes
needs: change-scope
if: needs.change-scope.outputs.full == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
- name: Partition test packages into sharded lanes
id: plan
env:
LANES: 5
run: node --experimental-strip-types scripts/ci-test-lanes.ts
discover-targeted-lanes:
name: Plan targeted test lanes
needs: change-scope
if: needs.change-scope.outputs.fast_tests == 'true' && needs.change-scope.outputs.full != 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
has_tests: ${{ steps.plan.outputs.has_tests }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
- name: Partition affected test packages into sharded lanes
id: plan
env:
CI_WORKSPACE_FILTERS: ${{ needs.change-scope.outputs.workspace_filters }}
LANES: 5
run: node --experimental-strip-types scripts/ci-test-lanes.ts
test-rest:
name: Fast tests ${{ matrix.lane }}
needs: [change-scope, discover-lanes]
if: needs.change-scope.outputs.full == 'true'
runs-on: ubuntu-latest
# Cap the run so a hung test fails fast instead of pinning a runner for
# GitHub's 6h default. Slow DB/integration/e2e specs run in separate jobs
# below so they can run in parallel instead of serially behind fast tests.
timeout-minutes: 30
strategy:
# Report every lane's result; don't cancel siblings on the first failure.
fail-fast: false
matrix: ${{ fromJSON(needs.discover-lanes.outputs.matrix) }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Restore Playwright browser cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: playwright-${{ runner.os }}-
- name: Install Playwright Chromium for browser-backed unit tests
run: |
pnpm exec playwright install --only-shell --force chromium
pnpm --filter @agent-native/creative-context exec playwright install --only-shell --force chromium
- name: Run core fast-test shard
if: matrix.coreShard != ''
# Core is too large to keep as one package-sized lane. Vitest's shard
# partition preserves the full suite while letting the existing lanes
# execute its files in parallel.
run: >-
pnpm --filter @agent-native/core exec vitest run --dir src
--shard=${{ matrix.coreShard }}
--exclude "**/*.db.test.ts"
--exclude "**/*.integration.spec.ts"
--exclude "**/*.integration.test.ts"
--exclude "**/*.e2e.spec.ts"
--exclude "**/*.e2e.test.ts"
--exclude "**/e2e/**"
--exclude "**/*.live.spec.ts"
--exclude "**/*.live.test.ts"
--exclude "**/*.perf.spec.ts"
--exclude "**/*.perf.test.ts"
--exclude "**/create-e2e.spec.ts"
- name: Run package fast tests for lane
if: matrix.filters != ''
# --workspace-concurrency=1: one package at a time so each gets the whole
# runner (vitest's default worker count ~= all cores). Packages run their
# own test script/config unchanged. Exclusions mirror the `test:fast`
# root script; keep the lists in sync.
run: >-
pnpm -r --no-bail --workspace-concurrency=1 ${{ matrix.filters }} run test
--exclude "**/*.db.test.ts"
--exclude "**/*.integration.spec.ts"
--exclude "**/*.integration.test.ts"
--exclude "**/*.e2e.spec.ts"
--exclude "**/*.e2e.test.ts"
--exclude "**/e2e/**"
--exclude "**/*.live.spec.ts"
--exclude "**/*.live.test.ts"
--exclude "**/*.perf.spec.ts"
--exclude "**/*.perf.test.ts"
--exclude "**/create-e2e.spec.ts"
test-targeted:
name: Fast tests targeted ${{ matrix.lane }}
needs: [change-scope, discover-targeted-lanes]
if: needs.change-scope.outputs.fast_tests == 'true' && needs.change-scope.outputs.full != 'true' && needs.discover-targeted-lanes.outputs.has_tests == 'true'
runs-on: ubuntu-latest
# Keep each runner bounded to one package at a time, but shard the affected
# package set across runners. Core is also split into a Vitest shard so a
# core change does not serialize its whole dependent graph behind one job.
timeout-minutes: 30
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.discover-targeted-lanes.outputs.matrix) }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Restore Playwright browser cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: playwright-${{ runner.os }}-
- name: Install Playwright Chromium for browser-backed unit tests
run: |
pnpm exec playwright install --only-shell --force chromium
pnpm --filter @agent-native/creative-context exec playwright install --only-shell --force chromium
- name: Run core fast-test shard
if: matrix.coreShard != ''
run: >-
pnpm --filter @agent-native/core exec vitest run --dir src
--shard=${{ matrix.coreShard }}
--exclude "**/*.db.test.ts"
--exclude "**/*.integration.spec.ts"
--exclude "**/*.integration.test.ts"
--exclude "**/*.e2e.spec.ts"
--exclude "**/*.e2e.test.ts"
--exclude "**/e2e/**"
--exclude "**/*.live.spec.ts"
--exclude "**/*.live.test.ts"
--exclude "**/*.perf.spec.ts"
--exclude "**/*.perf.test.ts"
--exclude "**/create-e2e.spec.ts"
- name: Run package fast tests for lane
if: matrix.filters != ''
run: >-
pnpm -r --no-bail --if-present --workspace-concurrency=1 ${{ matrix.filters }} run test
--exclude "**/*.db.test.ts"
--exclude "**/*.integration.spec.ts"
--exclude "**/*.integration.test.ts"
--exclude "**/*.e2e.spec.ts"
--exclude "**/*.e2e.test.ts"
--exclude "**/e2e/**"
--exclude "**/*.live.spec.ts"
--exclude "**/*.live.test.ts"
--exclude "**/*.perf.spec.ts"
--exclude "**/*.perf.test.ts"
--exclude "**/create-e2e.spec.ts"
# Stable required status check. This is a fan-in gate, not another test suite.
# Point branch protection at "Fast tests", not per-lane jobs whose names vary.
fast-tests:
name: Fast tests
needs:
[
change-scope,
docs,
discover-lanes,
discover-targeted-lanes,
test-rest,
test-targeted,
]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Gate on lane results
env:
CHANGE_SCOPE_RESULT: ${{ needs.change-scope.result }}
DOCS_ONLY: ${{ needs.change-scope.outputs.docs_only }}
CI_FULL: ${{ needs.change-scope.outputs.full }}
DOCS_RESULT: ${{ needs.docs.result }}
DISCOVER_LANES_RESULT: ${{ needs.discover-lanes.result }}
DISCOVER_TARGETED_LANES_RESULT: ${{ needs.discover-targeted-lanes.result }}
TEST_REST_RESULT: ${{ needs.test-rest.result }}
TEST_TARGETED_RESULT: ${{ needs.test-targeted.result }}
run: |
if [ "$CHANGE_SCOPE_RESULT" != "success" ]; then
echo "::error::change-scope did not succeed ($CHANGE_SCOPE_RESULT)"
exit 1
fi
if [ "$DOCS_ONLY" = "true" ]; then
if [ "$DOCS_RESULT" != "success" ]; then
echo "::error::docs checks did not succeed ($DOCS_RESULT)"
exit 1
fi
echo "Docs-only change: full fast-test lanes are not required."
exit 0
fi
if [ "$CI_FULL" != "true" ]; then
if [ "$DISCOVER_TARGETED_LANES_RESULT" != "success" ]; then
echo "::error::discover-targeted-lanes did not succeed ($DISCOVER_TARGETED_LANES_RESULT)"
exit 1
fi
if [ "${{ needs.discover-targeted-lanes.outputs.has_tests }}" = "true" ]; then
if [ "$TEST_TARGETED_RESULT" != "success" ]; then
echo "::error::test-targeted did not succeed ($TEST_TARGETED_RESULT)"
exit 1
fi
echo "Targeted workspace fast tests passed."
else
if [ "$TEST_TARGETED_RESULT" != "skipped" ] && [ "$TEST_TARGETED_RESULT" != "success" ]; then
echo "::error::targeted test job did not skip cleanly ($TEST_TARGETED_RESULT)"
exit 1
fi
echo "No affected workspace has a test script; targeted fast tests were skipped."
fi
exit 0
fi
for dep in "discover-lanes:$DISCOVER_LANES_RESULT" \
"test-rest:$TEST_REST_RESULT"; do
name="${dep%%:*}"; result="${dep##*:}"
if [ "$result" != "success" ]; then
echo "::error::$name did not succeed ($result)"
exit 1
fi
done
echo "Full fast-test suite passed across all sharded lanes."
docs:
name: Docs checks
needs: change-scope
if: needs.change-scope.outputs.docs_only == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Run docs package tests
run: pnpm --filter @agent-native/docs test
- name: Typecheck docs package
run: pnpm --filter @agent-native/docs typecheck
content-parity:
name: Content parity
needs: change-scope
if: needs.change-scope.outputs.content == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Run deterministic Content parity checks
run: pnpm test:content-parity
content-db-tests:
name: Content DB tests
needs: change-scope
if: needs.change-scope.outputs.content == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Run Content DB tests
run: pnpm test:content-db
# Runs on every PR regardless of scope, so it must not wait on change-scope:
# the gate only adds a second queue wait before an unconditional job.
pglite-tests:
name: PGlite lock and migration tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Install isolated PGlite test driver
run: >-
npm install --ignore-scripts --no-save
--prefix "$RUNNER_TEMP/s2573-pglite"
@electric-sql/pglite@0.5.8
- name: Run PGlite lock and migration action semantics tests
run: node scripts/test-content-database-lock-pglite.mjs
env:
S2573_PGLITE_INSTALL_PREFIX: ${{ runner.temp }}/s2573-pglite
content-db-postgres-tests:
name: Content DB PostgreSQL locking
needs: change-scope
if: needs.change-scope.outputs.content == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: content_migration_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d content_migration_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Run Content PostgreSQL lock integration tests
run: |
pnpm test:content-db-postgres
pnpm --filter content exec vitest --run actions/upsert-database-item-by-key.db.test.ts --config vitest.config.ts
env:
CONTENT_MIGRATION_POSTGRES_URL: postgres://postgres@127.0.0.1:5432/content_migration_test
CONTENT_ROW_MUTATION_POSTGRES_URL: postgres://postgres@127.0.0.1:5432/content_migration_test
core-integration-tests:
name: Core integration tests
needs: change-scope
if: needs.change-scope.outputs.core_integration == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Restore Playwright browser cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: playwright-${{ runner.os }}-
- name: Install Playwright Chromium for browser-backed integration tests
run: |
pnpm exec playwright install --only-shell --force chromium
pnpm --filter @agent-native/creative-context exec playwright install --only-shell --force chromium
- name: Run Core integration tests
run: pnpm test:core-integration
plan-e2e-tests:
name: Plan E2E tests
needs: change-scope
if: needs.change-scope.outputs.plan_e2e == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Run Plan action E2E tests
run: pnpm test:plan-e2e
brain-evals:
name: Brain evals
needs: change-scope
if: needs.change-scope.outputs.brain_evals == 'true'
runs-on: ubuntu-latest
# Brain evals are offline action/fixture retrieval checks; live LLM evals
# belong in the gated eval workflow.
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Run brain evals
run: pnpm test:brain-evals
brain-privacy-evals:
name: Brain privacy leakage evals
needs: change-scope
if: needs.change-scope.outputs.brain_privacy == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Block privacy leaks and false positives
run: pnpm test:brain-privacy-evals
build:
name: Build
needs: change-scope
if: needs.change-scope.result == 'success' && (needs.change-scope.outputs.docs_only == 'true' || needs.change-scope.outputs.build == 'true')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Build all packages
if: needs.change-scope.outputs.full == 'true'
run: pnpm build
- name: Build affected workspaces
if: needs.change-scope.outputs.build == 'true' && needs.change-scope.outputs.full != 'true'
env:
CI_WORKSPACE_FILTERS: ${{ needs.change-scope.outputs.workspace_filters }}
run: |
filters=()
while IFS= read -r filter; do
filters+=(--filter "$filter")
done < <(node -e 'for (const filter of JSON.parse(process.env.CI_WORKSPACE_FILTERS)) console.log(filter)')
pnpm -r --no-bail --if-present "${filters[@]}" run build
- name: Build docs site
if: needs.change-scope.outputs.docs_only == 'true'
run: pnpm --filter @agent-native/docs build
trusted-acceptance:
name: Trusted acceptance substrate
needs: change-scope
if: needs.change-scope.outputs.trusted_acceptance == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-pnpm
- name: Validate config, provenance, receipts, and workflow boundaries
run: pnpm test:trusted-acceptance
- name: Build the pilot as secretless Netlify artifacts
env:
AGENT_NATIVE_BUILD_SHA: ${{ github.sha }}
NITRO_PRESET: netlify
run: |
pnpm --filter calendar build
pnpm --filter content build
test -d templates/calendar/dist
test -f templates/calendar/.netlify/functions-internal/server/main.mjs
test -d templates/content/dist
test -f templates/content/.netlify/functions-internal/server/main.mjs
scaffold-e2e:
name: Scaffold E2E — create + pnpm install
needs: change-scope
if: needs.change-scope.outputs.scaffold == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Free disk for scaffold smoke
run: |
df -h
sudo rm -rf \
/opt/ghc \
/opt/hostedtoolcache/CodeQL \
/usr/local/.ghcup \
/usr/local/lib/android \
/usr/local/share/boost \
/usr/share/dotnet \
/usr/share/swift
docker system prune -af || true
sudo apt-get clean
df -h
# postinstall rebuilds any stale/missing dists (incremental tsc is fast on
# cache hit); no separate `pnpm build` step needed.
- uses: ./.github/actions/setup-pnpm
with:
restore-dist-cache: "true"
- name: Headless scaffold — pnpm install + typecheck + action
# Guards the documented one-file headless onboarding: a fresh
# `--headless` scaffold must `pnpm typecheck` (no inherited vite/client
# types) and `pnpm action hello` (server-safe package root, no React /
# react-query in the load graph) out of the box.
run: pnpm --filter @agent-native/core exec vitest run src/cli/create-e2e.spec.ts
- name: Headless onboarding — local core install + typecheck + action
# Keep the local package-linking setup scoped to the heavyweight test.
# Applying it to the whole create-e2e file makes lightweight scaffold
# tests spend their timeout packing the local Core and Toolkit.
env:
AGENT_NATIVE_CREATE_USE_LOCAL_CORE: "1"
run: >-
pnpm --filter @agent-native/core exec vitest run
src/cli/create-e2e.spec.ts
-t "pnpm install, pnpm typecheck, and pnpm action hello all succeed"
- name: Scaffold workspace (chat + calendar + dispatch) and verify pnpm install + build
# Dispatch is in the combo because it exercises the
# workspacify.ts dispatch-rewrite path (workspace:* → "latest" for
# @agent-native/dispatch, the only published package consumed as a
# workspace app). Calendar covers the requiredPackages →
# packages/scheduling scaffold path. Together they guard the two
# distinct ways a workspace template can break `pnpm install`.
env:
# The PR's core version may not be published to npm yet. Exercise
# the scaffold against the core build checked out by this job.
AGENT_NATIVE_CREATE_USE_LOCAL_CORE: "1"
run: |
cd "$(mktemp -d)"
node "$GITHUB_WORKSPACE/packages/core/dist/cli/index.js" create test-workspace --template chat,calendar,dispatch
cd test-workspace
# Required package must be scaffolded
if [ ! -d "packages/scheduling" ]; then
echo "::error::packages/scheduling not scaffolded for calendar template"
exit 1
fi
# Root must have postinstall for required packages
if ! grep -q 'postinstall' package.json; then
echo "::error::postinstall script missing for required packages"
exit 1
fi
# Every workspace:* dep in scaffolded apps must resolve to a
# package actually present in the workspace. Catches the class of
# bug Sami hit: a template depended on @agent-native/dispatch
# (a published package, not a workspace member) and `pnpm install`
# blew up with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND.
node -e '
const fs = require("fs");
const path = require("path");
const wsMembers = new Set();
for (const dir of ["apps", "packages"]) {
if (!fs.existsSync(dir)) continue;
for (const sub of fs.readdirSync(dir)) {
const pj = path.join(dir, sub, "package.json");
if (fs.existsSync(pj)) {
const n = JSON.parse(fs.readFileSync(pj, "utf8")).name;
if (n) wsMembers.add(n);
}
}
}
const offenders = [];
for (const dir of ["apps", "packages"]) {
if (!fs.existsSync(dir)) continue;
for (const sub of fs.readdirSync(dir)) {
const pj = path.join(dir, sub, "package.json");
if (!fs.existsSync(pj)) continue;
const pkg = JSON.parse(fs.readFileSync(pj, "utf8"));
for (const dt of ["dependencies","devDependencies","peerDependencies"]) {
for (const [name, ver] of Object.entries(pkg[dt] || {})) {
if (typeof ver === "string" && ver.startsWith("workspace:")) {
if (!wsMembers.has(name)) {
offenders.push(pj + ": " + dt + "." + name + "=" + ver);
}
}
}
}
}
}
if (offenders.length) {
console.error("::error::dangling workspace:* refs in scaffold (no matching workspace member):");
for (const o of offenders) console.error(" " + o);
process.exit(1);
}
'
pnpm install
# Build the calendar app — catches missing peer/runtime deps that
# `pnpm install` alone won't surface (e.g. core importing a peer
# dep that templates don't declare).
pnpm --filter ./apps/calendar build
ssr-boot-smoke:
name: SSR cold-start smoke
needs: change-scope
if: needs.change-scope.outputs.ssr_boot == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Skip postinstall build chain here — core is built explicitly below so we
# control the exact build order and avoid redundant parallel builds.
- uses: ./.github/actions/setup-pnpm
with:
install: no-scripts
# Build the extracted recap CLI before core so its compatibility shim is
# available to source imports, then produce the real serverless bundle.
- name: Build recap CLI, AgentKit, toolkit, core, and creative context
run: |
pnpm --filter @agent-native/recap-cli build
pnpm --filter @agent-native/agentkit build
pnpm --filter @agent-native/toolkit build
pnpm --filter @agent-native/core build
pnpm --filter @agent-native/creative-context build
- name: Build Content template (Cloudflare Pages preset)
run: NITRO_PRESET=cloudflare_pages pnpm --filter content build
# Three templates, one per size class, because building all 17 per PR is
# too slow: Plan covers diagram-heavy SSR, Clips covers public-share SSR,
# and Assets is the one that legitimately depends on creative-context — so
# it is the only build that exercises the browser-runtime size budget.
- name: Build Plan, Clips, and Assets templates (netlify preset)
run: NITRO_PRESET=netlify pnpm --filter plan --filter clips --filter assets build
- name: Import SSR handler — must not crash at cold-start
# External timeout is a backstop against a pathological synchronous hang;
# the script itself races module eval against a 30s window and force-exits.
# Match the deployed Netlify environment so local setup cannot call
# process.exit during this import-only probe.
# This lane intentionally probes the three built representatives above.
# The smoke script's optional `--report-uncovered` mode is fail-closed;
# use it only when the caller intends to gate on full template coverage.
run: NETLIFY=true timeout 180 node scripts/ssr-boot-smoke.mjs plan clips assets
# Exercise the two other Node serverless presets as well. These import
# their generated handlers, so an unresolved bare runtime dependency
# fails before a deployment can be published.
- name: Build and import Plan SSR handlers (Vercel and AWS Lambda)
run: |
NITRO_PRESET=vercel pnpm --filter plan build
VERCEL=true timeout 120 node scripts/ssr-boot-smoke.mjs --preset vercel plan
NITRO_PRESET=aws-lambda pnpm --filter plan build
AWS_LAMBDA_FUNCTION_NAME=ssr-smoke timeout 120 node scripts/ssr-boot-smoke.mjs --preset aws-lambda plan
guards:
name: Security guards
needs: change-scope
if: needs.change-scope.outputs.guards == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# Diff-scoped guards resolve GITHUB_BASE_REF to origin/main.
fetch-depth: 0
# Guard scripts only need dependencies and should not depend on package
# lifecycle builds; skipping scripts avoids native optional builds here.
- uses: ./.github/actions/setup-pnpm
with:
install: no-scripts
- name: Test production deploy concurrency guard
run: pnpm test:netlify-prebuilt-workflow
- name: Test serverless function size baseline guard
run: pnpm test:function-size-baseline
- name: Test AgentKit stream ownership guard
run: pnpm test:agentkit-stream-ownership
# Source-backed guards import published-style package entrypoints, so
# build the workspace packages they inspect before loading locale data.
- name: Build packages for source-backed guards
run: |
pnpm --filter @agent-native/recap-cli build
pnpm --filter @agent-native/toolkit build
pnpm --filter @agent-native/agentkit build
pnpm --filter @agent-native/core build
pnpm --filter @agent-native/creative-context build
- name: Run security guards
run: pnpm guards
guard-no-drizzle-push:
name: Guard — no drizzle-kit push in build paths
needs: change-scope
if: needs.change-scope.outputs.drizzle == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
# No install needed — the guard script only uses Node builtins.
- name: Scan netlify.toml + package.json for `drizzle-kit push`
run: node scripts/guard-no-drizzle-push.mjs
qa-static:
name: QA — static template checks
needs: change-scope
if: needs.change-scope.outputs.qa_static == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# These checks read source files only — no DB, browser, network, or
# secrets — so they're safe to run on every PR. (The heavier harnesses —
# test:mcp:e2e, qa:public-share, qa:composer-geometry — need a live
# server / DB / browser and belong in a separate, set-up job.)
- uses: ./.github/actions/setup-pnpm
with:
install: no-scripts
- name: Template route matrix (fails on drift)
run: pnpm qa:template-routes
- name: Template action audit (advisory)
run: pnpm actions:audit