-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrelease.sh
More file actions
executable file
·444 lines (400 loc) · 19.5 KB
/
Copy pathrelease.sh
File metadata and controls
executable file
·444 lines (400 loc) · 19.5 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
#!/usr/bin/env bash
#
# release.sh — Automate plugin release workflow
#
# Usage:
# ./release.sh # interactive bump (major/minor/patch)
# ./release.sh --version X # release exact version (e.g., first release)
#
# 1. Prompt for semantic version bump (or use --version)
# 2. Update version in .claude-plugin/plugin.json
# 3. Update README version badge
# 4. Update SKILL.md version and updated date
# 5. Update CHANGELOG date (TBD → today)
# 6. Commit, tag, and push
# 7. Print instructions for the marketplace PR
#
set -euo pipefail
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
RESET='\033[0m'
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PLUGIN_JSON="$SCRIPT_DIR/.claude-plugin/plugin.json"
MARKETPLACE_URL="https://github.com/CrowdStrike/fusion-skills.git"
EXPLICIT_VERSION=""
BUNDLE_ONLY=0
BUNDLE_REF=""
# Everything the OpenAI skills-only bundle must contain. Single source of truth:
# a skill that starts referencing a new top-level path needs adding here, and
# build_bundle fails loudly if a referenced path is missing from the archive.
#
# common/ and scripts/ are here because the skills run every Python script through
# scripts/python.sh (the managed-venv wrapper) and import common/scripts/auth.py —
# references that live in invocation snippets, not markdown links, so the link
# check below cannot see them. Omit either and the bundled scripts fail on Codex.
#
# assets/ holds the interface icons (composerIcon/logo) referenced by
# .codex-plugin/plugin.json; the portal rejects the upload if they are absent.
#
# Deliberately excluded: hooks/ (Codex discovers hooks/hooks.json and would run
# Claude-contract hooks unvalidated), .claude-plugin/ and the root plugin.json
# (one plugin root per archive, and only the Codex manifest is needed here).
BUNDLE_PATHS=(.codex-plugin assets skills use-cases common scripts requirements.txt LICENSE README.md)
# Argument parsing
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
EXPLICIT_VERSION="$2"
shift 2
;;
--bundle)
BUNDLE_REF="${2:-}"
shift
[[ -n "${BUNDLE_REF:-}" && "$BUNDLE_REF" != --* ]] && shift || BUNDLE_REF=""
BUNDLE_ONLY=1
;;
*)
printf "${RED}Usage: $0 [--version X.Y.Z] [--bundle [ref]]${RESET}\n" >&2
exit 1
;;
esac
done
# Read current version from plugin.json
get_current_version() {
jq -r '.version' "$PLUGIN_JSON" 2>/dev/null || echo "0.0.0"
}
# Parse semantic version into "major minor patch"
parse_version() {
local version=$1
if [[ $version =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
else
printf "${RED}ERROR: Invalid version format: %s${RESET}\n" "$version" >&2
exit 1
fi
}
# Calculate next version based on bump type
calculate_next_version() {
local major=$1 minor=$2 patch=$3 bump_type=$4
case "$bump_type" in
major) echo "$((major + 1)).0.0" ;;
minor) echo "$major.$((minor + 1)).0" ;;
patch) echo "$major.$minor.$((patch + 1))" ;;
esac
}
# Build the OpenAI skills-only submission bundle and verify it before upload.
# The portal validates structure after upload; this catches the same problems
# locally, where fixing them is cheap.
build_bundle() {
local ref="${1:-}"
if [[ -z "$ref" ]]; then
ref=$(git -C "$SCRIPT_DIR" describe --tags --abbrev=0 2>/dev/null || echo "")
[[ -z "$ref" ]] && { printf "${RED}✗${RESET} No tags found. Pass a ref: ./release.sh --bundle v1.1.0\n" >&2; return 1; }
printf "${YELLOW}⚠${RESET} No ref given, using latest tag: %s\n" "$ref"
fi
git -C "$SCRIPT_DIR" rev-parse --verify --quiet "$ref" >/dev/null \
|| { printf "${RED}✗${RESET} Ref not found: %s\n" "$ref" >&2; return 1; }
local version="${ref#v}"
local zip="$SCRIPT_DIR/crowdstrike-falcon-fusion-${version}.zip"
printf "\n${BLUE}Building skills-only bundle from %s${RESET}\n" "$ref"
# Build to a temp file and only move it into place after every check passes.
# git archive truncates its --output before it runs, so writing straight to
# "$zip" would leave a 0-byte file if the archive (or a later check) fails, and
# a 0-byte zip looks fine locally until the portal or Slack rejects it.
local tmpzip="${zip}.partial"
git -C "$SCRIPT_DIR" archive --format=zip --output="$tmpzip" "$ref" "${BUNDLE_PATHS[@]}" \
|| { printf "${RED}✗${RESET} git archive failed\n" >&2; rm -f "$tmpzip"; return 1; }
local work; work=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$work' '$tmpzip'" RETURN
unzip -q "$tmpzip" -d "$work" || { printf "${RED}✗${RESET} Archive is not readable\n" >&2; return 1; }
local fail=0
# Exactly one plugin root, at the archive root.
if [[ -f "$work/.codex-plugin/plugin.json" ]]; then
printf "${GREEN}✓${RESET} Codex manifest present at the archive root\n"
else
printf "${RED}✗${RESET} Missing .codex-plugin/plugin.json\n"; fail=1
fi
local roots
roots=$(find "$work" -maxdepth 2 -type d \( -name .codex-plugin -o -name .claude-plugin -o -name .agent-plugin \) | wc -l | tr -d ' ')
if [[ "$roots" == "1" ]]; then
printf "${GREEN}✓${RESET} Exactly one plugin root\n"
else
printf "${RED}✗${RESET} Found %s plugin manifests; the portal requires exactly one\n" "$roots"; fail=1
fi
# Interface icons. The portal requires interface.composerIcon and interface.logo,
# each pointing at a square image that ships inside the archive. A bundle missing
# them imports through the CLI fine but is rejected at upload ("Missing plugin
# composer icon" / "Missing plugin logo"), so catch it here where fixing is cheap.
local manifest="$work/.codex-plugin/plugin.json"
local icon_field icon_path resolved
for icon_field in composerIcon logo; do
icon_path=$(jq -r ".interface.${icon_field} // empty" "$manifest" 2>/dev/null)
if [[ -z "$icon_path" ]]; then
printf "${RED}✗${RESET} interface.%s missing from .codex-plugin/plugin.json; the portal rejects the upload\n" "$icon_field"; fail=1
continue
fi
# Paths are relative to the plugin root (the archive root), e.g. ./assets/logo.png.
resolved="$work/${icon_path#./}"
if [[ ! -f "$resolved" ]]; then
printf "${RED}✗${RESET} interface.%s -> %s is not in the archive; add its directory to BUNDLE_PATHS\n" "$icon_field" "$icon_path"; fail=1
continue
fi
# The portal requires a square image. For PNGs, read width/height from the IHDR
# chunk with the standard library so this works on CI without Pillow.
if [[ "$icon_path" == *.png ]] && ! python3 - "$resolved" <<'PY'
import struct, sys
with open(sys.argv[1], "rb") as fh:
head = fh.read(24)
# PNG signature (8 bytes) + IHDR: width/height are big-endian uint32 at bytes 16-24.
if head[:8] != b"\x89PNG\r\n\x1a\n" or head[12:16] != b"IHDR":
sys.exit(2)
width, height = struct.unpack(">II", head[16:24])
sys.exit(0 if width == height else 1)
PY
then
printf "${RED}✗${RESET} interface.%s -> %s is not a square PNG; the portal requires a square image\n" "$icon_field" "$icon_path"; fail=1
continue
fi
printf "${GREEN}✓${RESET} interface.%s resolves to %s inside the archive\n" "$icon_field" "$icon_path"
done
# At least one skill.
local skills
skills=$(find "$work/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')
if [[ "$skills" -ge 1 ]]; then
printf "${GREEN}✓${RESET} %s skills with a SKILL.md\n" "$skills"
else
printf "${RED}✗${RESET} No skills/<name>/SKILL.md found\n"; fail=1
fi
# Disqualifiers for a skills-only submission.
local bad=0
[[ -e "$work/.mcp.json" || -e "$work/.app.json" ]] && bad=1
grep -rqs "mcpServers" "$work/.codex-plugin/plugin.json" && bad=1
grep -rqs "screenshots" "$work/.codex-plugin/plugin.json" && bad=1
[[ -d "$work/hooks" ]] && { printf "${RED}✗${RESET} hooks/ is in the archive; Codex would run Claude-contract hooks unvalidated\n"; fail=1; }
if [[ "$bad" == "0" ]]; then
printf "${GREEN}✓${RESET} No mcpServers, .mcp.json, .app.json or interface.screenshots\n"
else
printf "${RED}✗${RESET} Archive contains something a skills-only plugin may not ship\n"; fail=1
fi
# Every relative path the skills reference must resolve inside the archive.
# This is what catches a skill that starts using a new top-level directory.
# `|| true` keeps the substitution from tripping `set -e`: under pipefail the
# inner grep exits non-zero for any file with no links, which would otherwise
# abort the whole script. The captured stdout (the MISSING lines) is unaffected.
local missing
missing=$(cd "$work" && find skills -name '*.md' | while read -r f; do
d=$(dirname "$f")
grep -ohE '\]\((\.\./)*[A-Za-z0-9_./-]+\.(md|py|sh|json|ya?ml)(#[^)]*)?\)' "$f" 2>/dev/null \
| sed 's/^](//; s/)$//; s/#.*$//' | while read -r link; do
target=$(python3 -c 'import os,sys; print(os.path.normpath(os.path.join(sys.argv[1], sys.argv[2])))' "$d" "$link")
[[ -e "$target" ]] || printf '%s -> %s\n' "$f" "$link"
done
done) || true
if [[ -z "$missing" ]]; then
printf "${GREEN}✓${RESET} Every path referenced by a skill resolves inside the archive\n"
else
printf "${RED}✗${RESET} Referenced paths missing from the archive:\n%s\n" "$missing"
printf " Add the containing directory to BUNDLE_PATHS in release.sh.\n"; fail=1
fi
# Runtime dependencies that no markdown link points at, so the link check above
# cannot see them. requirements.txt is named inside a shell snippet; use-cases is
# globbed at runtime by the workflows orchestrator; common/ and scripts/ are
# reached through the ../../ invocation snippets in the skills.
local dep
for dep in requirements.txt use-cases common scripts; do
if [[ -e "$work/$dep" ]]; then
printf "${GREEN}✓${RESET} %s present (referenced at runtime, not by a link)\n" "$dep"
else
printf "${RED}✗${RESET} %s missing; skills reference it outside a markdown link\n" "$dep"; fail=1
fi
done
local count size
count=$(find "$work" -type f | wc -l | tr -d ' ')
size=$(du -h "$tmpzip" | cut -f1 | tr -d ' ')
printf "\n %s files, %s\n" "$count" "$size"
if [[ "$fail" != "0" ]]; then
printf "\n${RED}Bundle failed validation. Do not upload it.${RESET}\n" >&2
return 1
fi
# Every check passed — publish the archive under its final name. On any failure
# above we return early and the RETURN trap deletes the partial file, so a
# broken build never leaves a zip (0-byte or otherwise) to upload by mistake.
mv -f "$tmpzip" "$zip"
printf "\n %s\n" "$zip"
printf "\n${GREEN}Bundle is ready to upload at https://platform.openai.com/plugins${RESET}\n"
}
main() {
if [[ "$BUNDLE_ONLY" == "1" ]]; then
build_bundle "$BUNDLE_REF"
return $?
fi
printf "${BLUE}=== Plugin Release Workflow ===${RESET}\n\n"
# Preflight checks
# Find the remote that points to the upstream AI repo (where marketplace looks for tags)
local remote=""
while IFS= read -r line; do
name=$(echo "$line" | awk '{print $1}')
url=$(echo "$line" | awk '{print $2}')
if [[ "$url" == *"CrowdStrike/fusion-skills"* ]]; then
remote="$name"
break
fi
done < <(git remote -v | grep push)
if [ -z "$remote" ]; then
printf "${RED}ERROR: No remote found pointing to CrowdStrike/fusion-skills${RESET}\n" >&2
printf "The marketplace references that repo. Add it with:\n" >&2
printf " git remote add upstream %s\n" "$MARKETPLACE_URL" >&2
exit 1
fi
local branch
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" != "main" ]; then
printf "${RED}ERROR: Must be on main branch (currently on %s)${RESET}\n" "$branch" >&2
exit 1
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
printf "${RED}ERROR: Working tree is dirty. Commit or stash changes first.${RESET}\n" >&2
exit 1
fi
CURRENT_VERSION=$(get_current_version)
printf "Current version: ${GREEN}${CURRENT_VERSION}${RESET}\n\n"
if [ -n "$EXPLICIT_VERSION" ]; then
# Validate the explicit version format
if ! [[ "$EXPLICIT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
printf "${RED}ERROR: Invalid version format: %s (expected X.Y.Z)${RESET}\n" "$EXPLICIT_VERSION" >&2
exit 1
fi
NEXT_VERSION="$EXPLICIT_VERSION"
printf "Explicit version: ${YELLOW}${NEXT_VERSION}${RESET}\n"
else
# Prompt for bump type
printf "Release type:\n"
printf " 1) Major (breaking changes)\n"
printf " 2) Minor (new features, backwards compatible)\n"
printf " 3) Patch (bugfixes)\n\n"
read -rp "Select [1-3]: " bump_choice
case "$bump_choice" in
1) BUMP_TYPE="major" ;;
2) BUMP_TYPE="minor" ;;
3) BUMP_TYPE="patch" ;;
*)
printf "${RED}Invalid choice${RESET}\n" >&2
exit 1
;;
esac
read -r major minor patch <<< "$(parse_version "$CURRENT_VERSION")"
NEXT_VERSION=$(calculate_next_version "$major" "$minor" "$patch" "$BUMP_TYPE")
printf "\nVersion: ${CURRENT_VERSION} → ${YELLOW}${NEXT_VERSION}${RESET}\n"
fi
# Check tag doesn't already exist
if git rev-parse "v${NEXT_VERSION}" >/dev/null 2>&1; then
printf "${RED}ERROR: Tag v${NEXT_VERSION} already exists${RESET}\n" >&2
exit 1
fi
read -rp "Proceed? (Y/n): " confirm
if [[ "$confirm" =~ ^[Nn]$ ]]; then
printf "Cancelled.\n"
exit 0
fi
printf "\n${BLUE}Step 1: Update plugin.json${RESET}\n"
jq --arg v "$NEXT_VERSION" '.version = $v' "$PLUGIN_JSON" > /tmp/plugin.json.tmp
mv /tmp/plugin.json.tmp "$PLUGIN_JSON"
printf "${GREEN}✓${RESET} Updated to v${NEXT_VERSION}\n"
printf "\n${BLUE}Step 1b: Update marketplace.json${RESET}\n"
MARKETPLACE_JSON="$SCRIPT_DIR/.claude-plugin/marketplace.json"
jq --arg v "$NEXT_VERSION" '.plugins[0].version = $v' "$MARKETPLACE_JSON" > /tmp/marketplace.json.tmp
mv /tmp/marketplace.json.tmp "$MARKETPLACE_JSON"
printf "${GREEN}✓${RESET} Updated to v${NEXT_VERSION}\n"
printf "\n${BLUE}Step 1c: Update Agent Plugins manifests${RESET}\n"
# The root plugin.json (Agent Plugins spec) and .codex-plugin/plugin.json are
# separate manifests for the non-Claude assistants; CI enforces that all four
# manifests share one version. Bump them alongside .claude-plugin/.
ROOT_PLUGIN_JSON="$SCRIPT_DIR/plugin.json"
CODEX_PLUGIN_JSON="$SCRIPT_DIR/.codex-plugin/plugin.json"
for manifest in "$ROOT_PLUGIN_JSON" "$CODEX_PLUGIN_JSON"; do
if [[ -f "$manifest" ]]; then
jq --arg v "$NEXT_VERSION" '.version = $v' "$manifest" > /tmp/agent-plugin.json.tmp
mv /tmp/agent-plugin.json.tmp "$manifest"
printf "${GREEN}✓${RESET} %s → v${NEXT_VERSION}\n" "${manifest#"$SCRIPT_DIR"/}"
fi
done
# The Agent Plugins marketplace catalog pins the plugin to a release tag, so
# its source.ref must move to the new tag. CI checks that this ref matches the
# version, so a stale ref fails the build rather than drifting silently.
AGENTS_MARKETPLACE_JSON="$SCRIPT_DIR/.agents/plugins/marketplace.json"
if [[ -f "$AGENTS_MARKETPLACE_JSON" ]]; then
jq --arg ref "v$NEXT_VERSION" \
'(.plugins[] | select(.name == "crowdstrike-falcon-fusion").source.ref) = $ref' \
"$AGENTS_MARKETPLACE_JSON" > /tmp/agents-marketplace.json.tmp
mv /tmp/agents-marketplace.json.tmp "$AGENTS_MARKETPLACE_JSON"
printf "${GREEN}✓${RESET} .agents/plugins/marketplace.json ref → v${NEXT_VERSION}\n"
fi
printf "\n${BLUE}Step 2: Update README badge${RESET}\n"
# Use portable sed with temp file pattern (macOS and Linux compatible)
sed 's/badge\/version-[0-9.]*-blue/badge\/version-'"$NEXT_VERSION"'-blue/' "$SCRIPT_DIR/README.md" > /tmp/README.md.tmp
mv /tmp/README.md.tmp "$SCRIPT_DIR/README.md"
# Update the badge release link target
sed 's|releases/tag/v[0-9.]*|releases/tag/v'"$NEXT_VERSION"'|' "$SCRIPT_DIR/README.md" > /tmp/README.md.tmp
mv /tmp/README.md.tmp "$SCRIPT_DIR/README.md"
printf "${GREEN}✓${RESET} README badge → v${NEXT_VERSION}\n"
printf "\n${BLUE}Step 3: Update SKILL.md versions${RESET}\n"
local today
today=$(date +%Y-%m-%d)
find "$SCRIPT_DIR/skills" -name SKILL.md | while read skill; do
sed "s/^version: .*/version: $NEXT_VERSION/; s/^updated: .*/updated: $today/" "$skill" > /tmp/SKILL.md.tmp
mv /tmp/SKILL.md.tmp "$skill"
done
printf "${GREEN}✓${RESET} All SKILL.md → v${NEXT_VERSION} (${today})\n"
printf "\n${BLUE}Step 4: Update CHANGELOG date${RESET}\n"
if grep -q "## \[${NEXT_VERSION}\] - TBD" "$SCRIPT_DIR/CHANGELOG.md"; then
sed "s/## \[${NEXT_VERSION}\] - TBD/## [${NEXT_VERSION}] - ${today}/" "$SCRIPT_DIR/CHANGELOG.md" > /tmp/CHANGELOG.md.tmp
mv /tmp/CHANGELOG.md.tmp "$SCRIPT_DIR/CHANGELOG.md"
printf "${GREEN}✓${RESET} CHANGELOG [${NEXT_VERSION}] date → ${today}\n"
else
printf "${YELLOW}⚠${RESET} No [${NEXT_VERSION}] - TBD entry found in CHANGELOG.md (already dated?)\n"
fi
printf "\n${BLUE}Step 5: Commit and create PR${RESET}\n"
local release_branch="release/v${NEXT_VERSION}"
git checkout -b "$release_branch"
git add "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$ROOT_PLUGIN_JSON" "$CODEX_PLUGIN_JSON" "$AGENTS_MARKETPLACE_JSON" "$SCRIPT_DIR/README.md" "$SCRIPT_DIR/skills"/*/SKILL.md "$SCRIPT_DIR/CHANGELOG.md" "$SCRIPT_DIR/release.sh"
git commit -m "Release v${NEXT_VERSION}"
printf "${GREEN}✓${RESET} Committed v${NEXT_VERSION}\n"
printf "\n${BLUE}Step 6: Push branch and create PR${RESET}\n"
git push -u "$remote" "$release_branch"
# Target the upstream repo explicitly. Without --repo, `gh pr create` aborts
# ("No default remote repository has been set") whenever more than one remote
# exists (e.g. a personal fork alongside origin). Derive the slug from the same
# remote we just pushed to, parsing both SSH and HTTPS URL forms.
local repo_slug
repo_slug=$(git remote get-url "$remote" | sed -E 's#^(git@github.com:|https://github.com/)##; s#\.git$##')
gh pr create --repo "$repo_slug" --base main --head "$release_branch" \
--title "Release v${NEXT_VERSION}" --body "Version bump to v${NEXT_VERSION}."
printf "${GREEN}✓${RESET} PR created\n"
git checkout main
printf "\n${BLUE}Step 7: After PR merges${RESET}\n"
printf "\nOnce the PR is approved and merged, create a draft GitHub release:\n"
printf " gh release create v${NEXT_VERSION} --target main --title \"v${NEXT_VERSION}\" --generate-notes --draft\n\n"
printf "This creates a draft release with auto-generated notes from merged PRs.\n"
printf "Review and edit the notes at https://github.com/CrowdStrike/fusion-skills/releases,\n"
printf "then click Publish when ready.\n\n"
printf "${BLUE}Step 8: Update Anthropic Plugin Marketplace${RESET}\n"
printf "\nAfter publishing the GitHub release, notify Anthropic of the new tag and SHA:\n"
printf " Tag: v${NEXT_VERSION}\n"
printf " SHA: \$(git rev-parse v${NEXT_VERSION})\n\n"
printf "Anthropic handles the marketplace pin bump internally. Do not open PRs to\n"
printf "anthropics/claude-plugins-official or re-submit through the plugin submission form.\n\n"
printf "${BLUE}Step 9: Update OpenAI Plugins Directory${RESET}\n"
printf "\nAfter publishing the GitHub release, build and verify the skills-only bundle:\n"
printf " ./release.sh --bundle v${NEXT_VERSION}\n\n"
printf "That archives only what the skills need, checks it against the portal's\n"
printf "structural rules, and refuses to produce an uploadable file if a skill\n"
printf "references a path the archive is missing. Then upload it at:\n"
printf " https://platform.openai.com/plugins\n\n"
printf "Submit the new version for review, then publish it after approval. The public\n"
printf "Plugins Directory uses reviewed snapshots rather than the repository marketplace ref.\n\n"
printf "${GREEN}Done.${RESET}\n"
}
main "$@"