1414
1515# TestGrid Publish (TG5)
1616#
17- # Publishes UAT evidence bundles to the AICR TestGrid dashboard after
18- # every completed UAT run (uat-run.yaml, which wraps the GCP or AWS
19- # pipeline — any conclusion; a failure or cancelled run that never
20- # reached conformance simply has nothing to publish and no-ops cleanly).
17+ # Publishes verified UAT evidence bundles to the AICR TestGrid dashboard.
18+ # Evidence ingest dispatches this workflow only after conformance produced and
19+ # verified a digest-pinned bundle.
2120#
2221# Disabled by default — gated on vars.TESTGRID_PUBLISH_ENABLED == 'true' —
2322# until the prod WIF pool/SA exist (see aicr-testgrid Terraform apply).
2423#
2524# Pipeline:
26- # 1. Validate the producer run is from this repository and branch
27- # 2. Download the evidence pointer artifact from the triggering UAT run
28- # 3. Extract + validate the OCI bundle ref + digest from pointer.yaml
29- # 4. Authenticate to GCP via WIF (publish SA, groups/ prefix write-only)
30- # 5. Log in to GHCR to pull the evidence OCI bundle
31- # 6. Build testgrid-publish from source
32- # 7. Pull the OCI bundle and write started/junit/finished to GCS
25+ # 1. Accept the verified, digest-pinned OCI bundle ref from evidence ingest
26+ # 2. Validate the bundle ref format
27+ # 3. Authenticate to GCP via WIF (publish SA, groups/ prefix write-only)
28+ # 4. Log in to GHCR to pull the evidence OCI bundle
29+ # 5. Build testgrid-publish from source
30+ # 6. Pull the OCI bundle and write started/junit/finished to GCS
3331#
3432# Auth:
3533# WIF pool: aicr-testgrid[-<env>]-github (aicr-testgrid Terraform)
@@ -56,48 +54,10 @@ permissions:
5654 contents : read
5755
5856on :
59- workflow_run :
60- # "UAT Run" (uat-run.yaml) is the only UAT workflow that always executes
61- # top-level: both a human (workflow_dispatch) and the nightly/daytime
62- # controllers trigger it via `gh workflow run` (a real dispatch, not
63- # `uses:`) specifically to keep it top-level — see the "Dispatch (not
64- # workflow_call)" comments in uat-nightly-batch.yaml / uat-daytime.yaml.
65- # uat-gcp.yaml / uat-aws.yaml are workflow_call-only reusable workflows
66- # invoked exclusively via `uses:` from uat-run.yaml's run-gcp/run-aws
67- # jobs — GitHub never emits a workflow_run event for a workflow that
68- # only ever executes as a workflow_call target, so watching them
69- # directly (as this file previously did) never fires.
70- #
71- # Jobs in a called reusable workflow share the caller's github.run_id,
72- # so github.event.workflow_run.id below still correctly matches the
73- # ${{ github.run_id }}-suffixed evidence-pointer artifact names
74- # uploaded from inside uat-gcp.yaml / uat-aws.yaml, and
75- # github.event.workflow_run.conclusion still reflects the real UAT
76- # outcome (run-gcp/run-aws call the reusable workflows with no
77- # continue-on-error, so a conformance failure propagates to
78- # uat-run.yaml's own conclusion).
79- workflows :
80- - " UAT Run"
81- types :
82- - completed
83- # Allow manual dispatch for testing and backfills.
84- #
85- # Trust model: the real enforcement is at the WIF layer, not the
86- # github.ref == 'refs/heads/main' check below (that check is just a
87- # fast, readable fail-closed guard, evaluated from whatever copy of this
88- # file was dispatched — it cannot be the actual security boundary).
89- # The publish SA's IAM binding (aicr-testgrid's wif-publish.tf,
90- # google_service_account_iam_member.publish_wif) grants
91- # roles/iam.workloadIdentityUser via a principalSet pinned to
92- # attribute.workflow_ref == ".../testgrid-publish.yml@refs/heads/main".
93- # GitHub's OIDC token embeds the actual ref the workflow file was loaded
94- # from as job_workflow_ref, so dispatching a modified copy of this file
95- # from any other branch produces a token that does not match the
96- # principalSet — WIF impersonation is rejected at the GCP layer
97- # regardless of what the dispatched copy's own if: checks say.
98- #
99- # bundle_ref is validated for format (ghcr.io/<org>/...@sha256:<64hex>)
100- # but not provenance/signature before the tool pulls and parses it.
57+ # Evidence ingest explicitly dispatches this workflow with the verified,
58+ # digest-pinned bundle ref. workflow_dispatch is an allowed GITHUB_TOKEN
59+ # recursion exception, unlike workflow_run chains from bot-dispatched UAT.
60+ # Manual dispatch uses the same input for testing and backfills.
10161 workflow_dispatch :
10262 inputs :
10363 bundle_ref :
12080 - staging
12181
12282concurrency :
123- # One publish per triggering run (workflow_run) or per environment+bundle
124- # (workflow_dispatch) — prevents duplicate/racing columns if the workflow
125- # is re-run, or the same bundle_ref is dispatched twice, while a prior
126- # publish is still in flight. environment is included in the dispatch key
127- # so a staging backfill and a prod publish of the same bundle_ref (which
128- # write to different buckets) don't serialize behind each other. Falls
129- # back to run_id only when neither is available.
130- group : testgrid-publish-${{ github.event.workflow_run.id || format('{0}-{1}', inputs.environment, inputs.bundle_ref) || github.run_id }}
83+ # Serialize publishes for the same environment and bundle while one is in
84+ # flight. The publisher's create-only writes guard against later duplicates.
85+ group : testgrid-publish-${{ format('{0}-{1}', inputs.environment, inputs.bundle_ref) }}
13186 cancel-in-progress : false # never cancel an in-flight GCS write
13287
13388jobs :
@@ -136,29 +91,14 @@ jobs:
13691 runs-on : ubuntu-latest
13792 timeout-minutes : 15 # Go build ~2 min + gcloud uploads ~3 min; 15 min is generous
13893
139- # Gate on vars.TESTGRID_PUBLISH_ENABLED so this workflow is a true no-op
140- # (skipped, not failed) until the prod WIF pool/SA exist in Terraform.
141- # Flip to 'true' once aicr-testgrid prod Terraform is applied — before
142- # that, every successful UAT would otherwise fail at the GCP auth step
143- # and produce CI noise.
144- #
145- # For workflow_run: run for ANY completed conclusion (success, failure,
146- # cancelled, timed_out, skipped, ...) — "Resolve bundle ref" already
147- # no-ops cleanly when no evidence pointer exists (only uploaded on a
148- # successful conformance phase), so non-terminal conclusions fall
149- # through to the same benign skip path instead of the job silently
150- # never running at all.
151- # For workflow_dispatch: restrict to main as a readable fail-closed
152- # guard — the actual security boundary is the WIF SA binding, not this
153- # check (see trust-model comment on the trigger above).
94+ # Stay disabled until the prod WIF pool/SA exists. The workflow is always
95+ # loaded from main; WIF's workflow_ref condition is the security boundary.
15496 if : >
15597 vars.TESTGRID_PUBLISH_ENABLED == 'true' &&
156- ((github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
157- github.event_name == 'workflow_run')
98+ github.ref == 'refs/heads/main'
15899
159100 permissions :
160101 contents : read
161- actions : read # required to download artifacts from the triggering run
162102 packages : read # required to pull the evidence OCI bundle from ghcr.io
163103 id-token : write # required for WIF token exchange
164104
@@ -171,38 +111,6 @@ jobs:
171111 with :
172112 persist-credentials : false
173113
174- # Sanity check before any credential exchange: confirm the producer
175- # run belongs to this repository. Note: github.event.workflow_run.
176- # repository is always the base repo that hosts the workflow (never
177- # a fork) — a repo-scoped workflow_run trigger cannot fire from a
178- # fork's own run in the first place, so this is defense-in-depth,
179- # not fork rejection. WIF's attribute_condition in wif-publish.tf is
180- # the actual security boundary; this check just fails fast with a
181- # clear message instead of a cryptic WIF rejection.
182- - name : Validate producer
183- if : github.event_name == 'workflow_run'
184- env :
185- PRODUCER_REPO : ${{ github.event.workflow_run.repository.full_name }}
186- PRODUCER_BRANCH : ${{ github.event.workflow_run.head_branch }}
187- run : |
188- set -euo pipefail
189- if [ "${PRODUCER_REPO}" != "${GITHUB_REPOSITORY}" ]; then
190- echo "::error::Producer workflow is from '${PRODUCER_REPO}', expected '${GITHUB_REPOSITORY}' — rejecting"
191- exit 1
192- fi
193- # Allowlist: main and release/* branches qualify for TestGrid.
194- # UAT supports workflow_dispatch on any branch, so without this
195- # an experimental dev-branch UAT would land a column in the
196- # dashboard. Reject anything else explicitly (fail closed).
197- case "${PRODUCER_BRANCH}" in
198- main|release/*)
199- ;;
200- *)
201- echo "::error::Producer ran on branch '${PRODUCER_BRANCH}' — only main and release/* branches publish to TestGrid"
202- exit 1
203- ;;
204- esac
205-
206114 # Resolve environment-specific GCP resource names.
207115 # prod → aicr-testgrid (no env suffix — matches Terraform cluster_name)
208116 # other → aicr-testgrid-<env>
@@ -238,103 +146,21 @@ jobs:
238146 id : versions
239147 uses : ./.github/actions/load-versions
240148
241- # Install yq (used to parse pointer.yaml) and Go using the repo's
242- # shared composite action — keeps tool versions in sync with .settings.yaml.
243- - name : Set up build tools
244- uses : ./.github/actions/setup-build-tools
245- with :
246- install_yq : ' true'
247- yq_version : ${{ steps.versions.outputs.yq }}
248149
249150 - name : Set up Go
250151 uses : actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
251152 with :
252153 go-version : ${{ steps.versions.outputs.go }}
253154 cache : false
254155
255- # Download the evidence pointer artifact from the triggering UAT run.
256- # The pointer is only uploaded when conformance succeeds — on failure runs
257- # with no pointer artifact the download action exits non-zero; we capture
258- # the outcome and distinguish "not found" (benign) from network errors.
259- - name : Download evidence pointer
260- id : download-pointer
261- if : github.event_name == 'workflow_run'
262- uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
263- with :
264- github-token : ${{ github.token }}
265- run-id : ${{ github.event.workflow_run.id }}
266- pattern : " *-evidence-pointer-${{ github.event.workflow_run.id }}"
267- merge-multiple : true
268- path : evidence-pointer/
269- continue-on-error : true # outcome checked explicitly in Resolve bundle ref
270-
271- # Extract the OCI ref + digest from pointer.yaml.
272- # For workflow_dispatch the bundle_ref input is used directly.
273- # All user-controlled values flow through env — never inline ${{ }}
274- # in shell scripts to prevent script injection.
275- #
276- # pointer.yaml schema:
277- # attestations:
278- # - bundle:
279- # oci: ghcr.io/nvidia/aicr-uat-gke-h100-training-<run_id>
280- # digest: sha256:abc123...
156+ # Validate the digest-pinned OCI ref supplied by evidence ingest or a
157+ # manual backfill. Pass it through env rather than inline shell expansion.
281158 - name : Resolve bundle ref
282159 id : bundle
283160 env :
284- EVENT_NAME : ${{ github.event_name }}
285- DISPATCH_BUNDLE_REF : ${{ inputs.bundle_ref }}
286- DOWNLOAD_POINTER_OUTCOME : ${{ steps.download-pointer.outcome }}
287- UAT_CONCLUSION : ${{ github.event.workflow_run.conclusion }}
161+ BUNDLE_REF : ${{ inputs.bundle_ref }}
288162 run : |
289163 set -euo pipefail
290- if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
291- BUNDLE_REF="${DISPATCH_BUNDLE_REF}"
292- else
293- # If the download step failed, distinguish "artifact not found"
294- # (conformance did not complete — benign skip) from a real error
295- # (network failure — should not silently drop the publish).
296- # `-print -quit` + `|| true` tolerates an absent evidence-pointer/
297- # directory under set -euo pipefail (find would otherwise exit
298- # non-zero and kill the script before the -z check below runs).
299- POINTER="$(find evidence-pointer/ -name 'pointer.yaml' -print -quit 2>/dev/null || true)"
300- DOWNLOAD_OUTCOME="${DOWNLOAD_POINTER_OUTCOME}"
301- if [ -z "${POINTER}" ]; then
302- if [ "${DOWNLOAD_OUTCOME}" = "failure" ] && [ "${UAT_CONCLUSION}" = "success" ]; then
303- # Narrow known edge case: a manual `skip_tests` UAT dispatch
304- # also concludes success with no pointer (conformance never
305- # ran), which looks identical to a network error from here.
306- # Accepted — this only affects operator-triggered skip_tests
307- # runs, which have nothing to publish anyway.
308- echo "::error::UAT succeeded but evidence pointer download failed — possible network error"
309- exit 1
310- fi
311- if [ "${DOWNLOAD_OUTCOME}" = "failure" ]; then
312- # Unlike the success case above, a non-success UAT conclusion
313- # can't be disambiguated: artifact absence is expected here
314- # (conformance may never have run), so a genuine transient
315- # download error looks identical. Surface it for visibility
316- # without failing the job — there's nothing to publish
317- # either way.
318- echo "::warning::Evidence pointer download failed for a non-success UAT run (conclusion=${UAT_CONCLUSION}) — could be genuine absence or a transient error; treating as skip"
319- fi
320- echo "::notice::No evidence pointer found (conformance did not complete) — skipping TestGrid publish"
321- echo "skip=true" >> "${GITHUB_OUTPUT}"
322- exit 0
323- fi
324- OCI=$(yq '.attestations[0].bundle.oci' "${POINTER}")
325- DIGEST=$(yq '.attestations[0].bundle.digest' "${POINTER}")
326- if [ -z "${OCI}" ] || [ "${OCI}" = "null" ]; then
327- echo "::error::Could not extract OCI ref from ${POINTER}"
328- cat "${POINTER}"
329- exit 1
330- fi
331- if [ -z "${DIGEST}" ] || [ "${DIGEST}" = "null" ]; then
332- echo "::error::Could not extract digest from ${POINTER}"
333- cat "${POINTER}"
334- exit 1
335- fi
336- BUNDLE_REF="${OCI}@${DIGEST}"
337- fi
338164 # Validate format: ghcr.io/<org>/...@sha256:<64 hex chars>
339165 # Namespace is intentionally NOT restricted to nvidia/ — community
340166 # bundles (source_class=community) are hosted under contributor
0 commit comments