From f026d81284f03ee18769d7cd9979c7fed04d2b74 Mon Sep 17 00:00:00 2001 From: Chris Bonilla Date: Tue, 25 Aug 2026 14:52:42 +0200 Subject: [PATCH 1/3] chore: add reusable git-ai notes-consolidation workflow Centralizes the git-ai AI-authorship notes CI that seven repos are about to adopt, so the pinned git-ai version and installer checksum live in one place instead of seven copies. Fork PRs are explicitly unsupported and skipped: a fork's read-only GITHUB_TOKEN cannot push refs/notes/ai. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Cxd3MFvDsVNfo734S8nzyx --- .github/workflows/git-ai-notes.yaml | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/git-ai-notes.yaml diff --git a/.github/workflows/git-ai-notes.yaml b/.github/workflows/git-ai-notes.yaml new file mode 100644 index 0000000..262e7f1 --- /dev/null +++ b/.github/workflows/git-ai-notes.yaml @@ -0,0 +1,78 @@ +--- +name: Git AI notes + +# Reusable workflow. Consolidates git-ai AI-authorship notes (refs/notes/ai) at +# PR-merge time, so attribution survives rebase and squash merges: those give the +# merged commit a new SHA, which strands the note on the pre-merge commit. +# +# Callers own the trigger and must grant `contents: write`, because a called +# workflow cannot widen its own permissions: +# +# name: Git AI +# on: +# pull_request: +# types: [closed, synchronize] +# permissions: +# contents: write +# jobs: +# git-ai: +# uses: gooddata/github-actions-public/.github/workflows/git-ai-notes.yaml@ +# +# FORK PULL REQUESTS ARE NOT SUPPORTED, BY DESIGN. +# `pull_request` runs originating from a fork get a read-only GITHUB_TOKEN, so +# the push to refs/notes/ai cannot succeed. Those runs are skipped explicitly +# below rather than left to fail with a confusing permissions error. +# Do NOT "fix" this by moving callers to `pull_request_target`: that runs in +# base-repo context with a write token and is a privilege-escalation vector. +# On a fork PR, attribution stays in-band via the commit's own trailers. +# +# Supply-chain hardening vs. the stock `git-ai ci github install` output: +# * Installs from the GitHub release asset, NOT usegitai.com, which removes the +# vendor host, and its telemetry, from the trust path. +# * Pins an exact git-ai version (GIT_AI_VERSION) instead of "latest". +# * Verifies the installer's SHA-256 (INSTALLER_SHA256) before running it, the +# same way this org SHA-pins third-party actions. The pinned installer in +# turn verifies the downloaded binary against its embedded checksums. +# +# Bump GIT_AI_VERSION and INSTALLER_SHA256 together, here, once, for every +# calling repository. Get the checksum from that release's SHA256SUMS: +# curl -sSL https://github.com/git-ai-project/git-ai/releases/download//SHA256SUMS + +on: + workflow_call: + +env: + GIT_AI_VERSION: v1.6.24 + # sha256 of install.sh for GIT_AI_VERSION, from that release's SHA256SUMS + INSTALLER_SHA256: ad175700758a9053d38b7221dede5cdc047131d8d6894c2c3398cc048952c687 + +jobs: + git-ai: + # Fail closed: require a pull_request payload, exclude forks (read-only + # token), then run on merge or on a push to an open PR. + if: >- + github.event.pull_request != null + && github.event.pull_request.head.repo.fork != true + && (github.event.pull_request.merged == true + || github.event.action == 'synchronize') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Install git-ai (pinned + checksum-verified) + run: | + set -euo pipefail + installer="$(mktemp)" + curl --fail --location --silent --show-error \ + -o "$installer" \ + "https://github.com/git-ai-project/git-ai/releases/download/${GIT_AI_VERSION}/install.sh" + echo "${INSTALLER_SHA256} ${installer}" | sha256sum -c - + bash "$installer" + echo "$HOME/.git-ai/bin" >> "$GITHUB_PATH" + - name: Run git-ai + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git-ai ci github run From 4cb46d5d6ae1fa958cf88b9a3e3c90150327081c Mon Sep 17 00:00:00 2001 From: Chris Bonilla Date: Tue, 25 Aug 2026 16:09:12 +0200 Subject: [PATCH 2/3] chore: harden the git-ai job condition against read-only-token runs Addresses CodeRabbit review on #30. Compare head.repo.full_name to github.repository instead of testing head.repo.fork. The fork test fails open: when a fork's head repository has been deleted, head.repo is null and `null != true` evaluates true, so the job would run and then fail pushing refs/notes/ai. The full_name comparison fails closed on the same input. Exclude dependabot[bot]. Dependabot branches live in the base repository, so fork is false, but GitHub still hands those runs a read-only GITHUB_TOKEN. None of the seven initial callers use Dependabot; this is a shared component and its future callers are unknown. Also records why concurrency is deliberately not serialized: a concurrency group keeps one pending run and cancels the rest, so it can evict a queued merge-time consolidation. For this job a dropped run is worse than a retried one, and git-ai already fetch-merge-retries the notes push. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Cxd3MFvDsVNfo734S8nzyx --- .github/workflows/git-ai-notes.yaml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/git-ai-notes.yaml b/.github/workflows/git-ai-notes.yaml index 262e7f1..3694fc0 100644 --- a/.github/workflows/git-ai-notes.yaml +++ b/.github/workflows/git-ai-notes.yaml @@ -25,6 +25,16 @@ name: Git AI notes # Do NOT "fix" this by moving callers to `pull_request_target`: that runs in # base-repo context with a write token and is a privilege-escalation vector. # On a fork PR, attribution stays in-band via the commit's own trailers. +# Dependabot is excluded for the same reason: its runs get a read-only token +# even though its branch lives in the base repository, so `fork` is false. +# +# CONCURRENCY IS DELIBERATELY NOT SERIALIZED. +# git-ai fetches, merges and retries a non-fast-forward push to the notes ref, +# so racing runs normally converge. A GitHub `concurrency` group would be worse +# than the race it prevents: a group holds one running plus one pending run and +# CANCELS further pending ones, so a queued merge-time consolidation can be +# evicted by a later synchronize. For a job whose entire purpose is complete +# attribution, a dropped run costs more than a retried one. # # Supply-chain hardening vs. the stock `git-ai ci github install` output: # * Installs from the GitHub release asset, NOT usegitai.com, which removes the @@ -48,11 +58,15 @@ env: jobs: git-ai: - # Fail closed: require a pull_request payload, exclude forks (read-only - # token), then run on merge or on a push to an open PR. + # Fail closed on every axis. Require a pull_request payload; require the head + # branch to live in this repository (an exact full_name match, not + # `head.repo.fork != true`, which passes when head.repo is null because the + # fork was deleted); exclude Dependabot, whose runs get a read-only token + # even from a base-repo branch. Then run on merge, or on a push to an open PR. if: >- github.event.pull_request != null - && github.event.pull_request.head.repo.fork != true + && github.event.pull_request.head.repo.full_name == github.repository + && github.actor != 'dependabot[bot]' && (github.event.pull_request.merged == true || github.event.action == 'synchronize') runs-on: ubuntu-latest From a3e3906ea7cc59c42d5d67b11947d34ec14b4ebc Mon Sep 17 00:00:00 2001 From: Chris Bonilla Date: Tue, 25 Aug 2026 17:15:49 +0200 Subject: [PATCH 3/3] chore: require the literal pull_request event for the git-ai job Addresses CodeRabbit review on #30. git-ai returns no CI context unless GITHUB_EVENT_NAME is exactly "pull_request" (src/ci/github.rs:46-49 at v1.6.24, `if env_event_name != "pull_request"`). The previous condition was satisfied by a same-repository pull_request_target caller, which would have produced a green run that consolidated nothing. This also makes the file's existing "do not use pull_request_target" warning mechanically true rather than advisory: a caller that ignores it now gets a skip instead of a silent no-op. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Cxd3MFvDsVNfo734S8nzyx --- .github/workflows/git-ai-notes.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/git-ai-notes.yaml b/.github/workflows/git-ai-notes.yaml index 3694fc0..6985ec5 100644 --- a/.github/workflows/git-ai-notes.yaml +++ b/.github/workflows/git-ai-notes.yaml @@ -58,13 +58,17 @@ env: jobs: git-ai: - # Fail closed on every axis. Require a pull_request payload; require the head + # Fail closed on every axis. Require the literal `pull_request` event, because + # git-ai bails when GITHUB_EVENT_NAME is anything else (src/ci/github.rs) and a + # pull_request_target caller would otherwise get a green run that did nothing; + # require a pull_request payload; require the head # branch to live in this repository (an exact full_name match, not # `head.repo.fork != true`, which passes when head.repo is null because the # fork was deleted); exclude Dependabot, whose runs get a read-only token # even from a base-repo branch. Then run on merge, or on a push to an open PR. if: >- - github.event.pull_request != null + github.event_name == 'pull_request' + && github.event.pull_request != null && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && (github.event.pull_request.merged == true