Skip to content

Merge branch 'main' into add-custom-kubeconfig-support-verification #3

Merge branch 'main' into add-custom-kubeconfig-support-verification

Merge branch 'main' into add-custom-kubeconfig-support-verification #3

# 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/<recipe>.yaml — because the nested
# <source> path segment derives from the signer a `--no-sign` pointer does
# not yet have:
#
# aicr validate -r recipes/overlays/<slug>.yaml -s snapshot.yaml \
# --emit-attestation ./out --push ghcr.io/<owner>/aicr-evidence --no-sign
# cp ./out/pointer.yaml recipes/evidence/<recipe>.yaml
#
# 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/<recipe>/<source>/<digest>.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-latest
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ${{ steps.versions.outputs.go }}
cache: false
- name: Build aicr
env:
GOFLAGS: -mod=vendor
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