Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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_target:
types: [opened, synchronize, reopened, ready_for_review]

concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
claude-review:
# 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)
# 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 }}
fetch-depth: 1
persist-credentials: false

- name: Run Claude Code Review
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
# 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 }}'
46 changes: 46 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]

# No concurrency limit: @claude mentions are independent, read-only requests;
# serializing would only delay responses and cancelling would drop them.
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
timeout-minutes: 30
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
# 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@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Allows Claude to read CI results on PRs
additional_permissions: |
actions: read
17 changes: 17 additions & 0 deletions sql/test_factory--0.5.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ EXCEPTION
END
$body$;

/*
* As of PG16, CREATE ROLE no longer grants the creating role a SET-enabled
* membership in the new role, so SET ROLE test_factory__owner below fails
* unless the current role is a superuser (which bypasses the check). Grant it
* explicitly WITH SET so a non-superuser install works too. Runs
* unconditionally, even when the role already existed and CREATE ROLE was a
* no-op. Gated on PG16+, where the WITH SET syntax exists; pre-16 GRANT ... TO
* already confers the ability to SET ROLE.
*/
DO $body$
BEGIN
IF current_setting('server_version_num')::int >= 160000 THEN
EXECUTE format('GRANT test_factory__owner TO %I WITH SET TRUE', current_user);
END IF;
END
$body$;

CREATE SCHEMA tf AUTHORIZATION test_factory__owner;
COMMENT ON SCHEMA tf IS $$Test factory. Tools for maintaining test data.$$;
GRANT USAGE ON SCHEMA tf TO public;
Expand Down
17 changes: 17 additions & 0 deletions sql/test_factory.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ EXCEPTION
END
$body$;

/*
* As of PG16, CREATE ROLE no longer grants the creating role a SET-enabled
* membership in the new role, so SET ROLE test_factory__owner below fails
* unless the current role is a superuser (which bypasses the check). Grant it
* explicitly WITH SET so a non-superuser install works too. Runs
* unconditionally, even when the role already existed and CREATE ROLE was a
* no-op. Gated on PG16+, where the WITH SET syntax exists; pre-16 GRANT ... TO
* already confers the ability to SET ROLE.
*/
DO $body$
BEGIN
IF current_setting('server_version_num')::int >= 160000 THEN
EXECUTE format('GRANT test_factory__owner TO %I WITH SET TRUE', current_user);
END IF;
END
$body$;

CREATE SCHEMA tf AUTHORIZATION test_factory__owner;
COMMENT ON SCHEMA tf IS $$Test factory. Tools for maintaining test data.$$;
GRANT USAGE ON SCHEMA tf TO public;
Expand Down
45 changes: 23 additions & 22 deletions test/expected/base.out
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
\set ECHO none
Creating extension test_factory
ok 1 - Register test customers
ok 2 - Create function customer__add
ok 3 - Register test invoices
ok 4 - Ensure original_role temp table was dropped
ok 5 - Ensure role is put back after install
ok 6 - Security definer function _tf.schema__getsert has search_path=pg_catalog
ok 7 - Security definer function _tf.test_factory__get has search_path=pg_catalog
ok 8 - Security definer function _tf.test_factory__set has search_path=pg_catalog
ok 9 - Security definer function _tf.table_create has search_path=pg_catalog
ok 10 - Security definer function _tf.get has search_path=pg_catalog
ok 11 - customer table is empty
ok 12 - invoice table is empty
ok 13 - invoice factory output
ok 14 - invoice table content
ok 15 - customer table content
ok 16 - invoice factory second call
ok 17 - invoice table content stayed constant
ok 18 - customer table content stayed constant
ok 19 - Test function factory
ok 20 - customer table has new row
ok 21 - truncate invoice
ok 22 - invoice factory get remains the same after truncate
ok 1 - Installing role has SET-enabled membership in test_factory__owner (issue #14)
ok 2 - Register test customers
ok 3 - Create function customer__add
ok 4 - Register test invoices
ok 5 - Ensure original_role temp table was dropped
ok 6 - Ensure role is put back after install
ok 7 - Security definer function _tf.schema__getsert has search_path=pg_catalog
ok 8 - Security definer function _tf.test_factory__get has search_path=pg_catalog
ok 9 - Security definer function _tf.test_factory__set has search_path=pg_catalog
ok 10 - Security definer function _tf.table_create has search_path=pg_catalog
ok 11 - Security definer function _tf.get has search_path=pg_catalog
ok 12 - customer table is empty
ok 13 - invoice table is empty
ok 14 - invoice factory output
ok 15 - invoice table content
ok 16 - customer table content
ok 17 - invoice factory second call
ok 18 - invoice table content stayed constant
ok 19 - customer table content stayed constant
ok 20 - Test function factory
ok 21 - customer table has new row
ok 22 - truncate invoice
ok 23 - invoice factory get remains the same after truncate
30 changes: 30 additions & 0 deletions test/sql/base.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@
\set extension_name test_factory
\i test/helpers/create_extension.sql

/*
* Regression test for issue #14. On PostgreSQL 16+, CREATE ROLE no longer
* grants the creating role a SET-enabled membership in the new role, so the
* install must GRANT test_factory__owner ... WITH SET TRUE or the SET ROLE
* performed during install fails for non-superuser installs (RDS/Aurora). A
* real superuser bypasses the SET ROLE check, so a plain install here cannot
* reproduce the failure; instead assert the SET-enabled membership the fix
* establishes. pg_auth_members.set_option only exists on PG16+, so the check is
* skipped (with identical TAP output) on older versions, where a plain
* GRANT ... TO already confers the ability to SET ROLE.
*/
SELECT (current_setting('server_version_num')::int >= 160000) AS pg16plus \gset
\if :pg16plus
SELECT ok(
EXISTS(
SELECT 1
FROM pg_auth_members
WHERE roleid = 'test_factory__owner'::regrole
AND member = current_user::regrole
AND set_option
)
, 'Installing role has SET-enabled membership in test_factory__owner (issue #14)'
);
\else
SELECT ok(
true
, 'Installing role has SET-enabled membership in test_factory__owner (issue #14)'
);
\endif

-- NOTE: This runs some tests itself
\i test/helpers/create.sql

Expand Down
Loading