|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | +# |
| 5 | +# Commit the signed/relocated evidence pointers back to the branch through |
| 6 | +# GitHub's GraphQL `createCommitOnBranch` mutation so the commit carries |
| 7 | +# GitHub's web-flow signature and shows the **Verified** badge (#1551). |
| 8 | +# |
| 9 | +# Why the API instead of `git push`: GitHub auto-signs only commits it creates |
| 10 | +# server-side (REST contents API, GraphQL createCommitOnBranch, web editor, |
| 11 | +# merge button). A commit that arrives via `git push` is never signed by |
| 12 | +# GitHub, so the runner's client-side commit-back was always Unverified — the |
| 13 | +# `github-actions[bot]` identity has no GPG/SSH key on the runner to `-S` with. |
| 14 | +# createCommitOnBranch authors the commit as the GITHUB_TOKEN identity |
| 15 | +# (github-actions[bot]) and GitHub signs it → Verified. |
| 16 | +# |
| 17 | +# The signing step's relocation is a delete (flat pointer) + add (nested |
| 18 | +# pointer) plus an in-place signer patch, so the mutation sends the FULL |
| 19 | +# fileChanges.additions / fileChanges.deletions set computed from the working |
| 20 | +# tree against HEAD. |
| 21 | +# |
| 22 | +# Behavior preserved from the previous `git push` implementation: |
| 23 | +# * Clean no-op when nothing under recipes/evidence/ changed (nothing to |
| 24 | +# sign): exit 0 without creating a commit. |
| 25 | +# * DCO sign-off — a `Signed-off-by:` trailer matching the bot author is |
| 26 | +# added to the commit body so the DCO check passes on the commit-back. |
| 27 | +# * Loop guard — createCommitOnBranch runs with the default GITHUB_TOKEN, and |
| 28 | +# GitHub does not trigger workflow runs for token-authored commits, so the |
| 29 | +# commit-back does not re-trigger the sign workflow. The headline is |
| 30 | +# unchanged so the workflow's belt-and-suspenders `startsWith(...)` guard |
| 31 | +# still matches for any fork pushing via a PAT. |
| 32 | +# |
| 33 | +# Required env: |
| 34 | +# GH_TOKEN token authenticating `gh api` (github.token) |
| 35 | +# GITHUB_REPOSITORY owner/repo (provided by Actions) |
| 36 | +# GITHUB_REF_NAME branch name to commit onto (provided by Actions) |
| 37 | + |
| 38 | +set -euo pipefail |
| 39 | + |
| 40 | +: "${GH_TOKEN:?GH_TOKEN is required (token authenticating gh api)}" |
| 41 | +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required (owner/repo)}" |
| 42 | +: "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required (branch name)}" |
| 43 | + |
| 44 | +# Match the bot author createCommitOnBranch stamps on the commit so the DCO |
| 45 | +# sign-off trailer is consistent with the commit author. |
| 46 | +readonly BOT_NAME="github-actions[bot]" |
| 47 | +readonly BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" |
| 48 | +readonly HEADLINE="chore(evidence): sign pending evidence pointers" |
| 49 | + |
| 50 | +# Stage the relocation (delete flat + add nested + in-place signer patch) so an |
| 51 | +# untracked relocated file is counted, then decide whether there is anything to |
| 52 | +# commit. --no-renames splits every rename into a delete + add pair, which is |
| 53 | +# exactly the shape createCommitOnBranch.fileChanges expects. |
| 54 | +git add -A recipes/evidence/ |
| 55 | +if git diff --cached --quiet -- recipes/evidence/; then |
| 56 | + echo "No pointer changes to commit (nothing to sign)." |
| 57 | + exit 0 |
| 58 | +fi |
| 59 | + |
| 60 | +additions='[]' |
| 61 | +deletions='[]' |
| 62 | +while IFS= read -r -d '' status && IFS= read -r -d '' path; do |
| 63 | + case "$status" in |
| 64 | + D) |
| 65 | + deletions=$(jq -c --arg p "$path" '. += [{path: $p}]' <<<"$deletions") |
| 66 | + ;; |
| 67 | + *) |
| 68 | + # A (add) or M (modify): send the full file contents, base64-encoded as |
| 69 | + # the GraphQL API requires. -w0 keeps it single-line (GNU coreutils on |
| 70 | + # the ubuntu runner). |
| 71 | + contents=$(base64 -w0 <"$path") |
| 72 | + additions=$(jq -c --arg p "$path" --arg c "$contents" \ |
| 73 | + '. += [{path: $p, contents: $c}]' <<<"$additions") |
| 74 | + ;; |
| 75 | + esac |
| 76 | +done < <(git diff --cached --name-status --no-renames -z -- recipes/evidence/) |
| 77 | + |
| 78 | +# expectedHeadOid pins the mutation to the branch tip we checked out; a |
| 79 | +# concurrent advance fails the mutation loudly (re-dispatch after pulling) |
| 80 | +# rather than silently racing. |
| 81 | +head_oid=$(git rev-parse HEAD) |
| 82 | +body="Signed-off-by: ${BOT_NAME} <${BOT_EMAIL}>" |
| 83 | + |
| 84 | +variables=$(jq -n \ |
| 85 | + --arg repo "$GITHUB_REPOSITORY" \ |
| 86 | + --arg branch "$GITHUB_REF_NAME" \ |
| 87 | + --arg oid "$head_oid" \ |
| 88 | + --arg headline "$HEADLINE" \ |
| 89 | + --arg body "$body" \ |
| 90 | + --argjson additions "$additions" \ |
| 91 | + --argjson deletions "$deletions" \ |
| 92 | + '{ |
| 93 | + input: { |
| 94 | + branch: {repositoryNameWithOwner: $repo, branchName: $branch}, |
| 95 | + expectedHeadOid: $oid, |
| 96 | + message: {headline: $headline, body: $body}, |
| 97 | + fileChanges: {additions: $additions, deletions: $deletions} |
| 98 | + } |
| 99 | + }') |
| 100 | + |
| 101 | +read -r -d '' query <<'GRAPHQL' || true |
| 102 | +mutation ($input: CreateCommitOnBranchInput!) { |
| 103 | + createCommitOnBranch(input: $input) { |
| 104 | + commit { |
| 105 | + oid |
| 106 | + url |
| 107 | + } |
| 108 | + } |
| 109 | +} |
| 110 | +GRAPHQL |
| 111 | + |
| 112 | +# Post {query, variables} to the GraphQL endpoint. `input` is an object |
| 113 | +# variable, so it cannot be passed via `gh api graphql -f input=...` (that |
| 114 | +# would send a string and fail type-checking) — build the full request body |
| 115 | +# and stream it in. |
| 116 | +commit_oid=$(jq -n --arg q "$query" --argjson v "$variables" '{query: $q, variables: $v}' \ |
| 117 | + | gh api graphql --input - --jq '.data.createCommitOnBranch.commit.oid') |
| 118 | + |
| 119 | +echo "Committed signed pointers as ${commit_oid} (GitHub-signed, Verified)." |
0 commit comments