Skip to content

Commit 30ddca4

Browse files
authored
ci(e2e): enable E2E to run on external forks throught the copy-pr-bot flow (#922)
1 parent 2f8e8ac commit 30ddca4

5 files changed

Lines changed: 250 additions & 46 deletions

File tree

.github/actions/pr-gate/action.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: PR Gate
2+
description: >
3+
Resolve PR metadata for a `pull-request/<N>` push from copy-pr-bot and decide
4+
whether the workflow should run. Sets `should-run=true` only when the pushed
5+
SHA still matches the PR head SHA and the PR carries the required label. For
6+
non-`push` events (e.g. `workflow_dispatch`), always sets `should-run=true`.
7+
8+
inputs:
9+
required_label:
10+
description: PR label required to enable the run (e.g. "test:e2e").
11+
required: true
12+
13+
outputs:
14+
should_run:
15+
description: "true if the workflow should proceed, false otherwise"
16+
value: ${{ steps.gate.outputs.should_run }}
17+
18+
runs:
19+
using: composite
20+
steps:
21+
- id: get_pr_info
22+
if: github.event_name == 'push'
23+
continue-on-error: true
24+
uses: nv-gha-runners/get-pr-info@main
25+
26+
- id: gate
27+
shell: bash
28+
env:
29+
EVENT_NAME: ${{ github.event_name }}
30+
GITHUB_SHA_VALUE: ${{ github.sha }}
31+
GET_PR_INFO_OUTCOME: ${{ steps.get_pr_info.outcome }}
32+
PR_INFO: ${{ steps.get_pr_info.outputs.pr-info }}
33+
REQUIRED_LABEL: ${{ inputs.required_label }}
34+
run: |
35+
if [ "$EVENT_NAME" != "push" ]; then
36+
echo "should_run=true" >> "$GITHUB_OUTPUT"
37+
exit 0
38+
fi
39+
40+
if [ "$GET_PR_INFO_OUTCOME" != "success" ]; then
41+
echo "should_run=false" >> "$GITHUB_OUTPUT"
42+
exit 0
43+
fi
44+
45+
head_sha="$(jq -r '.head.sha' <<< "$PR_INFO")"
46+
has_label="$(jq -r --arg L "$REQUIRED_LABEL" '[.labels[].name] | index($L) != null' <<< "$PR_INFO")"
47+
48+
# Only trust copied pull-request/* pushes that still match the PR head
49+
# SHA and carry the required label.
50+
if [ "$head_sha" = "$GITHUB_SHA_VALUE" ] && [ "$has_label" = "true" ]; then
51+
should_run=true
52+
else
53+
should_run=false
54+
fi
55+
56+
echo "should_run=$should_run" >> "$GITHUB_OUTPUT"

.github/workflows/branch-e2e.yml

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,59 @@
11
name: Branch E2E Checks
22

33
on:
4-
pull_request:
5-
types: [opened, synchronize, reopened, labeled]
4+
push:
5+
branches:
6+
- "pull-request/[0-9]+"
7+
workflow_dispatch: {}
68

7-
permissions:
8-
contents: read
9-
packages: write
9+
permissions: {}
1010

1111
jobs:
12+
pr_metadata:
13+
name: Resolve PR metadata
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
pull-requests: read
18+
outputs:
19+
should_run: ${{ steps.gate.outputs.should_run }}
20+
steps:
21+
- uses: actions/checkout@v4
22+
- id: gate
23+
uses: ./.github/actions/pr-gate
24+
with:
25+
required_label: test:e2e
26+
1227
build-gateway:
13-
if: contains(github.event.pull_request.labels.*.name, 'test:e2e')
28+
needs: [pr_metadata]
29+
if: needs.pr_metadata.outputs.should_run == 'true'
30+
permissions:
31+
contents: read
32+
packages: write
1433
uses: ./.github/workflows/docker-build.yml
1534
with:
1635
component: gateway
1736
platform: linux/arm64
1837
runner: build-arm64
1938

2039
build-cluster:
21-
if: contains(github.event.pull_request.labels.*.name, 'test:e2e')
40+
needs: [pr_metadata]
41+
if: needs.pr_metadata.outputs.should_run == 'true'
42+
permissions:
43+
contents: read
44+
packages: write
2245
uses: ./.github/workflows/docker-build.yml
2346
with:
2447
component: cluster
2548
platform: linux/arm64
2649
runner: build-arm64
2750

2851
e2e:
29-
needs: [build-gateway, build-cluster]
52+
needs: [pr_metadata, build-gateway, build-cluster]
53+
if: needs.pr_metadata.outputs.should_run == 'true'
54+
permissions:
55+
contents: read
56+
packages: read
3057
uses: ./.github/workflows/e2e-test.yml
3158
with:
3259
image-tag: ${{ github.sha }}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: E2E Gate Check
2+
3+
# Reusable gate that enforces: when `required_label` is present on a PR,
4+
# `workflow_file` must have completed successfully for the PR head SHA.
5+
#
6+
# Callers wire their own triggers (`pull_request` + `workflow_run` for the
7+
# workflow this gate guards) and pass in the label and workflow filename.
8+
9+
on:
10+
workflow_call:
11+
inputs:
12+
required_label:
13+
description: PR label that makes the gated workflow mandatory.
14+
required: true
15+
type: string
16+
workflow_file:
17+
description: Filename of the workflow whose run must have succeeded (e.g. "branch-e2e.yml").
18+
required: true
19+
type: string
20+
21+
permissions: {}
22+
23+
jobs:
24+
check:
25+
name: Enforce ${{ inputs.required_label }} runs when labeled
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
pull-requests: read
30+
actions: read
31+
steps:
32+
- name: Resolve PR context
33+
id: pr
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
GH_REPO: ${{ github.repository }}
37+
EVENT_NAME: ${{ github.event_name }}
38+
PR_HEAD_SHA_FROM_EVENT: ${{ github.event.pull_request.head.sha }}
39+
PR_LABELS_FROM_EVENT: ${{ toJSON(github.event.pull_request.labels.*.name) }}
40+
WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
41+
shell: bash
42+
run: |
43+
set -euo pipefail
44+
if [ "$EVENT_NAME" = "pull_request" ]; then
45+
head_sha="$PR_HEAD_SHA_FROM_EVENT"
46+
labels_json=$(jq -c . <<< "$PR_LABELS_FROM_EVENT")
47+
else
48+
head_sha="$WORKFLOW_RUN_HEAD_SHA"
49+
pr=$(gh api "repos/$GH_REPO/commits/$head_sha/pulls" --jq '.[0] // empty')
50+
if [ -z "$pr" ]; then
51+
echo "No PR associated with $head_sha; gate is a no-op."
52+
echo "skip=true" >> "$GITHUB_OUTPUT"
53+
exit 0
54+
fi
55+
labels_json=$(jq -c '[.labels[].name]' <<< "$pr")
56+
fi
57+
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
58+
echo "labels_json=$labels_json" >> "$GITHUB_OUTPUT"
59+
60+
- name: Evaluate gate
61+
if: steps.pr.outputs.skip != 'true'
62+
env:
63+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64+
GH_REPO: ${{ github.repository }}
65+
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
66+
LABELS_JSON: ${{ steps.pr.outputs.labels_json }}
67+
REQUIRED_LABEL: ${{ inputs.required_label }}
68+
WORKFLOW_FILE: ${{ inputs.workflow_file }}
69+
shell: bash
70+
run: |
71+
set -euo pipefail
72+
73+
has_label=$(jq -r --arg L "$REQUIRED_LABEL" 'any(.[]; . == $L)' <<< "$LABELS_JSON")
74+
if [ "$has_label" != "true" ]; then
75+
echo "::notice::$REQUIRED_LABEL not applied; gate passes."
76+
exit 0
77+
fi
78+
79+
runs=$(gh api "repos/$GH_REPO/actions/workflows/$WORKFLOW_FILE/runs?head_sha=$HEAD_SHA&event=push" --jq '.workflow_runs')
80+
latest=$(jq -c 'sort_by(.created_at) | reverse | .[0] // empty' <<< "$runs")
81+
82+
if [ -z "$latest" ]; then
83+
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."
84+
exit 1
85+
fi
86+
87+
status=$(jq -r '.status' <<< "$latest")
88+
conclusion=$(jq -r '.conclusion' <<< "$latest")
89+
90+
if [ "$conclusion" = "success" ]; then
91+
echo "$WORKFLOW_FILE succeeded for $HEAD_SHA."
92+
exit 0
93+
fi
94+
95+
if [ "$status" != "completed" ]; then
96+
echo "::error::$WORKFLOW_FILE is $status for $HEAD_SHA. This gate will re-evaluate on completion."
97+
exit 1
98+
fi
99+
100+
echo "::error::$WORKFLOW_FILE concluded as $conclusion for $HEAD_SHA."
101+
exit 1

.github/workflows/e2e-gate.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: E2E Gate
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review]
6+
workflow_run:
7+
workflows: ["Branch E2E Checks", "GPU Test"]
8+
types: [completed]
9+
10+
permissions: {}
11+
12+
jobs:
13+
e2e:
14+
name: E2E
15+
# Run on every PR event; on workflow_run, only when the upstream was the
16+
# matching gated workflow.
17+
if: >-
18+
github.event_name == 'pull_request' ||
19+
github.event.workflow_run.name == 'Branch E2E Checks'
20+
permissions:
21+
contents: read
22+
pull-requests: read
23+
actions: read
24+
uses: ./.github/workflows/e2e-gate-check.yml
25+
with:
26+
required_label: test:e2e
27+
workflow_file: branch-e2e.yml
28+
29+
gpu:
30+
name: GPU E2E
31+
if: >-
32+
github.event_name == 'pull_request' ||
33+
github.event.workflow_run.name == 'GPU Test'
34+
permissions:
35+
contents: read
36+
pull-requests: read
37+
actions: read
38+
uses: ./.github/workflows/e2e-gate-check.yml
39+
with:
40+
required_label: test:e2e-gpu
41+
workflow_file: test-gpu.yml

.github/workflows/test-gpu.yml

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,71 +7,50 @@ on:
77
workflow_dispatch: {}
88
# Add `schedule:` here when we want nightly coverage from the same workflow.
99

10-
permissions:
11-
contents: read
12-
pull-requests: read
13-
packages: write
10+
permissions: {}
1411

1512
jobs:
1613
pr_metadata:
1714
name: Resolve PR metadata
1815
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
pull-requests: read
1919
outputs:
2020
should_run: ${{ steps.gate.outputs.should_run }}
2121
steps:
22-
- id: get_pr_info
23-
if: github.event_name == 'push'
24-
continue-on-error: true
25-
uses: nv-gha-runners/get-pr-info@main
26-
22+
- uses: actions/checkout@v4
2723
- id: gate
28-
shell: bash
29-
env:
30-
EVENT_NAME: ${{ github.event_name }}
31-
GITHUB_SHA_VALUE: ${{ github.sha }}
32-
GET_PR_INFO_OUTCOME: ${{ steps.get_pr_info.outcome }}
33-
PR_INFO: ${{ steps.get_pr_info.outputs.pr-info }}
34-
run: |
35-
if [ "$EVENT_NAME" != "push" ]; then
36-
echo "should_run=true" >> "$GITHUB_OUTPUT"
37-
exit 0
38-
fi
39-
40-
if [ "$GET_PR_INFO_OUTCOME" != "success" ]; then
41-
echo "should_run=false" >> "$GITHUB_OUTPUT"
42-
exit 0
43-
fi
44-
45-
head_sha="$(jq -r '.head.sha' <<< "$PR_INFO")"
46-
has_gpu_label="$(jq -r '[.labels[].name] | index("test:e2e-gpu") != null' <<< "$PR_INFO")"
47-
48-
# Only trust copied pull-request/* pushes that still match the PR head SHA
49-
# and are explicitly labeled for GPU coverage.
50-
if [ "$head_sha" = "$GITHUB_SHA_VALUE" ] && [ "$has_gpu_label" = "true" ]; then
51-
should_run=true
52-
else
53-
should_run=false
54-
fi
55-
56-
echo "should_run=$should_run" >> "$GITHUB_OUTPUT"
24+
uses: ./.github/actions/pr-gate
25+
with:
26+
required_label: test:e2e-gpu
5727

5828
build-gateway:
5929
needs: [pr_metadata]
6030
if: needs.pr_metadata.outputs.should_run == 'true'
31+
permissions:
32+
contents: read
33+
packages: write
6134
uses: ./.github/workflows/docker-build.yml
6235
with:
6336
component: gateway
6437

6538
build-cluster:
6639
needs: [pr_metadata]
6740
if: needs.pr_metadata.outputs.should_run == 'true'
41+
permissions:
42+
contents: read
43+
packages: write
6844
uses: ./.github/workflows/docker-build.yml
6945
with:
7046
component: cluster
7147

7248
e2e-gpu:
7349
needs: [pr_metadata, build-gateway, build-cluster]
7450
if: needs.pr_metadata.outputs.should_run == 'true'
51+
permissions:
52+
contents: read
53+
packages: read
7554
uses: ./.github/workflows/e2e-gpu-test.yaml
7655
with:
7756
image-tag: ${{ github.sha }}

0 commit comments

Comments
 (0)