diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a94aba8..67b79ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} @@ -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 @@ -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' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 865f0af..dccc7a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: