test: stabilize Design tutorial interaction coverage #22154
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] | |
| # 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 |