Files
mims-harvard__tooluniverse/scripts/release-plugin.sh
Shanghua Gao e9bd73205c Add Codex plugin packaging for ToolUniverse (#249)
* Add Codex plugin packaging

* Wire Codex plugin into skill-sync + release pipeline; bring to parity

The Codex plugin packaging lived outside the Claude plugin's release flow, so its
bundled skills and version drifted (134 skills @ 1.2.4 vs Claude's 136 @ 1.2.18).
This puts both packagings on one pipeline:

- plugin/sync-skills.sh now rebuilds BOTH plugin/skills/ (Claude) and
  plugins/tooluniverse/skills/ (Codex) from the canonical skills/ tree; Codex
  drops Claude-Code-specific skills (tooluniverse-claude-code-plugin).
- scripts/release-plugin.sh now also bumps plugins/tooluniverse/.codex-plugin/
  plugin.json on release, so Codex's version tracks Claude's.

One-time parity:
- Codex skills resynced (135 = 136 user-facing minus the Claude-only skill);
  adds tooluniverse-peptide-target-deorphanization, drops stale entries.
- Codex plugin.json 1.2.4 -> 1.2.18 to match the current release.
- Restored skills/tooluniverse-custom-tool/references/json-tool.md: PR #194 edited
  only the Claude plugin copy, leaving the source missing the API-key declaration
  docs, which any resync would otherwise drop.

Codex auto-upgrades owner/repo Git marketplaces on startup, so this reaches Codex
users on next launch; Claude users get it via the version bump.

* Fix Codex skill sync: delegate to the Codex-specific normalizer

The prior pipeline commit rebuilt plugins/tooluniverse/skills/ with the same
filter as the Claude copy, which left in the disable-model-invocation marker
(rejected by Codex validation; 133 skills) and descriptions over Codex's
1024-char limit (4 skills). Have plugin/sync-skills.sh delegate the Codex copy to
scripts/sync-codex-plugin-skills.sh — which strips the marker and compacts long
descriptions — instead of duplicating and diverging from that logic. Also add an
evals/ exclude to the Codex sync so dev eval scaffolding does not ship, matching
the Claude filter.

* Run the Codex plugin packaging test in CI

tests/test_codex_plugin.py lived at the tests/ top level, outside pytest.ini's
testpaths (tests/unit, tests/integration, tests/tools), so CI never collected it
— its per-skill assertions (description <= 1024 chars, disable-model-invocation
not set) silently never ran, which is why the earlier broken Codex skill regen
passed CI. Move it under tests/unit/, fix the repo-root parents[] depth, and mark
it unit so the Codex skill set is validated on every run.

* Rename Codex marketplace from tooluniverse-local to tooluniverse

The catalog name/displayName carried a 'local' suffix left over from local-dir
testing, but this is the published GitHub marketplace users add via
`codex plugin marketplace add mims-harvard/ToolUniverse`. Drop the misleading
'local' so it lists as 'tooluniverse' / 'ToolUniverse'.

* Update Codex plugin README: add install steps, point sync at the shared script

- Add an Install section (codex plugin marketplace add mims-harvard/ToolUniverse
  + codex plugin add tooluniverse) — the README had no end-user install steps.
- Point the Development sync at plugin/sync-skills.sh, the unified entry point
  that rebuilds BOTH the Claude and Codex skill copies; the old instruction ran
  only the Codex-specific sync, so following it left the Claude copy stale.

* Version plugins as <package MAJOR.MINOR>.<plugin revision>

Tie both plugin versions to the PyPI package's MAJOR.MINOR while letting them
carry an independent PATCH (the plugin revision). release-plugin.sh now derives
the next version from pyproject.toml's MAJOR.MINOR and bumps the plugin PATCH
(resetting to .0 when the package's MAJOR.MINOR advances), instead of bumping the
plugin's own semver freely. This keeps Claude + Codex plugin versions coherent
with the package they serve while a skill-only change ships as a PATCH bump —
without forcing an empty PyPI package release.

* Update RELEASING.md for the package-tracked plugin version scheme

Document the <package MAJOR.MINOR>.<plugin revision> scheme, the 4 bumped plugin
manifests (Codex included), and that both Claude (version-driven) and Codex
(git-driven) plugins auto-update — replacing the stale level-based / 3-manifest /
Claude-only instructions.

* Add tooluniverse-codex-plugin install skill (Codex-only)

A one-step install + troubleshooting guide for the ToolUniverse plugin on OpenAI
Codex, mirroring tooluniverse-claude-code-plugin but adapted to Codex's commands
(codex plugin marketplace add / plugin add -m), startup auto-upgrade behavior,
and MCP-via-uvx model — and noting Codex ships MCP tools + skills only (no slash
commands). Symmetric to the Claude install skill: plugin/sync-skills.sh now
excludes it from the Claude packaging, while the Codex sync includes it (and
drops the Claude-only tooluniverse-claude-code-plugin). Description tuned via the
skill-creator trigger optimizer (perfect precision; install-skill recall is
inherently low and largely platform-bound).
2026-06-14 16:04:25 -07:00

225 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Release a new version of the ToolUniverse Claude Code AND Codex plugins.
#
# Versioning scheme:
# Both plugins share the PyPI `tooluniverse` package's MAJOR.MINOR and carry
# their own PATCH (the plugin revision). This keeps the plugin versions
# coherent with the package they serve over MCP, while letting plugin-only
# changes (e.g. skill edits) ship WITHOUT an empty package release:
# - plugin-only change -> PATCH bump (pkg 1.2.x -> plugin 1.2.18 → 1.2.19)
# - package MAJOR.MINOR moves-> reset to .0 (pkg 1.3.0 -> plugin 1.3.0)
# The package keeps its own independent PyPI release line (pyproject + vX.Y.Z
# tags); a plugin release never bumps the package.
#
# What this does:
# 1. Bump the version in all 4 plugin manifests:
# plugin/.claude-plugin/plugin.json, plugin/.claude-plugin/marketplace.json,
# .claude-plugin/marketplace.json, plugins/tooluniverse/.codex-plugin/plugin.json
# (absent manifests in the current layout are skipped)
# 2. Commit the bump
# 3. Tag the commit (vX.Y.Z)
# 4. Push commit + tag to origin
# 5. The release-plugin.yml workflow then builds the zip + creates a GitHub Release
#
# Users receive the new version automatically: Claude Code on the version bump
# (next startup), Codex on the git push (startup marketplace auto-upgrade).
#
# Usage:
# bash scripts/release-plugin.sh auto # next plugin revision (tracks the package)
# bash scripts/release-plugin.sh patch # same as auto (level is advisory only)
# bash scripts/release-plugin.sh 1.3.0 # explicit version (manual override)
# bash scripts/release-plugin.sh auto --dry-run # show planned changes only
# bash scripts/release-plugin.sh auto --no-push # bump+commit+tag, don't push
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
usage() {
cat <<EOF
Usage: $0 <auto|patch|minor|major|X.Y.Z> [--dry-run] [--no-push]
The plugin version tracks the PyPI package's MAJOR.MINOR and bumps its own PATCH.
auto/patch/minor/major all do the same thing (next plugin revision); pass an
explicit X.Y.Z only to override manually.
Examples:
$0 auto # next plugin revision (tracks the package MAJOR.MINOR)
$0 auto --dry-run # preview the bump without changing anything
$0 1.3.0 --no-push # explicit version; bump, commit, tag locally for review
Without --no-push, this script pushes commit + tag to origin and triggers the
release workflow.
Full reference (all 3 release paths, what gets bumped, safety mechanisms):
scripts/RELEASING.md
EOF
exit 1
}
LEVEL="${1:-}"
case "$LEVEL" in
-h|--help|help)
usage ;;
"")
usage ;;
esac
DRY_RUN=0
NO_PUSH=0
shift
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--no-push) NO_PUSH=1 ;;
*) echo "Unknown flag: $arg"; usage ;;
esac
done
# Baseline = the highest of the manifest version and any existing vX.Y.Z git tag.
# The plugin manifest can drift BEHIND the released tag line (e.g. PyPI-driven
# releases bump pyproject/tags but not plugin.json). Taking the max keeps the
# plugin PATCH (revision) monotonic and avoids colliding with an existing tag.
CURRENT=$(python3 -c "
import json, re, subprocess
def parse(v):
m = re.match(r'^(\d+)\.(\d+)\.(\d+)$', v)
return tuple(int(x) for x in m.groups()) if m else (0, 0, 0)
versions = [json.load(open('plugin/.claude-plugin/plugin.json'))['version']]
tags = subprocess.run(['git', 'tag', '--list', 'v*'],
capture_output=True, text=True).stdout.split()
versions += [t[1:] for t in tags if re.match(r'^v\d+\.\d+\.\d+$', t)]
print(max(versions, key=parse))
")
echo "Current plugin version: $CURRENT (max of plugin.json and existing v* tags)"
case "$LEVEL" in
patch|minor|major|auto)
# The plugin version tracks the PyPI package's MAJOR.MINOR and owns its
# PATCH (the plugin revision). The patch/minor/major argument is ignored:
# whether the plugin moves to a new MAJOR.MINOR is decided by the package,
# not by this script. A plugin-only change is always a PATCH bump, so it
# ships without forcing an (empty) package release.
NEW=$(python3 -c "
import re, tomllib
def parse(v):
m = re.match(r'^(\d+)\.(\d+)\.(\d+)$', v)
return tuple(int(x) for x in m.groups()) if m else (0, 0, 0)
pkg = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']
pmaj, pmin = (int(x) for x in re.match(r'^(\d+)\.(\d+)', pkg).groups())
cmaj, cmin, cpatch = parse('$CURRENT')
if (cmaj, cmin) == (pmaj, pmin):
# Same line as the package -> next plugin revision.
print(f'{pmaj}.{pmin}.{cpatch + 1}')
elif (pmaj, pmin) > (cmaj, cmin):
# Package advanced its MAJOR.MINOR -> resync the plugin to it.
print(f'{pmaj}.{pmin}.0')
else:
# Plugin already ahead of the package line (package not yet released for it);
# stay on the current line and bump the revision.
print(f'{cmaj}.{cmin}.{cpatch + 1}')
")
;;
[0-9]*.[0-9]*.[0-9]*)
if [[ ! "$LEVEL" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: '$LEVEL' is not a valid X.Y.Z version" >&2
exit 1
fi
NEW="$LEVEL"
;;
*)
echo "Error: '$LEVEL' is not a valid level or version" >&2
usage
;;
esac
if [ "$NEW" = "$CURRENT" ]; then
echo "Error: new version equals current version ($NEW). Nothing to do." >&2
exit 1
fi
if git rev-parse -q --verify "refs/tags/v$NEW" >/dev/null; then
echo "Error: tag v$NEW already exists. Choose a higher version." >&2
exit 1
fi
echo "New version: $NEW"
echo "Tag will be: v$NEW"
echo
BRANCH=$(git branch --show-current)
echo "Current branch: $BRANCH"
if [ -n "$(git status --porcelain)" ]; then
echo "Error: working tree has uncommitted changes. Commit or stash first." >&2
git status --short >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo
echo "(--dry-run) Would change (absent manifests are skipped):"
for f in plugin/.claude-plugin/plugin.json \
plugin/.claude-plugin/marketplace.json \
.claude-plugin/marketplace.json \
plugins/tooluniverse/.codex-plugin/plugin.json; do
[ -f "$f" ] && echo " $f $CURRENT → $NEW" || echo " $f (absent, skipped)"
done
echo " Then commit \"Release v$NEW\", tag v$NEW, push to origin/$BRANCH"
exit 0
fi
python3 - <<EOF
import json, pathlib
new = "$NEW"
files = [
("plugin/.claude-plugin/plugin.json", "version"),
("plugin/.claude-plugin/marketplace.json", "metadata.version"),
(".claude-plugin/marketplace.json", "metadata.version"),
("plugins/tooluniverse/.codex-plugin/plugin.json", "version"),
]
for path, field in files:
p = pathlib.Path(path)
if not p.exists():
# Not all manifests exist in every layout; skip rather than crash.
print(f" skip (absent) {path}")
continue
data = json.loads(p.read_text())
if field == "version":
data["version"] = new
elif field == "metadata.version":
data.setdefault("metadata", {})["version"] = new
p.write_text(json.dumps(data, indent=2) + "\n")
print(f" bumped {path}")
EOF
echo
for f in plugin/.claude-plugin/plugin.json \
plugin/.claude-plugin/marketplace.json \
.claude-plugin/marketplace.json \
plugins/tooluniverse/.codex-plugin/plugin.json; do
[ -f "$f" ] && git add "$f"
done
git commit -m "Release v$NEW"
git tag -a "v$NEW" -m "v$NEW"
echo
echo "Created commit + tag v$NEW locally."
if [ "$NO_PUSH" -eq 1 ]; then
echo
echo "(--no-push) Stopping here. To finish the release manually:"
echo " git push origin $BRANCH && git push origin v$NEW"
exit 0
fi
echo
echo "Pushing to origin..."
git push origin "$BRANCH"
git push origin "v$NEW"
echo
echo "✓ Released v$NEW"
echo " - Auto-update users will receive it on next Claude Code startup"
echo " - GitHub Release zip will be built and attached by the workflow:"
echo " https://github.com/mims-harvard/ToolUniverse/actions/workflows/release-plugin.yml"