Release Manager
Create and manage release PRs against master branch. Handles the full release workflow including merging release into master, version bumping in pyproject.toml, running uv lock, and creating GitHub releases.
Overview
Create and manage release PRs against master branch. Handles the full release workflow including merging release into master, version bumping in pyproject.toml, running uv lock, and creating GitHub releases.
This skill ships inside the Backend Release Manager plugin and can be installed through the Claude Code marketplace or directly in Codex from its skill path.
Parent Surface
Parent docs: Backend Release Manager
Related wrapper commands from the parent plugin:
/backend-release:check/backend-release:create/backend-release:publish When to Use This Skill
- Creating release PRs against master branch
- Preparing hotfix releases
- Bumping versions in pyproject.toml
- Resolving merge conflicts between release and master
- Publishing GitHub releases after PRs are merged
- Checking what commits are on release but not on master
Core Workflow
git diff --stat compares the actual tree state (file contents), not commit history. It is the only reliable way to determine whether there is something to release. If the output is empty, there is nothing to release — stop here.
If there ARE differences, identify which PRs they belong to. Use GitHub's PR metadata (merge timestamps), not git commit ancestry:
Why the release PR's mergedAt instead of publishedAt? GitHub release publishedAt is when a human clicks "Publish" — which can be minutes or hours after the release PR actually merges. PRs merged to release in that gap would be missed on the next check. The release PR's mergedAt is the definitive cutoff because git merge origin/release captures the exact state of the release branch at that moment.
Why GitHub metadata instead of git log? All git log-based approaches (master..release, --cherry-pick, --first-parent with tags) can return stale results due to historical cherry-pick artifacts and because release tags live on master's ancestry, not release's first-parent chain. PR merge timestamps from GitHub are immune to git ancestry issues.
Merge the release branch into a branch from master. This preserves commit ancestry so that git log master..release works correctly after the PR merges.
Why merge instead of cherry-pick? Cherry-picking creates new commits with different SHAs. Even with merge-back, git log master..release permanently shows the original commits as "pending" because git compares SHAs, not patches. Merging preserves the original commit objects so master and release share the same ancestry. After the release PR merges to master, git log master..release correctly shows only genuinely new commits.
Title patterns:
Body format:
If the merge has conflicts:
After PR is merged to master, create a GitHub release.
IMPORTANT: Merge Strategy — Release PRs to master MUST be merged using "Create a merge commit" (not squash). Squash merging breaks commit ancestry and causes master..release to grow unboundedly. If GitHub is configured to allow multiple merge strategies, always select "Create a merge commit" for release PRs.
This step is mandatory after every release PR merge. It keeps the branches in sync so that git diff --stat origin/master origin/release is clean and future releases start from a consistent baseline.
Why this matters: After a release PR merges into master, master has a merge commit and a version-bump commit that release doesn't. Without merge-back, git diff --stat origin/master origin/release shows the version bump as a pending difference, and the next git merge origin/release will conflict on pyproject.toml / uv.lock. The merge-back keeps both branches aligned.
- Regular release: Release: 21st January 2026
- Multiple same-day: Release 2: 21st January 2026
- Hotfix: Hotfix Release: 21st January 2026
git fetch origin master release
git diff --stat origin/master origin/release LAST_RELEASE_DATE=$(gh pr list --base master --state merged --limit 100 \
--json number,title,mergedAt \
--jq '[.[] | select(.title | test("^(Release|Hotfix)"))] | sort_by(.mergedAt) | last | .mergedAt // empty' \
2>/dev/null || echo "")
if [ -n "${LAST_RELEASE_DATE}" ]; then
gh pr list --base release --state merged --limit 100 --json number,title,mergedAt \
--jq "[.[] | select(.mergedAt > \"${LAST_RELEASE_DATE}\")] | sort_by(.mergedAt) | .[] | \"#\\(.number): \\(.title)\""
else
# No previous release — list recent merged PRs as candidates
gh pr list --base release --state merged --limit 20 --json number,title \
--jq '.[] | "#\(.number): \(.title)"'
fi Pre-Release Checks
Before creating a release PR, verify:
If it fails, fix with:
before final release readiness.
- Detect in this order unless repo docs/CI differ:
- ty (mandatory if configured)
- pyright
- mypy
- Run on touched paths at minimum, and run any repo-required broad gate before final release readiness.
./.security/ruff_pr_diff.sh .bin/ruff format <file> Output Shape
When reporting release status:
When listing releases:
Created: https://github.com/DiversioTeam/Django4Lyfe/pull/XXXX
**Summary:**
- Version: `YYYY.MM.DD[-N]`
- Title: "Release: DDth Month YYYY"
- Target: `master`
- Conflicts: None / Resolved
**Included PRs:**
- #XXXX - Description
- #YYYY - Description | Release | Tag | PRs Included |
|---------|-----|--------------|
| Release Name | `tag` | #PR1, #PR2 | Resources
Declared allowed tools:
BashReadEditGrepGlob Installation
Switch between Claude Code and Codex, then copy the install command for the runtime you use.
claude plugin marketplace add DiversioTeam/agent-skills-marketplace
claude plugin install backend-release@diversiotech CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
python3 "$CODEX_HOME/skills/.system/skill-installer/scripts/install-skill-from-github.py" \
--repo DiversioTeam/agent-skills-marketplace \
--path plugins/backend-release/skills/release-manager Invocation:
/backend-release:check
/backend-release:create
/backend-release:publish name: release-manager