fix(release): clear message when the handoff runs without --pack (was… #1075
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: 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 |