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 .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 45 additions & 36 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,81 +19,89 @@ 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]+$' | 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::.codex-plugin/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'
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 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

# 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
32 changes: 32 additions & 0 deletions .github/workflows/validate-version.yml
Original file line number Diff line number Diff line change
@@ -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"
13 changes: 8 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

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 checks 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

Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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<version>` 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<version>` 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.

---

Expand Down
2 changes: 1 addition & 1 deletion VENDOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Loading