From 3d27c9218023907951beaa68be2966450e5895a3 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sat, 18 Jul 2026 18:53:24 -0700 Subject: [PATCH] ci(bump-callers): generalize caller SHA-bump to the agents-md-integrity fleet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cursor-review caller auto-bump (a reusable-workflow SHA rots without it — the pins needed two manual bump PRs in one day) is now fleet-agnostic. The bump logic moves into one shared script, .github/bump-callers/bump-callers.sh, driven by two thin workflow entrypoints that differ only in their path-filter trigger and three parameters (caller variable, human tag, reusable-workflow filename): - bump-cursor-review-callers.yml -> CURSOR_REVIEW_CALLERS (unchanged behavior) - bump-agents-md-callers.yml -> AGENTS_MD_CALLERS (new; triggers on a change to agents-md-integrity.yml or .github/agents-md-integrity/**) One implementation, not a fork: shared machinery that got copied has drifted before. The entrypoints stay separate rather than a single matrix because a cursor-review.yml change must not spuriously bump agents-md callers, or vice versa. Preserved exactly for the cursor-review fleet: the ::add-mask:: privacy handling for private caller names in this public repo's logs, the PR-per-caller flow, the trailing-newline restore, and the single-line PR body. AGENTS_MD_CALLERS shares the same {repo,file,label} JSON shape and is seeded empty ([]) — an empty list is a clean no-op there (the fleet grows as callers land), while a fleet that must always have callers still hard-fails on empty. Tests: a bash functional suite stubs gh (no network) and asserts both variables parse, private names are masked, the pin + comment are rewritten, the trailing newline is kept, and the empty/malformed guards fire. Run in CI by test-bump-callers.yml alongside shellcheck. --- .github/bump-callers/README.md | 45 +++++ .github/bump-callers/bump-callers.sh | 185 +++++++++++++++++ .../bump-callers/tests/test_bump_callers.sh | 187 ++++++++++++++++++ .github/workflows/bump-agents-md-callers.yml | 72 +++++++ .../workflows/bump-cursor-review-callers.yml | 145 ++------------ .github/workflows/test-bump-callers.yml | 43 ++++ 6 files changed, 548 insertions(+), 129 deletions(-) create mode 100644 .github/bump-callers/README.md create mode 100755 .github/bump-callers/bump-callers.sh create mode 100755 .github/bump-callers/tests/test_bump_callers.sh create mode 100644 .github/workflows/bump-agents-md-callers.yml create mode 100644 .github/workflows/test-bump-callers.yml diff --git a/.github/bump-callers/README.md b/.github/bump-callers/README.md new file mode 100644 index 0000000..fb0bd1b --- /dev/null +++ b/.github/bump-callers/README.md @@ -0,0 +1,45 @@ +# bump-callers + +The shared machinery that keeps SHA-pinned **callers** of this repo's reusable +workflows from rotting. When a reusable workflow is updated on `main`, it opens +a SHA-bump PR in every repo that pins a caller against it — so consumers move +forward automatically instead of silently drifting commits behind. + +- **`bump-callers.sh`** — the one, fleet-agnostic bump script (parse the caller + list, mask private repo names, rewrite the pin, open one PR per caller). It is + the single source of truth; the two workflow entrypoints are thin wrappers + that only supply per-fleet parameters. A forked copy is how other shared + machinery in the org has drifted — this stays one file on purpose. +- **`tests/`** — a `bash` functional suite (stubs `gh`, no network), run by + [`test-bump-callers.yml`](../workflows/test-bump-callers.yml) plus shellcheck. + +## The two fleets + +| Entrypoint | Triggers on a change to | Caller variable | Seeded | +|---|---|---|---| +| [`bump-cursor-review-callers.yml`](../workflows/bump-cursor-review-callers.yml) | `cursor-review.yml` | `CURSOR_REVIEW_CALLERS` | non-empty (hard-fails if empty) | +| [`bump-agents-md-callers.yml`](../workflows/bump-agents-md-callers.yml) | `agents-md-integrity.yml` or `agents-md-integrity/**` | `AGENTS_MD_CALLERS` | empty `[]` (grows as callers land) | + +They stay as two thin entrypoints rather than one matrix because their triggers +differ: a `cursor-review.yml` change must not spuriously bump agents-md callers, +and vice versa. Everything else (masking, the PR-per-caller flow, the trailing- +newline fix, the single-line PR body) lives once in `bump-callers.sh`. + +## The caller variables + +This repo is **public** — the workflow files and Actions run logs are both +publicly viewable — and most callers are private, so caller names must never +appear in a committed file or in the logs. Each fleet's caller list lives in a +repo-level Actions **variable** (config, not a credential) as a JSON array of +`{"repo","file","label"}` objects (`label` optional). `bump-callers.sh` +`::add-mask::`es every repo name out of the run logs before echoing it. + +Adding/removing a caller needs **no public commit** — edit the variable: + +```bash +gh variable set AGENTS_MD_CALLERS --repo Comfy-Org/github-workflows \ + --body "$(jq -c . callers.json)" +``` + +Keep the canonical `callers.json` in a private infra/ops repo so variable edits +have a reviewed source of truth (the org audit log records each edit). diff --git a/.github/bump-callers/bump-callers.sh b/.github/bump-callers/bump-callers.sh new file mode 100755 index 0000000..1b1cf4d --- /dev/null +++ b/.github/bump-callers/bump-callers.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# +# Generalized caller SHA-bumper for the reusable workflows in this repo. +# +# When a reusable workflow (e.g. cursor-review.yml or agents-md-integrity.yml) +# is updated on main, this opens a SHA-bump PR in every repo that pins a caller +# against it. It is fleet-agnostic: the thin workflow entrypoints +# (bump-cursor-review-callers.yml, bump-agents-md-callers.yml) invoke it with a +# different caller list, human tag, and reusable-workflow filename. There is ONE +# implementation so the two fleets cannot drift apart (a forked copy is how +# other shared machinery in the org has rotted); the entrypoints differ only in +# their path-filter triggers and the parameters below. +# +# This repo is PUBLIC (both the workflow files and the Actions run logs are +# publicly viewable) and most callers are private, so caller names must never +# appear in a committed file or in the logs. Each caller list lives in a +# repo-level Actions variable (config, not a credential — a variable, not a +# secret) as a JSON array of {"repo","file","label"} objects, and every repo +# name is `::add-mask::`ed out of the run logs before it is ever echoed. +# +# Required environment: +# GH_TOKEN Token with contents:write + pull-requests:write on the callers +# (the Cloud Code Bot app token generated by the entrypoint). +# NEW_SHA The github-workflows commit to pin callers to (github.sha). +# CALLERS_JSON The caller list (value of the fleet's Actions variable). +# VAR_NAME Name of that variable — used only in error messages. +# TAG Human tag for branch/commit/PR text (e.g. "cursor-review"). +# WORKFLOW_FILE Reusable workflow filename referenced in the PR body. +# Optional: +# ALLOW_EMPTY "true" → an empty caller list is a clean no-op (a fleet that +# is seeded empty and grows). Default "false" → an empty/missing +# list is a hard error (a fleet that always has callers must +# never silently no-op — that would leave every caller un-bumped +# without anyone noticing). + +# NOTE: deliberately no `set -e`. Each caller is bumped independently in +# bump_one(); a failure there returns non-zero and is downgraded to a per-repo +# warning so one bad repo cannot abort the whole fan-out. We still fail the job +# at the end if any repo failed. +set -uo pipefail + +: "${NEW_SHA:?NEW_SHA is required}" +: "${VAR_NAME:?VAR_NAME is required}" +: "${TAG:?TAG is required}" +: "${WORKFLOW_FILE:?WORKFLOW_FILE is required}" +CALLERS_JSON="${CALLERS_JSON-}" +ALLOW_EMPTY="${ALLOW_EMPTY:-false}" + +SHORT="${NEW_SHA:0:7}" +BRANCH="ci/bump-${TAG}-${SHORT}" + +STRIPPED="${CALLERS_JSON//[[:space:]]/}" + +# Empty handling is fleet-specific. A fleet seeded empty (ALLOW_EMPTY=true) +# treats an empty/absent list as an expected clean no-op — its per-repo caller +# tickets add entries as callers land. Every other fleet keeps the original +# hard-fail so a silent no-op dispatcher (which would leave every caller +# un-bumped) is impossible. +if [[ "$ALLOW_EMPTY" == "true" ]] && { [[ -z "$STRIPPED" ]] || [[ "$STRIPPED" == "[]" ]]; }; then + echo "${VAR_NAME} has no callers yet — nothing to bump for ${TAG}." + exit 0 +fi + +# Load the caller list. Hard-fail loudly on a missing/empty/invalid value. +if [[ -z "$STRIPPED" ]]; then + echo "::error::${VAR_NAME} variable is missing or empty. Seed it with the caller list — see this workflow's header comment for the update flow." + exit 1 +fi +if ! jq -e 'type == "array" and length > 0 and all(.[]; (.repo | type == "string" and . != "") and (.file | type == "string" and . != ""))' <<<"$CALLERS_JSON" >/dev/null 2>&1; then + echo "::error::${VAR_NAME} is not a non-empty JSON array of {repo,file,label} objects (each needing a non-empty repo and file). Fix the variable — see the workflow header." + exit 1 +fi + +# Parse the JSON into repo|file|label tuples, and mask every repo name in the +# (publicly viewable) run logs BEFORE the loop that echoes it, so all per-repo +# output shows *** instead of a private repo name. +CALLERS=() +while IFS= read -r ENTRY; do + echo "::add-mask::${ENTRY%%|*}" + CALLERS+=("$ENTRY") +done < <(jq -r '.[] | "\(.repo)|\(.file)|\(.label // "")"' <<<"$CALLERS_JSON") + +if (( ${#CALLERS[@]} == 0 )); then + echo "::error::${VAR_NAME} parsed to zero callers — refusing to run a no-op dispatcher." + exit 1 +fi + +# Bump a single caller repo. Every external call is guarded, so a bad repo +# returns non-zero here (caught by the loop) instead of killing the run. An +# expected skip (file missing / already pinned) is a successful `return 0`. +bump_one() { + local ENTRY="$1" REPO FILE LABEL FILE_ENC + REPO=$(cut -d'|' -f1 <<<"$ENTRY") + FILE=$(cut -d'|' -f2 <<<"$ENTRY") + LABEL=$(cut -d'|' -f3 <<<"$ENTRY") + FILE_ENC="${FILE//\//%2F}" + + local DEFAULT_BRANCH + DEFAULT_BRANCH=$(gh api "repos/${REPO}" --jq '.default_branch') || { + echo "::warning::${REPO}: cannot read repo metadata — skipping" + return 1 + } + + # Fetch current file; a missing file is an expected skip (success). + local CURRENT + CURRENT=$(gh api "repos/${REPO}/contents/${FILE_ENC}?ref=${DEFAULT_BRANCH}" 2>/dev/null) || { + echo "::warning::${REPO}: ${FILE} not found — skipping" + return 0 + } + + local BLOB_SHA OLD_CONTENT + BLOB_SHA=$(jq -r '.sha' <<<"$CURRENT") + OLD_CONTENT=$(jq -r '.content' <<<"$CURRENT" | base64 -d) + + # Already pinned to this SHA → nothing to do (success). + if grep -qF "$NEW_SHA" <<<"$OLD_CONTENT"; then + echo "${REPO}: already at ${SHORT} — skipping" + return 0 + fi + + # Replace every 40-char hex SHA (the caller's pin — cursor-review has one, + # agents-md-integrity has two: the `uses: ...@` and its `workflows_ref`) + # and normalize the stale `# github-workflows#NN` pin comment. The comment + # rewrite is a no-op for callers that use a different comment form (e.g. + # agents-md-integrity's `# v1`), so it is safe to share across fleets. + local NEW_CONTENT + NEW_CONTENT=$(sed -E "s/[0-9a-f]{40}/${NEW_SHA}/g; s|# github-workflows#[0-9]+|# github-workflows main (${SHORT})|g" <<<"$OLD_CONTENT") + + # Create the branch from the default-branch tip (ignore 422 if it exists). + local MAIN_SHA + MAIN_SHA=$(gh api "repos/${REPO}/git/refs/heads/${DEFAULT_BRANCH}" --jq '.object.sha') || { + echo "::warning::${REPO}: cannot read ${DEFAULT_BRANCH} ref — skipping" + return 1 + } + gh api --method POST "repos/${REPO}/git/refs" \ + --field ref="refs/heads/${BRANCH}" \ + --field sha="${MAIN_SHA}" 2>/dev/null || true + + # Commit the updated file onto the branch. `$(...)` capture above (base64 -d, + # then sed) strips the file's trailing newline, so printf '%s\n' restores + # exactly one — otherwise the committed caller loses its EOF newline and trips + # the receiver repo's formatter (oxfmt/prettier both require a trailing + # newline). + local ENCODED + ENCODED=$(printf '%s\n' "$NEW_CONTENT" | base64 | tr -d '\n') + gh api --method PUT "repos/${REPO}/contents/${FILE_ENC}" \ + --field message="ci: bump ${TAG} to github-workflows@${SHORT}" \ + --field content="${ENCODED}" \ + --field sha="${BLOB_SHA}" \ + --field branch="${BRANCH}" \ + > /dev/null || { + echo "::warning::${REPO}: commit to ${BRANCH} failed — skipping" + return 1 + } + + local LABEL_ARGS=() + [[ -n "$LABEL" ]] && LABEL_ARGS=(--label "$LABEL") + + # Open the PR (a pre-existing PR for this branch is not an error). The body is + # a single line so the workflow parses it — a multi-line --body collapsed the + # step's YAML in an earlier revision. + if gh pr create \ + --repo "${REPO}" \ + --head "${BRANCH}" \ + --base "${DEFAULT_BRANCH}" \ + --title "ci: bump ${TAG} to github-workflows@${SHORT}" \ + --body "Automatic SHA bump — \`${WORKFLOW_FILE}\` was updated in \`Comfy-Org/github-workflows\` at [\`${SHORT}\`](https://github.com/Comfy-Org/github-workflows/commit/${NEW_SHA}). _Opened by the \`bump-${TAG}-callers\` workflow._" \ + "${LABEL_ARGS[@]}" 2>/dev/null; then + echo "${REPO}: PR opened" + else + echo "::warning::${REPO}: PR may already exist for ${BRANCH}" + fi + return 0 +} + +FAILED=() +for ENTRY in "${CALLERS[@]}"; do + bump_one "$ENTRY" || FAILED+=("${ENTRY%%|*}") +done + +if (( ${#FAILED[@]} )); then + printf '::error::bump failed for %d repo(s): %s\n' "${#FAILED[@]}" "${FAILED[*]}" + exit 1 +fi +echo "${TAG} bump complete for all callers." diff --git a/.github/bump-callers/tests/test_bump_callers.sh b/.github/bump-callers/tests/test_bump_callers.sh new file mode 100755 index 0000000..98e25b1 --- /dev/null +++ b/.github/bump-callers/tests/test_bump_callers.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# +# Functional tests for the generalized caller bumper (bump-callers.sh). +# +# The bump logic is bash, not Python, so — mirroring the test-cursor-review- +# scripts.yml / test-agents-md-integrity.yml lineage of guarding shared CI +# machinery with a unit run on change — this drives the real script against a +# stubbed `gh` and asserts the behavior that a consumer repo depends on: +# * BOTH caller variables (CURSOR_REVIEW_CALLERS + AGENTS_MD_CALLERS) parse, +# * every private repo name is masked out of the public run logs, +# * the caller's pinned SHA (and only it) is rewritten, the pin comment is +# normalized, and the committed file keeps its single trailing newline, +# * an empty seeded-empty fleet is a clean no-op while a must-have-callers +# fleet still hard-fails, and a malformed variable hard-fails. +# +# No network: `gh` is a PATH stub that serves a fixture file and captures the +# contents PUT so we can inspect exactly what would be committed. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUMP="${SCRIPT_DIR}/../bump-callers.sh" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +NEW_SHA="abcdef0123456789abcdef0123456789abcdef01" # 40 hex, != any fixture pin +SHORT="${NEW_SHA:0:7}" + +PASS=0 +FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok: $1"; } +bad() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } +check(){ if eval "$2"; then ok "$1"; else bad "$1 [$2]"; fi; } + +# --- portable base64 decode (BSD `-D` vs GNU `-d`) -------------------------- +b64d() { base64 -d 2>/dev/null || base64 -D; } +export -f b64d + +# --- build the gh stub on PATH ---------------------------------------------- +STUB_BIN="${WORK}/bin" +mkdir -p "$STUB_BIN" +cat > "${STUB_BIN}/gh" <<'STUB' +#!/usr/bin/env bash +# Minimal `gh` stub. Serves $STUB_CONTENT_FILE for a contents GET and captures +# the decoded contents PUT to $STUB_PUT_DIR; everything else returns a canned +# value so bump-callers.sh runs end to end offline. +sub="$1"; shift || true +if [[ "$sub" == "pr" ]]; then + echo "pr-create $*" >> "$STUB_PUT_DIR/pr.log" + exit 0 +fi +[[ "$sub" == "api" ]] || exit 0 + +method="GET"; path=""; content="" +args=("$@"); i=0 +while (( i < ${#args[@]} )); do + case "${args[$i]}" in + --method) method="${args[$((i+1))]}"; i=$((i+2));; + --jq) i=$((i+2));; + --field|-f|-F) + f="${args[$((i+1))]}" + [[ "$f" == content=* ]] && content="${f#content=}" + i=$((i+2));; + repos/*) path="${args[$i]}"; i=$((i+1));; + *) i=$((i+1));; + esac +done + +case "$method" in + POST) exit 0;; + PUT) + n=$(( $(cat "$STUB_PUT_DIR/count" 2>/dev/null || echo 0) + 1 )) + echo "$n" > "$STUB_PUT_DIR/count" + printf '%s' "$content" | { base64 -d 2>/dev/null || base64 -D; } > "$STUB_PUT_DIR/put.$n.txt" + cp "$STUB_PUT_DIR/put.$n.txt" "$STUB_PUT_DIR/put.last.txt" + exit 0;; +esac + +# GET dispatch by resource path. +if [[ "$path" == *"/contents/"* ]]; then + b64=$(base64 < "$STUB_CONTENT_FILE" | tr -d '\n') + printf '{"sha":"blobsha123","content":"%s"}' "$b64" +elif [[ "$path" == *"/git/refs/heads/"* ]]; then + echo "1234567890abcdef1234567890abcdef12345678" +else + echo "main" # repos/ default_branch +fi +exit 0 +STUB +chmod +x "${STUB_BIN}/gh" +export PATH="${STUB_BIN}:${PATH}" + +# fresh capture dir + fixture per case +new_case() { + STUB_PUT_DIR="${WORK}/put.$1"; rm -rf "$STUB_PUT_DIR"; mkdir -p "$STUB_PUT_DIR" + export STUB_PUT_DIR +} + +run_bump() { # runs the real script, capturing stdout+stderr and exit code. + # `env` sets the per-case NAME=value args ("$@") — words from an expansion are + # NOT recognized as assignment prefixes, so `env` is required here. + # OUT/RC are consumed by check()'s `eval`, which shellcheck can't see. + # shellcheck disable=SC2034 + OUT=$(env GH_TOKEN=x NEW_SHA="$NEW_SHA" STUB_CONTENT_FILE="$STUB_CONTENT_FILE" \ + STUB_PUT_DIR="$STUB_PUT_DIR" "$@" bash "$BUMP" 2>&1) + RC=$? +} +set +e # we manage errors explicitly below + +echo "== cursor-review fleet: single caller, pin + comment rewrite ==" +new_case cr +CR_FIXTURE="${WORK}/cr_caller.yml" +printf '%s\n' \ + 'name: CI cursor-review' \ + 'jobs:' \ + ' review:' \ + ' uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@1111111111111111111111111111111111111111 # github-workflows#27' \ + > "$CR_FIXTURE" +STUB_CONTENT_FILE="$CR_FIXTURE" run_bump \ + VAR_NAME=CURSOR_REVIEW_CALLERS TAG=cursor-review WORKFLOW_FILE=cursor-review.yml \ + CALLERS_JSON='[{"repo":"Comfy-Org/secret-alpha","file":".github/workflows/ci-cursor-review.yml","label":""}]' +check "exit 0" "[[ $RC -eq 0 ]]" +check "masked the private repo name" "grep -q '::add-mask::Comfy-Org/secret-alpha' <<<\"\$OUT\"" +check "reported PR opened" "grep -q 'PR opened' <<<\"\$OUT\"" +check "reported fleet complete" "grep -q 'cursor-review bump complete' <<<\"\$OUT\"" +PUT="${STUB_PUT_DIR}/put.last.txt" +check "committed file exists" "[[ -f \"$PUT\" ]]" +check "new SHA written" "grep -qF '$NEW_SHA' \"$PUT\"" +check "old pin removed" "! grep -qF '1111111111111111111111111111111111111111' \"$PUT\"" +check "pin comment normalized" "grep -qF '# github-workflows main ($SHORT)' \"$PUT\"" +check "stale pin comment removed" "! grep -qF '# github-workflows#27' \"$PUT\"" +# exactly one trailing newline (#23): last byte is \n (tail -c1 strips to empty), +# and the last two bytes are not both \n (tail -c2 keeps a non-newline byte). +check "single trailing newline" "[[ -z \"\$(tail -c1 \"$PUT\")\" && -n \"\$(tail -c2 \"$PUT\")\" ]]" + +echo "== agents-md fleet: two callers, two SHA refs, '# v1' preserved ==" +new_case amd +AMD_FIXTURE="${WORK}/amd_caller.yml" +printf '%s\n' \ + 'name: AGENTS.md Integrity' \ + 'jobs:' \ + ' agents-md:' \ + ' uses: Comfy-Org/github-workflows/.github/workflows/agents-md-integrity.yml@2222222222222222222222222222222222222222 # v1' \ + ' with:' \ + ' workflows_ref: 2222222222222222222222222222222222222222' \ + > "$AMD_FIXTURE" +STUB_CONTENT_FILE="$AMD_FIXTURE" run_bump \ + VAR_NAME=AGENTS_MD_CALLERS TAG=agents-md-integrity WORKFLOW_FILE=agents-md-integrity.yml ALLOW_EMPTY=true \ + CALLERS_JSON='[{"repo":"Comfy-Org/secret-beta","file":".github/workflows/agents-md-integrity.yml","label":""},{"repo":"Comfy-Org/secret-gamma","file":".github/workflows/agents-md-integrity.yml","label":"ci"}]' +check "exit 0" "[[ $RC -eq 0 ]]" +check "masked caller beta" "grep -q '::add-mask::Comfy-Org/secret-beta' <<<\"\$OUT\"" +check "masked caller gamma" "grep -q '::add-mask::Comfy-Org/secret-gamma' <<<\"\$OUT\"" +check "reported fleet complete" "grep -q 'agents-md-integrity bump complete' <<<\"\$OUT\"" +check "committed both callers" "[[ \$(cat \"\$STUB_PUT_DIR/count\") -eq 2 ]]" +PUT="${STUB_PUT_DIR}/put.last.txt" +check "both SHA refs rewritten (2 occurrences)" "[[ \$(grep -cF '$NEW_SHA' \"$PUT\") -eq 2 ]]" +check "old agents-md pin removed" "! grep -qF '2222222222222222222222222222222222222222' \"$PUT\"" +check "'# v1' comment left intact" "grep -qF '# v1' \"$PUT\"" + +echo "== agents-md fleet: empty list is a clean no-op (ALLOW_EMPTY) ==" +new_case empty +STUB_CONTENT_FILE="$CR_FIXTURE" run_bump \ + VAR_NAME=AGENTS_MD_CALLERS TAG=agents-md-integrity WORKFLOW_FILE=agents-md-integrity.yml ALLOW_EMPTY=true \ + CALLERS_JSON='[]' +check "exit 0 on empty" "[[ $RC -eq 0 ]]" +check "logged no-op" "grep -q 'no callers yet' <<<\"\$OUT\"" +check "no commit made" "[[ ! -f \"\$STUB_PUT_DIR/count\" ]]" + +echo "== cursor-review fleet: empty variable is a hard error ==" +new_case crempty +STUB_CONTENT_FILE="$CR_FIXTURE" run_bump \ + VAR_NAME=CURSOR_REVIEW_CALLERS TAG=cursor-review WORKFLOW_FILE=cursor-review.yml \ + CALLERS_JSON='' +check "exit 1 on empty must-have fleet" "[[ $RC -eq 1 ]]" +check "error names the variable" "grep -q 'CURSOR_REVIEW_CALLERS variable is missing or empty' <<<\"\$OUT\"" + +echo "== any fleet: malformed variable is a hard error ==" +new_case malformed +STUB_CONTENT_FILE="$CR_FIXTURE" run_bump \ + VAR_NAME=AGENTS_MD_CALLERS TAG=agents-md-integrity WORKFLOW_FILE=agents-md-integrity.yml ALLOW_EMPTY=true \ + CALLERS_JSON='{"not":"an array"}' +check "exit 1 on malformed" "[[ $RC -eq 1 ]]" +check "error explains shape" "grep -q 'not a non-empty JSON array' <<<\"\$OUT\"" + +echo +echo "== $PASS passed, $FAIL failed ==" +[[ $FAIL -eq 0 ]] diff --git a/.github/workflows/bump-agents-md-callers.yml b/.github/workflows/bump-agents-md-callers.yml new file mode 100644 index 0000000..772fa61 --- /dev/null +++ b/.github/workflows/bump-agents-md-callers.yml @@ -0,0 +1,72 @@ +name: Bump agents-md-integrity callers + +# When the agents-md-integrity reusable workflow (or its checker script) is +# updated on main, open a SHA-bump PR in every repo that pins a caller against +# it. PRs are opened by Cloud Code Bot so they are easy to filter and merge. +# +# This is a thin entrypoint over the shared bumper at +# .github/bump-callers/bump-callers.sh — the same script drives the cursor-review +# caller fleet (bump-cursor-review-callers.yml). Keeping ONE implementation is +# deliberate: a forked copy is how other shared machinery in the org has drifted. +# The two entrypoints differ only in their path-filter trigger and the three +# parameters passed below. (They stay separate rather than a single matrix +# because their triggers differ — a cursor-review.yml change must not spuriously +# bump agents-md callers, and vice versa.) +# +# The caller list is NOT hardcoded here. This repo is PUBLIC (workflow file and +# Actions run logs are both publicly viewable), and most callers are private, so +# their names must never appear in this file or its logs. The list lives in the +# repo-level Actions variable `AGENTS_MD_CALLERS` (config, not a credential — a +# variable, not a secret, since secrets are write-only via the API) as a JSON +# array of {"repo","file","label"} objects, same shape as CURSOR_REVIEW_CALLERS. +# Every repo name is `::add-mask::`ed out of the (public) run logs before it is +# ever echoed. The variable is seeded EMPTY (`[]`) — an empty list is a clean +# no-op here (ALLOW_EMPTY below); the rollout's per-repo caller tickets add +# entries as callers land. +# +# Update flow — adding/removing a caller needs NO public commit: +# gh variable set AGENTS_MD_CALLERS --repo Comfy-Org/github-workflows \ +# --body "$(jq -c . callers.json)" +# Keep the canonical callers.json in a PRIVATE infra/ops repo so variable edits +# have a reviewed source of truth; the org audit log records each edit. (The +# specific home repo is intentionally not named here — this file is public.) + +on: + workflow_dispatch: {} # allow on-demand runs (e.g. to re-bump callers) + push: + branches: [main] + paths: + - .github/workflows/agents-md-integrity.yml + - .github/agents-md-integrity/** + +jobs: + bump: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Generate Cloud Code Bot token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + id: token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.CLOUD_CODE_BOT_PRIVATE_KEY }} + owner: Comfy-Org + + - name: Bump SHA in caller repos + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + NEW_SHA: ${{ github.sha }} + VAR_NAME: AGENTS_MD_CALLERS + TAG: agents-md-integrity + WORKFLOW_FILE: agents-md-integrity.yml + # Seeded empty; an empty list is a clean no-op, not an error. + ALLOW_EMPTY: "true" + # JSON array of {"repo","file","label"} — see the header comment for the + # update flow. Kept in a variable (not the file) so private caller names + # never land in this public repo or its logs. + CALLERS_JSON: ${{ vars.AGENTS_MD_CALLERS }} + run: bash .github/bump-callers/bump-callers.sh diff --git a/.github/workflows/bump-cursor-review-callers.yml b/.github/workflows/bump-cursor-review-callers.yml index 073932d..489946f 100644 --- a/.github/workflows/bump-cursor-review-callers.yml +++ b/.github/workflows/bump-cursor-review-callers.yml @@ -4,6 +4,13 @@ name: Bump cursor-review callers # that pins a caller against it. PRs are opened by Cloud Code Bot so they are # easy to filter and merge. # +# This is a thin entrypoint over the shared bumper at +# .github/bump-callers/bump-callers.sh — the same script drives the +# agents-md-integrity caller fleet (bump-agents-md-callers.yml). Keeping ONE +# implementation is deliberate: a forked copy is how other shared machinery in +# the org has drifted. The two entrypoints differ only in their path-filter +# trigger and the three parameters passed below. +# # The caller list is NOT hardcoded here. This repo is PUBLIC (workflow file and # Actions run logs are both publicly viewable), and most callers are private, so # their names must never appear in this file or its logs. The list lives in the @@ -30,6 +37,11 @@ jobs: bump: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Generate Cloud Code Bot token uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 id: token @@ -42,136 +54,11 @@ jobs: env: GH_TOKEN: ${{ steps.token.outputs.token }} NEW_SHA: ${{ github.sha }} + VAR_NAME: CURSOR_REVIEW_CALLERS + TAG: cursor-review + WORKFLOW_FILE: cursor-review.yml # JSON array of {"repo","file","label"} — see the header comment for the # update flow. Kept in a variable (not the file) so private caller names # never land in this public repo or its logs. CALLERS_JSON: ${{ vars.CURSOR_REVIEW_CALLERS }} - run: | - # NOTE: deliberately no `set -e`. Each caller is bumped independently - # in bump_one(); a failure there returns non-zero and is downgraded to - # a per-repo warning so one bad repo cannot abort the whole fan-out. - # We still fail the job at the end if any repo failed. - set -uo pipefail - SHORT="${NEW_SHA:0:7}" - BRANCH="ci/bump-cursor-review-${SHORT}" - - # Load the caller list from the CURSOR_REVIEW_CALLERS variable. Hard-fail - # loudly on a missing/empty/invalid value — a silent no-op dispatcher - # (which would leave every caller un-bumped without anyone noticing) must - # not be possible. - if [[ -z "${CALLERS_JSON//[[:space:]]/}" ]]; then - echo "::error::CURSOR_REVIEW_CALLERS variable is missing or empty. Seed it with the caller list — see this workflow's header comment for the update flow." - exit 1 - fi - if ! jq -e 'type == "array" and length > 0 and all(.[]; (.repo | type == "string" and . != "") and (.file | type == "string" and . != ""))' <<<"$CALLERS_JSON" >/dev/null 2>&1; then - echo "::error::CURSOR_REVIEW_CALLERS is not a non-empty JSON array of {repo,file,label} objects (each needing a non-empty repo and file). Fix the variable — see the workflow header." - exit 1 - fi - - # Parse the JSON into repo|file|label tuples, and mask every repo name in - # the (publicly viewable) run logs BEFORE the loop that echoes it, so all - # per-repo output shows *** instead of a private repo name. - CALLERS=() - while IFS= read -r ENTRY; do - echo "::add-mask::${ENTRY%%|*}" - CALLERS+=("$ENTRY") - done < <(jq -r '.[] | "\(.repo)|\(.file)|\(.label // "")"' <<<"$CALLERS_JSON") - - if (( ${#CALLERS[@]} == 0 )); then - echo "::error::CURSOR_REVIEW_CALLERS parsed to zero callers — refusing to run a no-op dispatcher." - exit 1 - fi - - # Bump a single caller repo. Every external call is guarded, so a bad - # repo returns non-zero here (caught by the loop) instead of killing - # the run. An expected skip (file missing / already pinned) is a - # successful `return 0`. - bump_one() { - local ENTRY="$1" REPO FILE LABEL FILE_ENC - REPO=$(cut -d'|' -f1 <<<"$ENTRY") - FILE=$(cut -d'|' -f2 <<<"$ENTRY") - LABEL=$(cut -d'|' -f3 <<<"$ENTRY") - FILE_ENC="${FILE//\//%2F}" - - local DEFAULT_BRANCH - DEFAULT_BRANCH=$(gh api "repos/${REPO}" --jq '.default_branch') || { - echo "::warning::${REPO}: cannot read repo metadata — skipping" - return 1 - } - - # Fetch current file; a missing file is an expected skip (success). - local CURRENT - CURRENT=$(gh api "repos/${REPO}/contents/${FILE_ENC}?ref=${DEFAULT_BRANCH}" 2>/dev/null) || { - echo "::warning::${REPO}: ${FILE} not found — skipping" - return 0 - } - - local BLOB_SHA OLD_CONTENT - BLOB_SHA=$(jq -r '.sha' <<<"$CURRENT") - OLD_CONTENT=$(jq -r '.content' <<<"$CURRENT" | base64 -d) - - # Already pinned to this SHA → nothing to do (success). - if grep -qF "$NEW_SHA" <<<"$OLD_CONTENT"; then - echo "${REPO}: already at ${SHORT} — skipping" - return 0 - fi - - # Replace every 40-char hex SHA and update the inline comment. - local NEW_CONTENT - NEW_CONTENT=$(sed -E "s/[0-9a-f]{40}/${NEW_SHA}/g; s|# github-workflows#[0-9]+|# github-workflows main (${SHORT})|g" <<<"$OLD_CONTENT") - - # Create the branch from the default-branch tip (ignore 422 if it exists). - local MAIN_SHA - MAIN_SHA=$(gh api "repos/${REPO}/git/refs/heads/${DEFAULT_BRANCH}" --jq '.object.sha') || { - echo "::warning::${REPO}: cannot read ${DEFAULT_BRANCH} ref — skipping" - return 1 - } - gh api --method POST "repos/${REPO}/git/refs" \ - --field ref="refs/heads/${BRANCH}" \ - --field sha="${MAIN_SHA}" 2>/dev/null || true - - # Commit the updated file onto the branch. `$(...)` capture above - # (base64 -d, then sed) strips the file's trailing newline, so - # printf '%s\n' restores exactly one — otherwise the committed - # caller loses its EOF newline and trips the receiver repo's - # formatter (oxfmt/prettier both require a trailing newline). - local ENCODED - ENCODED=$(printf '%s\n' "$NEW_CONTENT" | base64 | tr -d '\n') - gh api --method PUT "repos/${REPO}/contents/${FILE_ENC}" \ - --field message="ci: bump cursor-review to github-workflows@${SHORT}" \ - --field content="${ENCODED}" \ - --field sha="${BLOB_SHA}" \ - --field branch="${BRANCH}" \ - > /dev/null || { - echo "::warning::${REPO}: commit to ${BRANCH} failed — skipping" - return 1 - } - - local LABEL_ARGS=() - [[ -n "$LABEL" ]] && LABEL_ARGS=(--label "$LABEL") - - # Open the PR (a pre-existing PR for this branch is not an error). - if gh pr create \ - --repo "${REPO}" \ - --head "${BRANCH}" \ - --base "${DEFAULT_BRANCH}" \ - --title "ci: bump cursor-review to github-workflows@${SHORT}" \ - --body "Automatic SHA bump — \`cursor-review.yml\` was updated in \`Comfy-Org/github-workflows\` at [\`${SHORT}\`](https://github.com/Comfy-Org/github-workflows/commit/${NEW_SHA}). _Opened by the \`bump-cursor-review-callers\` workflow._" \ - "${LABEL_ARGS[@]}" 2>/dev/null; then - echo "${REPO}: PR opened" - else - echo "::warning::${REPO}: PR may already exist for ${BRANCH}" - fi - return 0 - } - - FAILED=() - for ENTRY in "${CALLERS[@]}"; do - bump_one "$ENTRY" || FAILED+=("${ENTRY%%|*}") - done - - if (( ${#FAILED[@]} )); then - printf '::error::bump failed for %d repo(s): %s\n' "${#FAILED[@]}" "${FAILED[*]}" - exit 1 - fi - echo "cursor-review bump complete for all callers." + run: bash .github/bump-callers/bump-callers.sh diff --git a/.github/workflows/test-bump-callers.yml b/.github/workflows/test-bump-callers.yml new file mode 100644 index 0000000..a07bede --- /dev/null +++ b/.github/workflows/test-bump-callers.yml @@ -0,0 +1,43 @@ +name: Test bump-callers script + +# Runs the functional tests + shellcheck for the shared caller-bump script +# (.github/bump-callers/bump-callers.sh). That one script drives the SHA-bump +# fan-out for BOTH the cursor-review and agents-md-integrity caller fleets, so a +# regression here silently breaks every consumer repo's pin auto-bump (or, worse, +# leaks a private caller name into this public repo's run logs) — cheap to guard +# with a unit run on change. Mirrors test-cursor-review-scripts.yml / +# test-agents-md-integrity.yml. + +on: + pull_request: + paths: + - '.github/bump-callers/**' + - '.github/workflows/bump-cursor-review-callers.yml' + - '.github/workflows/bump-agents-md-callers.yml' + - '.github/workflows/test-bump-callers.yml' + push: + branches: [main] + paths: + - '.github/bump-callers/**' + - '.github/workflows/bump-cursor-review-callers.yml' + - '.github/workflows/bump-agents-md-callers.yml' + - '.github/workflows/test-bump-callers.yml' + +permissions: + contents: read + +jobs: + test: + name: shellcheck + functional + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: ShellCheck the bump script + tests + run: shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/tests/test_bump_callers.sh + + - name: Run bump-callers functional tests + run: bash .github/bump-callers/tests/test_bump_callers.sh