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 .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"metadata": {
"description": "JFrog Platform plugins for Cursor",
"version": "0.5.15",
"version": "0.5.16",
"pluginRoot": "plugins"
},
"plugins": [
Expand Down
81 changes: 50 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright (c) JFrog Ltd. 2026
#
# Cuts a GitHub Release when a release marker is merged to main.
# Cuts a GitHub Release when the version file on main is newer than the latest tag.
# Full flow and rationale: CONTRIBUTING.md#releasing
name: Release

Expand All @@ -24,29 +24,15 @@ jobs:
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

# plugin.json is canonical; marketplace.json carries its own copy, so the two are
# cross-checked here as well as by the validate-version PR check.
- name: Read version from the plugin manifest
if: steps.detect.outputs.triggered == 'true'
id: version
shell: bash
run: |
set -euo pipefail
VERSION=$(jq -er '.version' plugins/jfrog/.cursor-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::plugin.json version '$VERSION' is not X.Y.Z — refusing to release"
exit 1
fi
Expand All @@ -57,41 +43,74 @@ jobs:
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
shell: bash
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 the plugin manifests before merging a release marker"
set -euo pipefail
git fetch --tags origin
# The glob is only a prefilter; grep -E is what guarantees every candidate is a vX.Y.Z
# release tag, so an unrelated tag like v2-beta can't become the comparison baseline.
LATEST=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true)
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 the plugin manifests 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::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"

# Every gate above fails the step rather than skipping, so reaching this point already means
# a release is due — the steps below need no further condition.
- uses: actions/setup-node@v5
if: steps.detect.outputs.triggered == 'true'
with:
node-version: "20"

# validate-template.yml only runs on pull requests, so nothing checks the merge commit
# itself. Re-running its script here is what gates the release on it.
- name: Validate marketplace template before releasing
if: steps.detect.outputs.triggered == 'true'
run: node scripts/validate-template.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.
# --target creates the tag as part of the release, so a failure can't leave a tag behind with
# no release attached to it.
- 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

# gh release create publishes the release and tag before the asset upload finishes, so a
# failure there leaves a v${VERSION} that every later run would reject as already released.
# Gated on the version gate having passed, so a run that never got that far — and whose
# v${VERSION} therefore belongs to an earlier, healthy release — can't delete it.
- name: Remove a partially published release
if: failure() && steps.release_gate.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
if gh release view "v${VERSION}" >/dev/null 2>&1; then
echo "Deleting incomplete release v${VERSION} and its tag so the next run can retry"
gh release delete "v${VERSION}" --cleanup-tag --yes
fi
7 changes: 7 additions & 0 deletions .github/workflows/validate-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ jobs:
# Cursor's marketplace reads. plugin.json is canonical; this check keeps marketplace.json
# in step with it so a release can't ship two different version numbers.
- name: Check version consistency
shell: bash
run: |
set -euo pipefail
VERSION=$(jq -er '.version' plugins/jfrog/.cursor-plugin/plugin.json)
# Same X.Y.Z shape the release job demands, checked here so a malformed version is caught
# in review instead of failing the release after merge.
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::plugins/jfrog/.cursor-plugin/plugin.json version '$VERSION' is not X.Y.Z"
exit 1
fi
MARKET_VERSION=$(jq -er '.metadata.version' .cursor-plugin/marketplace.json)
if [ "$VERSION" != "$MARKET_VERSION" ]; then
echo "::error::Version mismatch: plugins/jfrog/.cursor-plugin/plugin.json is $VERSION but .cursor-plugin/marketplace.json .metadata.version is $MARKET_VERSION"
Expand Down
16 changes: 8 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contri
node scripts/validate-template.mjs
```

4. **Commit** with a clear, descriptive message.
5. Open a **pull request** against `main` with a summary of what changed and why.
4. Bump the version in **both** manifests — `.version` in [`plugins/jfrog/.cursor-plugin/plugin.json`](plugins/jfrog/.cursor-plugin/plugin.json) and `.metadata.version` in [`.cursor-plugin/marketplace.json`](.cursor-plugin/marketplace.json), kept identical.
5. **Commit** with a clear, descriptive message.
6. Open a **pull request** against `main` with a summary of what changed and why.

Step 4 applies to every pull request, including documentation-only and CI-only ones. Each merge to `main` cuts a release from the manifest version, so a merge that doesn't bump it fails the Release workflow with "already released". That is deliberate: the alternative is silently skipping releases or re-tagging a version that already shipped. See [Releasing](#releasing) for the details.

## Updating the vendored skills

Expand All @@ -41,14 +44,11 @@ See [`VENDOR.md`](VENDOR.md) for the full picture.
To cut a release:

1. In your PR, bump `.version` in [`plugins/jfrog/.cursor-plugin/plugin.json`](plugins/jfrog/.cursor-plugin/plugin.json) and sync `.metadata.version` in [`.cursor-plugin/marketplace.json`](.cursor-plugin/marketplace.json) to match. `plugin.json` is canonical; the `validate-version` PR check enforces that the two agree.
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.
2. Merge to `main`. 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 manifests fails the release rather than re-tagging a shipped version.
The bump is reviewed in the PR that makes it, and it is required of every PR — docs-only and CI-only changes included. Merging without bumping the manifests fails the release rather than silently skipping or re-tagging a shipped version.

The workflow reads the version from `plugin.json`, confirms `marketplace.json` agrees, refuses to continue if that version is already tagged, runs the same marketplace-template check as the `validate-template` 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 `plugin.json`, confirms `marketplace.json` agrees, runs the same marketplace-template check as the `validate-template` 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:

Expand Down
2 changes: 1 addition & 1 deletion plugins/jfrog/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jfrog",
"displayName": "JFrog Platform",
"version": "0.5.15",
"version": "0.5.16",
"description": "JFrog Platform integration with MCP, security skills, Agent Package Resolution, supply-chain best practices, and JFrog Agent Guard governance for adding, removing, and listing MCP servers.",
"author": {
"name": "JFrog",
Expand Down
Loading