diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9be9e8a3..2c830c25 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,8 +18,38 @@ env: CARGO_TERM_COLOR: always jobs: + # Trunk decides which jobs this PR needs. Fails open: no verdict means no + # output, so every gate below tests `!= 'false'`. Gating is scoped to + # pull_request -- push (main), tags and dispatch run everything outright. + dynamic-ci-filter: + name: Dynamic CI Filter + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name == 'pull_request' + # Per-job outputs are set at runtime, so they are re-exported here by name. + outputs: + build_release: ${{ steps.ci-filter.outputs.build_release }} + test: ${{ steps.ci-filter.outputs.test }} + trunk_check_runner: ${{ steps.ci-filter.outputs.trunk_check_runner }} + build_pyo3: ${{ steps.ci-filter.outputs.build_pyo3 }} + build_wasm: ${{ steps.ci-filter.outputs.build_wasm }} + steps: + - name: Run Dynamic CI Filter + id: ci-filter + uses: trunk-io/dynamic-ci@v1 + with: + # Prod, not staging: this repo's merge queue lives in prod. + token: ${{ secrets.TRUNK_PROD_ORG_API_TOKEN }} + build_release: name: Build CLI for ${{ matrix.platform.target }} + needs: [dynamic-ci-filter] + # Also runs when `test` is wanted: it consumes this job's binary artifact. + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.build_release != 'false' || + needs.dynamic-ci-filter.outputs.test != 'false') strategy: matrix: platform: @@ -97,7 +127,12 @@ jobs: test: name: Test for ${{ matrix.platform.target }} - needs: [build_release] + needs: [build_release, dynamic-ci-filter] + if: >- + !cancelled() && + needs.build_release.result == 'success' && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.test != 'false') strategy: matrix: platform: @@ -245,6 +280,11 @@ jobs: trunk_check_runner: name: Trunk Check runner [linux] runs-on: ubuntu-latest + needs: [dynamic-ci-filter] + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.trunk_check_runner != 'false') steps: - uses: actions/checkout@v4 @@ -279,6 +319,11 @@ jobs: build_pyo3: name: Build context-py runs-on: ubuntu-latest + needs: [dynamic-ci-filter] + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.build_pyo3 != 'false') steps: - uses: actions/checkout@v4 @@ -292,6 +337,11 @@ jobs: build_wasm: name: Build context-js (WASM) runs-on: ubuntu-latest + needs: [dynamic-ci-filter] + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.build_wasm != 'false') steps: - uses: actions/checkout@v4 @@ -301,3 +351,33 @@ jobs: - name: Setup and build wasm uses: ./.github/actions/setup_build_wasm + + gate: + name: PR Gate + runs-on: ubuntu-latest + timeout-minutes: 5 + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + if: always() + needs: [build_release, test, trunk_check_runner, build_pyo3, build_wasm] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" diff --git a/.github/workflows/pyo3.yml b/.github/workflows/pyo3.yml index 4eb7cd43..4732a812 100644 --- a/.github/workflows/pyo3.yml +++ b/.github/workflows/pyo3.yml @@ -19,6 +19,28 @@ permissions: id-token: write jobs: + # Trunk decides which jobs this PR needs. Fails open: no verdict means no + # output, so every gate below tests `!= 'false'`. Gating is scoped to + # pull_request -- push (main), tags and dispatch run everything outright. + dynamic-ci-filter: + name: Dynamic CI Filter + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name == 'pull_request' + # Per-job outputs are set at runtime, so they are re-exported here by name. + outputs: + linux: ${{ steps.ci-filter.outputs.linux }} + linux-pytest: ${{ steps.ci-filter.outputs.linux-pytest }} + macos: ${{ steps.ci-filter.outputs.macos }} + sdist: ${{ steps.ci-filter.outputs.sdist }} + steps: + - name: Run Dynamic CI Filter + id: ci-filter + uses: trunk-io/dynamic-ci@v1 + with: + # Prod, not staging: this repo's merge queue lives in prod. + token: ${{ secrets.TRUNK_PROD_ORG_API_TOKEN }} + get-date-sha: runs-on: ubuntu-latest outputs: @@ -37,7 +59,14 @@ jobs: echo "sha=${GIT_SHA}" >> $GITHUB_OUTPUT echo "reference=${DATE}/${GIT_SHA}" >> $GITHUB_OUTPUT linux: - needs: [get-date-sha] + needs: [get-date-sha, dynamic-ci-filter] + # Also runs when `linux-pytest` is wanted: it consumes this job's wheels. + if: >- + !cancelled() && + needs.get-date-sha.result == 'success' && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.linux != 'false' || + needs.dynamic-ci-filter.outputs.linux-pytest != 'false') runs-on: ${{ matrix.platform.runner }} strategy: fail-fast: false @@ -100,7 +129,12 @@ jobs: sha: ${{ needs.get-date-sha.outputs.sha }} linux-pytest: - needs: [linux] + needs: [linux, dynamic-ci-filter] + if: >- + !cancelled() && + needs.linux.result == 'success' && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.linux-pytest != 'false') runs-on: ${{ matrix.platform.runner }} strategy: fail-fast: false @@ -218,7 +252,12 @@ jobs: directory: ./context-py macos: - needs: [get-date-sha] + needs: [get-date-sha, dynamic-ci-filter] + if: >- + !cancelled() && + needs.get-date-sha.result == 'success' && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.macos != 'false') runs-on: ${{ matrix.platform.runner }} strategy: matrix: @@ -264,7 +303,12 @@ jobs: date: ${{ needs.get-date-sha.outputs.date }} sha: ${{ needs.get-date-sha.outputs.sha }} sdist: - needs: [get-date-sha] + needs: [get-date-sha, dynamic-ci-filter] + if: >- + !cancelled() && + needs.get-date-sha.result == 'success' && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.sdist != 'false') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -300,3 +344,35 @@ jobs: - name: Annotate workflow with S3 reference run: | echo "::notice title=Published bindings::context-py bindings published to S3 at ${{ needs.get-date-sha.outputs.reference }}" + + gate: + name: context-py Gate + runs-on: ubuntu-latest + timeout-minutes: 5 + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + # linux-pytest is omitted: it is not a required check today, and this job + # preserves that set rather than widening it. + if: always() + needs: [linux, macos, sdist] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 66360420..e3993627 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -14,8 +14,34 @@ permissions: contents: read jobs: + # Trunk decides which jobs this PR needs. Fails open: no verdict means no + # output, so every gate below tests `!= 'false'`. Gating is scoped to + # pull_request -- push (main), tags and dispatch run everything outright. + dynamic-ci-filter: + name: Dynamic CI Filter + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name == 'pull_request' + # Per-job outputs are set at runtime, so they are re-exported here by name. + outputs: + build: ${{ steps.ci-filter.outputs.build }} + build-ruby-gem: ${{ steps.ci-filter.outputs.build-ruby-gem }} + test-ruby-gem-uploads: ${{ steps.ci-filter.outputs.test-ruby-gem-uploads }} + steps: + - name: Run Dynamic CI Filter + id: ci-filter + uses: trunk-io/dynamic-ci@v1 + with: + # Prod, not staging: this repo's merge queue lives in prod. + token: ${{ secrets.TRUNK_PROD_ORG_API_TOKEN }} + build: runs-on: ubuntu-latest + needs: [dynamic-ci-filter] + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.build != 'false') steps: - uses: actions/checkout@v4 @@ -88,6 +114,13 @@ jobs: build-ruby-gem: name: Build Ruby gem runs-on: ubuntu-latest + needs: [dynamic-ci-filter] + # Also runs when `test-ruby-gem-uploads` is wanted: it consumes this gem. + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.build-ruby-gem != 'false' || + needs.dynamic-ci-filter.outputs.test-ruby-gem-uploads != 'false') steps: - uses: actions/checkout@v4 @@ -147,7 +180,12 @@ jobs: test-ruby-gem-uploads: name: Test Ruby gem uploads runs-on: ubuntu-latest - needs: build-ruby-gem + needs: [build-ruby-gem, dynamic-ci-filter] + if: >- + !cancelled() && + needs.build-ruby-gem.result == 'success' && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.test-ruby-gem-uploads != 'false') steps: - uses: actions/checkout@v4 @@ -204,3 +242,34 @@ jobs: platform: x86_64-linux artifact-pattern: "" knapsack-pro-test-suite-token-rspec: ${{ secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC }} + + gate: + name: rspec Gate + runs-on: ubuntu-latest + timeout-minutes: 5 + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + # build-ruby-gem / test-ruby-gem-uploads are omitted: not required checks today. + if: always() + needs: [build] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 53dd62b0..f77f8ace 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -14,8 +14,32 @@ permissions: contents: read jobs: + # Trunk decides which jobs this PR needs. Fails open: no verdict means no + # output, so every gate below tests `!= 'false'`. Gating is scoped to + # pull_request -- push (main), tags and dispatch run everything outright. + dynamic-ci-filter: + name: Dynamic CI Filter + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name == 'pull_request' + # Per-job outputs are set at runtime, so they are re-exported here by name. + outputs: + build: ${{ steps.ci-filter.outputs.build }} + steps: + - name: Run Dynamic CI Filter + id: ci-filter + uses: trunk-io/dynamic-ci@v1 + with: + # Prod, not staging: this repo's merge queue lives in prod. + token: ${{ secrets.TRUNK_PROD_ORG_API_TOKEN }} + build: runs-on: ubuntu-latest + needs: [dynamic-ci-filter] + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + needs.dynamic-ci-filter.outputs.build != 'false') steps: - uses: actions/checkout@v4 @@ -64,3 +88,33 @@ jobs: if: "!cancelled() && github.event_name != 'pull_request'" run: | echo "::notice title=Published bindings::context-js bindings published to S3 at ${{ steps.get-date-sha.outputs.reference }}" + + gate: + name: context-js Gate + runs-on: ubuntu-latest + timeout-minutes: 5 + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + if: always() + needs: [build] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}"