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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "jfrog",
"displayName": "JFrog",
"description": "Official JFrog plugin. Connect Claude Code to JFrog to manage, secure, and govern your software supply chain. Give agents the context to build secure, compliant software.",
"version": "0.2.21",
"version": "0.2.22",
"author": {
"name": "JFrog Ltd.",
"email": "devrel@jfrog.com",
Expand Down
71 changes: 40 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) JFrog Ltd. 2026
#
# Cuts a GitHub Release when a release marker is merged to main.
# Cuts a GitHub Release when `.claude-plugin/plugin.json` on main is newer than the
# latest tag. Equal or older versions fail the job (they do not no-op).
# Full flow and rationale: CONTRIBUTING.md#releasing
name: Release

Expand All @@ -19,73 +20,81 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
# Full history, so the tag check below can see existing tags.
# fetch-depth: 0 here, plus `git fetch --tags` in the gate below, so the gate sees every
# release tag — including one created by a prior run that this run was queued behind.
- uses: actions/checkout@v5
with:
fetch-depth: 0

# Subject line only, not the whole message. MSG goes through env rather than string
# interpolation, so a crafted commit subject can't inject shell.
- name: Detect release marker in commit subject
id: detect
env:
MSG: ${{ github.event.head_commit.message }}
run: |
SUBJECT=$(printf '%s\n' "$MSG" | head -1)
if printf '%s' "$SUBJECT" | grep -qE '\[(major|minor|patch)\]'; then
echo "triggered=true" >> "$GITHUB_OUTPUT"
else
echo "triggered=false" >> "$GITHUB_OUTPUT"
fi

# The manifest is the only place the version lives.
- name: Read version from the plugin manifest
if: steps.detect.outputs.triggered == 'true'
id: version
run: |
set -euo pipefail
VERSION=$(jq -er '.version' .claude-plugin/plugin.json)
if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::.claude-plugin/plugin.json version '$VERSION' is not X.Y.Z — refusing to release"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# A tag exists only if that version was released, so this catches a marker that was merged
# without a manifest bump.
- name: Refuse to re-release an existing version
if: steps.detect.outputs.triggered == 'true'
- name: Compare version against latest release tag
id: release_gate
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
TAG="v${{ steps.version.outputs.version }}"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::error::$TAG already exists — bump .claude-plugin/plugin.json before merging a release marker"
set -euo pipefail
git fetch --tags origin
# grep exits 1 when no tag matches, which pipefail would turn into a step failure —
# an empty LATEST is the legitimate first-release case, handled just below.
LATEST=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | { grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -V | tail -1)
if [ -z "$LATEST" ]; then
echo "No prior release tag — v${VERSION} will be the first release"
exit 0
fi
LATEST_VERSION="${LATEST#v}"
if [ "$VERSION" = "$LATEST_VERSION" ]; then
echo "::error::v${VERSION} was already released — bump .claude-plugin/plugin.json before merging"
exit 1
fi
TAG_FOR_VERSION="v${VERSION}"
LOWEST=$(printf '%s\n%s\n' "$TAG_FOR_VERSION" "$LATEST" | sort -V | head -1)
if [ "$LOWEST" = "$TAG_FOR_VERSION" ]; then
echo "::error::.claude-plugin/plugin.json version ${VERSION} is older than the latest release ${LATEST} — check for an accidental revert"
exit 1
fi
echo "Version ${VERSION} is newer than ${LATEST} — proceeding with release"

- uses: actions/setup-node@v5
if: steps.detect.outputs.triggered == 'true'
with:
node-version: "24"

# validate.yml runs on this same push, but as an independent workflow that can't gate this
# one. Re-running its check here is what actually gates the release on it.
- name: Validate plugin layout before releasing
if: steps.detect.outputs.triggered == 'true'
run: node scripts/validate-claude-plugin.mjs

# Tracked files at HEAD only, so nothing left on the runner can end up in the zip.
- name: Package release artifact
if: steps.detect.outputs.triggered == 'true'
run: git archive --format=zip --output=release.zip HEAD -- ':(exclude).github'

# --target creates the tag as part of the release, so a failure can't leave an orphan tag.
- name: Create GitHub Release
if: steps.detect.outputs.triggered == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
run: |
gh release create "v${{ steps.version.outputs.version }}" \
gh release create "v${VERSION}" \
release.zip \
--target "$GITHUB_SHA" \
--title "Release v${{ steps.version.outputs.version }}" \
--title "Release v${VERSION}" \
--generate-notes

# Only when the gate passed: a gate failure means the version was already released, and that
# release belongs to an earlier run — deleting it here would destroy a shipped release.
- name: Roll back a partially published release
if: failure() && steps.release_gate.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
run: gh release delete "v${VERSION}" --yes --cleanup-tag || true
11 changes: 11 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ jobs:
steps:
- uses: actions/checkout@v5

# Catches a malformed version in review, where it is cheap to fix. The release workflow
# compares it against the latest tag; that comparison only makes sense on main.
- name: Check the manifest version is X.Y.Z
run: |
set -euo pipefail
VERSION=$(jq -er '.version' .claude-plugin/plugin.json)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::.claude-plugin/plugin.json version '$VERSION' is not X.Y.Z"
exit 1
fi

- name: Set up Node.js
uses: actions/setup-node@v5
with:
Expand Down
17 changes: 7 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ This downloads the pinned upstream tarball and replaces the contents of `skills/

- [ ] `node scripts/validate-claude-plugin.mjs` passes.
- [ ] `claude plugin validate` passes (before directory submission or major releases).
- [ ] Version bumped in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) when the plugin changes.
- [ ] Version bumped in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) — required on every PR to `main`, not only when the plugin itself changes.
- [ ] No secrets, credentials, or files under `**/local-cache/` committed.
- [ ] If the skill tree changed: `pin` in `.github/scripts/sync-skills-vendor.json` matches the upstream tag the new tree was generated from.
- [ ] Smoke-test: `claude --plugin-dir .` from the repo root.
Expand All @@ -56,22 +56,19 @@ Compliance: [Anthropic Software Directory Terms](https://support.claude.com/en/a

## Releasing

To cut a release:
Every merge to `main` releases, so **every** PR to `main` must bump `.version` in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) — including docs- and CI-only changes. That manifest is the only place the version lives.

1. In your PR, bump `.version` in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json). That manifest is the only place the version lives.
2. Merge to `main` with `[major]`, `[minor]`, or `[patch]` in the commit **subject** - the first
line. A marker further down in the body is ignored on purpose: this repo squash-merges, and
GitHub pre-fills the squash body from the branch commits or the PR description, either of
which may quote a marker while only documenting it.
Every push to `main` compares the manifest version against the latest release tag: if the version is newer, a release proceeds; if it matches the latest tag, the workflow fails with a clear "already released" error; if it is older, it fails with a revert warning.

The marker only decides *whether* to release; the version comes from the manifest either way, so the bump is reviewed in the PR that makes it. There is no bot push to `main`. Merging a marker without bumping the manifest fails the release rather than re-tagging a shipped version.
A merge without a bump therefore turns `Release` red. That is by design, not a bug to work around: the bump is reviewed in the PR that makes it, and failing loudly beats silently skipping a release or re-tagging a shipped version.

The workflow reads the version from the manifest, refuses to continue if that version is already tagged, runs the same plugin-layout check as the `validate` PR workflow, packages the tracked files at `HEAD` (minus `.github/`) into `release.zip`, and creates the `vX.Y.Z` tag as part of publishing the GitHub Release.
The workflow reads the version from the manifest, runs the same plugin-layout check as the `validate` PR workflow, packages the tracked files at `HEAD` (minus `.github/`) into `release.zip`, and creates the `vX.Y.Z` tag as part of publishing the GitHub Release.

Two things to know before changing it:
Three things to know before changing it:

- Validation runs inside the release job. `validate.yml` triggers on the same push, but as an independent workflow, so it can be red while a release still goes out. Re-running its check in the release job is what actually gates the release on it.
- The tag is created by the release, not before it. `gh release create --target` does both in one API call, so a failed run can't leave a tag behind with no release attached to it.
- The rollback step only runs when the gate itself passed. A gate failure means the version was already released by an earlier run, and deleting that release would destroy something already shipped.

## Reporting Issues

Expand Down
Loading