| name | deploy |
| description | Deploy skill for mcp-server-polarion. Automate version bump, tag create, PyPI publish. Triggers on `/deploy` or any release/version bump/tag request. |
Deploy Skill
Orchestrate production release. Be deliberate. Never skip hooks. Never push without explicit user confirm.
Step 1 — Pre-flight checks (abort on any failure)
git status --porcelain
git rev-parse --abbrev-ref HEAD
git fetch origin main --tags
git rev-list --count HEAD..origin/main
git rev-list --count origin/main..HEAD
git config core.hooksPath
- Dirty tree → list offending files, stop. Do NOT auto-stash.
- Branch != main → ask user switch manually. Do NOT switch.
- Behind origin → tell user run
git pull --ff-only.
- Ahead of origin → tell user push or reset; release commit must sit on synced main.
core.hooksPath ≠ .githooks → WARN, ask user run git config core.hooksPath .githooks. Step 4 only prints 50/120 lengths for eye-check; with hook missing nothing enforces them, so restore hook (repo-wide net for every commit, not just this one) before commit.
Step 2 — Determine new version (interactive)
grep -m1 '^version =' pyproject.toml
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
[ -n "$PREV_TAG" ] && git log "$PREV_TAG..HEAD" --oneline || git log --oneline
Analyze commits since $PREV_TAG:
breaking / !: marker → recommend major
- any
feat: → recommend minor
- only
fix: / chore: / docs: → recommend patch
Call AskUserQuestion with options patch / minor / major / custom, note recommendation in question text. For custom, prompt exact X.Y.Z.
Verify tag does not already exist (Step 1's --tags fetch catches remote tags too, not just local):
git rev-parse "v${NEW_VERSION}" 2>/dev/null && abort "tag exists" || true
If exists, abort, tell user pick different version. Remote tag means version already shipped (or mid-publish) — never reuse. Local-only tag likely stale leftover from aborted prior run; user can delete manually (git tag -d v${NEW_VERSION}) after confirming it never reached origin.
Step 3 — Bump version
Edit ^version = "..." line near top of pyproject.toml to version = "X.Y.Z" using Edit tool (replace only that line).
uv lock
git diff pyproject.toml uv.lock
Step 4 — Draft commit (interactive)
Draft two body bullets:
- Bullet 1 (motivation/themes, ≤120 chars) — derive from commits since
$PREV_TAG.
- Bullet 2 (mechanical, fixed) — "Bump project version from PREV to NEW in pyproject.toml and uv.lock."
Show both bullets to user via AskUserQuestion (approve / edit / cancel). On edit, accept replacement text, re-validate.
Validate lengths before invoking git:
awk '{print length}' <<<"chore(meta): bump version to ${NEW_VERSION}"
awk '{print length}' <<<"- ${BULLET1}"
awk '{print length}' <<<"- ${BULLET2}"
Commit (never --no-verify). For co-author trailer use model running this deploy session — substitute its name (e.g. Opus 4.8) for <model> below; never hardcode fixed version, goes stale as model changes:
git add pyproject.toml uv.lock
git commit -m "$(cat <<'EOF'
chore(meta): bump version to X.Y.Z
- <bullet 1>
- <bullet 2>
Co-Authored-By: Claude <model> <noreply@anthropic.com>
EOF
)"
On commit-msg hook rejection: surface hook output, redo git commit with corrected message. Never --amend — hook rejection means no commit made, so amend would mutate previous (unrelated) commit and silently corrupt history.
Step 5 — Push commit (confirm #1)
AskUserQuestion: "Push commit to origin/main? Makes the bump public but does NOT trigger the publish workflow yet." Options: push / cancel.
git push --no-verify origin main
No auto-retry on failure — surface error, let user decide.
Step 6 — Create annotated tag with curated highlights (interactive)
Tag message = single source of truth for this release's curated highlights — kept in tag itself, not a committed file, so repo never accumulates per-release notes. Line 1 = dated marker; rest = curated bullet list that build_release_notes.py reads back from tag object on push, wraps under a ## Highlights heading, prepends above GitHub's categorized PR list. (Full categorized list generated by workflow — never hand-write into tag.)
From commits since $PREV_TAG, draft curated summary of what matters to user — few plain-language bullets (or short prose), not restatement of PR titles. Show via AskUserQuestion (approve / edit / skip). English only.
On skip, create date-only tag (release ships category list alone):
RELEASE_DATE=$(date +%F)
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION} (${RELEASE_DATE})"
Otherwise compose message as curated bullet list only — no ## Highlights heading in tag (build_release_notes.py wraps bullets under that heading when it renders GitHub Release). Create tag with -F:
RELEASE_DATE=$(date +%F)
cat > "/tmp/tag-v${NEW_VERSION}.txt" <<EOF
Release v${NEW_VERSION} (${RELEASE_DATE})
- <plain-language summary of a major change>
- <another>
EOF
git tag -a "v${NEW_VERSION}" -F "/tmp/tag-v${NEW_VERSION}.txt"
Verify either way:
git show "v${NEW_VERSION}" --no-patch
Step 7 — Push tag (confirm #2, with irreversibility warning)
AskUserQuestion with this exact warning text: "Pushing tag v${NEW_VERSION} triggers .github/workflows/publish.yml → publish gate (evals) → TestPyPI → PyPI → MCP Registry + GitHub Release. IRREVERSIBLE (PyPI blocks re-uploading the same version). Continue?" Options: push / cancel.
git push origin "v${NEW_VERSION}"
On failure: local tag still exists; user can retry with git push origin v${NEW_VERSION} or delete locally via git tag -d v${NEW_VERSION}.
On cancel: version-bump commit already on main (pushed in Step 5) and tag exists locally, but no release ships — main sits one commit ahead of last release. Nothing to clean up; finish later by pushing tag (git push origin v${NEW_VERSION}), or let next deploy carry it forward.
GitHub Release created automatically by release job in publish.yml after PyPI succeeds: build_release_notes.py reads Step 6 highlights back from tag object, prepends to GitHub's categorized notes, grouped per .github/release.yml. Do NOT create release by hand here; manual gh release create collides with workflow (release already exists). Categorization quality depends on each merged PR carrying right label, which .github/workflows/label-pr.yml stamps from PR's conventional-commit title.
Step 8 — Post-push summary
OWNER_REPO=$(git remote get-url origin | sed -E 's#.*github\.com[:/]([^/]+/[^/.]+).*#\1#')
echo "Released v${NEW_VERSION}"
echo "Release notes (auto-published after PyPI): https://github.com/${OWNER_REPO}/releases/tag/v${NEW_VERSION}"
echo "Watch: https://github.com/${OWNER_REPO}/actions"
Hard rules
- NEVER
--no-verify at commit (protects commit-msg hook), nor --no-edit, -c commit.gpgsign=false. Push-time --no-verify allowed ONLY at Step 5 main push (sanctioned pre-push exception).
- NEVER
git push --force.
- NEVER auto-retry a failed push.
- NEVER
git commit --amend after a hook rejection.
- ALWAYS confirm before commit push AND tag push (2 separate confirms).
- ALWAYS show diff / draft to user before destructive ops.
- NEVER hand-write categorized PR list into tag —
publish.yml generates that; tag body holds only dated marker (line 1) plus curated highlight bullets, no ## Highlights heading (release renderer adds it).
- NEVER run
gh release create by hand during deploy — workflow owns it; manual release collides with workflow's release job.