From 7c4522fc53a227694577628e6551cdabfdde21b6 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Thu, 20 Aug 2026 17:30:41 +0300 Subject: [PATCH 1/2] AX-2134: unify release trigger on version-file diff --- .github/workflows/release.yml | 54 +++++++++++++++++------------------ CONTRIBUTING.md | 9 ++---- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a94aba8..bd35686 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -24,24 +24,9 @@ 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 run: | set -euo pipefail @@ -57,36 +42,51 @@ 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 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 + VERSION="${{ steps.version.outputs.version }}" + git fetch --tags origin + LATEST=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1) + if [ -z "$LATEST" ]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + 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 "should_release=true" >> "$GITHUB_OUTPUT" + echo "Version ${VERSION} is newer than ${LATEST} — proceeding with release" - uses: actions/setup-node@v5 - if: steps.detect.outputs.triggered == 'true' + if: steps.release_gate.outputs.should_release == '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' + if: steps.release_gate.outputs.should_release == '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' + if: steps.release_gate.outputs.should_release == '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' + if: steps.release_gate.outputs.should_release == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 865f0af..553f7b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,14 +41,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. 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: From 248626c2922a9386311f7eb98a2af0f37f1eed96 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Sat, 22 Aug 2026 17:31:27 +0300 Subject: [PATCH 2/2] AX-2134: address review findings for version-file releases Co-authored-by: Cursor --- .cursor-plugin/marketplace.json | 2 +- .github/workflows/release.yml | 43 +++++++++++++++++------- .github/workflows/validate-version.yml | 7 ++++ CONTRIBUTING.md | 9 +++-- plugins/jfrog/.cursor-plugin/plugin.json | 2 +- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 92357cc..f62a7d4 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "JFrog Platform plugins for Cursor", - "version": "0.5.15", + "version": "0.5.16", "pluginRoot": "plugins" }, "plugins": [ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd35686..e4b1884 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,11 @@ jobs: # cross-checked here as well as by the validate-version PR check. - name: Read version from the plugin manifest 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 @@ -44,13 +45,16 @@ jobs: - name: Compare version against latest release tag id: release_gate + shell: bash + env: + VERSION: ${{ steps.version.outputs.version }} run: | set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" git fetch --tags origin - LATEST=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1) + # 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 "should_release=true" >> "$GITHUB_OUTPUT" echo "No prior release tag — v${VERSION} will be the first release" exit 0 fi @@ -65,33 +69,48 @@ jobs: echo "::error::plugin.json version ${VERSION} is older than the latest release ${LATEST} — check for an accidental revert" exit 1 fi - echo "should_release=true" >> "$GITHUB_OUTPUT" 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.release_gate.outputs.should_release == '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.release_gate.outputs.should_release == '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.release_gate.outputs.should_release == '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.release_gate.outputs.should_release == '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 diff --git a/.github/workflows/validate-version.yml b/.github/workflows/validate-version.yml index 19f56c5..a1f9f2d 100644 --- a/.github/workflows/validate-version.yml +++ b/.github/workflows/validate-version.yml @@ -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" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 553f7b7..4e3eb59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 @@ -43,7 +46,7 @@ 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`. 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 bump is reviewed in the PR that makes it. Merging without bumping the manifests fails the release rather than silently skipping or 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, 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. diff --git a/plugins/jfrog/.cursor-plugin/plugin.json b/plugins/jfrog/.cursor-plugin/plugin.json index f7cf10c..84ba9a8 100644 --- a/plugins/jfrog/.cursor-plugin/plugin.json +++ b/plugins/jfrog/.cursor-plugin/plugin.json @@ -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",