Skip to content

Commit 13dc294

Browse files
committed
fix(ci): dispatch TestGrid from evidence ingest
Signed-off-by: Sujan Rao <sujan@nvidia.com>
1 parent 1d94a04 commit 13dc294

2 files changed

Lines changed: 64 additions & 223 deletions

File tree

.github/workflows/evidence-ingest.yaml

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -233,42 +233,33 @@ jobs:
233233
done
234234
235235
# ---------------------------------------------------------------------
236-
# Job 3: refresh the published dashboard. Every successful ingest lands
237-
# new evidence in GCS, so the GP5 dashboard (evidence-dashboard-publish)
238-
# must re-render to surface it — otherwise the site is stale until the
239-
# next unrelated push to main. The push-to-main ingest path already
240-
# re-triggers the dashboard via its own `push:` trigger, but the
241-
# first-party UAT path is a NESTED workflow_call (up to four levels deep:
242-
# uat-run -> uat-{aws,gcp,azure} -> evidence-ingest), which emits no
243-
# top-level run event and cannot nest a fifth reusable workflow. A
244-
# workflow_dispatch decouples cleanly: it starts a fresh top-level run
245-
# regardless of how deep this ingest was invoked.
236+
# Job 3: trigger downstream evidence consumers. Every successful ingest lands
237+
# verified evidence in GCS, so the static dashboard must re-render and the
238+
# live TestGrid publisher must receive the same digest-pinned bundle ref.
246239
#
247-
# Held in a SEPARATE job so the GCS-credentialed publish job never also
248-
# carries actions:write. `needs: publish` inherits the produced gate — a
249-
# skipped/failed publish skips this too, so the dashboard only re-renders
250-
# after evidence is actually in the bucket. The dashboard's own
251-
# concurrency group ("pages", cancel-in-progress:false) coalesces the
252-
# flurry from a multi-cell nightly batch into a single queued rebuild
253-
# that the newest dispatch supersedes.
240+
# The first-party UAT path is a nested workflow_call (uat-run -> cloud UAT ->
241+
# evidence-ingest), where workflow_run chaining is suppressed for nightly runs
242+
# initiated by GITHUB_TOKEN. Explicit workflow_dispatch starts fresh top-level
243+
# runs regardless of nesting depth or the original UAT actor.
254244
#
255-
# The refresh is BEST-EFFORT: the evidence is already in the bucket and the
256-
# dashboard self-heals on the next ingest or push to main, so a transient
257-
# dispatch failure must never fail this workflow_call chain — otherwise the
258-
# nightly controller would record a fully-successful, multi-hour UAT cell as
259-
# a failed leg. The dispatch step retries, then warns (never fails), so
260-
# staleness is surfaced but not fatal — the same not-cry-wolf posture as
261-
# uat-superseded-notice.yaml.
245+
# Held in a separate job so the GCS-credentialed publish job never also
246+
# carries actions:write. `needs: publish` ensures downstream consumers are
247+
# triggered only after verified evidence is persisted.
248+
#
249+
# Dispatch is best-effort: evidence is already durable, and a transient
250+
# dispatch failure must not turn a successful multi-hour UAT cell red. Each
251+
# dispatch retries and emits a visible warning on exhaustion. The dashboard
252+
# self-heals on the next ingest; TestGrid can be backfilled by bundle ref.
262253
# ---------------------------------------------------------------------
263-
trigger-dashboard:
264-
name: Trigger dashboard publish
254+
trigger-publishes:
255+
name: Trigger evidence publishes
265256
needs: publish
266257
if: github.repository == 'nvidia/aicr'
267258
runs-on: ubuntu-latest
268259
timeout-minutes: 5
269260
permissions:
270261
contents: read
271-
actions: write # dispatch evidence-dashboard-publish.yaml
262+
actions: write # dispatch dashboard and TestGrid workflows
272263
steps:
273264
- name: Dispatch dashboard publish
274265
env:
@@ -293,3 +284,27 @@ jobs:
293284
sleep 5
294285
done
295286
echo "::warning::could not dispatch Evidence: Dashboard Publish after 3 attempts; the dashboard will refresh on the next ingest or push to main"
287+
288+
- name: Dispatch TestGrid publish
289+
if: >-
290+
inputs.bundle_ref != '' &&
291+
(github.ref_name == 'main' || startsWith(github.ref_name, 'release/'))
292+
env:
293+
BUNDLE_REF: ${{ inputs.bundle_ref }}
294+
GH_TOKEN: ${{ github.token }}
295+
REPO: ${{ github.repository }}
296+
run: |
297+
set -euo pipefail
298+
for attempt in 1 2 3; do
299+
if gh workflow run testgrid-publish.yml \
300+
--repo "${REPO}" --ref main \
301+
-f bundle_ref="${BUNDLE_REF}" \
302+
-f source_class=uat \
303+
-f environment=prod; then
304+
echo "dispatched TestGrid Publish on main"
305+
exit 0
306+
fi
307+
echo "dispatch attempt ${attempt} failed; retrying in 5s..."
308+
sleep 5
309+
done
310+
echo "::warning::could not dispatch TestGrid Publish after 3 attempts; backfill bundle ${BUNDLE_REF} manually"

.github/workflows/testgrid-publish.yml

Lines changed: 22 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,20 @@
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

5856
on:
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:
@@ -120,14 +80,9 @@ on:
12080
- staging
12181

12282
concurrency:
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

13388
jobs:
@@ -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

Comments
 (0)