Skip to content

feat(ci): guard against tracking generated artifacts (the footgun tha… #1070

feat(ci): guard against tracking generated artifacts (the footgun tha…

feat(ci): guard against tracking generated artifacts (the footgun tha… #1070

Workflow file for this run

name: Test & Quality
on:
push:
branches: [develop]
pull_request:
branches: [main, develop]
schedule:
- cron: "0 4 * * 1"
permissions:
contents: read
actions: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Opt-in early to the post-Node20 runner baseline to catch incompatibilities now.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
changes:
runs-on: ubuntu-latest
outputs:
base_sha: ${{ steps.detect.outputs.base_sha }}
head_sha: ${{ steps.detect.outputs.head_sha }}
turbo_filter: ${{ steps.detect.outputs.turbo_filter }}
ci_changes: ${{ steps.detect.outputs.ci_changes }}
code_changes: ${{ steps.detect.outputs.code_changes }}
tractor_gates: ${{ steps.detect.outputs.tractor_gates }}
tractor_heavy_gates: ${{ steps.detect.outputs.tractor_heavy_gates }}
run_build: ${{ steps.detect.outputs.run_build }}
run_e2e: ${{ steps.detect.outputs.run_e2e }}
run_web_smoke: ${{ steps.detect.outputs.run_web_smoke }}
run_task_smoke: ${{ steps.detect.outputs.run_task_smoke }}
run_dispatch_surface_ci: ${{ steps.detect.outputs.run_dispatch_surface_ci }}
run_audit: ${{ steps.detect.outputs.run_audit }}
run_deep: ${{ steps.detect.outputs.run_deep }}
text_quality_changes: ${{ steps.detect.outputs.text_quality_changes }}
skip_duplicate_push: ${{ steps.detect.outputs.skip_duplicate_push }}
quality_cache_key: ${{ steps.signatures.outputs.quality_key }}
e2e_cache_key: ${{ steps.signatures.outputs.e2e_key }}
tractor_heavy_cache_key: ${{ steps.signatures.outputs.tractor_heavy_key }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Detect changed areas
id: detect
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
skip_duplicate_push=false
run_deep=false
if [ "${{ github.event_name }}" = "schedule" ]; then
run_deep=true
fi
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ contains(github.event.pull_request.labels.*.name, 'ci:deep') }}" = "true" ]; then
run_deep=true
fi
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref_name }}" = "develop" ]; then
open_pr_count="$(gh api -X GET "repos/$GITHUB_REPOSITORY/pulls" -f state=open -f head="$GITHUB_REPOSITORY_OWNER:$GITHUB_REF_NAME" --jq 'length' 2>/dev/null || gh pr list --repo "$GITHUB_REPOSITORY" --head "$GITHUB_REF_NAME" --state open --json number --jq 'length' 2>/dev/null || echo 0)"
if [ "${open_pr_count:-0}" != "0" ]; then
echo "::notice::Skipping heavy push validation for develop because an open PR already validates this head branch. open_pr_count=$open_pr_count"
echo "skip_duplicate_push=true" >> "$GITHUB_OUTPUT"
echo "base_sha=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
echo "head_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "turbo_filter=" >> "$GITHUB_OUTPUT"
echo "ci_changes=false" >> "$GITHUB_OUTPUT"
echo "code_changes=false" >> "$GITHUB_OUTPUT"
echo "tractor_gates=false" >> "$GITHUB_OUTPUT"
echo "tractor_heavy_gates=false" >> "$GITHUB_OUTPUT"
echo "run_build=false" >> "$GITHUB_OUTPUT"
echo "run_e2e=false" >> "$GITHUB_OUTPUT"
echo "run_task_smoke=false" >> "$GITHUB_OUTPUT"
echo "run_dispatch_surface_ci=false" >> "$GITHUB_OUTPUT"
echo "run_audit=false" >> "$GITHUB_OUTPUT"
echo "run_deep=false" >> "$GITHUB_OUTPUT"
echo "text_quality_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
echo "skip_duplicate_push=false" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
else
BASE_SHA="${{ github.event.before }}"
HEAD_SHA="${{ github.sha }}"
# First push on branch may not have github.event.before resolved.
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
BASE_SHA="$(git rev-parse HEAD^ 2>/dev/null || true)"
fi
fi
if [ -n "$BASE_SHA" ] && ! git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then
echo "::warning::BASE_SHA $BASE_SHA is not available in this clone. This can happen after a force-with-lease branch alignment. Falling back to HEAD^ when available."
BASE_SHA="$(git rev-parse HEAD^ 2>/dev/null || true)"
fi
if [ -z "$BASE_SHA" ]; then
echo "::warning::Could not resolve BASE_SHA. Falling back to full checks."
echo "base_sha=" >> "$GITHUB_OUTPUT"
echo "head_sha=${HEAD_SHA:-}" >> "$GITHUB_OUTPUT"
echo "turbo_filter=" >> "$GITHUB_OUTPUT"
echo "ci_changes=true" >> "$GITHUB_OUTPUT"
echo "code_changes=true" >> "$GITHUB_OUTPUT"
echo "tractor_gates=true" >> "$GITHUB_OUTPUT"
echo "tractor_heavy_gates=true" >> "$GITHUB_OUTPUT"
echo "run_build=false" >> "$GITHUB_OUTPUT"
echo "run_e2e=true" >> "$GITHUB_OUTPUT"
echo "run_task_smoke=true" >> "$GITHUB_OUTPUT"
echo "run_dispatch_surface_ci=true" >> "$GITHUB_OUTPUT"
echo "run_audit=true" >> "$GITHUB_OUTPUT"
echo "run_deep=$run_deep" >> "$GITHUB_OUTPUT"
echo "text_quality_changes=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > /tmp/refarm-changed-files.txt
echo "Changed files (base=$BASE_SHA head=$HEAD_SHA):"
cat /tmp/refarm-changed-files.txt || true
has_match() {
local pattern="$1"
grep -Eq "$pattern" /tmp/refarm-changed-files.txt
}
ci_changes=false
if has_match '^(\.github/workflows/|\.github/actions/)'; then
ci_changes=true
fi
code_changes=false
if has_match '^(apps/|packages/|examples/|validations/|templates/|scripts/|turbo\.json$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig\.json$)'; then
code_changes=true
fi
tractor_gates=false
if has_match '^(packages/tractor/|packages/tractor-ts/|packages/agent/|packages/barn/|packages/plugin-manifest/|packages/storage-sqlite/|packages/storage-rest/|packages/sync-loro/|turbo\.json$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts/ci/smoke-runtime-descriptor-release-path\.mjs)'; then
tractor_gates=true
fi
tractor_heavy_gates=false
# Rust benchmark + coverage depend on Rust sources + Cargo.lock, NOT on pnpm-lock.yaml (JS deps).
# A JS-only lockfile change must not trigger the ~7min heavy Rust gates. pnpm-workspace.yaml is
# likewise omitted here: it can affect JS builds (covered by code_changes → build-wasm) but not
# Rust benchmark performance.
if has_match '^(packages/tractor/|packages/tractor-ts/|packages/agent/|packages/barn/|packages/plugin-manifest/|packages/storage-sqlite/|packages/storage-rest/|packages/sync-loro/|turbo\.json$|scripts/ci/smoke-runtime-descriptor-release-path\.mjs)'; then
tractor_heavy_gates=true
fi
# Standalone build artifact job is retired for normal PR validation.
# General build correctness is covered inside quality; E2E runs through
# Turbo and owns any build dependencies it needs in the E2E runner.
run_build=false
run_e2e=false
if has_match '^(apps/|validations/|packages/tractor/|packages/tractor-ts/|playwright\.config\.|turbo\.json$|package\.json$|pnpm-lock\.yaml$)'; then
run_e2e=true
fi
run_task_smoke=false
if has_match '^(apps/farmhand/|apps/refarm/|packages/context-provider-v1/|packages/effort-contract-v1/|packages/file-stream-transport/|packages/agent/|packages/plugin-wit/|packages/registry/|packages/sse-stream-transport/|packages/stream-contract-v1/|packages/tractor-ts/|packages/ws-stream-transport/|scripts/ci/smoke-task-execution-loop\.mjs|scripts/ci/smoke-task-agent-respond\.mjs|scripts/ci/smoke-refarm-agent-model-mock\.mjs|scripts/lib/cargo-target\.mjs|scripts/ci/subprocess-utils\.mjs|scripts/ci/esm-extension-loader\.mjs|scripts/ci/esm-extension-register\.mjs|scripts/ci/task-smoke-daemon\.mjs|scripts/ci/task-smoke-cli\.mjs|scripts/ci/refarm-telemetry-gate\.mjs|package\.json$|pnpm-lock\.yaml$|\.github/workflows/test\.yml$)'; then
run_task_smoke=true
fi
# The examples' web faces boot in a REAL browser only — jsdom cannot catch the B1 class
# (a node import that crashes a browser bundle at module-init). Fire the real-browser
# smoke when a face, its browser-safe framework, or the smoke itself changes.
run_web_smoke=false
if has_match '^(examples/|packages/capability-homestead-surface/|packages/capabilities/|scripts/ci/web-face-smoke\.mjs|\.github/workflows/test\.yml$)'; then
run_web_smoke=true
fi
dispatch_surface_change_check=$(bash scripts/ci/release-dispatch-surface-build.sh "$BASE_SHA" "$HEAD_SHA" --check)
run_dispatch_surface_ci=false
if echo "$dispatch_surface_change_check" | grep -q '^run_dispatch_surface_ci=true'; then
run_dispatch_surface_ci=true
fi
run_audit=false
if [ "$code_changes" = "true" ]; then
run_audit=true
fi
if [ "${{ github.event_name }}" = "schedule" ]; then
run_audit=true
fi
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.base_ref }}" = "main" ]; then
run_audit=true
fi
text_quality_changes=false
if has_match '^(scripts/ci/(check-text-quality|test-text-quality-lib|text-quality-config-schema|text-quality-lib)\.mjs|docs/(ENVIRONMENT_SUBSTRATE_INVENTORY|NIXOS_CURATED_RUNTIME_STRATEGY|POC_VALIDATION_PRESSURE|POC_PRIZE_READINESS|POC_WRITING_HANDOFF|TEXT_QUALITY_CONFIG|VAULT_SEED_CONVERGENCE)\.md)$'; then
text_quality_changes=true
fi
turbo_filter="...[$BASE_SHA]"
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "turbo_filter=$turbo_filter" >> "$GITHUB_OUTPUT"
echo "ci_changes=$ci_changes" >> "$GITHUB_OUTPUT"
echo "code_changes=$code_changes" >> "$GITHUB_OUTPUT"
echo "tractor_gates=$tractor_gates" >> "$GITHUB_OUTPUT"
echo "tractor_heavy_gates=$tractor_heavy_gates" >> "$GITHUB_OUTPUT"
echo "run_build=$run_build" >> "$GITHUB_OUTPUT"
echo "run_e2e=$run_e2e" >> "$GITHUB_OUTPUT"
echo "run_web_smoke=$run_web_smoke" >> "$GITHUB_OUTPUT"
echo "run_task_smoke=$run_task_smoke" >> "$GITHUB_OUTPUT"
echo "run_dispatch_surface_ci=$run_dispatch_surface_ci" >> "$GITHUB_OUTPUT"
echo "run_audit=$run_audit" >> "$GITHUB_OUTPUT"
echo "run_deep=$run_deep" >> "$GITHUB_OUTPUT"
echo "text_quality_changes=$text_quality_changes" >> "$GITHUB_OUTPUT"
echo "::notice::changes: ci=$ci_changes code=$code_changes tractor_gates=$tractor_gates tractor_heavy_gates=$tractor_heavy_gates build=$run_build e2e=$run_e2e task_smoke=$run_task_smoke dispatch-surface-ci=$run_dispatch_surface_ci audit=$run_audit deep=$run_deep text_quality=$text_quality_changes filter=$turbo_filter"
- name: Compute validation signatures
id: signatures
shell: bash
run: |
set -euo pipefail
cat > /tmp/refarm-quality-signature-patterns.txt <<'EOF'
apps
packages
examples
validations
templates
scripts
.github/actions
.github/workflows/test.yml
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
turbo.json
tsconfig.json
.cargo/config.toml
EOF
cat > /tmp/refarm-e2e-signature-patterns.txt <<'EOF'
apps
packages
validations
templates
playwright.config.js
playwright.config.mjs
playwright.config.ts
.github/actions/setup
.github/workflows/test.yml
scripts/ci/content-signature.mjs
package.json
pnpm-lock.yaml
turbo.json
tsconfig.json
.cargo/config.toml
EOF
cat > /tmp/refarm-tractor-heavy-signature-patterns.txt <<'EOF'
packages/tractor
packages/tractor-ts
scripts/ci/fetch-runtime-descriptor-revocation-baseline.mjs
scripts/ci/smoke-runtime-descriptor-release-path.mjs
package.json
turbo.json
.cargo/config.toml
.github/actions/setup
.github/workflows/test.yml
EOF
node scripts/ci/content-signature.mjs \
--name test-quality \
--output quality_key \
--patterns-file /tmp/refarm-quality-signature-patterns.txt
node scripts/ci/content-signature.mjs \
--name test-e2e \
--output e2e_key \
--patterns-file /tmp/refarm-e2e-signature-patterns.txt
node scripts/ci/content-signature.mjs \
--name tractor-heavy \
--output tractor_heavy_key \
--patterns-file /tmp/refarm-tractor-heavy-signature-patterns.txt
validate-scaffold:
name: Validate package scaffold and action pins
runs-on: ubuntu-latest
timeout-minutes: 5
needs: changes
if: needs.changes.outputs.code_changes == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Validate package scaffold
run: node scripts/validate-packages.mjs
- name: Validate GitHub Action pins
run: node scripts/ci/check-github-action-pins.mjs
- name: Validate GitHub Action contracts
run: node scripts/ci/check-github-actions-contracts.mjs
# The Rust → WASM plugin packages (agent, tractor-rs, delegate, heartwood,
# lsp-code-ops, scarecrow-plugin) build through `cargo component` + a `check:wit`
# gate that needs the Rust toolchain and the standalone wasm-tools binary. The
# JS `quality` job has neither, so their `build` only ever succeeded there on a
# warm Turbo remote-cache hit — a cold cache failed the whole Verify step. This
# job gives those builds a real home: it provisions the toolchain (rust-target)
# and runs their Turbo `build`, populating the shared remote cache so `quality`
# (which depends on it) resolves them as cache hits instead of rebuilding blind.
build-wasm:
runs-on: ubuntu-latest
# 25 → 35: the runtime e2e smoke rides this job; a cold rust-cache pays the full
# tractor release build before the smoke starts.
timeout-minutes: 35
needs: changes
if: needs.changes.outputs.skip_duplicate_push != 'true' && (needs.changes.outputs.code_changes == 'true' || needs.changes.outputs.run_task_smoke == 'true')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Setup
uses: ./.github/actions/setup
with:
rust-target: "wasm32-wasip1"
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Build Rust/WASM plugin packages (warms the Turbo remote cache)
run: >-
pnpm turbo run build
--filter=@refarm.dev/agent
--filter=@refarm.dev/tractor-rs
--filter=@refarm.dev/delegate
--filter=@refarm.dev/heartwood
--filter=@refarm.dev/lsp-code-ops
--filter=@refarm.dev/scarecrow-plugin
--cache-dir=.turbo --output-logs=new-only --ui=stream
# The no-token runtime e2e smoke runs HERE because this is the one PR job with the
# wasm toolchain and the tractor build already provisioned. The direct pnpm build
# (not turbo) guarantees the release binary exists even on a turbo cache hit — the
# tractor build task declares no outputs, so a hit restores nothing; cargo itself
# is the incremental no-op when the rust-cache is warm.
- name: Ensure the tractor release binary (cargo no-op when fresh)
if: needs.changes.outputs.run_task_smoke == 'true' || needs.changes.outputs.tractor_gates == 'true'
run: pnpm --filter @refarm.dev/tractor-rs run build
# The smoke wrapper builds its leaves with bare workspace scripts (no turbo), so
# the app's workspace dependencies must exist as dist first — turbo builds the
# closure in dependency order, mostly as remote-cache hits.
- name: Build the e2e smoke closure (apps/refarm + model-mock, with dependencies)
if: needs.changes.outputs.run_task_smoke == 'true' || needs.changes.outputs.tractor_gates == 'true'
run: >-
pnpm turbo run build
--filter=@refarm.dev/refarm
--filter=@refarm.dev/model-mock
--cache-dir=.turbo --output-logs=new-only --ui=stream
- name: Runtime agent e2e smoke (no-token, model-mock)
if: needs.changes.outputs.run_task_smoke == 'true' || needs.changes.outputs.tractor_gates == 'true'
run: pnpm run refarm:agent:e2e:mock
# Proof-in-CI for the WASM component features. The reference components
# (sovereign identity, vault surface, quality checker) transpile their WASM to
# a gitignored `pkg/`; their suites SKIP (describe.skipIf(!componentBuilt)) when
# it is absent — which is every other job (no Rust toolchain). This job BUILDS
# the components, so those suites actually RUN here: it is the only place the T2
# sovereign-identity + T3 vault + quality-checker claims are verified in CI.
wasm-component-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: changes
if: needs.changes.outputs.skip_duplicate_push != 'true' && (needs.changes.outputs.code_changes == 'true' || needs.changes.outputs.run_task_smoke == 'true')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Setup
uses: ./.github/actions/setup
with:
rust-target: "wasm32-wasip1"
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Build the component packages + their TS consumers
run: >-
pnpm turbo run build
--filter=@refarm.dev/identity-provider-ref
--filter=@refarm.dev/vault-surface-ref
--filter=@refarm.dev/quality-checker-ref
--filter=@refarm.dev/quality-checker-plugin
--filter=@refarm.dev/wallet
--cache-dir=.turbo --output-logs=new-only --ui=stream
- name: Transpile the WASM components (produces the pkg/ the suites gate on)
run: >-
pnpm --workspace-concurrency=1
--filter=@refarm.dev/identity-provider-ref
--filter=@refarm.dev/vault-surface-ref
--filter=@refarm.dev/quality-checker-ref
--filter=@refarm.dev/quality-checker-plugin
run build:component
- name: Run the gated WASM component suites (now that pkg/ exists)
run: >-
pnpm --workspace-concurrency=1
--filter=@refarm.dev/identity-provider-ref
--filter=@refarm.dev/vault-surface-ref
--filter=@refarm.dev/quality-checker-ref
--filter=@refarm.dev/quality-checker-plugin
--filter=@refarm.dev/wallet
run test
quality:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [changes, build-wasm]
# Run whether build-wasm ran or was skipped (docs-only commits skip it), but
# not if it FAILED — a broken Rust/WASM build must block quality rather than
# let it rebuild blind. `!failure() && !cancelled()` allows the skipped case
# through; the skip_duplicate_push gate is preserved.
if: ${{ !failure() && !cancelled() && needs.changes.outputs.skip_duplicate_push != 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Restore Test & Quality result cache
id: quality_cache
if: needs.changes.outputs.quality_cache_key != ''
uses: ./.github/actions/cache
with:
path: .artifacts/validation-cache/test-quality
key: ${{ needs.changes.outputs.quality_cache_key }}
- name: Test & Quality cache hit
if: steps.quality_cache.outputs.cache-hit == 'true'
run: echo "::notice::Reusing previous successful Test & Quality validation for unchanged inputs."
- name: Validate .project cross-block consistency
run: node scripts/ci/project-block-consistency.mjs
- name: Text quality verify
if: needs.changes.outputs.text_quality_changes == 'true'
run: |
node --test scripts/ci/test-text-quality-lib.mjs
node scripts/ci/check-text-quality.mjs docs/ENVIRONMENT_SUBSTRATE_INVENTORY.md docs/NIXOS_CURATED_RUNTIME_STRATEGY.md docs/POC_VALIDATION_PRESSURE.md docs/POC_PRIZE_READINESS.md docs/POC_WRITING_HANDOFF.md docs/TEXT_QUALITY_CONFIG.md docs/VAULT_SEED_CONVERGENCE.md
- name: Setup
id: setup
if: steps.quality_cache.outputs.cache-hit != 'true' && (needs.changes.outputs.code_changes == 'true' || needs.changes.outputs.run_task_smoke == 'true' || needs.changes.outputs.run_dispatch_surface_ci == 'true')
uses: ./.github/actions/setup
with:
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
rust-coverage: ${{ needs.changes.outputs.tractor_heavy_gates }}
# Turbo Cache is now handled in ./.github/actions/setup
# - name: Cache Turbo
# uses: actions/cache@...
# with:
# path: .turbo
# ...
- name: Security audit
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes == 'true'
run: pnpm audit --audit-level=high
- name: Dispatch-surface parity CI
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.run_dispatch_surface_ci == 'true'
run: pnpm run dispatch-surface:ci
- name: TSConfig preflight
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes == 'true'
run: pnpm run tsconfig:guard
- name: Carry-forward policy unit tests
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.ci_changes == 'true'
run: node --test scripts/ci/test-carry-forward-status-lib.mjs
- name: Farmhand task execution smoke (CLI ↔ sidecar)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.run_task_smoke == 'true'
run: pnpm run task:execution:smoke
- name: Farmhand agent respond smoke (effort round-trip)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.run_task_smoke == 'true'
run: pnpm run task:execution:smoke:agent
- name: Telemetry strict gate (blocking on develop + PR -> main)
id: telemetry_gate
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.run_task_smoke == 'true'
continue-on-error: ${{ !((github.event_name == 'pull_request' && github.base_ref == 'main') || (github.event_name == 'push' && github.ref_name == 'develop')) }}
run: pnpm run refarm:telemetry:gate:ci -- --out .artifacts/telemetry/gate-quality.json --timeout-ms 20000
- name: Telemetry gate warning (non-blocking branches)
if: always() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.run_task_smoke == 'true' && steps.telemetry_gate.outcome == 'failure' && !((github.event_name == 'pull_request' && github.base_ref == 'main') || (github.event_name == 'push' && github.ref_name == 'develop'))
run: |
echo "::warning::Telemetry gate failed on non-blocking branch/event."
echo "::warning::See artifact: telemetry-gate-report"
- name: Quality checks skipped
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes != 'true'
run: echo "::notice::Skipping quality heavy checks (no code-impacting changes detected)."
- name: Tractor health probe smoke
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
run: pnpm --filter @refarm.dev/tractor-rs run test:smoke:health
- name: Browser runtime descriptor gate (Tractor TS)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
run: pnpm -C packages/tractor-ts run runtime-module:ci
- name: Revocation diagnostics report smoke (Tractor TS)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
run: pnpm run runtime-descriptor:revocation-report:sample
- name: Revocation diagnostics baseline lookup (Tractor TS)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
node scripts/ci/fetch-runtime-descriptor-revocation-baseline.mjs \
--workflow-name "${{ github.workflow }}" \
--branch "${{ github.ref_name }}" \
--exclude-run-id "${{ github.run_id }}" \
--current-report .artifacts/runtime-descriptor-revocation-report/summary.json \
--reports-file .artifacts/runtime-descriptor-revocation-history/reports.txt
- name: Revocation diagnostics history smoke (Tractor TS)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
run: |
pnpm run runtime-descriptor:revocation-history -- \
--reports-file .artifacts/runtime-descriptor-revocation-history/reports.txt \
--out-dir .artifacts/runtime-descriptor-revocation-history
- name: Restore Tractor heavy result cache
id: tractor_heavy_cache
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && needs.changes.outputs.tractor_heavy_cache_key != ''
uses: ./.github/actions/cache
with:
path: .artifacts/validation-cache/tractor-heavy
key: ${{ needs.changes.outputs.tractor_heavy_cache_key }}
- name: Tractor heavy cache hit
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit == 'true'
run: echo "::notice::Reusing previous successful Tractor benchmark/coverage validation for unchanged heavy inputs."
# --continue: run ALL tasks even after one fails, so a cache-invalidation that surfaces several
# latent failures reports them in ONE run instead of one-per-push (the "cache roulette"). The gate
# still fails if any task fails; it just no longer stops at the first.
- name: Verify (Full Turbo)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes == 'true' && needs.changes.outputs.turbo_filter != ''
run: pnpm turbo run build web:build lint type-check test test:coverage test:unit test:integration --filter=${{ needs.changes.outputs.turbo_filter }} --cache-dir=.turbo --output-logs=new-only --ui=stream --continue
- name: Verify (Full Turbo fallback)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes == 'true' && needs.changes.outputs.turbo_filter == ''
run: pnpm turbo run build web:build lint type-check test test:coverage test:unit test:integration --cache-dir=.turbo --output-logs=new-only --ui=stream --continue
- name: Verify (Full Turbo) skipped
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes != 'true'
run: echo "::notice::Skipping Full Turbo verification (no code-impacting changes detected)."
# Runs AFTER Verify (the build phase): it is a release-path validation whose tractor-ts
# resolver tests import workspace packages via their built dist, so it belongs after the build,
# not before it. Verify's build is FILTERED (--filter=...[base]) and so does not guarantee a
# dependency like @refarm.dev/registry is built for a tractor-rs-only change — the smoke keeps
# its own `turbo run build --filter=@refarm.dev/tractor` guarantee, which here is a warm
# cache-hit rather than the cold build it used to be when this step ran before Verify.
- name: Runtime descriptor release-path smoke (Tractor TS)
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
run: pnpm run runtime-descriptor:release-smoke -- --sha "${{ github.sha }}"
- name: Append detailed Vitest report
if: always() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes == 'true'
run: node scripts/ci/summarize-vitest-reports.mjs
- name: Upload Vitest detailed report artifact
if: always() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.code_changes == 'true'
uses: ./.github/actions/upload-artifact
with:
name: vitest-detailed-report
path: .artifacts/vitest
retention-days: 14
if-no-files-found: warn
# Specialized gates for Tractor (Performance & Coverage)
# These remain separate to use the GHA script logic but are fast due to cache hits above
- name: Benchmark Quality Gate (Tractor)
id: benchmark
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit != 'true'
run: |
BENCH_BASELINE_COPY="$(mktemp)"
BASELINE_READY=false
# Prefer the checked-out branch baseline to allow intentional benchmark
# re-baselining in the same PR, avoiding non-deterministic branch
# switching behavior.
if [ -f "packages/tractor/benchmarks/baseline.json" ]; then
cp packages/tractor/benchmarks/baseline.json "$BENCH_BASELINE_COPY"
echo "::notice::Using checked-out Tractor baseline for benchmark comparison."
BASELINE_READY=true
elif git show "origin/main:packages/tractor/benchmarks/baseline.json" > "$BENCH_BASELINE_COPY" 2>/dev/null; then
echo "::notice::Falling back to committed Tractor baseline from main for benchmark comparison."
BASELINE_READY=true
else
echo "::warning::Could not read local or main baseline from git."
fi
pnpm install --ignore-scripts --frozen-lockfile
cd packages/tractor
if [ "$BASELINE_READY" = "true" ]; then
cp "$BENCH_BASELINE_COPY" benchmarks/baseline.json
fi
pnpm run bench:check
# dispatch-stress suite: uses its own committed baseline (baseline-dispatch.json)
# to guard the async-dispatch drain cost against regression.
pnpm run bench:check:dispatch
# grant suite: committed baseline (baseline-grant.json) guards that per-load
# grant resolution (fs ∩ node, deny-dominates) never makes plugin load pathological.
pnpm run bench:check:grant
# Export payload to GHA env vars if the script exited cleanly
if [ -f "benchmarks/gha-payload.json" ]; then
IMPROVED=$(jq -r '.improved' benchmarks/gha-payload.json)
DIFF=$(jq -r '.diff' benchmarks/gha-payload.json)
THRESH=$(jq -r '.threshold' benchmarks/gha-payload.json)
echo "improved=$IMPROVED" >> $GITHUB_OUTPUT
echo "diff=$DIFF" >> $GITHUB_OUTPUT
echo "threshold=$THRESH" >> $GITHUB_OUTPUT
fi
- name: Comment PR on Benchmark Increase
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && steps.benchmark.outputs.improved == 'true'
continue-on-error: true
uses: ./.github/actions/github-script
with:
script: |
const diff = parseFloat('${{ steps.benchmark.outputs.diff }}');
const threshold = parseFloat('${{ steps.benchmark.outputs.threshold }}');
// Fetch existing comments from the bot
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComments = comments.filter(c => c.user.login === 'github-actions[bot]' && c.body.includes('🚀 Tractor Performance Increased!'));
// Find the high-water mark from previous comments
let highWaterMark = threshold;
const regex = /\+\*\*([0-9.]+)\%\*\* Average Improvement/;
for (const comment of botComments) {
const match = comment.body.match(regex);
if (match && match[1]) {
const pct = parseFloat(match[1]);
if (pct > highWaterMark) {
highWaterMark = pct;
}
}
}
// Only comment if the current performance strictly beats the highest water mark
if (diff > highWaterMark) {
let body = `### 🚀 Tractor Performance Increased!\n\n`;
body += `Great job! You've successfully optimized the Tractor Engine core.\n\n`;
body += `- **Threshold Required:** +${threshold}%\n`;
body += `- **Previous High-Water Mark:** +${highWaterMark}%\n`;
body += `- **Current PR Speedup:** +**${diff}%** Average Improvement\n\n`;
body += `> *Note: Please remember to run \`pnpm run bench:save\` locally to re-calibrate your development baseline, though the repository baseline is generated dynamically from main.*`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
} else {
console.log(`Benchmark diff (+${diff}%) is not higher than the previous high-water mark (+${highWaterMark}%). Skipping comment.`);
}
- name: Coverage Quality Gate (Tractor)
id: coverage
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit != 'true'
run: |
cd packages/tractor
pnpm run coverage:check
# Export payload to GHA env vars if the script exited cleanly
if [ -f "coverage/gha-payload.json" ]; then
IMPROVED=$(jq -r '.improved' coverage/gha-payload.json)
PREV=$(jq -r '.previous' coverage/gha-payload.json)
CURR=$(jq -r '.current' coverage/gha-payload.json)
DIFF=$(jq -r '.diff' coverage/gha-payload.json)
echo "improved=$IMPROVED" >> $GITHUB_OUTPUT
echo "previous=$PREV" >> $GITHUB_OUTPUT
echo "current=$CURR" >> $GITHUB_OUTPUT
echo "diff=$DIFF" >> $GITHUB_OUTPUT
fi
- name: Comment PR on Coverage Increase
if: steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && steps.coverage.outputs.improved == 'true'
continue-on-error: true
uses: ./.github/actions/github-script
with:
script: |
const currentCoverage = parseFloat('${{ steps.coverage.outputs.current }}');
const previousCoverage = parseFloat('${{ steps.coverage.outputs.previous }}');
const diff = '${{ steps.coverage.outputs.diff }}';
// Fetch existing comments from the bot
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComments = comments.filter(c => c.user.login === 'github-actions[bot]' && c.body.includes('🚀 Tractor Coverage Increased!'));
// Find the high-water mark from previous comments
let highWaterMark = previousCoverage;
const regex = /Current High Score: \*\*([0-9.]+)\%\*\*/;
for (const comment of botComments) {
const match = comment.body.match(regex);
if (match && match[1]) {
const pct = parseFloat(match[1]);
if (pct > highWaterMark) {
highWaterMark = pct;
}
}
}
// Only comment if the current coverage strictly beats the highest water mark
if (currentCoverage > highWaterMark) {
let body = `### 🚀 Tractor Coverage Increased!\n\n`;
body += `Great job! You've increased the test coverage for the Tractor Engine core.\n\n`;
body += `- **Previous Baseline (or last high-water):** ${highWaterMark}%\n`;
body += `- **Current High Score: **${currentCoverage}%\n`;
body += `- **Improvement:** +${(currentCoverage - highWaterMark).toFixed(2)}%\n\n`;
if (currentCoverage >= 100) {
body += `\n\n🎉 **PERFECT COVERAGE!** 💯 You've reached the holy grail.`;
} else {
body += `\n> *Note: Please remember to run \`pnpm run coverage:save\` and commit the new baseline so your changes protect the project moving forward.*`;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
} else {
console.log(`Coverage (${currentCoverage}%) is not higher than the previous high-water mark (${highWaterMark}%). Skipping comment.`);
}
- name: Upload coverage
if: always() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit != 'true' && env.CODECOV_TOKEN != ''
uses: ./.github/actions/codecov-upload
with:
files: ./packages/tractor/coverage/lcov.info
flags: unittests
name: codecov-refarm
token: ${{ env.CODECOV_TOKEN }}
- name: Upload revocation diagnostics report
if: always() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_gates == 'true'
uses: ./.github/actions/upload-artifact
with:
name: runtime-descriptor-revocation-report
path: |
.artifacts/runtime-descriptor-revocation-report
.artifacts/runtime-descriptor-revocation-baseline
.artifacts/runtime-descriptor-revocation-history
retention-days: 14
if-no-files-found: warn
- name: Upload telemetry gate artifact
if: always() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.run_task_smoke == 'true'
uses: ./.github/actions/upload-artifact
with:
name: telemetry-gate-report
path: .artifacts/telemetry/gate-quality.json
retention-days: 14
if-no-files-found: warn
- name: Record Test & Quality cache marker
if: success() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.quality_cache_key != ''
run: |
mkdir -p .artifacts/validation-cache/test-quality
printf '%s\n' "${{ github.sha }}" > .artifacts/validation-cache/test-quality/last-successful-sha.txt
- name: Record Tractor heavy cache marker
if: success() && steps.quality_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_gates == 'true' && steps.tractor_heavy_cache.outputs.cache-hit != 'true' && needs.changes.outputs.tractor_heavy_cache_key != ''
run: |
mkdir -p .artifacts/validation-cache/tractor-heavy
printf '%s\n' "${{ github.sha }}" > .artifacts/validation-cache/tractor-heavy/last-successful-sha.txt
platform-compat:
name: Platform compatibility (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 25
needs: changes
if: needs.changes.outputs.skip_duplicate_push != 'true' && (needs.changes.outputs.code_changes == 'true' || needs.changes.outputs.ci_changes == 'true')
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-2025-vs2026]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Setup
id: setup
uses: ./.github/actions/setup
with:
rust-target: "wasm32-wasip1"
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
env-safety-mode: strict
- name: Check platform compatibility
run: node scripts/ci/check-platform-compat.mjs
build:
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [changes, quality]
if: needs.changes.outputs.run_build == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Setup
id: setup
uses: ./.github/actions/setup
with:
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Build (affected)
if: needs.changes.outputs.turbo_filter != ''
run: pnpm turbo run build --filter=${{ needs.changes.outputs.turbo_filter }} --cache-dir=.turbo --output-logs=new-only --ui=stream
- name: Build (full fallback)
if: needs.changes.outputs.turbo_filter == ''
run: pnpm run build
- name: Upload build artifacts
if: success()
uses: ./.github/actions/upload-artifact
with:
name: workspace-build
path: |
apps/**/dist
packages/**/dist
retention-days: 7
if-no-files-found: warn
e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [changes, quality]
if: needs.changes.outputs.run_e2e == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Restore E2E result cache
id: e2e_cache
if: needs.changes.outputs.e2e_cache_key != ''
uses: ./.github/actions/cache
with:
path: .artifacts/validation-cache/test-e2e
key: ${{ needs.changes.outputs.e2e_cache_key }}
- name: E2E cache hit
if: steps.e2e_cache.outputs.cache-hit == 'true'
run: echo "::notice::Reusing previous successful E2E validation for unchanged inputs."
- name: Setup
id: setup
if: steps.e2e_cache.outputs.cache-hit != 'true'
uses: ./.github/actions/setup
with:
playwright-setup: "true"
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Check E2E script status
id: e2e_check
if: steps.e2e_cache.outputs.cache-hit != 'true'
shell: bash
run: |
set -euo pipefail
SCRIPT=$(node -p "(require('./package.json').scripts || {})['test:e2e'] || ''")
if echo "$SCRIPT" | grep -qi "No E2E tests configured yet"; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::E2E job skipped because test:e2e is still a placeholder."
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi
# Guard against SCM gaps: only use affected filter when base commit is resolvable locally.
- name: Resolve E2E turbo filter
id: e2e_filter
if: "steps.e2e_cache.outputs.cache-hit != 'true' && steps.e2e_check.outputs.enabled == 'true' && !contains(github.event.pull_request.labels.*.name, 'phase:sdd') && !contains(github.event.pull_request.labels.*.name, 'phase:bdd')"
shell: bash
run: |
set -euo pipefail
FILTER="${{ needs.changes.outputs.turbo_filter }}"
BASE_SHA="${{ needs.changes.outputs.base_sha }}"
if [ -n "$FILTER" ] && [ -n "$BASE_SHA" ] && git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "mode=affected" >> "$GITHUB_OUTPUT"
echo "filter=$FILTER" >> "$GITHUB_OUTPUT"
echo "::notice::Using affected E2E filter: $FILTER"
else
echo "mode=full" >> "$GITHUB_OUTPUT"
echo "filter=" >> "$GITHUB_OUTPUT"
echo "::warning::Base commit unavailable for E2E filter (base=$BASE_SHA). Falling back to full test:e2e."
fi
- name: Run E2E Tests (affected)
if: steps.e2e_cache.outputs.cache-hit != 'true' && steps.e2e_filter.outputs.mode == 'affected'
run: pnpm turbo run test:e2e --filter=${{ steps.e2e_filter.outputs.filter }} --cache-dir=.turbo --output-logs=new-only --ui=stream
- name: Run E2E Tests (full fallback)
if: steps.e2e_cache.outputs.cache-hit != 'true' && steps.e2e_filter.outputs.mode == 'full'
run: pnpm turbo run test:e2e --cache-dir=.turbo --output-logs=new-only --ui=stream
- name: Upload Playwright report
if: always() && steps.e2e_cache.outputs.cache-hit != 'true' && steps.e2e_check.outputs.enabled == 'true'
uses: ./.github/actions/upload-artifact
with:
name: playwright-report
path: playwright-report/
retention-days: 30
if-no-files-found: ignore
- name: Record E2E cache marker
if: success() && steps.e2e_cache.outputs.cache-hit != 'true' && needs.changes.outputs.e2e_cache_key != ''
run: |
mkdir -p .artifacts/validation-cache/test-e2e
printf '%s\n' "${{ github.sha }}" > .artifacts/validation-cache/test-e2e/last-successful-sha.txt
deep-regression:
runs-on: ubuntu-latest
timeout-minutes: 45
needs: changes
if: needs.changes.outputs.run_deep == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
id: setup
uses: ./.github/actions/setup
with:
playwright-setup: "true"
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Deep regression (full stack)
run: pnpm turbo run build lint type-check test:coverage test:unit test:integration test:e2e --cache-dir=.turbo --output-logs=new-only --ui=stream
checkers:
# Deterministic repo-invariant checks, UNGATED (every push/PR) — they are sub-second string/YAML
# parses, and their whole value is being unskippable: a --no-verify, a fresh clone, or a
# GitHub-UI merge must not be able to bypass them. Home for the workspace-coverage checker (every
# pnpm root must be wired into code_changes + the quality signature, so no root silently skips
# Verify or rides a stale cache), the face-smoke-coverage checker (every example page has a
# web-face smoke entry, so a new face can't silently go un-browser-tested), and the phantom-dep /
# test-runner-contract checkers that until now lived only in the never-invoked gate:full:colony
# bundle + a bypassable pre-push hook.
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
id: setup
uses: ./.github/actions/setup
with:
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: CI workspace coverage (every pnpm root wired into code_changes + quality signature)
run: pnpm run ci:workspace-coverage:check
- name: Face smoke coverage (every example page has a web-face smoke entry)
run: pnpm run ci:face-smoke-coverage:check
- name: Phantom workspace deps in tool configs
run: pnpm run deps:vitest-config-check
- name: Source dependency manifest
run: pnpm run deps:src-manifest-check
- name: Test-runner contracts
run: pnpm run test-runner:contracts
web-face-smoke:
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: needs.changes.outputs.run_web_smoke == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
id: setup
uses: ./.github/actions/setup
with:
playwright-setup: "true"
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Build the example web faces
run: pnpm turbo run web:build --filter=devbench-t1 --filter=wallet-t2 --filter=reqbench-t3 --cache-dir=.turbo --output-logs=new-only --ui=stream
- name: Real-browser web-face smoke (boots each declared face in headless Chromium)
run: pnpm web:smoke
audit-moderate:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: needs.changes.outputs.run_audit == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
id: setup
uses: ./.github/actions/setup
with:
turbo-cache-api: ${{ secrets.TURBO_CACHE_API_URL }}
turbo-cache-token: ${{ secrets.TURBO_CACHE_TOKEN }}
trusted-pr-cache: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
is-pr: ${{ github.event_name == 'pull_request' }}
pr-number: ${{ github.event.pull_request.number }}
- name: Audit moderate report (non-blocking)
id: audit_report
uses: ./.github/actions/npm-audit-report
with:
json-output: audit-moderate.json
markdown-output: audit-moderate.md
- name: Audit moderate notice
if: steps.audit_report.outputs.total != '0'
run: |
echo "::warning::Security audit report: total=${{ steps.audit_report.outputs.total }} moderate=${{ steps.audit_report.outputs.moderate }} high=${{ steps.audit_report.outputs.high }} critical=${{ steps.audit_report.outputs.critical }}"
- name: Audit moderate clean notice
if: steps.audit_report.outputs.total == '0'
run: |
echo "::notice::Security audit report: no vulnerabilities found"
- name: Upload audit report artifact
if: always()
uses: ./.github/actions/upload-artifact
with:
name: npm-audit-moderate-report
path: |
audit-moderate.json
audit-moderate.md
retention-days: 30
summary:
if: always()
needs:
[
changes,
checkers,
build-wasm,
wasm-component-tests,
quality,
platform-compat,
build,
e2e,
web-face-smoke,
audit-moderate,
deep-regression,
]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Carry forward status for skipped gates
id: carry_forward
if: needs.changes.outputs.skip_duplicate_push != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
CODE_CHANGES: ${{ needs.changes.outputs.code_changes }}
RUN_TASK_SMOKE: ${{ needs.changes.outputs.run_task_smoke }}
TRACTOR_GATES: ${{ needs.changes.outputs.tractor_gates }}
TRACTOR_HEAVY_GATES: ${{ needs.changes.outputs.tractor_heavy_gates }}
RUN_AUDIT: ${{ needs.changes.outputs.run_audit }}
RUN_BUILD: ${{ needs.changes.outputs.run_build }}
RUN_E2E: ${{ needs.changes.outputs.run_e2e }}
RUN_DEEP: ${{ needs.changes.outputs.run_deep }}
shell: bash
run: node scripts/ci/carry-forward-status.mjs
- name: Check all checks
run: |
echo "changes: skip_duplicate_push=${{ needs.changes.outputs.skip_duplicate_push }} ci=${{ needs.changes.outputs.ci_changes }} code=${{ needs.changes.outputs.code_changes }} build=${{ needs.changes.outputs.run_build }} e2e=${{ needs.changes.outputs.run_e2e }} tractor_gates=${{ needs.changes.outputs.tractor_gates }} tractor_heavy_gates=${{ needs.changes.outputs.tractor_heavy_gates }} audit=${{ needs.changes.outputs.run_audit }} deep=${{ needs.changes.outputs.run_deep }} base=${{ needs.changes.outputs.base_sha }} filter=${{ needs.changes.outputs.turbo_filter }}"
if [ "${{ needs.changes.outputs.skip_duplicate_push }}" = "true" ]; then
echo "::notice::Heavy push validation skipped because the open PR run is canonical for this develop head."
exit 0
fi
if [ "${{ steps.carry_forward.outputs.has_failure }}" = "true" ]; then
echo "::error::One or more skipped gates reused a prior failing status."
exit 1
fi
# Deterministic repo-invariant checkers run ungated on every push/PR — always required.
if [ "${{ needs.checkers.result }}" != "success" ]; then
echo "::error::Deterministic checkers failed (workspace coverage / phantom deps / test-runner contracts)"
exit 1
fi
# build-wasm is skipped on docs-only commits (result "skipped" is fine);
# only a real "failure" here is a gate breach.
if [ "${{ needs.build-wasm.result }}" = "failure" ]; then
echo "::error::Rust/WASM plugin build failed"
exit 1
fi
# wasm-component-tests skips on docs-only commits too; only a real
# "failure" (a sovereign/vault/quality component suite broke) is a breach.
if [ "${{ needs.wasm-component-tests.result }}" = "failure" ]; then
echo "::error::WASM component suites failed"
exit 1
fi
if [ "${{ needs.quality.result }}" != "success" ]; then
echo "::error::Quality checks failed"
exit 1
fi
if [ "${{ needs.changes.outputs.code_changes }}" = "true" ] || [ "${{ needs.changes.outputs.ci_changes }}" = "true" ]; then
if [ "${{ needs.platform-compat.result }}" != "success" ]; then
echo "::error::Platform compatibility failed"
exit 1
fi
fi
if [ "${{ needs.changes.outputs.run_audit }}" = "true" ] && [ "${{ needs.audit-moderate.result }}" != "success" ]; then
echo "::error::Audit moderate job failed"
exit 1
fi
if [ "${{ needs.changes.outputs.run_build }}" = "true" ] && [ "${{ needs.build.result }}" != "success" ]; then
echo "::error::Build failed"
exit 1
fi
if [ "${{ needs.changes.outputs.run_e2e }}" = "true" ] && [ "${{ needs.e2e.result }}" != "success" ]; then
echo "::error::E2E tests failed"
exit 1
fi
if [ "${{ needs.changes.outputs.run_web_smoke }}" = "true" ] && [ "${{ needs.web-face-smoke.result }}" != "success" ]; then
echo "::error::Web-face smoke failed"
exit 1
fi
if [ "${{ needs.changes.outputs.run_deep }}" = "true" ] && [ "${{ needs.deep-regression.result }}" != "success" ]; then
echo "::error::Deep regression failed"
exit 1
fi
echo "::notice::All checks passed!"
ci-metrics:
if: always() && needs.changes.outputs.skip_duplicate_push != 'true'
needs: [changes, summary]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Export run metrics
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
curl -sSfL \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" \
> /tmp/ci-jobs.json
node scripts/ci/export-ci-run-metrics.mjs --input /tmp/ci-jobs.json --output ci-run-metrics.json
- name: Upload CI metrics artifact
if: always()
uses: ./.github/actions/upload-artifact
with:
name: ci-run-metrics
path: ci-run-metrics.json
retention-days: 14
if-no-files-found: error