ci: make affected planning fail closed (#2401) #1
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: Dev CI | |
| on: | |
| push: | |
| branches: [dev] | |
| pull_request: | |
| branches: [dev] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Planner: resolve changed-path relevance and the affected task plan once, then | |
| # fan the plan out into per-task shards. Emits the matrix, has_tasks/has_native | |
| # flags, and the resolved changed paths so every downstream job reuses this | |
| # exact diff via CI_DEV_CHANGED_PATHS instead of re-resolving the base ref. | |
| affected-plan: | |
| name: Affected path validation / plan | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 10 | |
| outputs: | |
| relevant: ${{ steps.relevance.outputs.relevant }} | |
| matrix: ${{ steps.plan.outputs.matrix }} | |
| has_tasks: ${{ steps.plan.outputs.has_tasks }} | |
| has_native: ${{ steps.plan.outputs.has_native }} | |
| changed_paths: ${{ steps.plan.outputs.changed_paths }} | |
| plan_mode: ${{ steps.plan.outputs.plan_mode }} | |
| plan_digest: ${{ steps.plan.outputs.plan_digest }} | |
| plan_source_sha: ${{ steps.plan.outputs.plan_source_sha }} | |
| env: | |
| GITHUB_EVENT_BEFORE: ${{ github.event.before }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| CI_DEV_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Verify checked-out source head | |
| shell: bash | |
| run: | | |
| head="$(git rev-parse HEAD)" | |
| test "$head" = "$CI_DEV_SOURCE_SHA" || { echo "Checked-out SHA $head does not match $CI_DEV_SOURCE_SHA"; exit 1; } | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: "1.3" | |
| - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly | |
| with: | |
| toolchain: nightly-2026-04-29 | |
| - name: Compute changed-path relevance | |
| id: relevance | |
| run: bun scripts/ci-job-relevance.ts | |
| - name: Compute affected task matrix | |
| id: plan | |
| run: bun scripts/ci-dev-affected.ts --matrix-json | |
| - name: Upload canonical affected plan | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: dev-affected-plan-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: .ci-dev-affected-plan.json | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 1 | |
| windows-dev-doctor: | |
| name: Windows dev:doctor | |
| needs: [affected-plan] | |
| if: ${{ needs.affected-plan.outputs.relevant == 'true' && contains(needs.affected-plan.outputs.changed_paths, 'scripts/dev-link') }} | |
| runs-on: windows-latest | |
| timeout-minutes: 60 | |
| env: | |
| CI_DEV_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Verify checked-out source head | |
| shell: pwsh | |
| run: | | |
| $head = (git rev-parse HEAD).Trim() | |
| if ($head -ne $env:CI_DEV_SOURCE_SHA) { throw "Checked-out SHA $head does not match $env:CI_DEV_SOURCE_SHA" } | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: "1.3" | |
| - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly | |
| with: | |
| toolchain: nightly-2026-04-29 | |
| - name: Prepend rustup toolchain bin to PATH | |
| shell: bash | |
| run: | | |
| toolchain_bin="$(dirname "$(rustup which cargo)")" | |
| echo "$toolchain_bin" >> "$GITHUB_PATH" | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| shared-key: windows-dev-doctor-win32-x64 | |
| cache-on-failure: true | |
| save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' }} | |
| cache-workspace-crates: true | |
| - name: Cache bun dependencies | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| - run: bun install --frozen-lockfile | |
| - name: Build native addon (win32-x64 baseline) | |
| env: | |
| TARGET_PLATFORM: win32 | |
| TARGET_ARCH: x64 | |
| TARGET_VARIANTS: baseline | |
| run: bun run ci:build:native | |
| - name: Verify Windows workspace shim and doctor | |
| shell: pwsh | |
| run: | | |
| bun test scripts/dev-link.test.ts | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| bun run dev:doctor | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Native addon build runs once per run and publishes the built `.node` files as | |
| # an artifact the runtime-dependent shards download. It is skipped when the | |
| # plan needs no native build at all. | |
| affected-native: | |
| name: Affected path validation / native-build | |
| needs: [affected-plan] | |
| if: ${{ needs.affected-plan.outputs.has_native == 'true' }} | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 30 | |
| env: | |
| CI_DEV_CHANGED_PATHS: ${{ needs.affected-plan.outputs.changed_paths }} | |
| CI_DEV_PLAN_MODE: ${{ needs.affected-plan.outputs.plan_mode }} | |
| CI_DEV_AFFECTED_PLAN: .ci-dev-affected-plan.json | |
| CI_DEV_PLAN_DIGEST: ${{ needs.affected-plan.outputs.plan_digest }} | |
| CI_DEV_PLAN_SOURCE_SHA: ${{ needs.affected-plan.outputs.plan_source_sha }} | |
| CI_DEV_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Verify checked-out source head | |
| shell: bash | |
| run: | | |
| head="$(git rev-parse HEAD)" | |
| test "$head" = "$CI_DEV_SOURCE_SHA" || { echo "Checked-out SHA $head does not match $CI_DEV_SOURCE_SHA"; exit 1; } | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: "24" | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: "1.3" | |
| - name: Download and validate canonical affected plan | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dev-affected-plan-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: . | |
| - run: bun scripts/ci-dev-affected.ts --validate-plan | |
| - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly | |
| with: | |
| toolchain: nightly-2026-04-29 | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| shared-key: dev-affected-native-linux-x64 | |
| cache-on-failure: true | |
| save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' }} | |
| cache-workspace-crates: true | |
| - name: Cache bun dependencies | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| - name: Install system deps | |
| run: bash scripts/ci-install-system-deps.sh | |
| - run: bun install --frozen-lockfile | |
| - name: Build affected native addon(s) | |
| run: bun scripts/ci-dev-affected.ts --native-build | |
| - name: Verify required native addon variants | |
| run: | | |
| test -f packages/natives/native/pi_natives.linux-x64-baseline.node | |
| test -f packages/natives/native/pi_natives.linux-x64-modern.node | |
| - name: Upload native addon(s) | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: dev-affected-native-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| packages/natives/native/pi_natives.linux-x64-baseline.node | |
| packages/natives/native/pi_natives.linux-x64-modern.node | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # One shard per planned task on the broad runner. Native build tasks are | |
| # excluded (they run in affected-native); shards that load the native addon at | |
| # runtime download the prebuilt artifact instead of rebuilding it. | |
| affected-shards: | |
| name: Affected path validation / ${{ matrix.key }} | |
| needs: [affected-plan, affected-native] | |
| if: ${{ always() && needs.affected-plan.outputs.has_tasks == 'true' && needs.affected-native.result != 'failure' && needs.affected-native.result != 'cancelled' }} | |
| runs-on: ubuntu-22.04 | |
| # Broad push-mode coding-agent/root test shards can need up to 90 minutes, but | |
| # the bounded root-check must retain the same 30-minute fail-fast contract as | |
| # Main CI. SDK closure remains outside that CI command. | |
| timeout-minutes: ${{ matrix.key == 'root-check' && 30 || 90 }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 8 | |
| matrix: ${{ fromJSON(needs.affected-plan.outputs.matrix) }} | |
| env: | |
| CI_DEV_CHANGED_PATHS: ${{ needs.affected-plan.outputs.changed_paths }} | |
| CI_DEV_PLAN_MODE: ${{ needs.affected-plan.outputs.plan_mode }} | |
| CI_DEV_AFFECTED_PLAN: .ci-dev-affected-plan.json | |
| CI_DEV_PLAN_DIGEST: ${{ needs.affected-plan.outputs.plan_digest }} | |
| CI_DEV_PLAN_SOURCE_SHA: ${{ needs.affected-plan.outputs.plan_source_sha }} | |
| CI_DEV_MATRIX_KEY: ${{ matrix.key }} | |
| CI_DEV_MATRIX_IDENTITY: ${{ matrix.identity }} | |
| CI_DEV_SHARD_INDEX: ${{ strategy.job-index }} | |
| CI_DEV_MATRIX_RUST: ${{ matrix.rust }} | |
| CI_DEV_MATRIX_NEXTEST: ${{ matrix.nextest }} | |
| CI_DEV_MATRIX_NATIVE: ${{ matrix.native }} | |
| CI_DEV_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Verify checked-out source head | |
| shell: bash | |
| run: | | |
| head="$(git rev-parse HEAD)" | |
| test "$head" = "$CI_DEV_SOURCE_SHA" || { echo "Checked-out SHA $head does not match $CI_DEV_SOURCE_SHA"; exit 1; } | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: "24" | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: "1.3" | |
| - name: Download and validate canonical affected plan | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dev-affected-plan-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: . | |
| - run: bun scripts/ci-dev-affected.ts --validate-plan | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| if: ${{ startsWith(matrix.key, 'python-') }} | |
| with: | |
| python-version: "3.11" | |
| - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly | |
| if: ${{ matrix.rust }} | |
| with: | |
| toolchain: nightly-2026-04-29 | |
| - uses: taiki-e/install-action@56545b37b57562edd73171cb6c62cc509db4c34e # v2 | |
| if: ${{ matrix.nextest }} | |
| with: | |
| tool: nextest | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| if: ${{ matrix.rust }} | |
| with: | |
| shared-key: dev-affected-rust-linux-x64 | |
| cache-on-failure: true | |
| save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' }} | |
| cache-workspace-crates: true | |
| - name: Cache bun dependencies | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| - name: Install system deps | |
| run: bash scripts/ci-install-system-deps.sh | |
| - run: bun install --frozen-lockfile | |
| - name: Download native addon(s) | |
| if: ${{ matrix.native }} | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dev-affected-native-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: packages/natives/native | |
| - name: Run affected task shard | |
| env: | |
| AFFECTED_TASK_KEY: ${{ matrix.key }} | |
| GITHUB_EVENT_BEFORE: ${{ github.event.before }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: bun scripts/ci-dev-affected.ts --task="$AFFECTED_TASK_KEY" | |
| - name: Write shard completion receipt | |
| run: | | |
| bun -e 'await Bun.write(`.ci-dev-shard-receipts/${process.env.CI_DEV_SHARD_INDEX}.json`, JSON.stringify({ key: process.env.AFFECTED_TASK_KEY, identity: process.env.CI_DEV_MATRIX_IDENTITY }))' | |
| env: | |
| AFFECTED_TASK_KEY: ${{ matrix.key }} | |
| - name: Upload shard completion receipt | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: dev-affected-shard-${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }} | |
| path: .ci-dev-shard-receipts/${{ strategy.job-index }}.json | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Branch protection must keep requiring this stable aggregate status. It validates | |
| # the canonical plan and receipts, then passes iff the planner, native build, | |
| # every affected shard, and Windows doctor satisfy their planned contracts. The | |
| # job name must stay exactly "Affected path validation". | |
| affected: | |
| name: Affected path validation | |
| if: ${{ always() }} | |
| needs: [affected-plan, affected-native, affected-shards, windows-dev-doctor] | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 5 | |
| env: | |
| CI_DEV_AFFECTED_PLAN: .ci-dev-affected-plan.json | |
| CI_DEV_PLAN_DIGEST: ${{ needs.affected-plan.outputs.plan_digest }} | |
| CI_DEV_PLAN_SOURCE_SHA: ${{ needs.affected-plan.outputs.plan_source_sha }} | |
| CI_DEV_SHARD_RECEIPTS: .ci-dev-shard-receipts | |
| CI_DEV_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Verify checked-out source head | |
| shell: bash | |
| run: | | |
| head="$(git rev-parse HEAD)" | |
| test "$head" = "$CI_DEV_SOURCE_SHA" || { echo "Checked-out SHA $head does not match $CI_DEV_SOURCE_SHA"; exit 1; } | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: "1.3" | |
| - name: Download canonical affected plan | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dev-affected-plan-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: . | |
| - name: Validate canonical affected plan | |
| run: bun scripts/ci-dev-affected.ts --validate-plan | |
| - name: Download shard completion receipts | |
| if: ${{ needs.affected-plan.result == 'success' && needs.affected-plan.outputs.has_tasks == 'true' }} | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: dev-affected-shard-${{ github.run_id }}-${{ github.run_attempt }}-* | |
| path: .ci-dev-shard-receipts | |
| merge-multiple: true | |
| - name: Validate canonical shard completion | |
| if: ${{ needs.affected-plan.result == 'success' && needs.affected-plan.outputs.has_tasks == 'true' }} | |
| run: bun scripts/ci-dev-affected.ts --validate-shard-receipts | |
| - name: Aggregate affected path validation shards | |
| env: | |
| CI_DEV_PLAN_RESULT: ${{ needs.affected-plan.result }} | |
| CI_DEV_NATIVE_RESULT: ${{ needs.affected-native.result }} | |
| CI_DEV_SHARDS_RESULT: ${{ needs.affected-shards.result }} | |
| CI_DEV_HAS_NATIVE: ${{ needs.affected-plan.outputs.has_native }} | |
| CI_DEV_HAS_TASKS: ${{ needs.affected-plan.outputs.has_tasks }} | |
| CI_DEV_WINDOWS_DOCTOR_RESULT: ${{ needs.windows-dev-doctor.result }} | |
| CI_DEV_WINDOWS_DOCTOR_REQUIRED: ${{ contains(needs.affected-plan.outputs.changed_paths, 'scripts/dev-link') }} | |
| run: bun scripts/ci-dev-affected.ts --validate-aggregate | |
| gjc-state-gates-matrix: | |
| name: gjc-state-gates / ${{ matrix.group }} | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| group: [static, runtime, integrity, read] | |
| env: | |
| GITHUB_EVENT_BEFORE: ${{ github.event.before }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: "24" | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: "1.3" | |
| - name: Restore bun dependency cache | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - run: bun install --frozen-lockfile | |
| - name: Run GJC state gate shard | |
| run: bun scripts/ci-gjc-state-gates.ts --group=${{ matrix.group }} | |
| # Branch protection must keep requiring this stable aggregate status. | |
| gjc-state-gates: | |
| name: gjc-state-gates | |
| if: ${{ always() }} | |
| needs: [gjc-state-gates-matrix] | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Aggregate GJC state gate shards | |
| run: | | |
| result='${{ needs.gjc-state-gates-matrix.result }}' | |
| echo "gjc-state-gates shard result: $result" | |
| test "$result" = success |