From 24f694f5630c0643eb736e85000a10ffc44b49f6 Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 12:58:49 -0700 Subject: [PATCH 1/7] ci(e2e): gate non-GPU E2E behind copy-pr-bot Move `Branch E2E Checks` to the same `pull-request/` + `get-pr-info` pattern used by GPU tests so self-hosted runners only execute code that has been explicitly mirrored by copy-pr-bot (automatically for trusted PRs, via `/ok to test ` for untrusted ones). A `test:e2e` label on the PR is still required. Also: - Extract the shared gate logic into `.github/actions/pr-gate` so `branch-e2e.yml` and `test-gpu.yml` no longer duplicate the SHA/label check. The required label is passed as an input. - Scope permissions per-job in both workflows. Workflow-level defaults to `permissions: {}`; builds grant `packages: write`, E2E gets `packages: read`, `pr_metadata` gets `contents: read` + `pull-requests: read`. - Document the new flow (label + re-run) in CONTRIBUTING.md. Release integration is unchanged: `release-tag.yml` and `release-dev.yml` continue to call `e2e-test.yml` directly on trusted refs (main / tags). Signed-off-by: Piotr Mlocek --- .github/actions/pr-gate/action.yml | 56 +++++++++++++++++++++++++++++ .github/workflows/branch-e2e.yml | 43 +++++++++++++++++----- .github/workflows/test-gpu.yml | 57 ++++++++++-------------------- CONTRIBUTING.md | 25 +++++++++++++ 4 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 .github/actions/pr-gate/action.yml diff --git a/.github/actions/pr-gate/action.yml b/.github/actions/pr-gate/action.yml new file mode 100644 index 000000000..9450233e6 --- /dev/null +++ b/.github/actions/pr-gate/action.yml @@ -0,0 +1,56 @@ +name: PR Gate +description: > + Resolve PR metadata for a `pull-request/` push from copy-pr-bot and decide + whether the workflow should run. Sets `should-run=true` only when the pushed + SHA still matches the PR head SHA and the PR carries the required label. For + non-`push` events (e.g. `workflow_dispatch`), always sets `should-run=true`. + +inputs: + required-label: + description: PR label required to enable the run (e.g. "test:e2e"). + required: true + +outputs: + should-run: + description: "true if the workflow should proceed, false otherwise" + value: ${{ steps.gate.outputs.should_run }} + +runs: + using: composite + steps: + - id: get_pr_info + if: github.event_name == 'push' + continue-on-error: true + uses: nv-gha-runners/get-pr-info@main + + - id: gate + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + GITHUB_SHA_VALUE: ${{ github.sha }} + GET_PR_INFO_OUTCOME: ${{ steps.get_pr_info.outcome }} + PR_INFO: ${{ steps.get_pr_info.outputs.pr-info }} + REQUIRED_LABEL: ${{ inputs.required-label }} + run: | + if [ "$EVENT_NAME" != "push" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$GET_PR_INFO_OUTCOME" != "success" ]; then + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + head_sha="$(jq -r '.head.sha' <<< "$PR_INFO")" + has_label="$(jq -r --arg L "$REQUIRED_LABEL" '[.labels[].name] | index($L) != null' <<< "$PR_INFO")" + + # Only trust copied pull-request/* pushes that still match the PR head + # SHA and carry the required label. + if [ "$head_sha" = "$GITHUB_SHA_VALUE" ] && [ "$has_label" = "true" ]; then + should_run=true + else + should_run=false + fi + + echo "should_run=$should_run" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index ad53bb635..8480e6a39 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -1,16 +1,35 @@ name: Branch E2E Checks on: - pull_request: - types: [opened, synchronize, reopened, labeled] + push: + branches: + - "pull-request/[0-9]+" + workflow_dispatch: {} -permissions: - contents: read - packages: write +permissions: {} jobs: + pr_metadata: + name: Resolve PR metadata + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + should_run: ${{ steps.gate.outputs.should-run }} + steps: + - uses: actions/checkout@v4 + - id: gate + uses: ./.github/actions/pr-gate + with: + required-label: test:e2e + build-gateway: - if: contains(github.event.pull_request.labels.*.name, 'test:e2e') + needs: [pr_metadata] + if: needs.pr_metadata.outputs.should_run == 'true' + permissions: + contents: read + packages: write uses: ./.github/workflows/docker-build.yml with: component: gateway @@ -18,7 +37,11 @@ jobs: runner: build-arm64 build-cluster: - if: contains(github.event.pull_request.labels.*.name, 'test:e2e') + needs: [pr_metadata] + if: needs.pr_metadata.outputs.should_run == 'true' + permissions: + contents: read + packages: write uses: ./.github/workflows/docker-build.yml with: component: cluster @@ -26,7 +49,11 @@ jobs: runner: build-arm64 e2e: - needs: [build-gateway, build-cluster] + needs: [pr_metadata, build-gateway, build-cluster] + if: needs.pr_metadata.outputs.should_run == 'true' + permissions: + contents: read + packages: read uses: ./.github/workflows/e2e-test.yml with: image-tag: ${{ github.sha }} diff --git a/.github/workflows/test-gpu.yml b/.github/workflows/test-gpu.yml index df953b5d3..09a0e000c 100644 --- a/.github/workflows/test-gpu.yml +++ b/.github/workflows/test-gpu.yml @@ -7,57 +7,30 @@ on: workflow_dispatch: {} # Add `schedule:` here when we want nightly coverage from the same workflow. -permissions: - contents: read - pull-requests: read - packages: write +permissions: {} jobs: pr_metadata: name: Resolve PR metadata runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read outputs: - should_run: ${{ steps.gate.outputs.should_run }} + should_run: ${{ steps.gate.outputs.should-run }} steps: - - id: get_pr_info - if: github.event_name == 'push' - continue-on-error: true - uses: nv-gha-runners/get-pr-info@main - + - uses: actions/checkout@v4 - id: gate - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - GITHUB_SHA_VALUE: ${{ github.sha }} - GET_PR_INFO_OUTCOME: ${{ steps.get_pr_info.outcome }} - PR_INFO: ${{ steps.get_pr_info.outputs.pr-info }} - run: | - if [ "$EVENT_NAME" != "push" ]; then - echo "should_run=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [ "$GET_PR_INFO_OUTCOME" != "success" ]; then - echo "should_run=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - head_sha="$(jq -r '.head.sha' <<< "$PR_INFO")" - has_gpu_label="$(jq -r '[.labels[].name] | index("test:e2e-gpu") != null' <<< "$PR_INFO")" - - # Only trust copied pull-request/* pushes that still match the PR head SHA - # and are explicitly labeled for GPU coverage. - if [ "$head_sha" = "$GITHUB_SHA_VALUE" ] && [ "$has_gpu_label" = "true" ]; then - should_run=true - else - should_run=false - fi - - echo "should_run=$should_run" >> "$GITHUB_OUTPUT" + uses: ./.github/actions/pr-gate + with: + required-label: test:e2e-gpu build-gateway: needs: [pr_metadata] if: needs.pr_metadata.outputs.should_run == 'true' + permissions: + contents: read + packages: write uses: ./.github/workflows/docker-build.yml with: component: gateway @@ -65,6 +38,9 @@ jobs: build-cluster: needs: [pr_metadata] if: needs.pr_metadata.outputs.should_run == 'true' + permissions: + contents: read + packages: write uses: ./.github/workflows/docker-build.yml with: component: cluster @@ -72,6 +48,9 @@ jobs: e2e-gpu: needs: [pr_metadata, build-gateway, build-cluster] if: needs.pr_metadata.outputs.should_run == 'true' + permissions: + contents: read + packages: read uses: ./.github/workflows/e2e-gpu-test.yaml with: image-tag: ${{ github.sha }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2852bfa43..0f8945e67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -254,6 +254,31 @@ See [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx) for the current docs authorin 3. Run `mise run ci` to verify. 4. Open a PR using the `create-github-pr` skill or manually following the [PR template](.github/PULL_REQUEST_TEMPLATE.md). +### CI Lanes + +Most CI runs automatically on every PR push (lint, unit tests, docs validation). Expensive lanes that need self-hosted runners — **Branch E2E Checks** and **GPU Test** — are gated to protect those runners from untrusted code. + +Both lanes run against PR code only after it has been mirrored to a `pull-request/` branch in this repository by [`copy-pr-bot`](https://docs.gha-runners.nvidia.com/platform/apps/copy-pr-bot/). They also require an explicit label on the PR: + +| Lane | Label | Runner | +| -------------------- | -------------- | ---------------- | +| Branch E2E Checks | `test:e2e` | `build-arm64` | +| GPU Test | `test:e2e-gpu` | Self-hosted GPU | + +**Trusted PRs** (org members, vouched contributors with signed commits) are mirrored automatically when marked ready-for-review. + +**Untrusted PRs** (unvouched users, unsigned commits) need a vetter to comment `/ok to test ` on the PR. `copy-pr-bot` mirrors the code once the vetter approves. + +The workflow re-checks the label and head SHA on every `pull-request/` push. A maintainer typically: + +1. Reviews the PR for safety. +2. Applies `test:e2e` (and/or `test:e2e-gpu`). +3. Triggers the run: + - If the label was applied **before** `copy-pr-bot` mirrored the code, the push already ran the workflow and it will now pick up the label automatically on the next commit. + - If `copy-pr-bot` already mirrored and the initial run showed `should_run=false`, re-run that workflow from the Actions UI (or `gh run rerun `). The gate fetches live PR metadata, sees the label, and proceeds. + +Release builds (`Release Tag`, `Release Dev`) run the same E2E reusable workflow on `main` / tag pushes — those refs are inherently trusted, so they do not go through `copy-pr-bot`. + ### Commit Messages This project uses [Conventional Commits](https://www.conventionalcommits.org/). All commit messages must follow the format: From 026eeda15536c7d94c745b0a443fa32ba9f1ec81 Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 13:10:23 -0700 Subject: [PATCH 2/7] docs(contributing): drop internal CI-lanes section Keep copy-pr-bot / `test:e2e` flow details internal until the worker migration lands. We'll publish external guidance then. --- CONTRIBUTING.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f8945e67..2852bfa43 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -254,31 +254,6 @@ See [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx) for the current docs authorin 3. Run `mise run ci` to verify. 4. Open a PR using the `create-github-pr` skill or manually following the [PR template](.github/PULL_REQUEST_TEMPLATE.md). -### CI Lanes - -Most CI runs automatically on every PR push (lint, unit tests, docs validation). Expensive lanes that need self-hosted runners — **Branch E2E Checks** and **GPU Test** — are gated to protect those runners from untrusted code. - -Both lanes run against PR code only after it has been mirrored to a `pull-request/` branch in this repository by [`copy-pr-bot`](https://docs.gha-runners.nvidia.com/platform/apps/copy-pr-bot/). They also require an explicit label on the PR: - -| Lane | Label | Runner | -| -------------------- | -------------- | ---------------- | -| Branch E2E Checks | `test:e2e` | `build-arm64` | -| GPU Test | `test:e2e-gpu` | Self-hosted GPU | - -**Trusted PRs** (org members, vouched contributors with signed commits) are mirrored automatically when marked ready-for-review. - -**Untrusted PRs** (unvouched users, unsigned commits) need a vetter to comment `/ok to test ` on the PR. `copy-pr-bot` mirrors the code once the vetter approves. - -The workflow re-checks the label and head SHA on every `pull-request/` push. A maintainer typically: - -1. Reviews the PR for safety. -2. Applies `test:e2e` (and/or `test:e2e-gpu`). -3. Triggers the run: - - If the label was applied **before** `copy-pr-bot` mirrored the code, the push already ran the workflow and it will now pick up the label automatically on the next commit. - - If `copy-pr-bot` already mirrored and the initial run showed `should_run=false`, re-run that workflow from the Actions UI (or `gh run rerun `). The gate fetches live PR metadata, sees the label, and proceeds. - -Release builds (`Release Tag`, `Release Dev`) run the same E2E reusable workflow on `main` / tag pushes — those refs are inherently trusted, so they do not go through `copy-pr-bot`. - ### Commit Messages This project uses [Conventional Commits](https://www.conventionalcommits.org/). All commit messages must follow the format: From 572ffe5febd7c6e9f1f1d384db12291a5636baae Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 13:15:02 -0700 Subject: [PATCH 3/7] ci(e2e): add E2E Gate to enforce test:e2e label intent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Label-gating alone is opt-in: a PR labeled `test:e2e` that never gets mirrored by copy-pr-bot produces no workflow run, and nothing warns the maintainer. The gate fixes that. The new `E2E Gate` workflow runs on PR events and on `Branch E2E Checks` completion. When the `test:e2e` label is present, it looks up the most recent `Branch E2E Checks` run for the PR head SHA and fails unless that run concluded successfully. Without the label it passes as a no-op. Making this a required status check in branch protection is the follow-up step — it's the bit that actually blocks merge. Signed-off-by: Piotr Mlocek --- .github/workflows/e2e-gate.yml | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/e2e-gate.yml diff --git a/.github/workflows/e2e-gate.yml b/.github/workflows/e2e-gate.yml new file mode 100644 index 000000000..442a8674d --- /dev/null +++ b/.github/workflows/e2e-gate.yml @@ -0,0 +1,88 @@ +name: E2E Gate + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review] + workflow_run: + workflows: ["Branch E2E Checks"] + types: [completed] + +permissions: {} + +jobs: + check: + name: Enforce test:e2e runs when labeled + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + actions: read + steps: + - name: Resolve PR context + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + PR_HEAD_SHA_FROM_EVENT: ${{ github.event.pull_request.head.sha }} + PR_LABELS_FROM_EVENT: ${{ toJSON(github.event.pull_request.labels.*.name) }} + WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + shell: bash + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "pull_request" ]; then + head_sha="$PR_HEAD_SHA_FROM_EVENT" + labels_json="$PR_LABELS_FROM_EVENT" + else + head_sha="$WORKFLOW_RUN_HEAD_SHA" + pr=$(gh api "repos/$GH_REPO/commits/$head_sha/pulls" --jq '.[0] // empty') + if [ -z "$pr" ]; then + echo "No PR associated with $head_sha; gate is a no-op." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + labels_json=$(echo "$pr" | jq -c '[.labels[].name]') + fi + echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" + echo "labels_json=$labels_json" >> "$GITHUB_OUTPUT" + + - name: Evaluate gate + if: steps.pr.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + LABELS_JSON: ${{ steps.pr.outputs.labels_json }} + shell: bash + run: | + set -euo pipefail + + has_label=$(jq -r 'any(.[]; . == "test:e2e")' <<< "$LABELS_JSON") + if [ "$has_label" != "true" ]; then + echo "::notice::test:e2e label not applied; gate passes." + exit 0 + fi + + runs=$(gh api "repos/$GH_REPO/actions/workflows/branch-e2e.yml/runs?head_sha=$HEAD_SHA&event=push" --jq '.workflow_runs') + latest=$(jq -c 'sort_by(.created_at) | reverse | .[0] // empty' <<< "$runs") + + if [ -z "$latest" ]; then + echo "::error::test:e2e is applied but Branch E2E Checks has not run for $HEAD_SHA. Wait for copy-pr-bot to mirror the PR, or re-run the gate once Branch E2E Checks completes." + exit 1 + fi + + status=$(jq -r '.status' <<< "$latest") + conclusion=$(jq -r '.conclusion' <<< "$latest") + + if [ "$conclusion" = "success" ]; then + echo "Branch E2E Checks succeeded for $HEAD_SHA." + exit 0 + fi + + if [ "$status" != "completed" ]; then + echo "::error::Branch E2E Checks is $status for $HEAD_SHA. This gate will re-evaluate on completion." + exit 1 + fi + + echo "::error::Branch E2E Checks concluded as $conclusion for $HEAD_SHA." + exit 1 From 258317541f6117f31bf9b5db20924c4c6790332f Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 13:16:15 -0700 Subject: [PATCH 4/7] fix(ci): compact labels JSON before writing to GITHUB_OUTPUT The pretty-printed JSON from toJSON(...) contains newlines, which the file-command protocol for $GITHUB_OUTPUT rejects without a heredoc delimiter. Flatten it with jq -c instead. Signed-off-by: Piotr Mlocek --- .github/workflows/e2e-gate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-gate.yml b/.github/workflows/e2e-gate.yml index 442a8674d..38fcfd79c 100644 --- a/.github/workflows/e2e-gate.yml +++ b/.github/workflows/e2e-gate.yml @@ -32,7 +32,7 @@ jobs: set -euo pipefail if [ "$EVENT_NAME" = "pull_request" ]; then head_sha="$PR_HEAD_SHA_FROM_EVENT" - labels_json="$PR_LABELS_FROM_EVENT" + labels_json=$(jq -c . <<< "$PR_LABELS_FROM_EVENT") else head_sha="$WORKFLOW_RUN_HEAD_SHA" pr=$(gh api "repos/$GH_REPO/commits/$head_sha/pulls" --jq '.[0] // empty') @@ -41,7 +41,7 @@ jobs: echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi - labels_json=$(echo "$pr" | jq -c '[.labels[].name]') + labels_json=$(jq -c '[.labels[].name]' <<< "$pr") fi echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" echo "labels_json=$labels_json" >> "$GITHUB_OUTPUT" From ba3e1e0d6c2e1cee8392195a1f4808ed7554b4cf Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 13:31:32 -0700 Subject: [PATCH 5/7] ci(e2e): add GPU gate and extract shared gate logic Add `GPU E2E Gate` that enforces the `test:e2e-gpu` label the same way `E2E Gate` enforces `test:e2e`: if the label is applied, the corresponding `GPU Test` run must have completed successfully for the PR head SHA. Extract the gate check into a reusable workflow `.github/workflows/e2e-gate-check.yml` that takes `required_label` and `workflow_file` as inputs. Both gate workflows become thin wrappers that wire up their own `pull_request` + `workflow_run` triggers and call the shared check. Also normalize the `pr-gate` composite action's input/output names to use underscores (`required_label`, `should_run`), matching the existing style in `branch-e2e.yml` and `test-gpu.yml`. Signed-off-by: Piotr Mlocek --- .github/actions/pr-gate/action.yml | 6 +- .github/workflows/branch-e2e.yml | 4 +- .github/workflows/e2e-gate-check.yml | 101 +++++++++++++++++++++++++++ .github/workflows/e2e-gate.yml | 75 ++------------------ .github/workflows/gpu-e2e-gate.yml | 21 ++++++ .github/workflows/test-gpu.yml | 4 +- 6 files changed, 133 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/e2e-gate-check.yml create mode 100644 .github/workflows/gpu-e2e-gate.yml diff --git a/.github/actions/pr-gate/action.yml b/.github/actions/pr-gate/action.yml index 9450233e6..be1a5b19f 100644 --- a/.github/actions/pr-gate/action.yml +++ b/.github/actions/pr-gate/action.yml @@ -6,12 +6,12 @@ description: > non-`push` events (e.g. `workflow_dispatch`), always sets `should-run=true`. inputs: - required-label: + required_label: description: PR label required to enable the run (e.g. "test:e2e"). required: true outputs: - should-run: + should_run: description: "true if the workflow should proceed, false otherwise" value: ${{ steps.gate.outputs.should_run }} @@ -30,7 +30,7 @@ runs: GITHUB_SHA_VALUE: ${{ github.sha }} GET_PR_INFO_OUTCOME: ${{ steps.get_pr_info.outcome }} PR_INFO: ${{ steps.get_pr_info.outputs.pr-info }} - REQUIRED_LABEL: ${{ inputs.required-label }} + REQUIRED_LABEL: ${{ inputs.required_label }} run: | if [ "$EVENT_NAME" != "push" ]; then echo "should_run=true" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 8480e6a39..1312c66b4 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -16,13 +16,13 @@ jobs: contents: read pull-requests: read outputs: - should_run: ${{ steps.gate.outputs.should-run }} + should_run: ${{ steps.gate.outputs.should_run }} steps: - uses: actions/checkout@v4 - id: gate uses: ./.github/actions/pr-gate with: - required-label: test:e2e + required_label: test:e2e build-gateway: needs: [pr_metadata] diff --git a/.github/workflows/e2e-gate-check.yml b/.github/workflows/e2e-gate-check.yml new file mode 100644 index 000000000..81b71ce64 --- /dev/null +++ b/.github/workflows/e2e-gate-check.yml @@ -0,0 +1,101 @@ +name: E2E Gate Check + +# Reusable gate that enforces: when `required_label` is present on a PR, +# `workflow_file` must have completed successfully for the PR head SHA. +# +# Callers wire their own triggers (`pull_request` + `workflow_run` for the +# workflow this gate guards) and pass in the label and workflow filename. + +on: + workflow_call: + inputs: + required_label: + description: PR label that makes the gated workflow mandatory. + required: true + type: string + workflow_file: + description: Filename of the workflow whose run must have succeeded (e.g. "branch-e2e.yml"). + required: true + type: string + +permissions: {} + +jobs: + check: + name: Enforce ${{ inputs.required_label }} runs when labeled + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + actions: read + steps: + - name: Resolve PR context + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + PR_HEAD_SHA_FROM_EVENT: ${{ github.event.pull_request.head.sha }} + PR_LABELS_FROM_EVENT: ${{ toJSON(github.event.pull_request.labels.*.name) }} + WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + shell: bash + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "pull_request" ]; then + head_sha="$PR_HEAD_SHA_FROM_EVENT" + labels_json=$(jq -c . <<< "$PR_LABELS_FROM_EVENT") + else + head_sha="$WORKFLOW_RUN_HEAD_SHA" + pr=$(gh api "repos/$GH_REPO/commits/$head_sha/pulls" --jq '.[0] // empty') + if [ -z "$pr" ]; then + echo "No PR associated with $head_sha; gate is a no-op." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + labels_json=$(jq -c '[.labels[].name]' <<< "$pr") + fi + echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" + echo "labels_json=$labels_json" >> "$GITHUB_OUTPUT" + + - name: Evaluate gate + if: steps.pr.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + LABELS_JSON: ${{ steps.pr.outputs.labels_json }} + REQUIRED_LABEL: ${{ inputs.required_label }} + WORKFLOW_FILE: ${{ inputs.workflow_file }} + shell: bash + run: | + set -euo pipefail + + has_label=$(jq -r --arg L "$REQUIRED_LABEL" 'any(.[]; . == $L)' <<< "$LABELS_JSON") + if [ "$has_label" != "true" ]; then + echo "::notice::$REQUIRED_LABEL not applied; gate passes." + exit 0 + fi + + runs=$(gh api "repos/$GH_REPO/actions/workflows/$WORKFLOW_FILE/runs?head_sha=$HEAD_SHA&event=push" --jq '.workflow_runs') + latest=$(jq -c 'sort_by(.created_at) | reverse | .[0] // empty' <<< "$runs") + + if [ -z "$latest" ]; then + echo "::error::$REQUIRED_LABEL is applied but $WORKFLOW_FILE has not run for $HEAD_SHA. Wait for copy-pr-bot to mirror the PR, or re-run the gate once the workflow completes." + exit 1 + fi + + status=$(jq -r '.status' <<< "$latest") + conclusion=$(jq -r '.conclusion' <<< "$latest") + + if [ "$conclusion" = "success" ]; then + echo "$WORKFLOW_FILE succeeded for $HEAD_SHA." + exit 0 + fi + + if [ "$status" != "completed" ]; then + echo "::error::$WORKFLOW_FILE is $status for $HEAD_SHA. This gate will re-evaluate on completion." + exit 1 + fi + + echo "::error::$WORKFLOW_FILE concluded as $conclusion for $HEAD_SHA." + exit 1 diff --git a/.github/workflows/e2e-gate.yml b/.github/workflows/e2e-gate.yml index 38fcfd79c..b7e61bd06 100644 --- a/.github/workflows/e2e-gate.yml +++ b/.github/workflows/e2e-gate.yml @@ -11,78 +11,11 @@ permissions: {} jobs: check: - name: Enforce test:e2e runs when labeled - runs-on: ubuntu-latest permissions: contents: read pull-requests: read actions: read - steps: - - name: Resolve PR context - id: pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - EVENT_NAME: ${{ github.event_name }} - PR_HEAD_SHA_FROM_EVENT: ${{ github.event.pull_request.head.sha }} - PR_LABELS_FROM_EVENT: ${{ toJSON(github.event.pull_request.labels.*.name) }} - WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - shell: bash - run: | - set -euo pipefail - if [ "$EVENT_NAME" = "pull_request" ]; then - head_sha="$PR_HEAD_SHA_FROM_EVENT" - labels_json=$(jq -c . <<< "$PR_LABELS_FROM_EVENT") - else - head_sha="$WORKFLOW_RUN_HEAD_SHA" - pr=$(gh api "repos/$GH_REPO/commits/$head_sha/pulls" --jq '.[0] // empty') - if [ -z "$pr" ]; then - echo "No PR associated with $head_sha; gate is a no-op." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - labels_json=$(jq -c '[.labels[].name]' <<< "$pr") - fi - echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" - echo "labels_json=$labels_json" >> "$GITHUB_OUTPUT" - - - name: Evaluate gate - if: steps.pr.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - HEAD_SHA: ${{ steps.pr.outputs.head_sha }} - LABELS_JSON: ${{ steps.pr.outputs.labels_json }} - shell: bash - run: | - set -euo pipefail - - has_label=$(jq -r 'any(.[]; . == "test:e2e")' <<< "$LABELS_JSON") - if [ "$has_label" != "true" ]; then - echo "::notice::test:e2e label not applied; gate passes." - exit 0 - fi - - runs=$(gh api "repos/$GH_REPO/actions/workflows/branch-e2e.yml/runs?head_sha=$HEAD_SHA&event=push" --jq '.workflow_runs') - latest=$(jq -c 'sort_by(.created_at) | reverse | .[0] // empty' <<< "$runs") - - if [ -z "$latest" ]; then - echo "::error::test:e2e is applied but Branch E2E Checks has not run for $HEAD_SHA. Wait for copy-pr-bot to mirror the PR, or re-run the gate once Branch E2E Checks completes." - exit 1 - fi - - status=$(jq -r '.status' <<< "$latest") - conclusion=$(jq -r '.conclusion' <<< "$latest") - - if [ "$conclusion" = "success" ]; then - echo "Branch E2E Checks succeeded for $HEAD_SHA." - exit 0 - fi - - if [ "$status" != "completed" ]; then - echo "::error::Branch E2E Checks is $status for $HEAD_SHA. This gate will re-evaluate on completion." - exit 1 - fi - - echo "::error::Branch E2E Checks concluded as $conclusion for $HEAD_SHA." - exit 1 + uses: ./.github/workflows/e2e-gate-check.yml + with: + required_label: test:e2e + workflow_file: branch-e2e.yml diff --git a/.github/workflows/gpu-e2e-gate.yml b/.github/workflows/gpu-e2e-gate.yml new file mode 100644 index 000000000..69315a9d1 --- /dev/null +++ b/.github/workflows/gpu-e2e-gate.yml @@ -0,0 +1,21 @@ +name: GPU E2E Gate + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review] + workflow_run: + workflows: ["GPU Test"] + types: [completed] + +permissions: {} + +jobs: + check: + permissions: + contents: read + pull-requests: read + actions: read + uses: ./.github/workflows/e2e-gate-check.yml + with: + required_label: test:e2e-gpu + workflow_file: test-gpu.yml diff --git a/.github/workflows/test-gpu.yml b/.github/workflows/test-gpu.yml index 09a0e000c..dd406701f 100644 --- a/.github/workflows/test-gpu.yml +++ b/.github/workflows/test-gpu.yml @@ -17,13 +17,13 @@ jobs: contents: read pull-requests: read outputs: - should_run: ${{ steps.gate.outputs.should-run }} + should_run: ${{ steps.gate.outputs.should_run }} steps: - uses: actions/checkout@v4 - id: gate uses: ./.github/actions/pr-gate with: - required-label: test:e2e-gpu + required_label: test:e2e-gpu build-gateway: needs: [pr_metadata] From ba57b863df98374ead23971b307a0628b5cc2f0a Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 13:33:46 -0700 Subject: [PATCH 6/7] refactor(ci): merge E2E gates into a single workflow Combine `E2E Gate` and `GPU E2E Gate` into one workflow with two jobs. The `workflow_run` trigger listens for both upstream workflows, and each job guards itself with an `if:` that lets it run on every PR event but only on the matching upstream `workflow_run` event. The shared `e2e-gate-check.yml` reusable workflow is unchanged. One fewer workflow file to reason about, and both required status checks (`E2E Gate / E2E` and `E2E Gate / GPU E2E`) now live under one workflow name. Signed-off-by: Piotr Mlocek --- .github/workflows/e2e-gate.yml | 24 ++++++++++++++++++++++-- .github/workflows/gpu-e2e-gate.yml | 21 --------------------- 2 files changed, 22 insertions(+), 23 deletions(-) delete mode 100644 .github/workflows/gpu-e2e-gate.yml diff --git a/.github/workflows/e2e-gate.yml b/.github/workflows/e2e-gate.yml index b7e61bd06..757a95466 100644 --- a/.github/workflows/e2e-gate.yml +++ b/.github/workflows/e2e-gate.yml @@ -4,13 +4,19 @@ on: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review] workflow_run: - workflows: ["Branch E2E Checks"] + workflows: ["Branch E2E Checks", "GPU Test"] types: [completed] permissions: {} jobs: - check: + e2e: + name: E2E + # Run on every PR event; on workflow_run, only when the upstream was the + # matching gated workflow. + if: >- + github.event_name == 'pull_request' || + github.event.workflow_run.name == 'Branch E2E Checks' permissions: contents: read pull-requests: read @@ -19,3 +25,17 @@ jobs: with: required_label: test:e2e workflow_file: branch-e2e.yml + + gpu: + name: GPU E2E + if: >- + github.event_name == 'pull_request' || + github.event.workflow_run.name == 'GPU Test' + permissions: + contents: read + pull-requests: read + actions: read + uses: ./.github/workflows/e2e-gate-check.yml + with: + required_label: test:e2e-gpu + workflow_file: test-gpu.yml diff --git a/.github/workflows/gpu-e2e-gate.yml b/.github/workflows/gpu-e2e-gate.yml deleted file mode 100644 index 69315a9d1..000000000 --- a/.github/workflows/gpu-e2e-gate.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: GPU E2E Gate - -on: - pull_request: - types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review] - workflow_run: - workflows: ["GPU Test"] - types: [completed] - -permissions: {} - -jobs: - check: - permissions: - contents: read - pull-requests: read - actions: read - uses: ./.github/workflows/e2e-gate-check.yml - with: - required_label: test:e2e-gpu - workflow_file: test-gpu.yml From 1b3555d1e142c77d3e5912273dd67195858926e4 Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Wed, 22 Apr 2026 13:40:08 -0700 Subject: [PATCH 7/7] chore: test signed empty commit Signed-off-by: Piotr Mlocek