Repository navigation
Expand file tree
/
Copy pathbump-plugin-version.sh
More file actions
executable file
·173 lines (164 loc) · 8.42 KB
/
Copy pathbump-plugin-version.sh
File metadata and controls
executable file
·173 lines (164 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env bash
# Bump a plugin's version across all four manifests that must agree.
#
# A plugin's version lives in four places — the portable manifest, the strict Claude manifest,
# and one entry in each of the two marketplace manifests — and validate-manifests.sh fails if
# they disagree. Doing that by hand is four edits and an easy way to ship a half-bumped plugin,
# so this is the one supported way to move a version.
#
# Consumers cache plugins by version, so a content change that leaves the version untouched
# never reaches them; check-plugin-version-bump.sh is the gate that catches it, and this is the
# fix that gate points at.
#
# Usage:
# ./scripts/bump-plugin-version.sh <plugin> [patch|minor|major] # bump one plugin
# ./scripts/bump-plugin-version.sh --changed-since <ref> [level] # bump every plugin whose
# # content changed since <ref>
#
# The --changed-since mode is what the automated skill sync uses: it bumps exactly the plugins
# whose shipped content moved, so the daily update PR satisfies the gate without a human edit.
set -euo pipefail
CLAUDE_MANIFEST=".claude-plugin/marketplace.json"
COPILOT_MANIFEST=".github/plugin/marketplace.json"
# shellcheck source=scripts/plugin-version.lib.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/plugin-version.lib.sh"
plugin_version_git_context
# shellcheck source=scripts/json-object.lib.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/json-object.lib.sh"
# shellcheck source=scripts/atomic-write.lib.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/atomic-write.lib.sh"
# All generated bytes stay outside the repository until the whole plan is valid.
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
: > "$work/changes"
for directory in .claude-plugin .github .github/plugin; do
if [ ! -d "$directory" ] || [ -L "$directory" ]; then
echo '::error::Manifest parent must be a real checkout directory.' >&2; exit 1
fi
done
for manifest in "$CLAUDE_MANIFEST" "$COPILOT_MANIFEST"; do
[ -f "$manifest" ] && [ ! -L "$manifest" ] || exit 1
snapshot="$work/$(basename "$(dirname "$manifest")").original"
cp "$manifest" "$snapshot"
json_object_unique "$snapshot" || { echo '::error::Marketplace declarations must be unambiguous; no manifests were changed.' >&2; exit 1; }
jq -es 'length==1 and (.[0] | type == "object" and (.plugins | type == "array"))' "$snapshot" >/dev/null || exit 1
cp "$snapshot" "$work/$(basename "$(dirname "$manifest")").json"
done
# Validate one plugin's four-manifest parity and stage its increment without changing the checkout.
plan_one() {
local name="$1" level="$2" dir="plugins/$1" current='' new manifest staged entry observed original
[[ "$name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || { echo '::error::Invalid plugin identity.' >&2; return 1; }
plugin_version_local_parents "$name" || return 1
for manifest in "$dir/plugin.json" "$dir/.claude-plugin/plugin.json"; do
[ -f "$manifest" ] && [ ! -L "$manifest" ] || return 1
original="$work/original-$name-$(basename "$(dirname "$manifest")")"
cp "$manifest" "$original" || return 1
json_object_unique "$original" || { echo '::error::Plugin declarations must be unambiguous; no manifests were changed.' >&2; return 1; }
observed=$(plugin_version_read "$original" "$name") || return 1
[ -n "$current" ] || current=$observed
[ "$observed" = "$current" ] || {
echo "::error::$manifest: plugin identity or version parity is invalid; no manifests were changed." >&2
return 1
}
done
for manifest in "$CLAUDE_MANIFEST" "$COPILOT_MANIFEST"; do
staged="$work/$(basename "$(dirname "$manifest")").json"
jq -e --arg n "$name" --arg v "$current" '
[.plugins[] | select(.name == $n)] | length == 1 and
(.[0] | .version == $v and .source == ("./plugins/" + $n))' "$staged" >/dev/null || {
echo "::error::$manifest: expected one matching plugin at the same version; no manifests were changed." >&2
return 1
}
done
# Changed-since must validate even a plugin already bumped on the branch.
if [ "$level" = keep ]; then return 0; fi
new=$(plugin_version_next "$current" "$level") || return 1
for manifest in "$dir/plugin.json" "$dir/.claude-plugin/plugin.json"; do
entry="$work/plugin-$(wc -c < "$work/changes" | tr -d ' ')"
original="$work/original-$name-$(basename "$(dirname "$manifest")")"
jq --arg v "$new" '.version = $v' "$original" > "$entry" || return 1
printf '%s\0%s\0%s\0' "$manifest" "$entry" "$original" >> "$work/changes"
done
for manifest in "$CLAUDE_MANIFEST" "$COPILOT_MANIFEST"; do
staged="$work/$(basename "$(dirname "$manifest")").json"
jq --arg n "$name" --arg v "$new" '.plugins |= map(if .name == $n then .version = $v else . end)' "$staged" > "$work/next" || return 1
mv "$work/next" "$staged"
done
echo "✓ $name $current → $new"
}
# Select a single plugin or finish every changed-plugin observation before writing versions.
main() {
local level plugins
if [ "${1:-}" = "--changed-since" ]; then
local base="${2:?--changed-since needs a base ref}"
level="${3:-patch}"
local base_sha head_sha
plugin_version_history || return 1
head_sha=$(git rev-parse --verify 'HEAD^{commit}') || return 1
if ! base_sha=$(git merge-base --all "$base" "$head_sha" 2>/dev/null) ||
! [[ "$base_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Cannot resolve a merge base for '$base'...HEAD." >&2
exit 1
fi
# Complete all observations before bump_one writes any manifest. A failed
# producer may have emitted a plausible prefix; that prefix is not a census.
local changed plugin_dir=''
if ! git ls-tree -d --name-only -z "$head_sha" plugins/ > "$work/inventory"; then
echo "::error::Cannot enumerate plugins at HEAD; no versions were changed." >&2
return 1
fi
plugins=""
while IFS= read -r -d '' plugin_dir; do
[[ "$plugin_dir" =~ ^plugins/[a-z0-9]+(-[a-z0-9]+)*$ ]] || {
echo '::error::Unsupported plugin directory identity.' >&2; return 1;
}
if ! changed=$(git diff --no-ext-diff --no-textconv --no-relative --name-only "$base_sha" "$head_sha" -- "$plugin_dir/"); then
echo "::error::Cannot inspect changed content for '$plugin_dir'; no versions were changed." >&2
return 1
fi
[ -n "$changed" ] || continue
# The goal is "this plugin's version moved", not "add one every run". If a previous
# pass (or a human) already bumped it relative to the base, re-running must be a
# no-op — otherwise a re-run of the sync workflow inflates the version each time.
local manifest_rel=".claude-plugin/plugin.json"
local base_v head_v
local base_entry
base_entry=$(git ls-tree "$base_sha" -- "$plugin_dir/$manifest_rel") || return 1
if [ -z "$base_entry" ]; then
plan_one "${plugin_dir#plugins/}" keep || return 1
echo "✓ ${plugin_dir#plugins/} is new — keeping its initial version"
continue
fi
base_v=$(plugin_version_at "$base_sha" "${plugin_dir#plugins/}") || return 1
plugin_version_local_parents "${plugin_dir#plugins/}" || return 1
head_v=$(plugin_version_read "$plugin_dir/$manifest_rel" "${plugin_dir#plugins/}") || return 1
if [ -n "$base_v" ] && [ "$base_v" != "$head_v" ]; then
plugin_version_increases "$base_v" "$head_v" || { echo '::error::Cache versions must increase.' >&2; return 1; }
plan_one "${plugin_dir#plugins/}" keep || return 1
echo "✓ ${plugin_dir#plugins/} already moved $base_v → $head_v — leaving it"
continue
fi
plugins="$plugins ${plugin_dir#plugins/}"
done < "$work/inventory"
[ -z "$plugin_dir" ] || { echo '::error::Unterminated plugin inventory.' >&2; return 1; }
if [ -z "${plugins// /}" ]; then
echo "✓ No plugin content changed since $base — nothing to bump"
return 0
fi
else
plugins="${1:?usage: bump-plugin-version.sh <plugin> [patch|minor|major]}"
level="${2:-patch}"
fi
local p
for p in $plugins; do
plan_one "$p" "$level"
done
# Only complete plans are installed; a bad later input cannot half-bump a plugin.
local destination staged
for destination in "$CLAUDE_MANIFEST" "$COPILOT_MANIFEST"; do
staged="$work/$(basename "$(dirname "$destination")").json"
printf '%s\0%s\0%s\0' "$destination" "$staged" "$work/$(basename "$(dirname "$destination")").original" >> "$work/changes"
done
atomic_write_batch "$work/changes"
}
main "$@"