Skip to content
Merged
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
213 changes: 213 additions & 0 deletions .github/workflows/publish-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
name: Publish plugin repository
# Publishes the Claude Code plugin package to the lightweight repository
# github.com/pgrundev/pgx-plugin: what `claude plugin marketplace add
# pgrundev/pgx-plugin` and Anthropic's plugin directory read. The tree is
# assembled by `plugins/sync.sh --standalone` (plugins/pgx + LICENSE +
# plugins/standalone) so the standalone repository is always exactly the
# canonical plugin; nothing is edited there by hand.
#
# push to main (plugin inputs changed) assemble, validate, publish if different
# pull_request (plugin inputs changed) assemble, validate, report drift; never publishes
# workflow_dispatch dry_run=true (default) reports; dry_run=false publishes
#
# Guards: runs only in pgrundev/pgx and publishes only from main (never a
# feature branch, never a fork); nothing is pushed unless the assembled tree
# passes `claude plugin validate --strict` and the directory profile; commits
# are regular fast-forward pushes (no force); the publish job runs in the
# `plugin-publish` environment, so required reviewers can be added there.
# pgx-plugin never pushes back here, so there is no synchronization loop.
#
# Authentication (publish job only). The token is minted per run, scoped to
# pgrundev/pgx-plugin, and never printed:
# 1. GitHub App (preferred): repository secrets PGX_PLUGIN_APP_ID and
# PGX_PLUGIN_APP_PRIVATE_KEY for an app installed on pgrundev/pgx-plugin
# with "Contents: Read and write" and nothing else.
# 2. Deploy key: repository secret PGX_PLUGIN_DEPLOY_KEY, the private half of
# a write-enabled deploy key registered on pgrundev/pgx-plugin.
# With neither configured the publish job stops at the authentication step
# and prints the setup instructions; nothing is published. No personal
# access token is used.
on:
push:
branches: [main]
paths:
- "skills/pgx/**"
- "plugins/**"
- "LICENSE"
- ".github/workflows/publish-plugin.yml"
pull_request:
paths:
- "skills/pgx/**"
- "plugins/**"
- "LICENSE"
- ".github/workflows/publish-plugin.yml"
workflow_dispatch:
inputs:
dry_run:
description: Assemble, validate and report drift only (no push to pgx-plugin)
type: boolean
default: true
permissions:
contents: read
concurrency:
group: publish-plugin
cancel-in-progress: false
env:
STANDALONE_REPO: pgrundev/pgx-plugin
CLAUDE_CODE_VERSION: 2.1.311
jobs:
assemble:
name: Assemble and validate the standalone plugin
runs-on: ubuntu-24.04
outputs:
changed: ${{ steps.drift.outputs.changed }}
version: ${{ steps.meta.outputs.version }}
env:
DISABLE_TELEMETRY: "1"
DISABLE_AUTOUPDATER: "1"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Install the Claude Code CLI (pinned; validate needs no login)
run: npm install -g "@anthropic-ai/claude-code@$CLAUDE_CODE_VERSION" && claude --version
- name: The plugin copy of the skill is the canonical skill
run: plugins/sync.sh --check
- name: Assemble the standalone tree
run: plugins/sync.sh --standalone "$RUNNER_TEMP/pgx-plugin"
- name: Validate the assembled tree (strict, then the directory profile)
run: |
claude plugin validate --strict "$RUNNER_TEMP/pgx-plugin"
claude plugin validate --profile directory "$RUNNER_TEMP/pgx-plugin"
- name: Plugin metadata
id: meta
run: |
v=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$RUNNER_TEMP/pgx-plugin/.claude-plugin/plugin.json")
s=$(grep -E '^ version: "' "$RUNNER_TEMP/pgx-plugin/skills/pgx/SKILL.md" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
[ "$v" = "$s" ] || { echo "::error::plugin.json version $v != SKILL.md metadata.version $s" >&2; exit 1; }
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "plugin version $v"
- name: Drift against the published repository
id: drift
run: |
set -eu
if git clone -q --depth 1 "https://github.com/$STANDALONE_REPO.git" "$RUNNER_TEMP/published" 2>/dev/null \
&& [ -n "$(ls -A "$RUNNER_TEMP/published" | grep -v '^\.git$')" ]; then
if diff -r --exclude=.git "$RUNNER_TEMP/pgx-plugin" "$RUNNER_TEMP/published" > "$RUNNER_TEMP/drift.txt" 2>&1; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "$STANDALONE_REPO already matches the canonical plugin" | tee -a "$GITHUB_STEP_SUMMARY"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
{ echo "### $STANDALONE_REPO differs from the canonical plugin"; echo; echo '```'; head -60 "$RUNNER_TEMP/drift.txt"; echo '```'; } | tee -a "$GITHUB_STEP_SUMMARY"
fi
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "$STANDALONE_REPO is empty or unreachable; a publish would initialise it" | tee -a "$GITHUB_STEP_SUMMARY"
fi
- uses: actions/upload-artifact@v4
with:
name: pgx-plugin
path: ${{ runner.temp }}/pgx-plugin
include-hidden-files: true
if-no-files-found: error
publish:
name: Push to pgrundev/pgx-plugin
needs: assemble
if: >-
github.repository == 'pgrundev/pgx' &&
github.ref == 'refs/heads/main' &&
github.event_name != 'pull_request' &&
needs.assemble.outputs.changed == 'true' &&
(github.event_name == 'push' || github.event.inputs.dry_run == 'false')
runs-on: ubuntu-24.04
environment: plugin-publish
env:
HAS_APP: ${{ secrets.PGX_PLUGIN_APP_ID != '' && secrets.PGX_PLUGIN_APP_PRIVATE_KEY != '' }}
HAS_DEPLOY_KEY: ${{ secrets.PGX_PLUGIN_DEPLOY_KEY != '' }}
steps:
- uses: actions/download-artifact@v4
with:
name: pgx-plugin
path: ${{ runner.temp }}/pgx-plugin
- name: Mint a GitHub App installation token (scoped to pgx-plugin)
if: env.HAS_APP == 'true'
id: app
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.PGX_PLUGIN_APP_ID }}
private-key: ${{ secrets.PGX_PLUGIN_APP_PRIVATE_KEY }}
owner: pgrundev
repositories: pgx-plugin
permission-contents: write
- name: Configure the credential
env:
APP_TOKEN: ${{ steps.app.outputs.token }}
DEPLOY_KEY: ${{ secrets.PGX_PLUGIN_DEPLOY_KEY }}
run: |
set -eu
if [ "$HAS_APP" = true ] && [ -n "$APP_TOKEN" ]; then
git config --global credential.helper '!f() { echo "username=x-access-token"; echo "password=$APP_TOKEN"; }; f'
echo "REMOTE=https://github.com/$STANDALONE_REPO.git" >> "$GITHUB_ENV"
echo "auth: GitHub App installation token"
elif [ "$HAS_DEPLOY_KEY" = true ]; then
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/pgx_plugin_deploy && chmod 600 ~/.ssh/pgx_plugin_deploy
ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts 2>/dev/null
echo "GIT_SSH_COMMAND=ssh -i ~/.ssh/pgx_plugin_deploy -o IdentitiesOnly=yes" >> "$GITHUB_ENV"
echo "REMOTE=git@github.com:$STANDALONE_REPO.git" >> "$GITHUB_ENV"
echo "auth: deploy key"
else
cat >&2 <<'MSG'
::error::No credential for pgrundev/pgx-plugin is configured, so nothing was published.
Configure ONE of these in pgrundev/pgx (Settings → Secrets and variables → Actions):
GitHub App (preferred): create an app under the pgrundev organization with repository
permission "Contents: Read and write" only, install it on pgrundev/pgx-plugin only,
and add secrets PGX_PLUGIN_APP_ID (the app id) and PGX_PLUGIN_APP_PRIVATE_KEY (a PEM).
Deploy key: ssh-keygen -t ed25519 -f pgx_plugin_deploy -N '' -C publish-plugin;
add pgx_plugin_deploy.pub to pgrundev/pgx-plugin (Settings → Deploy keys, allow write)
and the private file as secret PGX_PLUGIN_DEPLOY_KEY.
Then re-run this workflow (workflow_dispatch with dry_run=false, or the next push to main).
MSG
exit 1
fi
- name: Clone the published repository
run: |
set -eu
git clone "$REMOTE" "$RUNNER_TEMP/published"
cd "$RUNNER_TEMP/published"
git checkout -q main 2>/dev/null || git checkout -q -b main
git config user.name "pgx-plugin-publisher[bot]"
git config user.email "pgx-plugin-publisher[bot]@users.noreply.github.com"
- name: Replace the tree with the assembled plugin
run: |
set -eu
cd "$RUNNER_TEMP/published"
find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
cp -R "$RUNNER_TEMP/pgx-plugin/." .
chmod +x skills/pgx/scripts/*.sh
git add -A
if git diff --cached --quiet; then echo "nothing to publish"; echo "PUBLISH=no" >> "$GITHUB_ENV"; exit 0; fi
echo "PUBLISH=yes" >> "$GITHUB_ENV"
git -c core.pager=cat diff --cached --stat | tee -a "$GITHUB_STEP_SUMMARY"
- name: Commit and push (fast-forward only)
if: env.PUBLISH == 'yes'
env:
VERSION: ${{ needs.assemble.outputs.version }}
SRC_SHA: ${{ github.sha }}
run: |
set -eu
cd "$RUNNER_TEMP/published"
git commit -q -m "sync: plugin ${VERSION} from pgrundev/pgx@${SRC_SHA:0:12}" \
-m "Generated by plugins/sync.sh --standalone in pgrundev/pgx (.github/workflows/publish-plugin.yml)." \
-m "Source: https://github.com/pgrundev/pgx/commit/${SRC_SHA}"
git push origin HEAD:main
tag="pgx--v${VERSION}"
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then
echo "tag $tag already exists; left as is"
else
git tag -a "$tag" -m "pgx plugin ${VERSION} (pgrundev/pgx@${SRC_SHA:0:12})"
git push origin "refs/tags/$tag"
echo "tagged $tag"
fi
echo "published plugin ${VERSION} to ${STANDALONE_REPO} (${SRC_SHA:0:12})" | tee -a "$GITHUB_STEP_SUMMARY"
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
</p>

```text
# Claude Code: install inside a session (native plugin)
/plugin marketplace add pgrundev/pgx
/plugin install pgx@pgrundev
# Claude Code: install inside a session (native plugin, lightweight repository)
/plugin marketplace add pgrundev/pgx-plugin
/plugin install pgx@pgx-plugin
```

```bash
Expand Down Expand Up @@ -405,11 +405,18 @@ permanent or system databases, and resumes cleanup automatically after a
Install the Skill, then ask your agent. In Claude Code, from inside a session:

```text
/plugin marketplace add pgrundev/pgx
/plugin install pgx@pgrundev
/plugin marketplace add pgrundev/pgx-plugin
/plugin install pgx@pgx-plugin
/pgx:pgx Set up isolated Postgres databases for this project.
```

[`pgrundev/pgx-plugin`](https://github.com/pgrundev/pgx-plugin) holds only
the plugin (about 110 KB, generated from this repository on every change to
`main`), so the install takes seconds. Adding this repository itself as the
marketplace still works (`/plugin marketplace add pgrundev/pgx`, then
`/plugin install pgx@pgrundev`) but clones about 2 GB. Install from one of
the two, not both.

For Cursor, Codex or several hosts at once, the Agent Skills CLI installs the
same Skill into the project:

Expand Down Expand Up @@ -587,7 +594,7 @@ any semantic difference (`classify-regress-diffs.py --fail-on-semantic`).
| [`v0.1-readiness.md`](docs/pgx/v0.1-readiness.md) | what is ready, known limitations, approved use |
| [`linux-results.md`](docs/pgx/linux-results.md) | the full benchmark report |
| [`compatibility-gate.md`](docs/pgx/compatibility-gate.md) | how to decide whether a database can run on PGX |
| [`local-dev/claude-plugin.md`](docs/pgx/local-dev/claude-plugin.md) | the PGX plugin for Claude Code: `/plugin marketplace add pgrundev/pgx`, `/plugin install pgx@pgrundev`, how it relates to the Skills CLI install, updates, removal |
| [`local-dev/claude-plugin.md`](docs/pgx/local-dev/claude-plugin.md) | the PGX plugin for Claude Code: `/plugin marketplace add pgrundev/pgx-plugin`, `/plugin install pgx@pgx-plugin`, the lightweight `pgrundev/pgx-plugin` repository and how it is generated, how it relates to the Skills CLI install, updates, removal, the Anthropic directory submission |
| [`local-dev/agents.md`](docs/pgx/local-dev/agents.md) | the PGX Skill and `pgx agent`: automatic per-session isolation for Claude Code, Cursor and Codex, with the compatibility matrix |
| [`local-dev/copy.md`](docs/pgx/local-dev/copy.md) | `pgx copy`: import an existing PostgreSQL database and branch it per agent |
| [`pgrun-integration.md`](docs/pgx/pgrun-integration.md) | the host-agent API PGRun builds against |
Expand Down
Loading
Loading