Repository navigation
Expand file tree
/
Copy pathrelease
More file actions
executable file
·453 lines (413 loc) · 15.6 KB
/
Copy pathrelease
File metadata and controls
executable file
·453 lines (413 loc) · 15.6 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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
#!/usr/bin/env bash
# release — agent-friendly release CLI for this omarchy plugin.
#
# Resource: manifest.json (version), git (main/origin), gh (releases, issues)
# Marketplace repo: omacom/omarchy-plugin-marketplace (exact-commit verification)
set -euo pipefail
REPO_URL="https://github.com/Dahep/omarchy-plugin-opencode-usage"
MARKET_REPO="omacom/omarchy-plugin-marketplace"
MANIFEST="manifest.json"
SCRIPT_NAME="release"
TAG_PREFIX="v"
usage() {
cat <<EOF
Release CLI for the omarchy-plugin marketplace flow.
Usage:
$SCRIPT_NAME <command> [args] [--dry-run]
Commands:
status Show version, HEAD, origin/main, tag/verify state, drift check
bump <version> Set manifest.json version (feature branch, before merge)
tag [--dry-run] On main: tag v<version> at HEAD and push the tag
publish [--dry-run] Create GitHub release + open [Verify] issue with full HEAD SHA
revalidate <issue> [--dry-run]
Real-content edit on an existing issue to retrigger validation
wait <issue> Poll bot comments until they pin the current expected SHA
help [command] Show this help, or help for one command
Examples:
$SCRIPT_NAME status
$SCRIPT_NAME bump 1.1.0
$SCRIPT_NAME tag --dry-run
$SCRIPT_NAME tag
$SCRIPT_NAME publish --dry-run
$SCRIPT_NAME publish
$SCRIPT_NAME revalidate 6119
$SCRIPT_NAME wait 6123
Typical flow:
1. On the feature branch: $SCRIPT_NAME bump <version> (then commit + merge)
2. On main after merge: $SCRIPT_NAME tag && $SCRIPT_NAME publish
3. Freeze main until the marketplace maintainer approves the [Verify] issue.
EOF
}
cmd_help() {
case "${1:-}" in
"") usage ;;
status|bump|tag|publish|revalidate|wait) "help_$1" ;;
*) die "Unknown help topic: $1" "$SCRIPT_NAME help" ;;
esac
}
help_status() { cat <<'EOF'
status — show release state (read-only)
Prints manifest version, local HEAD, origin/main, whether the version tag
exists, open [Verify] issues, and whether origin/main has drifted from any
pending verification SHA.
Examples:
release status
EOF
}
help_bump() { cat <<'EOF'
bump <version> — set manifest.json version
Run on the feature branch BEFORE merging; the marketplace binds the manifest
version to the exact verified commit.
Examples:
release bump 1.1.0
EOF
}
help_tag() { cat <<'EOF'
tag [--dry-run] — tag the merge commit on main
Asserts you are on main and origin/main is fetched and current. Creates
v<manifest-version> at HEAD and pushes it. Idempotent: an existing tag at
HEAD is a no-op; a tag at a different commit is an error.
Examples:
release tag --dry-run
release tag
EOF
}
help_publish() { cat <<'EOF'
publish [--dry-run] — GitHub release + [Verify] issue
Creates a GitHub release for the version tag and opens a
"[Verify]: <name> <version>" issue on omacom/omarchy-plugin-marketplace with
the full 40-char HEAD SHA. Idempotent: skips steps already done. Refuses if
the tag is missing or HEAD is not the tagged commit.
Examples:
release publish --dry-run
release publish
EOF
}
help_revalidate() { cat <<'EOF'
revalidate <issue> [--dry-run] — retrigger marketplace validation
GitHub fires no `edited` event for identical content. This appends/refreshes
a revalidation note (real content delta) to the issue body so the
marketplace re-runs validation at the current HEAD. Use for unblocking a
stale baseline; for new releases use `publish` (a fresh [Verify] issue).
Examples:
release revalidate 6119 --dry-run
release revalidate 6119
EOF
}
help_wait() { cat <<'EOF'
wait <issue> — poll until bot reports pin the expected SHA
Waits (up to 15 min) for both the marketplace-validation and security-baseline
bot comments on the issue to cite the expected full SHA, then prints their
state. Checks bot comments, not the workflow-run list.
Examples:
release wait 6123
EOF
}
die() { echo "Error: $1" >&2; [ -n "${2:-}" ] && echo " $2" >&2; exit 1; }
info() { echo "$@"; }
require_repo() {
git rev-parse --show-toplevel >/dev/null 2>&1 || die "Not inside the plugin repo" "cd $(dirname "$(readlink -f "$0")")"
command -v gh >/dev/null || die "gh CLI not found" "install gh and run: gh auth login"
command -v jq >/dev/null || die "jq not found" "install jq"
}
get_version() {
jq -r .version "$MANIFEST" 2>/dev/null || die "Cannot read version from $MANIFEST" "run from the repo root: $SCRIPT_NAME status"
}
fetch_origin_main() {
git fetch origin main --quiet 2>/dev/null || \
die "git fetch origin main failed" "check network/remote access, then re-run"
}
assert_on_main() {
local branch
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
[ "$branch" = "main" ] || die "Not on main (on: ${branch:-detached HEAD})" \
"$SCRIPT_NAME tag/publish run after merging: git checkout main && git pull"
}
assert_main_synced() {
local local_main remote_main
fetch_origin_main
local_main=$(git rev-parse HEAD)
remote_main=$(git rev-parse origin/main)
[ "$local_main" = "$remote_main" ] || die "Local main ($local_main) != origin/main ($remote_main)" \
"git pull --ff-only, then re-run"
}
check_pending_verify_drift() {
# Fatal if any open [Verify] issue pins a SHA other than current origin/main:
# moving main while an update is pending makes its baseline stale.
local issues current
fetch_origin_main
current=$(git rev-parse origin/main)
issues=$(find_verify_issue)
[ -n "$issues" ] || return 0
local num sha
while IFS=$'\t' read -r num sha; do
if [ "$sha" != "$current" ]; then
die "open [Verify] issue #$num pins $sha but origin/main is $current — baseline would go stale" \
"wait for approval/close of #$num, or revalidate it ($SCRIPT_NAME revalidate $num) before moving main"
fi
done <<<"$issues"
}
verify_issue_fields() {
echo "### Verification action"
echo ""
echo "Verify and publish a newer upstream commit"
echo ""
echo "### Plugin ID"
echo ""
jq -r .id "$MANIFEST"
echo ""
echo "### Repository URL"
echo ""
echo "$REPO_URL"
echo ""
echo "### Target commit"
echo ""
echo "$1"
echo ""
echo "### Verification acknowledgment"
echo ""
echo "- [x] I understand that only the exact target commit can become a verified marketplace snapshot and that verification is not a security audit."
}
# Find open [Verify] issues for this repo. Echoes "issue|sha" per match.
find_verify_issue() {
gh issue list --repo "$MARKET_REPO" --state open \
--search "$REPO_URL in:body" --json number,title,body --limit 100 2>/dev/null \
| jq -r '.[]
| select(.title | startswith("[Verify]:"))
| "\(.number)\t\(.body)"' \
| while IFS=$'\t' read -r num body; do
local sha
sha=$(awk '/^### Target commit$/{f=1;next} f&&NF{print;exit}' <<<"$body")
[ -n "$sha" ] && echo "$num|$sha"
done
}
cmd_status() {
require_repo
local ver head_sha main_sha tag_sha verify rc=0
ver=$(get_version)
head_sha=$(git rev-parse HEAD)
fetch_origin_main
main_sha=$(git rev-parse origin/main)
info "version: $ver"
info "HEAD: $head_sha ($(git symbolic-ref --short HEAD 2>/dev/null || echo detached))"
info "origin/main: $main_sha"
tag_sha=$(git rev-parse -q --verify "refs/tags/$TAG_PREFIX$ver^{commit}" 2>/dev/null || echo "")
if [ -z "$tag_sha" ]; then
info "tag: $TAG_PREFIX$ver not created"
elif [ "$tag_sha" = "$head_sha" ]; then
info "tag: $TAG_PREFIX$ver at HEAD"
else
info "tag: $TAG_PREFIX$ver at $tag_sha (differs from HEAD)"
fi
verify=$(find_verify_issue)
if [ -n "$verify" ]; then
while IFS=$'\t' read -r num sha; do
info "verify: #$num pins $sha"
if [ "$sha" != "$main_sha" ]; then
info "DRIFT: pending verification #$num is stale (origin/main moved)"
rc=1
fi
done <<<"$verify"
else
info "verify: no open [Verify] issue"
fi
return "$rc"
}
cmd_bump() {
require_repo
local new="$1" dry=0; [ "${2:-}" = "--dry-run" ] && dry=1
[[ "$new" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
die "Invalid version: $new (expected semver like 1.2.3)" "$SCRIPT_NAME bump <version> e.g. $SCRIPT_NAME bump 1.1.0"
git diff --quiet HEAD -- "$MANIFEST" || die "$MANIFEST has uncommitted changes" "commit or stash first"
local old
old=$(get_version)
[ "$new" != "$old" ] || { info "already $old — nothing to do"; exit 0; }
if [ "$dry" = 1 ]; then
info "[dry-run] $MANIFEST version: $old -> $new (no changes written)"
exit 0
fi
local tmp
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
jq -c --arg v "$new" '.version = $v' "$MANIFEST" > "$tmp"
chmod 644 "$tmp"
mv "$tmp" "$MANIFEST"
trap - EXIT
info "bumped $MANIFEST: $old -> $new"
info "next: commit this on the feature branch, then merge to main"
}
cmd_tag() {
require_repo
local dry=0; [ "${1:-}" = "--dry-run" ] && dry=1
local ver tag head_sha
ver=$(get_version)
tag="$TAG_PREFIX$ver"
assert_on_main
assert_main_synced
check_pending_verify_drift
head_sha=$(git rev-parse HEAD)
local existing
existing=$(git rev-parse -q --verify "refs/tags/$tag^{commit}" 2>/dev/null || echo "")
if [ -n "$existing" ]; then
if [ "$existing" = "$head_sha" ]; then
if git ls-remote --tags origin "refs/tags/$tag" | grep -q "$tag"; then
info "tag $tag already at HEAD ($head_sha) and pushed — nothing to do"
else
info "tag $tag exists locally at HEAD but is not pushed"
if [ "$dry" = 1 ]; then
info "[dry-run] git push origin $tag"
else
git push origin "$tag"
info "pushed $tag"
fi
fi
else
die "tag $tag exists at $existing, but HEAD is $head_sha" \
"bump the version for a new tag; never move an existing one"
fi
exit 0
fi
if [ "$dry" = 1 ]; then
info "[dry-run] git tag -a $tag -m \"$ver\" $head_sha"
info "[dry-run] git push origin $tag"
exit 0
fi
git tag -a "$tag" -m "$ver" "$head_sha"
git push origin "$tag"
info "tagged $tag at $head_sha and pushed"
info "next: $SCRIPT_NAME publish"
}
cmd_publish() {
require_repo
local dry=0; [ "${1:-}" = "--dry-run" ] && dry=1
local ver tag name head_sha bodyf title
ver=$(get_version)
tag="$TAG_PREFIX$ver"
name=$(jq -r .name "$MANIFEST")
assert_on_main
assert_main_synced
check_pending_verify_drift
head_sha=$(git rev-parse HEAD)
local tagged
tagged=$(git rev-parse -q --verify "refs/tags/$tag^{commit}" 2>/dev/null || echo "")
[ -n "$tagged" ] || die "tag $tag does not exist" "$SCRIPT_NAME tag && $SCRIPT_NAME publish"
[ "$tagged" = "$head_sha" ] || die "tag $tag points at $tagged, HEAD is $head_sha" \
"the release must point at the verified commit"
# Idempotency: release
if gh release view "$tag" --repo "${REPO_URL#https://github.com/}" >/dev/null 2>&1; then
info "release $tag already exists — skipping release creation"
else
if [ "$dry" = 1 ]; then
info "[dry-run] gh release create $tag --generate-notes --verify-tag --repo ${REPO_URL#https://github.com/}"
else
gh release create "$tag" --generate-notes --verify-tag --repo "${REPO_URL#https://github.com/}" >/dev/null
info "created release $tag"
fi
fi
# Idempotency: verify issue for this exact SHA
local existing_issue="" num sha
while IFS=$'\t' read -r num sha; do
if [ "$sha" = "$head_sha" ]; then existing_issue="$num"; fi
done < <(find_verify_issue)
if [ -n "$existing_issue" ]; then
info "[Verify] issue #$existing_issue already pins $head_sha — skipping issue creation"
info "next: $SCRIPT_NAME wait $existing_issue"
exit 0
fi
local bodyf
bodyf=$(mktemp)
trap 'rm -f "$bodyf"' EXIT
verify_issue_fields "$head_sha" > "$bodyf"
title="[Verify]: $name $ver"
if [ "$dry" = 1 ]; then
info "[dry-run] gh issue create --repo $MARKET_REPO --title \"$title\" --body-file <verify-issue-body>"
echo "--- issue body ---"
cat "$bodyf"
echo "--- end body ---"
else
local url
url=$(gh issue create --repo "$MARKET_REPO" --title "$title" --body-file "$bodyf")
info "opened $url"
info "next: FREEZE main until approved; poll with $SCRIPT_NAME wait <issue-number>"
fi
}
cmd_revalidate() {
require_repo
local issue="$1" dry=0; [ "${2:-}" = "--dry-run" ] && dry=1
local repo_full="${REPO_URL#https://github.com/}"
local body marker state
marker="### Revalidation request"
state=$(gh issue view "$issue" --repo "$MARKET_REPO" --json state --jq .state) \
|| die "Cannot read issue #$issue in $MARKET_REPO" "check the issue number"
[ "$state" = "OPEN" ] || die "issue #$issue is $state — editing a closed issue triggers no validation" \
"open a fresh [Verify] issue instead: $SCRIPT_NAME publish"
body=$(gh issue view "$issue" --repo "$MARKET_REPO" --json body --jq .body)
local head_sha
fetch_origin_main
head_sha=$(git rev-parse origin/main)
# Idempotency: already requested for this exact SHA?
if grep -qF "$head_sha" <<<"$body" && grep -qF "$marker" <<<"$body"; then
info "issue #$issue already has a revalidation note for $head_sha — nothing to do"
exit 0
fi
# Replace any previous revalidation section (keeps the body bounded), else append.
local newbody
if grep -qF "$marker" <<<"$body"; then
newbody=$(awk -v m="$marker" 'index($0,m)==1{skip=1} !skip{print}' <<<"$body")
else
newbody="$body"
fi
newbody+="$marker
HEAD moved to $head_sha (repo: $repo_full). Requesting fresh validation/security-baseline results for this commit.
"
if [ "$dry" = 1 ]; then
info "[dry-run] gh api -X PATCH repos/$MARKET_REPO/issues/$issue -f body=<diff below>"
echo "--- body diff ---"
diff <(printf '%s' "$body") <(printf '%s' "$newbody") || true
echo "--- end diff ---"
else
gh api -X PATCH "repos/$MARKET_REPO/issues/$issue" -f body="$newbody" --jq .updated_at >/dev/null
info "issue #$issue edited with new content at $(date -u +%H:%M:%SZ)"
info "next: $SCRIPT_NAME wait $issue"
fi
}
cmd_wait() {
require_repo
local issue="$1"
fetch_origin_main
local expected
expected=$(git rev-parse origin/main)
local deadline=$(( $(date +%s) + 900 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
local bodies hits
bodies=$(gh issue view "$issue" --repo "$MARKET_REPO" --json comments,labels \
--jq '.comments[] | select(.author.login=="github-actions" or .author.login=="github-actions[bot]") | .body' 2>/dev/null)
# Bot comments cite abbreviated SHAs in prose; count distinct bot comments pinning our HEAD.
hits=$(grep -cE "${expected:0:7}(${expected:7:33})?" <<<"$bodies" || true)
local labels
labels=$(gh issue view "$issue" --repo "$MARKET_REPO" --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null)
if [ "${hits:-0}" -ge 2 ]; then
info "done: both bot reports pin $expected (labels: ${labels:-none})"
exit 0
fi
info "waiting: $hits/2 bot reports pin $expected (labels: ${labels:-none})"
sleep 45
done
die "timed out after 15 min waiting for bot reports at $expected" \
"check issue comments and marketplace workflow runs, then re-run: $SCRIPT_NAME wait $issue"
}
main() {
[ $# -ge 1 ] || { usage >&2; exit 1; }
local cmd="$1"; shift
case "$cmd" in
status) cmd_status "$@" ;;
bump) [ $# -ge 1 ] || die "No version specified." "$SCRIPT_NAME bump <version> e.g. $SCRIPT_NAME bump 1.1.0"; cmd_bump "$@" ;;
tag) cmd_tag "$@" ;;
publish) cmd_publish "$@" ;;
revalidate)[ $# -ge 1 ] || die "No issue specified." "$SCRIPT_NAME revalidate <issue-number> e.g. $SCRIPT_NAME revalidate 6119"; cmd_revalidate "$@" ;;
wait) [ $# -ge 1 ] || die "No issue specified." "$SCRIPT_NAME wait <issue-number> e.g. $SCRIPT_NAME wait 6119"; cmd_wait "$@" ;;
help|--help|-h) cmd_help "${1:-}" ;;
*) die "Unknown command: $cmd" "$SCRIPT_NAME help" ;;
esac
}
main "$@"