diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 9ef0c11..16a577d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jfrog", - "version": "0.1.2", + "version": "0.1.3", "description": "JFrog skills and the JFrog MCP server for Codex \u2014 interact with the JFrog Platform.", "author": { "name": "JFrog", diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dc53687..6bf5d79 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,12 +19,6 @@ jobs: run: node --test scripts/validate.test.mjs - name: Validate manifests + skills run: node scripts/validate.mjs - - name: Version consistency (plugin.json == package.json) - run: | - PJ=$(jq -r .version .codex-plugin/plugin.json) - PK=$(jq -r .version package.json) - echo "plugin.json=$PJ package.json=$PK" - test "$PJ" = "$PK" || { echo "::error::version mismatch: plugin.json=$PJ package.json=$PK"; exit 1; } - name: Vendor-sync drift check run: | node scripts/sync-skills.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36cafc3..bf02a7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 the version file on main is newer than the latest tag. +# Full flow and rationale: CONTRIBUTING.md#releasing name: Release on: @@ -18,81 +19,90 @@ 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 was still queued behind this one. - 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 - # plugin.json is canonical; package.json carries its own copy, so the two are - # cross-checked here as well as by the PR version-consistency check. + # cross-checked here as well as by the validate-version PR workflow. - name: Read version from the plugin manifest - if: steps.detect.outputs.triggered == 'true' id: version run: | set -euo pipefail VERSION=$(jq -er '.version' .codex-plugin/plugin.json) - if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "::error::plugin.json version '$VERSION' is not X.Y.Z — refusing to release" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::.codex-plugin/plugin.json version '$VERSION' is not X.Y.Z — refusing to release" exit 1 fi PK_VERSION=$(jq -er '.version' package.json) if [ "$VERSION" != "$PK_VERSION" ]; then - echo "::error::plugin.json is $VERSION but package.json is $PK_VERSION — sync them before releasing" + echo "::error::.codex-plugin/plugin.json is $VERSION but package.json is $PK_VERSION — sync them before releasing" 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 the plugin manifests before merging a release marker" + set -euo pipefail + git fetch --tags origin + 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 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::.codex-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: "20" - # pr.yml only runs on pull requests, so nothing checks the merge commit itself. - # Re-running validation here is what gates the release on it. + # pr.yml runs on this same push, but as an independent workflow that can't gate this one. + # Re-running its checks here (unit tests, manifest validate, vendor-sync drift) is what + # actually gates the release on them. - name: Validate before release - if: steps.detect.outputs.triggered == 'true' run: | node --test scripts/validate.test.mjs node scripts/validate.mjs + node scripts/sync-skills.mjs + git diff --exit-code -- skills/ # 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 diff --git a/.github/workflows/validate-version.yml b/.github/workflows/validate-version.yml new file mode 100644 index 0000000..2ce1ce9 --- /dev/null +++ b/.github/workflows/validate-version.yml @@ -0,0 +1,32 @@ +# Copyright (c) JFrog Ltd. 2026 +name: Validate version + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + validate-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + # plugin.json is canonical; package.json carries its own copy. This check keeps them + # in step so a release can't ship two different version numbers. + - name: Check version consistency + run: | + set -euo pipefail + VERSION=$(jq -er '.version' .codex-plugin/plugin.json) + PK_VERSION=$(jq -er '.version' package.json) + if [ "$VERSION" != "$PK_VERSION" ]; then + echo "::error::Version mismatch: .codex-plugin/plugin.json is $VERSION but package.json is $PK_VERSION" + exit 1 + fi + if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::.codex-plugin/plugin.json version '$VERSION' is not X.Y.Z" + exit 1 + fi + echo "Versions consistent: $VERSION" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55111fb..602f439 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,16 +49,19 @@ This downloads the pinned upstream tarball and replaces the contents of `skills/ ## Releasing -To cut a release: +Every merge to `main` cuts a release, so **every** PR must bump the version: 1. In your PR, bump `version` in **both** [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) and [`package.json`](package.json) to the same, not-yet-released value. `plugin.json` is canonical; `package.json` carries its own copy, and the two are cross-checked. -2. Merge to `main` with `[major]`, `[minor]`, or `[patch]` in the commit **subject** — the first line. A marker elsewhere in the body is ignored on purpose. +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 manifests either way, so the bump is reviewed in the PR that makes it. Merging a marker without bumping the manifests fails the release rather than re-tagging a shipped version. +There is no opt-out. A PR that leaves the manifests untouched — docs, chores, or fixes alike — fails the release workflow on merge rather than silently skipping or re-tagging a shipped version. The bump is reviewed in the PR that makes it. -[`.github/workflows/release.yml`](.github/workflows/release.yml) reads the version from `.codex-plugin/plugin.json` (cross-checked against `package.json`), refuses to continue if that version is already tagged, re-runs the same validation as the 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. +[`.github/workflows/release.yml`](.github/workflows/release.yml) reads the version from `.codex-plugin/plugin.json` (cross-checked against `package.json`), re-runs the PR workflow's unit tests, `validate.mjs`, and vendor-sync drift check, 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. -A PR without a marker (docs, chores, or fixes that don't ship a new plugin version) merges normally and cuts no release. +Two things to know before changing it: + +- Validation runs inside the release job. [`pr.yml`](.github/workflows/pr.yml) triggers on the same push to `main`, but as an independent workflow, so it can be red while a release still goes out. Re-running its unit tests, `validate.mjs`, and vendor-sync drift check in the release job is what actually gates the release on them. +- 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. ## Reporting Issues diff --git a/README.md b/README.md index 5bf79a9..ddc0e7c 100644 --- a/README.md +++ b/README.md @@ -163,8 +163,7 @@ To pull a newer upstream release into this repo: 4. Update the pinned-version link in the [Prerequisites](#prerequisites) section so the skill runtime requirements point at the new tag. 5. Commit the pin bump, the regenerated `skills/` tree, the version bump, and the - README link bump together, and open a PR whose merge commit subject carries a - `[patch]` / `[minor]` / `[major]` marker (see [Releasing](#releasing)). + README link bump together, and open a PR (see [Releasing](#releasing)). See [`VENDOR.md`](VENDOR.md) for the full picture. @@ -173,12 +172,11 @@ See [`VENDOR.md`](VENDOR.md) for the full picture. ## Releasing Releases are cut automatically by [`.github/workflows/release.yml`](.github/workflows/release.yml) -when a commit lands on `main` whose **subject line** contains a -`[major]` / `[minor]` / `[patch]` marker. The workflow reads the version from -`.codex-plugin/plugin.json` (cross-checked against `package.json`), refuses to -re-release an existing tag, and publishes a GitHub Release `v` with a -zipped artifact. A version is released only when both a manifest bump **and** a -marker commit reach `main`. +when a commit lands on `main` with a version in `.codex-plugin/plugin.json` that is newer than +the latest release tag (cross-checked against `package.json`). The workflow publishes a GitHub +Release `v` with a zipped artifact. Every merge to `main` must bump both manifests — +a commit that lands without a bump fails the workflow instead of releasing. See +[CONTRIBUTING.md](CONTRIBUTING.md#releasing) for the full flow. --- diff --git a/VENDOR.md b/VENDOR.md index b2dece3..b798bfb 100644 --- a/VENDOR.md +++ b/VENDOR.md @@ -20,7 +20,7 @@ When the upstream repo publishes a new release, refresh the vendored tree via a 2. Re-syncs and commits the refreshed `skills/` tree. 3. Bumps `version` in **both** [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) and [`package.json`](package.json) — they must match (CI enforces this) — so the published plugin version reflects the new skills bundle. -Merging the PR does not publish on its own: the merge commit **subject** must carry a `[patch]` / `[minor]` / `[major]` marker, which [`.github/workflows/release.yml`](.github/workflows/release.yml) enforces as the release gate. See the README's [Releasing](README.md#releasing) and [Updating the vendored skills](README.md#updating-the-vendored-skills) sections for the full workflow. +Merging the PR does not publish on its own: the version in [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) must be bumped to a value newer than the latest release tag, which [`.github/workflows/release.yml`](.github/workflows/release.yml) enforces on every push to `main`. See the README's [Releasing](README.md#releasing) and [Updating the vendored skills](README.md#updating-the-vendored-skills) sections for the full workflow. To regenerate the tree locally before opening the PR: diff --git a/package.json b/package.json index 09855c1..32d168e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codex-plugin", - "version": "0.1.2", + "version": "0.1.3", "private": true, "type": "module", "description": "JFrog skills and MCP server plugin for OpenAI Codex.",