-
Notifications
You must be signed in to change notification settings - Fork 72
256 lines (246 loc) · 11.3 KB
/
Copy pathuat-run.yaml
File metadata and controls
256 lines (246 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: UAT Run
# A distinct run title per dispatch so the nightly controller can identify the
# run it dispatched — `gh workflow run` does not return a run id, so the
# controller matches on this display title. The controller supplies a unique
# dispatch_key (its run id + cell index) that is appended here, so a concurrent
# manual dispatch of the same reservation/version can never be mistaken for the
# controller's run. A manual dispatch omits the key and the suffix is dropped.
# Quoted so YAML does not treat the ` #` in the dispatch_key suffix as a comment.
run-name: "UAT ${{ inputs.reservation }} @ ${{ inputs.aicr_version || 'main' }}${{ inputs.dispatch_key != '' && format(' #{0}', inputs.dispatch_key) || '' }}"
# The shared UAT dispatch surface and reservation-lease owner (#1274, DC1).
# A human (workflow_dispatch) or the nightly batch (workflow_call) requests a
# run against a named reservation; this workflow holds the per-reservation
# concurrency lease, resolves the reservation row from the checked-in registry
# (infra/uat/reservations.yaml) via the uat-broker helper, and dispatches the
# cloud-appropriate reusable pipeline. Because every run for a reservation
# routes through here and shares the lease, contending runs QUEUE instead of
# racing the hardware.
#
# DC2 (#1275) adds two orthogonal dials, both forwarded to the cloud pipeline:
# - intent (training|inference) selects the per-intent test config +
# recipe criteria.
# - lifecycle (nightly|daytime-up|daytime-down) selects the cluster
# lifecycle: the nightly provision→CUJ→teardown, the daytime
# provision-and-hold (stable lease-tagged name, no teardown), or the
# evening teardown of that held daytime cluster.
on:
workflow_dispatch:
inputs:
reservation:
description: 'Reservation name from infra/uat/reservations.yaml (e.g. aws-h100, gcp-h100, azure-h100).'
type: string
required: true
aicr_version:
description: 'Released aicr version to test (e.g. v1.2.3); empty = build from source (main tip).'
type: string
default: ''
dispatch_key:
description: 'Unique correlation key set by the nightly controller so it can find this run; leave empty for manual runs.'
type: string
default: ''
intent:
description: 'Recipe intent — selects the per-intent test config (h100-<intent>-config.yaml) and recipe criteria.'
type: choice
options: [training, inference]
default: training
lifecycle:
description: 'Cluster lifecycle. nightly: provision→CUJ→teardown. daytime-up: provision+deploy+HOLD (stable name, no teardown). daytime-down: tear down the held daytime cluster.'
type: choice
options: [nightly, daytime-up, daytime-down]
default: nightly
skip_delete:
description: 'Skip cluster teardown (manual debugging only; ignored for daytime lifecycles).'
type: boolean
default: false
skip_tests:
description: 'Skip UAT test execution (manual debugging only).'
type: boolean
default: false
workflow_call:
inputs:
reservation:
type: string
required: true
aicr_version:
type: string
default: ''
dispatch_key:
type: string
default: ''
intent:
type: string
default: training
lifecycle:
type: string
default: nightly
skip_delete:
type: boolean
default: false
skip_tests:
type: boolean
default: false
permissions:
contents: read
# The reservation lease. Every run targeting a given reservation serializes on
# this group, so a contender waits rather than racing the reservation;
# cancel-in-progress:false keeps an in-progress run (and its teardown) from
# being killed.
#
# NOTE: this is a single-slot lease, not a full queue. GitHub concurrency holds
# at most one in-progress + one PENDING run per group; a third contender
# supersedes (cancels) the older pending one — cancel-in-progress:false only
# protects the in-progress run, not the pending slot. Acceptable at launch (two
# reservations, at most cron + one ad-hoc each); preserving every request would
# need the deferred standing broker (see docs/contributor/uat.md, #1264).
#
# github.event.inputs is used for the dispatch path because the inputs context
# is unreliable in a top-level concurrency group on workflow_dispatch
# (community #45734/#35341); inputs.* covers the workflow_call path, where
# github.event.inputs is empty.
concurrency:
group: uat-${{ github.event.inputs.reservation || inputs.reservation }}
cancel-in-progress: false
jobs:
# Resolve the reservation row to the cloud + on-disk config paths the
# cloud-specific pipeline consumes. A typo'd reservation name fails here
# (uat-broker exits NOT_FOUND), so the registry is the single source of truth.
resolve:
if: github.repository == 'nvidia/aicr'
runs-on: ubuntu-latest
outputs:
cloud: ${{ steps.row.outputs.cloud }}
cluster_config_path: ${{ steps.row.outputs['cluster-config-path'] }}
test_config_dir: ${{ steps.row.outputs['test-config-dir'] }}
# Accelerator selects the per-accelerator test-config filename
# (<accelerator>-<intent>-config.yaml) in the AWS pipeline. The AWS
# account (tenancy) is NOT plumbed here: the AWS pipeline derives it from
# the cluster-config's .deployment.tenancy, the single source of truth.
accelerator: ${{ steps.row.outputs.accelerator }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Load versions
id: versions
uses: ./.github/actions/load-versions
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '${{ steps.versions.outputs.go }}'
cache: true
cache-dependency-path: |
go.sum
vendor/modules.txt
- name: Resolve reservation row
id: row
env:
GOFLAGS: -mod=vendor
# Pass via env (not inline interpolation) to avoid expression
# injection through the dispatch input.
RESERVATION: ${{ inputs.reservation }}
run: |
set -euo pipefail
go build -o ./bin/uat-broker ./tools/uat-broker
./bin/uat-broker reservations --name "$RESERVATION" >> "$GITHUB_OUTPUT"
# One of run-aws / run-gcp / run-azure fires based on the resolved cloud
# (the registry validates cloud membership, so at most one matches; the
# unmapped-cloud job below fails closed if a registry cloud has no
# run-<cloud> job here yet). The caller job grants the permission SUPERSET
# its callee needs — a reusable workflow's jobs are ceilinged by the calling
# job's permissions, and the called pipeline does OIDC, GHCR push, and
# (nested) evidence-ingest GCS WIF.
run-aws:
needs: resolve
if: needs.resolve.outputs.cloud == 'aws'
# Superset ceiling for the nested pipeline (a reusable workflow's jobs
# cannot exceed the caller's permissions).
permissions:
contents: read # checkout inside the called pipeline
actions: read # required by the nested uat-{aws,gcp} + evidence-ingest jobs
id-token: write # cloud OIDC/WIF: provision, keyless cosign, and GCS evidence publish
packages: write # push validator + snapshot-agent images to GHCR
uses: ./.github/workflows/uat-aws.yaml
with:
reservation: ${{ inputs.reservation }}
aicr_version: ${{ inputs.aicr_version }}
intent: ${{ inputs.intent }}
lifecycle: ${{ inputs.lifecycle }}
cluster_config_path: ${{ needs.resolve.outputs.cluster_config_path }}
test_config_dir: ${{ needs.resolve.outputs.test_config_dir }}
accelerator: ${{ needs.resolve.outputs.accelerator }}
skip_delete: ${{ inputs.skip_delete }}
skip_tests: ${{ inputs.skip_tests }}
run-gcp:
needs: resolve
if: needs.resolve.outputs.cloud == 'gcp'
# Superset ceiling for the nested pipeline (a reusable workflow's jobs
# cannot exceed the caller's permissions).
permissions:
contents: read # checkout inside the called pipeline
actions: read # required by the nested uat-{aws,gcp} + evidence-ingest jobs
id-token: write # cloud OIDC/WIF: provision, keyless cosign, and GCS evidence publish
packages: write # push validator + snapshot-agent images to GHCR
uses: ./.github/workflows/uat-gcp.yaml
with:
reservation: ${{ inputs.reservation }}
aicr_version: ${{ inputs.aicr_version }}
intent: ${{ inputs.intent }}
lifecycle: ${{ inputs.lifecycle }}
cluster_config_path: ${{ needs.resolve.outputs.cluster_config_path }}
test_config_dir: ${{ needs.resolve.outputs.test_config_dir }}
skip_delete: ${{ inputs.skip_delete }}
skip_tests: ${{ inputs.skip_tests }}
run-azure:
needs: resolve
if: needs.resolve.outputs.cloud == 'azure'
# Superset ceiling for the nested pipeline (a reusable workflow's jobs
# cannot exceed the caller's permissions).
permissions:
contents: read # checkout inside the called pipeline
actions: read # required by the nested uat-* + evidence-ingest jobs
id-token: write # cloud OIDC/WIF: provision, keyless cosign, and GCS evidence publish
packages: write # push validator + snapshot-agent images to GHCR
uses: ./.github/workflows/uat-azure.yaml
with:
reservation: ${{ inputs.reservation }}
aicr_version: ${{ inputs.aicr_version }}
intent: ${{ inputs.intent }}
lifecycle: ${{ inputs.lifecycle }}
cluster_config_path: ${{ needs.resolve.outputs.cluster_config_path }}
test_config_dir: ${{ needs.resolve.outputs.test_config_dir }}
skip_delete: ${{ inputs.skip_delete }}
skip_tests: ${{ inputs.skip_tests }}
# Fail closed on an unmapped cloud. The broker validates cloud membership
# against its own set, but a cloud can be added there (and to the registry)
# before its run-<cloud> job exists here — without this job the dispatch
# would silently no-op: the lease is taken, no pipeline runs, and the
# requester sees a green run that did nothing.
unmapped-cloud:
needs: resolve
if: >-
needs.resolve.outputs.cloud != 'aws' &&
needs.resolve.outputs.cloud != 'gcp' &&
needs.resolve.outputs.cloud != 'azure'
runs-on: ubuntu-latest
steps:
- name: Fail on unmapped cloud
env:
CLOUD: ${{ needs.resolve.outputs.cloud }}
run: |
set -euo pipefail
echo "::error::reservation resolved to cloud '${CLOUD}', which has no run-<cloud> job in uat-run.yaml — add one (see run-azure for the shape)."
exit 1