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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
# Keeps the pinned action commits current. Each update arrives as a PR that
# Validate checks and a maintainer reviews.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
# Don't propose an action release until it's at least 3 days old.
cooldown:
default-days: 3
71 changes: 54 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,82 @@ name: Release

# Merging a version bump is the approval to publish. When main carries a
# version with no release yet, tag it and attach one .plugin archive per
# plugin. A push that doesn't change the version finds its release already
# plugin. A merge that doesn't change the version finds its release already
# there and does nothing, and so does a main with no plugins in it yet.
#
# Only main publishes, and there is no manual trigger: a run from any other
# branch would publish that branch's unreviewed content, and tags here can't
# be moved or deleted.
on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: write

# Two quick merges must not race on the same tag.
concurrency:
group: release
cancel-in-progress: false

jobs:
release:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Every plugin carries the same version; the tag is built from it, so
# anything other than X.Y.Z is refused before it reaches a tag name.
- name: Work out the version
id: version
run: |
if ! compgen -G 'plugins/claude/*/.claude-plugin/plugin.json' >/dev/null; then
echo "No plugins yet; nothing to release."
echo "none=true" >> "$GITHUB_OUTPUT"
exit 0
fi
version="$(python3 -c "import glob, json; print(json.load(open(sorted(glob.glob('plugins/claude/*/.claude-plugin/plugin.json'))[0]))['version'])")"
echo "tag=v${version}-beta" >> "$GITHUB_OUTPUT"
echo "slug=v${version//./-}-beta" >> "$GITHUB_OUTPUT"
python3 - <<'PY'
import glob, json, os, re
manifests = sorted(glob.glob("plugins/claude/*/.claude-plugin/plugin.json"))
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
if not manifests:
print("No plugins yet; nothing to release.")
out.write("none=true\n")
raise SystemExit
versions = {json.load(open(m))["version"] for m in manifests}
if len(versions) != 1:
raise SystemExit(f"Plugins disagree on the version: {sorted(versions)}")
version = versions.pop()
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
raise SystemExit(f"Version {version!r} isn't X.Y.Z")
out.write(f"tag=v{version}-beta\n")
out.write(f"slug=v{version.replace('.', '-')}-beta\n")
PY

# An existing release is fine only if it was cut from main and the
# plugins haven't changed since. Otherwise this version was published
# from somewhere else, or the plugins changed without a version bump,
# and quietly skipping would leave main and the release disagreeing.
- name: Check whether this version is already released
id: released
if: steps.version.outputs.none != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.version.outputs.tag }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
if ! gh release view "$TAG" >/dev/null 2>&1; then
echo "exists=false" >> "$GITHUB_OUTPUT"
exit 0
fi
status="$(gh api "repos/$GITHUB_REPOSITORY/compare/$TAG...$GITHUB_SHA" -q .status)"
if [ "$status" != "ahead" ] && [ "$status" != "identical" ]; then
echo "::error::$TAG was released from a commit that isn't on main."
exit 1
fi
changed="$(gh api "repos/$GITHUB_REPOSITORY/compare/$TAG...$GITHUB_SHA" -q '[.files[]?.filename | select(startswith("plugins/") or startswith(".claude-plugin/"))] | length')"
if [ "$changed" != "0" ]; then
echo "::error::The plugins changed since $TAG without a version bump."
exit 1
fi
echo "exists=true" >> "$GITHUB_OUTPUT"

- name: Archive each plugin
if: steps.released.outputs.exists == 'false'
Expand All @@ -52,11 +87,13 @@ jobs:
mkdir dist
for dir in plugins/claude/*/; do
name="$(basename "$dir")"
(cd "$dir" && zip -qr "../../../dist/${name}-${SLUG}.plugin" . -x '*.DS_Store')
# -y stores a symlink as a link instead of following it.
(cd "$dir" && zip -qry "../../../dist/${name}-${SLUG}.plugin" . -x '*.DS_Store')
done

- name: Publish the release
if: steps.released.outputs.exists == 'false'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.version.outputs.tag }}
run: gh release create "$TAG" dist/*.plugin --target "$GITHUB_SHA" --title "$TAG" --prerelease --generate-notes
26 changes: 23 additions & 3 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Validate

# Runs on every pull request, so a reviewer opens a diff that already passes.
# A green run means the plugins are well-formed, not that they're safe: it
# passes hooks, MCP server commands and bin/ files, so reviewers still read
# those.
on:
pull_request:
push:
Expand All @@ -12,14 +15,31 @@ permissions:
jobs:
validate:
runs-on: ubuntu-latest
env:
# validate needs nothing from the network once the CLI is installed.
DISABLE_TELEMETRY: "1"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# validate reads through a symlinked manifest and prints what it points
# at, and the release zip would follow links too. Nothing here is one.
- name: Refuse symlinks
run: |
links="$(find . -path ./.git -prune -o -type l -print)"
if [ -n "$links" ]; then
echo "$links"
echo "::error::Symlinks aren't allowed in this repository."
exit 1
fi

# Pinned rather than "latest": the CLI's rules change between releases,
# and a moving target would turn a PR red with no change to the plugin.
# Bump it on purpose.
# Bump it on purpose, to a release at least 3 days old.
- name: Install the Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.1.280
run: npm install -g @anthropic-ai/claude-code@2.1.284

- name: Validate the marketplace
run: claude plugin validate --strict .
Expand Down
Loading