# 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. # Fork-based evidence signing — the Fulcio-bound leg of two-phase publish. # # A contributor publishes an UNSIGNED evidence bundle locally (where the # cluster lives, often behind a VPN that blocks Sigstore) and commits the # pointer FLAT — recipes/evidence/.yaml — because the nested # path segment derives from the signer a `--no-sign` pointer does # not yet have: # # aicr validate -r recipes/overlays/.yaml -s snapshot.yaml \ # --emit-attestation ./out --push ghcr.io//aicr-evidence --no-sign # cp ./out/pointer.yaml recipes/evidence/.yaml # # Profiled families (AKS/GKE gpuStack): snapshot with --aks-gpu-pools on # AKS (a plain snapshot on GKE) and pass -r a recipe hydrated via # `aicr recipe -s snapshot.yaml --intent [--platform ] # --profile gpuStack=` — the raw overlay resolves only the # declaration default, and intent/platform are author-selected (not in # the fingerprint), so state the target leaf's intent and optional # platform explicitly. # # The h100 GKE kubeflow training leaf also ships torch-distributed-tcpxo, # which requires the ordered GPU-NIC interface mapping. Overlay-direct # `validate`/`bundle`/`evidence digest` on it fails closed (auto-hydration # supplies no mapping), so hydrate the recipe first and add # `--gke-tcpxo-interfaces eth1=,...,eth8=` (or set # spec.recipe.configuration.gke.tcpxoInterfaces in --config). # See docs/contributor/evidence-publishing.md. # # They then run THIS workflow from their fork's Actions tab. It scans every # flat pointer under recipes/evidence/ except allowlist.yaml and runs # `aicr evidence sign --relocate` on each: an unsigned pointer is signed with # the runner's ambient OIDC identity (where Sigstore IS reachable) and then # relocated; an already-signed flat pointer left by a partial prior run is # relocated without re-signing (idempotent). The destination is the canonical # per-source path recipes/evidence///.yaml (the layout # the per-source contract gate requires), and the move is committed back to the # branch. Clean no-op when there are no flat pointers. This is the # commit-flat -> CI-sign -> CI-relocate-to-nested flow (#1530). # # Why workflow_dispatch (Option A): no recipe-name or bundle-ref inputs to # thread through — the committed pointer is the work item, and it already # carries bundle.oci + bundle.digest. Manual dispatch keeps the commit-back # predictable (no surprise commits) and avoids the loop-guards an automatic # push trigger would require. # # Two triggers: # * workflow_dispatch — manual, works anywhere (incl. upstream). # * push to recipes/evidence/** — auto-sign (Option B), forks only. # # Loop / scope guards for the auto-trigger (it commits back to the branch): # * GitHub does not re-run workflows for pushes made with the default # GITHUB_TOKEN, so the commit-back below does not re-trigger this on its # own. The job `if:` also skips a push whose head commit is our own # signing commit — belt-and-suspenders for forks pushing via a PAT. # * The job `if:` restricts the push path to forks (github.repository != # 'NVIDIA/aicr') and skips the fork's default branch (matched dynamically # via github.event.repository.default_branch, so a non-`main` default is # still covered), so it never auto-commits to a default branch. # * The signer script no-ops cleanly when no unsigned pointer is present. # * On forks with Actions or id-token disabled, the workflow simply does # not run / the sign step fails closed with a clear message. # # Security note: this runs in the *contributor's fork* on a branch they # control, signing with the fork's own GitHub Actions OIDC identity. The # id-token:write / contents:write grants act on the fork, not upstream — # there is no pull_request_target / untrusted-ref exposure (it is manual # dispatch on the fork's own code). # # The fork's aicr-evidence registry package must be PUBLIC: the upstream # evidence gate fetches the bundle anonymously, and a private package returns # HTTP 403. The signing step fails with a clear message pointing at this when # it can't pull. The signature ATTACH (a referrer write) is authenticated # separately by the ghcr-login step + packages:write below — public visibility # alone does not grant write. name: "Recipe Evidence: Sign" on: workflow_dispatch: {} push: paths: - 'recipes/evidence/**' permissions: contents: write # commit the patched pointer(s) back to the branch id-token: write # ambient OIDC for keyless Fulcio/Rekor signing # packages:write attaches the Sigstore bundle as an OCI referrer of the # already-pushed artifact (a registry write — making the package public only # enables the anonymous pre-sign pull, not the signature attach). packages: write concurrency: # Serialize per branch; do NOT cancel in progress — a cancel mid-sign # could leave a bundle signed in the registry but its pointer un-patched. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false jobs: sign: name: Sign unsigned evidence pointers runs-on: ubuntu-24.04 timeout-minutes: 20 # Manual dispatch runs anywhere. The push auto-trigger runs only in forks # (never the canonical repo), never on the fork's default branch (matched # dynamically so a renamed default is still covered), and skips our own # signing commit so the commit-back can't loop (GITHUB_TOKEN pushes already # don't re-trigger; this also covers forks pushing via a PAT). if: >- github.event_name == 'workflow_dispatch' || (github.repository != 'NVIDIA/aicr' && github.ref_name != github.event.repository.default_branch && !startsWith(github.event.head_commit.message, 'chore(evidence): sign pending evidence pointers')) steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Do not persist a token in .git/config: it would sit there across # the fork-controlled `go build` step. The commit-back does not need # it — it goes through the GitHub API (createCommitOnBranch), which # authenticates via GH_TOKEN in the commit step's env. persist-credentials: false - name: Load versions id: versions uses: ./.github/actions/load-versions - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ steps.versions.outputs.go }} cache: false - name: Build aicr run: | # Verify the vendored dependency tree against go.sum before building # the binary that will sign with the runner's OIDC identity. go mod verify go build -o ./bin/aicr ./cmd/aicr # `aicr evidence sign` attaches the Sigstore bundle as an OCI referrer — # a registry write — so authenticate first (ORAS reads Docker creds). The # public-package setting only enables the anonymous pre-sign pull. For a # non-GHCR registry, swap this for that registry's login. - name: Authenticate to GHCR uses: ./.github/actions/ghcr-login - name: Sign unsigned evidence pointers id: sign env: AICR: ${{ github.workspace }}/bin/aicr run: .github/scripts/evidence-sign-unsigned.sh - name: Commit signed pointers # always(), with no signed-count guard: the git-diff check inside the # script decides whether there is anything to commit. This is # load-bearing for partial failure — `aicr evidence sign` patches the # signer block into the flat pointer BEFORE the relocation step, so if # relocation then fails the bundle is already signed in the registry. # Committing the signed (but not-yet-relocated) pointer persists that # work, so the next dispatch takes the idempotent relocate-only path # instead of re-signing and attaching a duplicate referrer. The job # still fails (the sign step exited non-zero), surfacing the failure. # # The commit-back is created via GitHub's GraphQL createCommitOnBranch # mutation rather than `git push`, so GitHub applies its web-flow # signature and the commit shows Verified (#1551). The DCO sign-off is # preserved in the commit body; the loop guard holds because a commit # authored by GITHUB_TOKEN does not trigger workflow runs. if: ${{ always() }} env: GH_TOKEN: ${{ github.token }} run: .github/scripts/evidence-commit-signed.sh