Skip to content
Closed
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
55 changes: 30 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Copyright (c) JFrog Ltd. 2026
#
# Cuts a GitHub Release when a release marker is merged to main.
# Cuts a GitHub Release when main's plugin.json version is not yet tagged.
# Full flow and rationale: CONTRIBUTING.md#releasing
name: Release

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -24,24 +25,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
Expand All @@ -55,18 +41,37 @@ jobs:
echo "::error::plugin.json is $VERSION but marketplace.json .metadata.version is $MARKET_VERSION — sync them before releasing"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
TAG="v${VERSION}"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "tagged=true" >> "$GITHUB_OUTPUT"
else
echo "tagged=false" >> "$GITHUB_OUTPUT"
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'
# Deterministic: the tag is the source of truth, not the squash subject.
# Untagged manifest → publish. Already tagged → skip (or fail on a manual
# dispatch, so a forgotten bump cannot re-tag a shipped release).
- name: Decide whether to release
id: detect
env:
EVENT_NAME: ${{ github.event_name }}
TAGGED: ${{ steps.version.outputs.tagged }}
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"
exit 1
set -euo pipefail
TAG="v${VERSION}"
if [ "$TAGGED" = true ]; then
if [ "$EVENT_NAME" = workflow_dispatch ]; then
echo "::error::${TAG} already exists — bump the plugin manifests before requesting a release"
exit 1
fi
echo "triggered=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping: ${TAG} already released"
exit 0
fi
echo "triggered=true" >> "$GITHUB_OUTPUT"
echo "::notice::Publishing ${TAG} (manifest is untagged on main)"

- uses: actions/setup-node@v5
if: steps.detect.outputs.triggered == 'true'
Expand Down
23 changes: 15 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,21 @@ 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.

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 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.
2. Merge to `main`. If that version is not tagged yet, the Release workflow publishes it. The
version comes from the manifest, so the bump stays reviewable in the PR that makes it. There
is no bot push to `main`.

The gate is the tag, not the commit subject. Squash-merge titles, PR-title rewrites, and
`[major]` / `[minor]` / `[patch]` markers do not decide whether to release. An untagged
`plugin.json` on `main` always publishes; a version that already has `vX.Y.Z` is skipped. A
manual `workflow_dispatch` against a tagged version fails rather than re-tagging a shipped
release. If a publish fails, the tag is not created, so the next push to `main` retries.

The workflow reads the version from `plugin.json`, confirms `marketplace.json` agrees, skips
when that version is already tagged (or fails if dispatch asked to re-release it), 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
Loading