mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
0bdf539455
* feat(release): the Claude Code plugin follows stable releases; the release workflow pins the marketplace (TASK-3487) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(release): the plugin pin never moves backwards; force the tag fetch (TASK-3487 codex r1) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
140 lines
5.4 KiB
Bash
Executable File
140 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# pin-plugin-marketplace.sh <tag> — point the Claude Code plugin marketplace
|
|
# at a release tag (TASK-3487).
|
|
#
|
|
# `.claude-plugin/marketplace.json` on main is what `/plugin marketplace add
|
|
# PerpetualSoftware/pad` reads. Its pad entry used to say `"source":
|
|
# "./plugin"`, so every user ran the plugin as of main, ahead of the CLI they
|
|
# had installed. After this runs, the entry is a `git-subdir` source pinned to
|
|
# the tag:
|
|
#
|
|
# {"source": "git-subdir", "url": "https://github.com/PerpetualSoftware/pad.git",
|
|
# "path": "plugin", "ref": "<tag>"}
|
|
#
|
|
# The url is the full https URL, never the owner/repo shorthand: Claude Code
|
|
# clones the shorthand over SSH (git@github.com:), so a user with no GitHub
|
|
# SSH key could not install the plugin even though the repo is public
|
|
# (measured with `claude plugin install` 2.1.294).
|
|
#
|
|
# and users get the plugin as of that release. With no `version` field, Claude
|
|
# Code versions a git-subdir plugin by its commit SHA, so moving `ref` is what
|
|
# delivers an update (BUG-3463's "no pinned version" still holds).
|
|
#
|
|
# STABLE TAGS ONLY. The Homebrew cask never receives a prerelease (goreleaser
|
|
# `skip_upload: auto`, BUG-2524), so most installed CLIs are stable releases.
|
|
# A plugin that tracked rc tags could tell the agent to use a command the
|
|
# user's CLI does not have yet. Pinned to stable, plugin vX pairs with CLI vX.
|
|
#
|
|
# It also refuses a tag whose plugin/.claude-plugin/plugin.json pins a
|
|
# `version`: that tag would freeze every install at the pinned string.
|
|
#
|
|
# It rewrites only the pad entry's `source`; every other byte of the file is
|
|
# kept. Re-running for the tag already pinned changes nothing and exits 0, and
|
|
# so does a tag OLDER than the one pinned: releases can finish out of order.
|
|
#
|
|
# Usage:
|
|
# scripts/pin-plugin-marketplace.sh v0.18.0 # rewrite the file
|
|
# scripts/pin-plugin-marketplace.sh --push v0.18.0 # also commit and push
|
|
#
|
|
# --push commits as github-actions[bot] and pushes to the current branch,
|
|
# retrying a rejected push after a fetch and rebase (the nix heal job's
|
|
# pattern). The release workflow runs it after the release is published; a
|
|
# push that fails every retry exits 3 so the caller can warn without failing
|
|
# the release.
|
|
set -euo pipefail
|
|
|
|
push=0
|
|
if [ "${1:-}" = "--push" ]; then
|
|
push=1
|
|
shift
|
|
fi
|
|
tag="${1:-}"
|
|
die() {
|
|
echo "pin-plugin-marketplace: $*" >&2
|
|
exit 2
|
|
}
|
|
|
|
[ -n "$tag" ] || die "usage: $0 [--push] <vX.Y.Z>"
|
|
if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
die "$tag is not a stable release tag (vX.Y.Z); the plugin follows stable releases only"
|
|
fi
|
|
|
|
root=$(git rev-parse --show-toplevel)
|
|
cd "$root"
|
|
market=".claude-plugin/marketplace.json"
|
|
[ -f "$market" ] || die "$market not found"
|
|
|
|
git rev-parse -q --verify "refs/tags/$tag^{commit}" >/dev/null ||
|
|
git fetch -q --force origin "+refs/tags/$tag:refs/tags/$tag" ||
|
|
die "tag $tag not found"
|
|
|
|
manifest=$(git show "$tag:plugin/.claude-plugin/plugin.json" 2>/dev/null) ||
|
|
die "$tag has no plugin/.claude-plugin/plugin.json"
|
|
if printf '%s' "$manifest" | python3 -c 'import json,sys; sys.exit(0 if "version" in json.load(sys.stdin) else 1)'; then
|
|
die "$tag's plugin.json pins a version; installs from it would never update (BUG-3463)"
|
|
fi
|
|
|
|
changed=$(python3 - "$market" "$tag" <<'PY'
|
|
import json, sys
|
|
path, tag = sys.argv[1], sys.argv[2]
|
|
with open(path, encoding="utf-8") as f:
|
|
text = f.read()
|
|
doc = json.loads(text)
|
|
want = {"source": "git-subdir", "url": "https://github.com/PerpetualSoftware/pad.git", "path": "plugin", "ref": tag}
|
|
hits = [p for p in doc.get("plugins", []) if p.get("name") == "pad"]
|
|
if len(hits) != 1:
|
|
sys.exit("pin-plugin-marketplace: expected exactly one plugin named pad, found %d" % len(hits))
|
|
cur = hits[0].get("source")
|
|
if cur == want:
|
|
print("no")
|
|
sys.exit(0)
|
|
# Never move backwards (codex r1): two stable releases can finish out of order,
|
|
# and the older one must not replace a newer pin.
|
|
def ver(t):
|
|
return tuple(int(x) for x in t[1:].split("."))
|
|
if isinstance(cur, dict) and cur.get("source") == "git-subdir":
|
|
ref = cur.get("ref", "")
|
|
import re
|
|
if re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", ref) and ver(ref) > ver(tag):
|
|
print("newer:" + ref)
|
|
sys.exit(0)
|
|
hits[0]["source"] = want
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(json.dumps(doc, indent=2, ensure_ascii=False) + "\n")
|
|
print("yes")
|
|
PY
|
|
)
|
|
|
|
if [ "$changed" = "no" ]; then
|
|
echo "pin-plugin-marketplace: already pinned to $tag"
|
|
exit 0
|
|
fi
|
|
if [ "${changed#newer:}" != "$changed" ]; then
|
|
echo "pin-plugin-marketplace: already pinned to ${changed#newer:}, newer than $tag; left as it is"
|
|
exit 0
|
|
fi
|
|
echo "pin-plugin-marketplace: pinned the pad plugin to $tag"
|
|
[ "$push" = 1 ] || exit 0
|
|
|
|
git config user.name 'github-actions[bot]'
|
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
|
git add "$market"
|
|
git commit -q -m "chore(plugin): the marketplace follows $tag (TASK-3487)"
|
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
for attempt in 1 2 3 4 5; do
|
|
if git push -q origin "HEAD:$branch"; then
|
|
echo "pin-plugin-marketplace: pushed to $branch"
|
|
exit 0
|
|
fi
|
|
echo "pin-plugin-marketplace: push rejected (attempt $attempt); rebasing onto origin/$branch" >&2
|
|
git fetch -q origin "$branch"
|
|
if ! git rebase -q "origin/$branch"; then
|
|
git rebase --abort || true
|
|
echo "pin-plugin-marketplace: rebase conflicted; giving up" >&2
|
|
exit 3
|
|
fi
|
|
sleep $((attempt * 5))
|
|
done
|
|
echo "pin-plugin-marketplace: push failed after 5 attempts" >&2
|
|
exit 3
|