Skip to content

Commit 5d87042

Browse files
committed
ci: cut over non-release workflows to shared runners
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
1 parent a255ad9 commit 5d87042

7 files changed

Lines changed: 137 additions & 41 deletions

File tree

.github/workflows/branch-checks.yml

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,45 @@
11
name: Branch Checks
22

33
on:
4-
pull_request:
4+
push:
5+
branches:
6+
- "pull-request/[0-9]+"
7+
workflow_dispatch:
58

69
env:
710
CARGO_TERM_COLOR: always
811
CARGO_INCREMENTAL: "0"
912
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13+
SCCACHE_GHA_ENABLED: "true"
1014

1115
permissions:
1216
contents: read
1317
packages: read
1418

19+
concurrency:
20+
group: ${{ github.workflow }}-${{ github.ref }}
21+
cancel-in-progress: true
22+
1523
jobs:
24+
pr_metadata:
25+
name: Resolve PR metadata
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
pull-requests: read
30+
outputs:
31+
should_run: ${{ steps.gate.outputs.should_run }}
32+
steps:
33+
- uses: actions/checkout@v6
34+
35+
- id: gate
36+
uses: ./.github/actions/pr-gate
37+
1638
mise-lockfile:
1739
name: mise Lockfile
18-
runs-on: build-amd64
40+
needs: pr_metadata
41+
if: needs.pr_metadata.outputs.should_run == 'true'
42+
runs-on: linux-amd64-cpu8
1943
container:
2044
image: ghcr.io/nvidia/openshell/ci:latest
2145
credentials:
@@ -27,16 +51,7 @@ jobs:
2751
- name: Mark workspace as safe for git
2852
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
2953

30-
- name: Detect mise config changes
31-
id: changed
32-
uses: tj-actions/changed-files@aa08304bd477b800d468db44fe10f6c61f7f7b11 # v42.1.0
33-
with:
34-
files: |
35-
mise.toml
36-
mise.lock
37-
3854
- name: Verify mise.lock is in sync with mise.toml
39-
if: steps.changed.outputs.any_changed == 'true'
4055
run: |
4156
mise lock
4257
if ! git diff --exit-code mise.lock; then
@@ -46,7 +61,9 @@ jobs:
4661
4762
license-headers:
4863
name: License Headers
49-
runs-on: build-amd64
64+
needs: pr_metadata
65+
if: needs.pr_metadata.outputs.should_run == 'true'
66+
runs-on: linux-amd64-cpu8
5067
container:
5168
image: ghcr.io/nvidia/openshell/ci:latest
5269
credentials:
@@ -63,11 +80,15 @@ jobs:
6380

6481
rust:
6582
name: Rust (${{ matrix.runner }})
83+
needs: pr_metadata
84+
if: needs.pr_metadata.outputs.should_run == 'true'
6685
strategy:
6786
fail-fast: false
6887
matrix:
69-
runner: [build-amd64, build-arm64]
88+
runner: [linux-amd64-cpu8, linux-arm64-cpu8]
7089
runs-on: ${{ matrix.runner }}
90+
env:
91+
SCCACHE_GHA_VERSION: branch-checks-rust-${{ matrix.runner }}
7192
container:
7293
image: ghcr.io/nvidia/openshell/ci:latest
7394
credentials:
@@ -79,9 +100,8 @@ jobs:
79100
- name: Install tools
80101
run: mise install --locked
81102

82-
- name: Configure sccache remote cache
83-
if: vars.SCCACHE_MEMCACHED_ENDPOINT != ''
84-
run: echo "SCCACHE_MEMCACHED_ENDPOINT=${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}" >> "$GITHUB_ENV"
103+
- name: Configure GHA sccache backend
104+
uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
85105

86106
- name: Cache Rust target and registry
87107
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
@@ -103,14 +123,24 @@ jobs:
103123

104124
- name: sccache stats
105125
if: always()
106-
run: mise x -- sccache --show-stats
126+
run: |
127+
set +e
128+
stats_bin="${SCCACHE_PATH:-sccache}"
129+
"$stats_bin" --show-stats
130+
status=$?
131+
if [[ $status -ne 0 ]]; then
132+
echo "::warning::sccache stats unavailable (exit $status)"
133+
fi
134+
exit 0
107135
108136
python:
109137
name: Python (${{ matrix.runner }})
138+
needs: pr_metadata
139+
if: needs.pr_metadata.outputs.should_run == 'true'
110140
strategy:
111141
fail-fast: false
112142
matrix:
113-
runner: [build-amd64, build-arm64]
143+
runner: [linux-amd64-cpu8, linux-arm64-cpu8]
114144
runs-on: ${{ matrix.runner }}
115145
container:
116146
image: ghcr.io/nvidia/openshell/ci:latest
@@ -134,7 +164,9 @@ jobs:
134164

135165
markdown:
136166
name: Markdown
137-
runs-on: build-amd64
167+
needs: pr_metadata
168+
if: needs.pr_metadata.outputs.should_run == 'true'
169+
runs-on: linux-amd64-cpu8
138170
container:
139171
image: ghcr.io/nvidia/openshell/ci:latest
140172
credentials:
@@ -144,7 +176,7 @@ jobs:
144176
- uses: actions/checkout@v6
145177

146178
- name: Install tools
147-
run: mise install
179+
run: mise install --locked
148180

149181
- name: Lint
150182
run: mise run markdown:lint

.github/workflows/branch-e2e.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ jobs:
3434
with:
3535
component: gateway
3636
platform: linux/arm64
37-
runner: build-arm64
3837

3938
build-cluster:
4039
needs: [pr_metadata]
@@ -46,7 +45,6 @@ jobs:
4645
with:
4746
component: cluster
4847
platform: linux/arm64
49-
runner: build-arm64
5048

5149
e2e:
5250
needs: [pr_metadata, build-gateway, build-cluster]
@@ -57,4 +55,4 @@ jobs:
5755
uses: ./.github/workflows/e2e-test.yml
5856
with:
5957
image-tag: ${{ github.sha }}
60-
runner: build-arm64
58+
runner: linux-arm64-cpu8

.github/workflows/ci-image.yml

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,19 @@ permissions:
2121

2222
jobs:
2323
build-ci-image:
24-
name: Build
25-
runs-on: build-amd64
24+
name: Build (${{ matrix.arch }})
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
include:
29+
- arch: amd64
30+
platform: linux/amd64
31+
runner: linux-amd64-cpu8
32+
- arch: arm64
33+
platform: linux/arm64
34+
runner: linux-arm64-cpu8
35+
runs-on: ${{ matrix.runner }}
36+
timeout-minutes: 60
2637
steps:
2738
- uses: actions/checkout@v6
2839

@@ -33,18 +44,66 @@ jobs:
3344
username: ${{ github.actor }}
3445
password: ${{ secrets.GITHUB_TOKEN }}
3546

47+
- name: Resolve BuildKit config
48+
id: buildkit
49+
run: |
50+
if [[ -r /etc/buildkit/buildkitd.toml ]]; then
51+
echo "config=/etc/buildkit/buildkitd.toml" >> "$GITHUB_OUTPUT"
52+
else
53+
echo "config=" >> "$GITHUB_OUTPUT"
54+
fi
55+
3656
- name: Set up Docker Buildx
3757
uses: ./.github/actions/setup-buildx
58+
with:
59+
driver: local
60+
buildkitd-config: ${{ steps.buildkit.outputs.config }}
3861

3962
- name: Build and push CI image
4063
env:
4164
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65+
ARCH_IMAGE: ${{ env.CI_IMAGE }}:${{ github.sha }}-${{ matrix.arch }}
4266
run: |
67+
set -euo pipefail
4368
docker buildx build \
44-
--platform linux/amd64,linux/arm64 \
69+
--builder openshell \
70+
--platform "${{ matrix.platform }}" \
4571
--secret id=MISE_GITHUB_TOKEN,env=MISE_GITHUB_TOKEN \
72+
--cache-from "type=gha,scope=ci-image-${{ matrix.arch }}" \
73+
--cache-to "type=gha,mode=max,scope=ci-image-${{ matrix.arch }}" \
4674
--push \
47-
-t ${{ env.CI_IMAGE }}:${{ github.sha }} \
48-
-t ${{ env.CI_IMAGE }}:latest \
75+
-t "$ARCH_IMAGE" \
4976
-f deploy/docker/Dockerfile.ci \
5077
.
78+
79+
- name: Smoke check CI image
80+
env:
81+
ARCH_IMAGE: ${{ env.CI_IMAGE }}:${{ github.sha }}-${{ matrix.arch }}
82+
run: |
83+
set -euo pipefail
84+
docker run --rm --platform "${{ matrix.platform }}" "$ARCH_IMAGE" mise --version
85+
docker run --rm --platform "${{ matrix.platform }}" "$ARCH_IMAGE" gh --version
86+
docker run --rm --platform "${{ matrix.platform }}" "$ARCH_IMAGE" docker buildx version
87+
88+
merge-ci-image:
89+
name: Merge manifest
90+
needs: build-ci-image
91+
runs-on: linux-amd64-cpu8
92+
timeout-minutes: 10
93+
steps:
94+
- name: Log in to GitHub Container Registry
95+
uses: docker/login-action@v3
96+
with:
97+
registry: ${{ env.REGISTRY }}
98+
username: ${{ github.actor }}
99+
password: ${{ secrets.GITHUB_TOKEN }}
100+
101+
- name: Create multi-arch manifests
102+
run: |
103+
set -euo pipefail
104+
docker buildx imagetools create \
105+
--prefer-index=false \
106+
-t "${CI_IMAGE}:${GITHUB_SHA}" \
107+
-t "${CI_IMAGE}:latest" \
108+
"${CI_IMAGE}:${GITHUB_SHA}-amd64" \
109+
"${CI_IMAGE}:${GITHUB_SHA}-arm64"

.github/workflows/docker-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ on:
2626
description: "Deprecated; per-arch native runners are selected automatically"
2727
required: false
2828
type: string
29-
default: "build-amd64"
29+
default: "linux-amd64-cpu8"
3030
cargo-version:
3131
description: "Pre-computed cargo version (skips internal git-based computation)"
3232
required: false

.github/workflows/e2e-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
description: "GitHub Actions runner label"
1212
required: false
1313
type: string
14-
default: "build-amd64"
14+
default: "linux-amd64-cpu8"
1515

1616
permissions:
1717
contents: read

CI.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ For local test commands see [TESTING.md](TESTING.md). For PR conventions see [CO
66

77
## Overview
88

9-
E2E tests run on self-hosted runners (`build-arm64`, GPU runners). To keep untrusted PR code off those runners we use NVIDIA's copy-pr-bot, which mirrors trusted PR commits to internal `pull-request/<N>` branches in this repository. The gated workflows trigger on pushes to those branches, not on the original PR.
9+
PR CI that runs on NVIDIA self-hosted runners uses NVIDIA's copy-pr-bot. The bot mirrors trusted PR commits to internal `pull-request/<N>` branches in this repository. The gated workflows trigger on pushes to those branches, not on the original PR.
10+
11+
`Branch Checks` run automatically after copy-pr-bot mirrors the PR. E2E suites are opt-in because they are more expensive and publish temporary images.
1012

1113
Two opt-in labels enable the suites:
1214

@@ -63,11 +65,11 @@ Prerequisites:
6365
Flow:
6466

6567
1. Open the PR. copy-pr-bot mirrors it to `pull-request/<N>` automatically.
66-
2. The first push of `pull-request/<N>` runs `Branch E2E Checks`, but it skips the build/E2E jobs because no label is set yet. The PR's `E2E Gate` check passes as a no-op (no label, no requirement).
67-
3. A maintainer applies `test:e2e` and/or `test:e2e-gpu`. `E2E Label Help` posts a comment with a link to the existing `Branch E2E Checks` run.
68+
2. The mirror push runs `Branch Checks` automatically. The first `Branch E2E Checks` / `GPU Test` run only resolves metadata and skips expensive jobs unless the matching label is already set.
69+
3. A maintainer applies `test:e2e` and/or `test:e2e-gpu`. `E2E Label Help` posts a comment with a link to the existing gated workflow run.
6870
4. The maintainer opens that link and clicks **Re-run all jobs**. This time `pr_metadata` sees the label and the build/E2E jobs run.
6971
5. When the run finishes, the `E2E Gate` check on the PR flips to green automatically.
70-
6. New commits push to the mirror automatically and re-trigger `Branch E2E Checks`. Because the label is still set, those runs execute the build/E2E jobs without manual re-run.
72+
6. New commits push to the mirror automatically and re-trigger `Branch Checks` plus any labeled E2E/GPU workflows.
7173

7274
### Forked PR
7375

@@ -102,6 +104,7 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma
102104

103105
| File | Role |
104106
|---|---|
107+
| `.github/workflows/branch-checks.yml` | Required non-E2E PR checks. Triggers on `push: pull-request/[0-9]+`. |
105108
| `.github/workflows/branch-e2e.yml` | Non-GPU E2E. Triggers on `push: pull-request/[0-9]+`. |
106109
| `.github/workflows/test-gpu.yml` | GPU E2E. Triggers on `push: pull-request/[0-9]+`. |
107110
| `.github/actions/pr-gate/action.yml` | Composite action that resolves PR metadata and verifies the required label is set. |

architecture/ci-e2e.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This document describes the architecture of the E2E CI flow: every workflow invo
66

77
Three independent goals shape the design:
88

9-
1. **Self-hosted runner safety.** E2E runs on `build-arm64` and on GPU runners. GitHub's [security hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners) states bluntly: "Self-hosted runners should almost never be used for public repositories on GitHub, because any user can open pull requests against the repository and compromise the environment." Our workaround is the same one used elsewhere in NVIDIA's GHA infrastructure: copy-pr-bot mirrors trusted PRs into `pull-request/<N>` branches inside this repository, and the self-hosted workflows trigger on `push` to those mirror branches rather than on `pull_request`.
9+
1. **Self-hosted runner safety.** Required PR checks, E2E, and GPU tests run on NVIDIA self-hosted runners. GitHub's [security hardening guide](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners) states bluntly: "Self-hosted runners should almost never be used for public repositories on GitHub, because any user can open pull requests against the repository and compromise the environment." Our workaround is the same one used elsewhere in NVIDIA's GHA infrastructure: copy-pr-bot mirrors trusted PRs into `pull-request/<N>` branches inside this repository, and the self-hosted workflows trigger on `push` to those mirror branches rather than on `pull_request`.
1010
2. **Label as a hard merge gate.** When a PR carries `test:e2e` (or `test:e2e-gpu`), the corresponding suite *must* have actually executed and passed for the PR head SHA. The label has to be enforcing, not advisory: it blocks merge unless the suite ran with the label set.
1111
3. **Per-job least privilege on the GitHub token.** Each workflow declares `permissions: {}` at the top, and each job declares only what it needs. This follows the hardening pattern described at <https://astral.sh/blog/open-source-security-at-astral>.
1212

@@ -17,20 +17,21 @@ These three goals do not compose cleanly: the safety goal forces `push: pull-req
1717
| File | Trigger | Role |
1818
|---|---|---|
1919
| `.github/copy-pr-bot.yaml` | (config) | Tells copy-pr-bot to mirror trusted PRs into `pull-request/<N>` branches. Pre-existed. |
20-
| `.github/workflows/branch-e2e.yml` | `push: pull-request/[0-9]+` + `workflow_dispatch` | Runs non-GPU E2E on `build-arm64`. |
20+
| `.github/workflows/branch-checks.yml` | `push: pull-request/[0-9]+` + `workflow_dispatch` | Runs required branch checks on `linux-amd64-cpu8` and `linux-arm64-cpu8`. |
21+
| `.github/workflows/branch-e2e.yml` | `push: pull-request/[0-9]+` + `workflow_dispatch` | Runs non-GPU E2E on `linux-arm64-cpu8`. |
2122
| `.github/workflows/test-gpu.yml` | `push: pull-request/[0-9]+` + `workflow_dispatch` | Runs GPU E2E on self-hosted GPU runners. |
22-
| `.github/workflows/shadow-branch-e2e.yml` | `push: pull-request/[0-9]+` + `workflow_dispatch` | Non-required shared-runner E2E shadow coverage for OS-49 Phase 5. |
23+
| `.github/workflows/shadow-branch-e2e.yml` | `push: pull-request/[0-9]+` + `workflow_dispatch` | Historical non-required shared-runner E2E shadow coverage for OS-49 Phase 5. |
2324
| `.github/actions/pr-gate/action.yml` | (composite) | Resolves PR metadata for a `pull-request/<N>` push and decides whether the run should proceed. Label enforcement is optional, so non-required shadows can validate mirror metadata without introducing another PR label. |
2425
| `.github/workflows/e2e-gate.yml` | `pull_request` + `workflow_run` | Posts the required `E2E Gate` check on the PR. Re-evaluates after the gated workflow completes. |
2526
| `.github/workflows/e2e-gate-check.yml` | `workflow_call` | Reusable gate logic shared by E2E and GPU E2E. |
2627
| `.github/workflows/e2e-label-help.yml` | `pull_request_target: [labeled]` | Posts a PR comment when a `test:e2e*` label is applied, telling the maintainer the next manual step (re-run an existing run, or `/ok to test <SHA>` to refresh the mirror). Does *not* dispatch the workflow itself - see "Why we don't auto-dispatch" below. |
2728
| `.github/workflows/e2e-test.yml`, `e2e-gpu-test.yaml`, `docker-build.yml` | `workflow_call` | Reusable worker workflows. Unchanged by this design - called from the gated workflows and from release workflows. |
2829

29-
## OS-49 shadow runner coverage
30+
## OS-49 runner migration
3031

31-
OS-49 Phase 5 adds non-required shadow workflows for the non-release workflows being prepared for shared-runner cutover. They all use `workflow_dispatch` for manual bake runs and `push: pull-request/[0-9]+` for copy-pr-bot mirrored PRs.
32+
OS-49 Phase 5 added non-required shadow workflows for the non-release workflows being prepared for shared-runner cutover. Phase 6 promotes the validated shared-runner path into the real non-release workflows.
3233

33-
`shadow-branch-checks.yml` and `shadow-ci-image.yml` use `pr-gate` without a required label. That still verifies the mirror SHA matches the source PR head SHA, but does not require a new GitHub label for every ordinary CI shadow run. `shadow-branch-e2e.yml` keeps the existing `test:e2e` gate because it publishes temporary images and runs the expensive E2E suite. It shadows the top-level `branch-e2e.yml` workflow, which already exercises the reusable `e2e-test.yml` worker path, so Phase 5 does not keep a second direct `e2e-test.yml` shadow workflow.
34+
`branch-checks.yml` uses `pr-gate` without a required label. That still verifies the mirror SHA matches the source PR head SHA, but does not require a new GitHub label for ordinary required checks. `branch-e2e.yml` keeps the existing `test:e2e` gate because it publishes temporary images and runs the expensive E2E suite. `ci-image.yml` now builds amd64 and arm64 CI images natively on shared CPU runners and merges the multi-arch manifest after both per-arch images are pushed.
3435

3536
## Trigger taxonomy
3637

@@ -147,10 +148,13 @@ Labels persist as PR metadata and survive re-runs and force-pushes. Comment-base
147148

148149
## Permission posture
149150

150-
Every workflow declares `permissions: {}` at the top. Per-job grants are the minimum needed:
151+
The gated E2E workflows declare `permissions: {}` at the top. Branch checks and CI image publishing use the minimum workflow/job grants needed for checkout, package pulls, and package pushes.
151152

152153
| Workflow | Job | Grants |
153154
|---|---|---|
155+
| `branch-checks.yml` | workflow default | `contents: read`, `packages: read` |
156+
| | `pr_metadata` | `contents: read`, `pull-requests: read` |
157+
| `ci-image.yml` | workflow default | `contents: read`, `packages: write` |
154158
| `branch-e2e.yml`, `test-gpu.yml` | `pr_metadata` | `contents: read`, `pull-requests: read` |
155159
| | `build-*` | `contents: read`, `packages: write` |
156160
| | `e2e*` | `contents: read`, `packages: read` |

0 commit comments

Comments
 (0)