From 3a6cb68f43bf52a1356d99636b4c7ce53a6dddff Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 13 Jul 2026 17:14:09 -0500 Subject: [PATCH 1/6] Add Claude Code GitHub Actions workflows --- .github/workflows/claude-code-review.yml | 44 +++++++++++++++++++++ .github/workflows/claude.yml | 50 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..b5e8cfd --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..6b15fac --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (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 + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr *)' + From 7286b3db752f200858c4475530ae05b3efd7bb40 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 13 Jul 2026 17:30:45 -0500 Subject: [PATCH 2/6] ci: review fork PRs via pull_request_target (gated to jnasbyupgrade) `claude-code-review.yml` used `on: pull_request`, which gets no secrets on fork PRs, so CLAUDE_CODE_OAUTH_TOKEN was empty and review never ran for our fork-based PRs. Switch to `pull_request_target` (runs in the base repo with the secret available) and gate the job to PRs from the jnasbyupgrade fork only, so external forks can't trigger the secret-bearing job. Check out the PR head read-only (persist-credentials: false); no PR code is built or executed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude-code-review.yml | 42 +++++++++++------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b5e8cfd..951337c 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,44 +1,42 @@ name: Claude Code Review +# Runs on PRs INTO this repo. We use pull_request_target (not pull_request) so +# that PRs from a fork can access CLAUDE_CODE_OAUTH_TOKEN — GitHub withholds +# secrets from `pull_request` runs triggered by forks, which is why the plain +# `pull_request` version never worked for fork PRs. +# +# SECURITY: pull_request_target runs in the BASE repo with secrets and a +# write-capable token. The job is gated to PRs from the trusted `jnasbyupgrade` +# fork only — an arbitrary external fork can never trigger this secret-bearing +# job. The workflow file always comes from the base branch (master), so a PR +# cannot modify the reviewer that runs on it. We check out the PR head only for +# read context (persist-credentials: false) and never build or execute PR code. on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] jobs: claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + # Only the trusted fork. To add more trusted owners, extend this condition. + if: github.event.pull_request.head.repo.owner.login == 'jnasbyupgrade' runs-on: ubuntu-latest permissions: contents: read - pull-requests: read - issues: read + pull-requests: write # post the review comments id-token: write - steps: - - name: Checkout repository + - name: Check out PR head (read-only context) uses: actions/checkout@v4 with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 1 + persist-credentials: false - name: Run Claude Code Review - id: claude-review uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - From 73691bacda0820cc094ee8f5dad54f57de264e00 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 13 Jul 2026 18:09:27 -0500 Subject: [PATCH 3/6] ci: harden claude workflows (CodeRabbit review on #41) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add concurrency groups (cancel stale review runs; serialize @claude without cancelling in-progress responses). - Add timeout-minutes: 30 to both jobs. - Skip draft PRs in the auto-review. - Pin actions/checkout and anthropics/claude-code-action to immutable SHAs. - persist-credentials: false on all checkouts. (plugin_marketplaces can't be pinned — it tracks the marketplace default branch.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude-code-review.yml | 18 ++++++++++++++---- .github/workflows/claude.yml | 24 +++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 951337c..b1f6cd5 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -15,18 +15,26 @@ on: pull_request_target: types: [opened, synchronize, reopened, ready_for_review] +concurrency: + group: claude-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: claude-review: - # Only the trusted fork. To add more trusted owners, extend this condition. - if: github.event.pull_request.head.repo.owner.login == 'jnasbyupgrade' + # Trusted fork only, and skip drafts (don't spend API/CI on unfinished PRs). + # To add more trusted owners, extend the head-owner check. + if: >- + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.owner.login == 'jnasbyupgrade' runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read pull-requests: write # post the review comments id-token: write steps: - name: Check out PR head (read-only context) - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} @@ -34,9 +42,11 @@ jobs: persist-credentials: false - name: Run Claude Code Review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # NOTE: plugin_marketplaces can't be pinned — it tracks the + # marketplace repo's default branch (upstream anthropics/claude-code). plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6b15fac..b5638d2 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -10,6 +10,12 @@ on: pull_request_review: types: [submitted] +# Serialize per issue/PR. cancel-in-progress is false on purpose: cancelling +# would kill Claude mid-response if another comment lands while it's working. +concurrency: + group: claude-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false + jobs: claude: if: | @@ -18,6 +24,7 @@ jobs: (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 permissions: contents: read pull-requests: read @@ -26,25 +33,16 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 + persist-credentials: false - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs + # Allows Claude to read CI results on PRs additional_permissions: | actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr *)' - From a5dc6d6c94fbe90818110efebae6a9f4abca33e8 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 13 Jul 2026 18:20:34 -0500 Subject: [PATCH 4/6] ci: track major-version action tags instead of pinned SHAs Revert the SHA pins on actions/checkout and anthropics/claude-code-action back to @v4/@v1. For these first-party actions we want upstream fixes to flow in automatically rather than freezing at a SHA. (CodeRabbit/zizmor will flag the unpinned refs; that's an accepted trade-off here.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude-code-review.yml | 6 ++++-- .github/workflows/claude.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b1f6cd5..377996a 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -34,7 +34,9 @@ jobs: id-token: write steps: - name: Check out PR head (read-only context) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + # Intentionally tracks the major-version tag (not a pinned SHA) so + # upstream fixes are picked up automatically. + uses: actions/checkout@v4 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} @@ -42,7 +44,7 @@ jobs: persist-credentials: false - name: Run Claude Code Review - uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1 + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # NOTE: plugin_marketplaces can't be pinned — it tracks the diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b5638d2..b316236 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -33,14 +33,16 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + # Intentionally tracks the major-version tag (not a pinned SHA) so + # upstream fixes are picked up automatically. + uses: actions/checkout@v4 with: fetch-depth: 1 persist-credentials: false - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1 + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Allows Claude to read CI results on PRs From 28586d7b57760a8e8386e5fdb32aecdcd2a4ebcc Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 13 Jul 2026 18:27:57 -0500 Subject: [PATCH 5/6] ci: drop concurrency limit from the @claude workflow @claude mentions are independent, read-only requests; serializing only delays responses and cancelling would drop them. Concurrency stays on claude-code-review.yml (cancel stale review runs), where it's useful. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/claude.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b316236..c85ec00 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -10,12 +10,8 @@ on: pull_request_review: types: [submitted] -# Serialize per issue/PR. cancel-in-progress is false on purpose: cancelling -# would kill Claude mid-response if another comment lands while it's working. -concurrency: - group: claude-${{ github.event.issue.number || github.event.pull_request.number }} - cancel-in-progress: false - +# No concurrency limit: @claude mentions are independent, read-only requests; +# serializing would only delay responses and cancelling would drop them. jobs: claude: if: | From ec1150d80c5f7efffeb375b28395274691af568e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 13 Jul 2026 18:34:52 -0500 Subject: [PATCH 6/6] Prune pgxntool's dev-only dirs from consumers on sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git subtree copies pgxntool's entire tree into embedding projects, including dev-only dirs (.github/, .claude/) that are export-ignored from `make dist` and don't belong in a consumer. subtree doesn't honor export-ignore, so update-setup-files.sh (run by pgxntool-sync) now prunes them via a new prune_pgxntool_dev_dirs helper in lib.sh. GitHub only runs root-level workflows, so a consumer's pgxntool/.github never executed anyway — this just keeps the embedded copy clean. Related changes in pgxntool-test: - Test that update-setup-files.sh prunes pgxntool/.github Co-Authored-By: Claude Opus 4.8 (1M context) --- lib.sh | 23 +++++++++++++++++++++++ update-setup-files.sh | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/lib.sh b/lib.sh index ea38665..853dc84 100644 --- a/lib.sh +++ b/lib.sh @@ -76,3 +76,26 @@ debug() { echo "DEBUG[$level]: $message" >&2 fi } + +# Remove pgxntool's own dev-only directories from a consuming project. +# +# `git subtree` copies the ENTIRE pgxntool tree into the consumer, including +# dev-only dirs like .github/ (pgxntool's CI) and .claude/. Those are +# export-ignored from `make dist` and don't belong in a project that merely +# embeds pgxntool. (GitHub only runs workflows at the repo root, so a consumer's +# pgxntool/.github never executes anyway — but it's still clutter.) git subtree +# doesn't honor export-ignore, so we prune them here after a sync. +# +# Must be run from the project root (the dir containing pgxntool/). Safe to call +# repeatedly; a no-op once the dirs are gone. +prune_pgxntool_dev_dirs() { + local d + for d in .github .claude; do + [ -e "pgxntool/$d" ] || continue + echo " pgxntool/$d: pruning (pgxntool dev-only, not for embedding projects)" + # Stage the removal if tracked; rm -rf guarantees it's gone even if not. + # || : keeps this best-effort under `set -e` (rm -rf is the real cleanup). + git rm -rq --ignore-unmatch "pgxntool/$d" >/dev/null 2>&1 || : + rm -rf "pgxntool/$d" + done +} diff --git a/update-setup-files.sh b/update-setup-files.sh index 94ae75d..980726c 100755 --- a/update-setup-files.sh +++ b/update-setup-files.sh @@ -172,5 +172,9 @@ for entry in "${SETUP_SYMLINKS[@]}"; do process_symlink "$dest" "$target" done +# Prune pgxntool's own dev-only dirs (.github/, .claude/) that the subtree pull +# re-introduces but that don't belong in a consuming project (see lib.sh). +prune_pgxntool_dev_dirs + echo echo "Done. Review changes with 'git diff' and commit when ready."