From cb68dcf4b944234bb2908e85e263bb4d01f43262 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 17 Sep 2026 17:07:30 -0500 Subject: [PATCH 1/4] Add shared claude-code-review.yml reusable workflow Every repo carried its own drifted copy of the Claude review workflow, so a fix (like cat_tools' allowedTools fix) had to be hand-copied N times. Adds .github/workflows/claude-code-review.yml here as a workflow_call reusable workflow, and CI-WORKFLOWS.md documenting the thin caller template: a mandate to read this doc plus an `Exceptions:` line, no restated rationale, every consumer pins @main permanently (no version tag, no staged rollback beyond a revert commit to this repo's main). Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/claude-code-review.yml | 205 +++++++++++++++++++++++ CI-WORKFLOWS.md | 115 +++++++++++++ CLAUDE.md | 2 + 3 files changed, 322 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 CI-WORKFLOWS.md diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..a524850 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,205 @@ +name: Claude Code Review (shared) + +# Single source of truth for the Claude PR review job across Postgres-Extensions. +# Consumers hold a thin caller file; see ai/CLAUDE.md for the template. +# +# SECURITY: callers invoke this from `pull_request_target`, which runs in the +# BASE repo with org secrets and a write-capable token. Three things keep that +# safe, none of which a PR can subvert: +# 1. GitHub always reads the CALLER file from the PR's base branch, and +# `uses:` cannot contain contexts or expressions -- so a PR can neither +# edit the caller that runs on it nor redirect which ref of THIS file runs. +# 2. `trusted_authors` below is a required input with no default: a caller +# that omits it fails workflow-graph validation rather than silently +# reviewing arbitrary fork PRs with org secrets in scope. +# 3. Nothing here ever checks out the PR head. claude-code-action fetches it +# itself via the base repo's refs/pull//head (setupBranch() in +# src/github/operations/branch.ts) -- which is why the checkout step below +# must NOT set repository:/ref:. Pointing `origin` at the fork breaks that +# fetch with "couldn't find remote ref pull//head". +# +# Input names use underscores, not hyphens: `inputs.some-name` is ambiguous +# with subtraction in the expression parser. + +on: + workflow_call: + inputs: + trusted_authors: + description: >- + Comma-separated GitHub logins (no spaces) whose PRs may run this job. + SECURITY-CRITICAL -- see note above. Required on purpose; there is no + safe default. + required: true + type: string + debug_label: + description: >- + PR label that skips the cost gate and turns on show_full_output. + Must match the label name hardcoded in the caller's `concurrency:` + group (the caller cannot read `inputs` there). Set to '' to disable. + required: false + type: string + default: claude-debug + +jobs: + review: + # Skip drafts (don't spend on unfinished work); trusted authors only; and + # for a `labeled` event, proceed only when the label IS the debug label -- + # otherwise every unrelated label would trigger another paid review. + # + # Checks the PR AUTHOR (user.login), not head.repo.owner.login: the latter + # is this org for any PR whose head branch lives in the base repo (gh + # stack, or `gh pr create` with no fork), so an owner-based check silently + # skipped review on every such PR regardless of who opened it. + if: >- + github.event.pull_request.draft == false && + contains(format(',{0},', inputs.trusted_authors), format(',{0},', github.event.pull_request.user.login)) && + (github.event.action != 'labeled' || github.event.label.name == inputs.debug_label) + runs-on: ubuntu-latest + timeout-minutes: 60 + # No `permissions:` block on purpose. A called workflow can only narrow + # what the caller granted, never widen it, so the ceiling lives in each + # caller; declaring a block here would break any caller granting less. + steps: + # DEBUG MODE: add the debug label to a PR to (a) skip the cost gate -- + # a debug iteration shouldn't wait 5-20 min on sibling CI -- and (b) get + # show_full_output on the review step (see that input's WARNING below). + # Queried live rather than from github.event.pull_request.labels: + # "Re-run jobs" replays the ORIGINAL stored payload, so a payload-based + # check would never see a label added after the run started. + - name: Check for the debug label + id: debug + if: inputs.debug_label != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + LABEL: ${{ inputs.debug_label }} + run: | + enabled=$(gh pr view "$PR" --repo "$REPO" --json labels \ + --jq 'any(.labels[]; .name == env.LABEL)' 2>/dev/null) || enabled=false + echo "enabled=$enabled" >> "$GITHUB_OUTPUT" + echo "debug label '$LABEL' present: $enabled" + + # COST GATE: the paid review runs last. Wait for the PR head's OTHER + # check-runs and proceed only if they are clean -- no point paying to + # review a PR already known to be broken. Sibling checks are discovered + # dynamically, so this needs no per-repo workflow names. + # decision=run : every sibling completed with a good conclusion, or no + # siblings exist after a ~3 min grace window. + # decision=skip : a sibling failed/cancelled, or we timed out waiting. + # + # Self-exclusion matches each check-run's own details_url against this + # run's id (github.run_id) -- every check-run already carries a + # details_url of the form ".../actions/runs//job/", so + # this needs no extra API call and no permission beyond the checks:read + # the caller already grants for the sibling lookup below. + # + # Do NOT resolve the run's own check-SUITE id via a separate `gh api` + # call instead: that call has no retry/fallback like the paginated + # lookup below, so under `set -e` a transient GitHub error there aborts + # the whole job red instead of just skipping the review -- worse than + # having no gate at all. + # + # Do NOT switch this to matching on check-run name either: as a + # reusable workflow this job's check-run is named " / + # review", so a name filter would fail to exclude it, the gate would + # wait on itself to the timeout, and the review would be skipped on + # every single PR. + - name: Wait for CI; skip the paid review if any check failed + id: gate + if: steps.debug.outputs.enabled != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha }} + RUN_ID: ${{ github.run_id }} + run: | + set -euo pipefail + + decision=skip + for i in $(seq 1 72); do # ~24 min max + # --slurp (not --jq) because --paginate --jq emits one array PER + # PAGE; `jq length` over that yields one number per page and the + # arithmetic below then fails. --slurp is mutually exclusive with + # --jq, hence the pipe to real jq. + pages=$(gh api "repos/$REPO/commits/$SHA/check-runs?per_page=100" \ + --paginate --slurp 2>/dev/null) || pages='' + [ -z "$pages" ] && { sleep 20; continue; } + siblings=$(jq --arg rid "$RUN_ID" \ + '[.[].check_runs[] | select(.details_url // "" | contains("/actions/runs/" + $rid + "/") | not)]' <<<"$pages") + total=$(jq 'length' <<<"$siblings") + if [ "$total" -eq 0 ]; then + [ "$i" -ge 9 ] && { decision=run; break; } # ~3 min grace + sleep 20; continue + fi + pending=$(jq '[.[]|select(.status!="completed")]|length' <<<"$siblings") + if [ "$pending" -eq 0 ]; then + bad=$(jq '[.[]|select((.conclusion//"")|test("^(failure|cancelled|timed_out|action_required|stale)$"))]|length' <<<"$siblings") + [ "$bad" -eq 0 ] && decision=run || decision=skip + break + fi + sleep 20 + done + echo "decision=$decision" >> "$GITHUB_OUTPUT" + echo "gate decision: $decision" + + - name: Check out base branch + if: steps.debug.outputs.enabled == 'true' || steps.gate.outputs.decision == 'run' + # Tracks the major-version tag, not a pinned SHA, so upstream fixes are + # picked up automatically. No repository:/ref: -- see SECURITY (3) above. + uses: actions/checkout@v7 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Run Claude Code Review + if: steps.debug.outputs.enabled == 'true' || steps.gate.outputs.decision == 'run' + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # WARNING (from this input's own description): outputs ALL Claude + # messages including tool execution results, which may contain + # secrets, into publicly visible Actions logs. Debug label only. + show_full_output: ${{ steps.debug.outputs.enabled == 'true' }} + # Supplying github_token makes the action use it directly instead of + # the OIDC->GitHub-App-token exchange, which 401s under + # pull_request_target. GITHUB_TOKEN is repo/workflow-scoped + # (independent of the actor's role) and has pull-requests: write. + github_token: ${{ secrets.GITHUB_TOKEN }} + # A `prompt:` input puts the action in automation mode, which posts + # nothing until the whole run finishes. track_progress forces a + # tracking comment with a live checklist, so a slow run is visible. + track_progress: true + # NOTE: plugin_marketplaces can't be pinned -- it tracks the + # marketplace repo's default branch (anthropics/claude-code). + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + # --comment is required: without it the plugin prints findings to the + # job log only and posts nothing to the PR. + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }} --comment' + # A direct `prompt:` (no @claude mention) runs the action in agent + # mode. There, claude-code-action installs the github_inline_comment + # MCP server only if it sees mcp__github_inline_comment__create_inline_comment + # in --allowedTools (src/modes/agent/parse-tools.ts) -- it does NOT + # consult the plugin's own allowed-tools frontmatter. Without it the + # server never starts and the plugin silently degrades to one + # consolidated comment instead of inline line comments. + # + # --allowedTools is an allowlist in agent mode, so anything not named + # here is silently DENIED. The Bash(gh ...) entries mirror the + # plugin's own allowed-tools frontmatter (anthropics/claude-code + # plugins/code-review/commands/code-review.md) verbatim. `Task` isn't + # in that frontmatter (core tools need no declaration in a normal + # session) but the command's steps 1-5 launch subagents to do the + # actual review -- without it there is no reviewer left to run. + # `TodoWrite` likewise isn't in the frontmatter or the action's + # baseline set, and the plugin's Notes unconditionally require a todo + # list before starting. + # + # The whole value MUST stay one shell-quote token. YAML's outer single + # quotes are consumed by the YAML parser and never reach the action; + # what it receives is re-tokenized by the `shell-quote` npm package, + # splitting on whitespace. The inner double quotes are what keep the + # spaces inside each Bash(gh ...) entry from splitting the value into + # garbage tokens. + claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Task,TodoWrite,Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' diff --git a/CI-WORKFLOWS.md b/CI-WORKFLOWS.md new file mode 100644 index 0000000..037a87f --- /dev/null +++ b/CI-WORKFLOWS.md @@ -0,0 +1,115 @@ +# Shared CI workflows + +`.github/workflows/claude-code-review.yml` in this repo is the single source +of truth for the Claude Code review job used across Postgres-Extensions. A +consuming repo holds only a thin caller file that invokes it via +`workflow_call`; all trigger logic, the cost gate, and the `claude-code-action` +configuration live here, in one place, so a fix lands for every repo at once +instead of needing to be copied out by hand. + +## Adding the review job to a repo + +Add `.github/workflows/claude-code-review.yml` with the content below, +replacing `trusted_authors` with the repo's own list of trusted GitHub +logins — that's the only line every consuming repo edits. The caller holds +nothing but the mandate-to-read comment, an `Exceptions:` line (see below), +and the minimum YAML GitHub requires to live outside the called workflow. +The rationale for why that YAML is shaped this way lives here, not +duplicated as comments in every caller: + +- **The trigger** (`on: pull_request_target`) can only be declared by the + caller — a called (`workflow_call`) workflow cannot declare its own + trigger. `labeled` is included so adding the `claude-debug` label can + start a run with no push needed; the called workflow's own `if:` scopes + that down to only the debug label actually proceeding. +- **Workflow-level `concurrency:`** must live in the caller too: only a + workflow-level block can cancel the whole caller run outright — + `jobs..concurrency` on the caller's own job can't, and the per-label + cancellation below needs exactly that. A non-debug `labeled` event gets + its own per-label group so it can never cancel an in-progress real review + — cancellation resolves against whichever run is admitted, before any + `if:` runs, so an `if:` can only no-op itself, not restore what it + displaced. A `labeled`-with-`claude-debug` event deliberately keeps the + plain group instead, since it's meant to supersede a running review. + `claude-debug` is spelled out literally rather than read from `inputs` + because `inputs` isn't available inside a workflow-level `concurrency:` + block, so it must match the called workflow's `debug_label` default by + hand. +- **The `permissions:` block** is repeated in every caller rather than + declared once on the called workflow's own job: GitHub only lets a called + workflow narrow the permissions the caller already granted, never widen + them. With these repos' `read`-only default workflow permissions, a + caller that omitted this block and relied on the callee to grant + `pull-requests: write` would silently end up with a read-only token, + breaking the review's ability to post comments. +- **`pull-requests: write`** is what lets the review post its comments. +- **`checks: read`** lets the called workflow's cost gate read the PR + head's sibling check-runs, so it can wait for them before spending on a + review. +- **`actions: write`** specifically, not `read`: it's the only scope that + permits an Actions cache write, and no narrower one exists. + +```yaml +name: Claude Code Review + +# MANDATORY: read ../ai/CI-WORKFLOWS.md (Postgres-Extensions/ai) in full +# before changing anything below. If you cannot find or read that file for +# any reason, STOP and report an error -- do not guess at what it says or +# proceed without having actually read it. +# +# Exceptions to that file's design, specific to this repo: none. +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review, labeled] + +concurrency: + group: claude-review-${{ github.event.pull_request.number }}${{ (github.event.action == 'labeled' && github.event.label.name != 'claude-debug') && format('-{0}', github.event.label.name) || '' }} + cancel-in-progress: true + +jobs: + claude-review: + uses: Postgres-Extensions/ai/.github/workflows/claude-code-review.yml@main + permissions: + contents: read + pull-requests: write + checks: read + actions: write + secrets: inherit + with: + trusted_authors: your-github-login-here +``` + +## The `Exceptions:` line + +Every caller states its deviations from this design explicitly, right in +its header comment — `Exceptions: none` when there are none, never a bare +omission that leaves a reader guessing whether an exception was considered +and rejected, or never considered at all. + +A real exception names the specific technical difference and the mechanism +this doc already provides for it, without re-explaining that mechanism's +own rationale — that rationale stays here, the single source of truth, not +copied into a caller file. A repo needing a repo-local pre-gate job before +the review runs, for example, adds a `needs:`/`if:` to the `claude-review` +job and notes exactly that fact under `Exceptions:`. Never change the +job's `uses:` or `with:` to express a deviation — that's not an exception, +it's a fork of the shared workflow. Change `ai/` instead so the fix or +feature reaches every consuming repo. + +## Every consumer pins `@main` + +Every caller — no exceptions, no separate canary — pins +`Postgres-Extensions/ai/.github/workflows/claude-code-review.yml@main`. A +change to `claude-code-review.yml` takes effect for every consuming repo the +moment it's merged to `main`; there is no intermediate tag to advance. + +This means a bad change to `main` affects every consuming repo immediately, +with no staged rollout and no tag to roll back — a revert commit to +`ai/main` is the only way back. + +## Adding a new `workflow_call` input + +A new input requires an actual second consumer that needs a different value +from what every other repo already passes. Adding one "for flexibility," with +no concrete repo that needs it, is drift with extra steps — it recreates the +per-repo divergence this shared workflow exists to eliminate. diff --git a/CLAUDE.md b/CLAUDE.md index b4739cd..035c7b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,8 @@ should say so rather than silently falling back to an easily-forgotten - [RELEASE.md](./RELEASE.md) — shared release process for repos that distribute a PGXN extension via pgxntool (versioning, tagging, PGXN upload) +- [CI-WORKFLOWS.md](./CI-WORKFLOWS.md) — shared reusable GitHub Actions + workflows (the Claude Code review job, caller template) ## GitHub comments: identify as an agent From 045b7b40f48ecc7d1dec6aadbc26f0bab6f497b1 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 17 Sep 2026 17:24:34 -0500 Subject: [PATCH 2/4] Add shared claude.yml reusable workflow for @claude mentions Centralizes the drifted per-repo claude.yml (actions: write, not the buggy actions: read some repos carry) as a workflow_call, parameterizing the hardcoded trust-gate actor with a trusted_actors input. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/claude.yml | 64 ++++++++++++++++++++++++++++ CI-WORKFLOWS.md | 81 +++++++++++++++++++++++++++++++----- 2 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..d420f0e --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,64 @@ +name: Claude Code (shared) + +# Single source of truth for the @claude interactive job across +# Postgres-Extensions. Consumers hold a thin caller file; see +# ai/CI-WORKFLOWS.md for the template. +# +# Unlike claude-code-review.yml, none of this workflow's triggers +# (issue_comment, pull_request_review_comment, issues, pull_request_review) +# ever read anything from a PR head branch -- every one of them reads the +# workflow file from the current default branch, so there is no +# pull_request_target trust subtlety here to guard against. +# +# Input names use underscores, not hyphens: `inputs.some-name` is ambiguous +# with subtraction in the expression parser. + +on: + workflow_call: + inputs: + trusted_actors: + description: >- + Comma-separated GitHub logins (no spaces) allowed to trigger this + job via @claude mentions. Required on purpose; there is no safe + default. + required: true + type: string + +jobs: + claude: + # SECURITY: restricts @claude to trusted_actors, not just matching + # comment text -- otherwise anyone could trigger this job. + # + # No concurrency limit: @claude mentions are independent, read-only + # requests; serializing would only delay responses and cancelling would + # drop them. + if: | + contains(format(',{0},', inputs.trusted_actors), format(',{0},', github.actor)) && + ( + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ) + runs-on: ubuntu-latest + timeout-minutes: 30 + # No `permissions:` block on purpose. A called workflow can only narrow + # what the caller granted, never widen it, so the ceiling lives in each + # caller; declaring a block here would break any caller granting less. + steps: + - name: Checkout repository + # Tracks the major-version tag, not a pinned SHA, so upstream fixes + # are picked up automatically. + uses: actions/checkout@v7 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Allows Claude to read CI results on PRs + additional_permissions: | + actions: read diff --git a/CI-WORKFLOWS.md b/CI-WORKFLOWS.md index 037a87f..d1cd8ce 100644 --- a/CI-WORKFLOWS.md +++ b/CI-WORKFLOWS.md @@ -1,11 +1,12 @@ # Shared CI workflows -`.github/workflows/claude-code-review.yml` in this repo is the single source -of truth for the Claude Code review job used across Postgres-Extensions. A -consuming repo holds only a thin caller file that invokes it via -`workflow_call`; all trigger logic, the cost gate, and the `claude-code-action` -configuration live here, in one place, so a fix lands for every repo at once -instead of needing to be copied out by hand. +This repo is the single source of truth for two reusable Claude Code jobs +used across Postgres-Extensions: `.github/workflows/claude-code-review.yml` +(automated PR review) and `.github/workflows/claude.yml` (the interactive +`@claude`-mention job). A consuming repo holds only a thin caller file for +each that invokes it via `workflow_call`; all trigger logic, gating, and the +`claude-code-action` configuration live here, in one place, so a fix lands +for every repo at once instead of needing to be copied out by hand. ## Adding the review job to a repo @@ -79,6 +80,66 @@ jobs: trusted_authors: your-github-login-here ``` +## Adding the @claude job to a repo + +Add `.github/workflows/claude.yml` with the content below, replacing +`trusted_actors` with the repo's own list of trusted GitHub logins — the +only line every consuming repo edits. As with the review job's caller, the +rationale for the YAML's shape lives here, not duplicated as comments in +every caller: + +- **The trigger** (`issue_comment`, `pull_request_review_comment`, + `issues`, `pull_request_review`) can only be declared by the caller — a + called (`workflow_call`) workflow cannot declare its own trigger. Unlike + `claude-code-review.yml`'s `pull_request_target`, none of these events + read anything from a PR head branch, so there's no fork-trust subtlety + and no `concurrency:` block is needed here. +- **The `permissions:` block** is repeated in every caller for the same + reason as the review job's: a called workflow can only narrow what the + caller already granted, never widen it. +- **`pull-requests: read`** and **`issues: read`** let Claude read PR and + issue context when responding to a mention. +- **`id-token: write`** is required for `claude-code-action`'s OIDC token + exchange. +- **`actions: write`** specifically, not `read`: it's the only scope that + lets the action's own setup step write to the Actions cache. `read` + still works but produces a harmless, noisy "Cache reservation failed: + cache write denied" warning on every run — the bug this centralization + fixes, since most Postgres-Extensions repos currently grant only `read`. + +```yaml +name: Claude Code + +# MANDATORY: read ../ai/CI-WORKFLOWS.md (Postgres-Extensions/ai) in full +# before changing anything below. If you cannot find or read that file for +# any reason, STOP and report an error -- do not guess at what it says or +# proceed without having actually read it. +# +# Exceptions to that file's design, specific to this repo: none. +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + uses: Postgres-Extensions/ai/.github/workflows/claude.yml@main + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: write + secrets: inherit + with: + trusted_actors: your-github-login-here +``` + ## The `Exceptions:` line Every caller states its deviations from this design explicitly, right in @@ -98,10 +159,10 @@ feature reaches every consuming repo. ## Every consumer pins `@main` -Every caller — no exceptions, no separate canary — pins -`Postgres-Extensions/ai/.github/workflows/claude-code-review.yml@main`. A -change to `claude-code-review.yml` takes effect for every consuming repo the -moment it's merged to `main`; there is no intermediate tag to advance. +Every caller — no exceptions, no separate canary — pins its `uses:` at +`@main`, whether that's `claude-code-review.yml` or `claude.yml`. A change +to either takes effect for every consuming repo the moment it's merged to +`main`; there is no intermediate tag to advance. This means a bad change to `main` affects every consuming repo immediately, with no staged rollout and no tag to roll back — a revert commit to From 7adc2eb8b423fd1a75414f70da5106b728c7576e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 17 Sep 2026 17:25:31 -0500 Subject: [PATCH 3/4] CLAUDE.md: update CI-WORKFLOWS.md doc-index bullet for claude.yml Was left singular (review job only) after adding the claude.yml reusable workflow in the prior commit. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 035c7b2..01d3787 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,7 +98,8 @@ should say so rather than silently falling back to an easily-forgotten distribute a PGXN extension via pgxntool (versioning, tagging, PGXN upload) - [CI-WORKFLOWS.md](./CI-WORKFLOWS.md) — shared reusable GitHub Actions - workflows (the Claude Code review job, caller template) + workflows (the Claude Code review job and the @claude-mention job, caller + templates for each) ## GitHub comments: identify as an agent From 3f4320e8bd254aeaab42887679bda46c08510db2 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 17 Sep 2026 17:31:30 -0500 Subject: [PATCH 4/4] claude.yml: split combined comment block into two The SECURITY/trust-gate and no-concurrency-limit rationales were merged into one comment block; pgxntool's original kept them separate, at the scope each actually applies to (job-level if:, and the workflow's jobs). Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/claude.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d420f0e..6e9fc92 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -24,14 +24,13 @@ on: required: true type: string +# No concurrency limit: @claude mentions are independent, read-only +# requests; serializing would only delay responses and cancelling would +# drop them. jobs: claude: # SECURITY: restricts @claude to trusted_actors, not just matching # comment text -- otherwise anyone could trigger this job. - # - # No concurrency limit: @claude mentions are independent, read-only - # requests; serializing would only delay responses and cancelling would - # drop them. if: | contains(format(',{0},', inputs.trusted_actors), format(',{0},', github.actor)) && (