| name | release-manager |
| description | Create and manage release PRs against master branch. Use this when preparing releases, bumping versions, resolving merge conflicts, and publishing GitHub releases. Handles the full release workflow including merging release into master, version bumping in pyproject.toml, running uv lock, and creating GitHub releases. Use when this capability is needed. |
| metadata | {"author":"diversioteam"} |
Release Manager Skill
Manages the full release workflow for Django4Lyfe backend releases to production.
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
1. Check What Needs Releasing
git fetch origin master release
git diff --stat origin/master origin/release
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:
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
gh pr list --base release --state merged --limit 20 --json number,title \
--jq '.[] | "#\(.number): \(.title)"'
fi
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.
2. Create Release PR (Merge Method)
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.
git checkout -b releases/YYYY.MM.DD[-N] origin/master
git merge origin/release --no-edit
uv lock
git add pyproject.toml uv.lock
git commit -m "Version bump to YYYY.MM.DD[-N]"
git push -u origin releases/YYYY.MM.DD[-N]
gh pr create --base master --title "Release: DDth Month YYYY" --body "..."
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.
3. Version Numbering Convention
| Scenario | Version Format | Example |
|---|
| First release of day | YYYY.MM.DD | 2026.01.21 |
| Second release | YYYY.MM.DD-2 | 2026.01.21-2 |
| Third release | YYYY.MM.DD-3 | 2026.01.21-3 |
| Hotfix release | YYYY.MM.DD or YYYY.MM.DD-N | 2026.01.21 |
4. PR Title and Body Format
Title patterns:
- Regular release:
Release: 21st January 2026
- Multiple same-day:
Release 2: 21st January 2026
- Hotfix:
Hotfix Release: 21st January 2026
Body format:
- https://github.com/DiversioTeam/Django4Lyfe/pull/XXXX
- https://github.com/DiversioTeam/Django4Lyfe/pull/YYYY
5. Resolve Conflicts (If Any)
If the merge has conflicts:
git checkout --theirs uv.lock
uv lock
git add <resolved-files>
git commit -m "Merge origin/release into releases/YYYY.MM.DD"
6. Publish GitHub Release
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.
Step 1: Verify PR is merged
gh pr view <PR_NUMBER> --json state,mergeCommit,mergedAt
Step 2: Check recent releases for format consistency
gh release list --limit 5
Step 3: Get PR details for release notes
gh pr view <PR_NUMBER> --json body,title
Step 4: Create the GitHub release
gh release create YYYY.MM.DD[-N] \
--title "Release Title" \
--notes "$(cat <<'EOF'
- https://github.com/DiversioTeam/Django4Lyfe/pull/XXXX
- https://github.com/DiversioTeam/Django4Lyfe/pull/YYYY
EOF
)" \
--target master
GitHub Release Title Patterns
| Release Type | Tag | Title |
|---|
| First of day | 2026.01.21 | January 21st 2026 |
| Second release | 2026.01.21-2 | Release 2: January 21st 2026 |
| Third release | 2026.01.21-3 | Release 3: January 21st 2026 |
| Hotfix | 2026.01.21 | Hotfix Release: January 21st 2026 |
Step 5: Verify release was created
gh release list --limit 3
gh release view YYYY.MM.DD[-N]
Complete Example
gh pr view 2608 --json state,mergeCommit,mergedAt
gh release create 2026.01.21 \
--title "January 21st 2026" \
--notes "$(cat <<'EOF'
- https://github.com/DiversioTeam/Django4Lyfe/pull/2607
EOF
)" \
--target master
gh release list --limit 3
7. Merge Master Back Into Release
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.
git fetch origin
git checkout release
git merge origin/master --no-edit
git push origin release
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.
Pre-Release Checks
Before creating a release PR, verify:
-
Ruff formatting passes:
./.security/ruff_pr_diff.sh
If it fails, fix with:
.bin/ruff format <file>
-
Active Python type gate passes (strict):
- 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.
-
RLS policies for new models:
.bin/django optimo_bootstrap_support_shell_rls
.bin/django optimo_bootstrap_support_shell_rls --apply
Output Shape
When reporting release status:
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
When listing releases:
| Release | Tag | PRs Included |
|---------|-----|--------------|
| Release Name | `tag` | #PR1, #PR2 |
Important Rules
- Always merge release into the release PR branch — Do not cherry-pick. Merging preserves commit ancestry so
git log master..release works correctly. Cherry-picking creates duplicate commits with different SHAs, causing stale "pending" commits that were already shipped.
- Never force push — Release branches should have clean history
- Check date before versioning — Use current date, not yesterday's
- Run uv lock after version bump — Lock file must match pyproject.toml
- List all PRs in release body — Use full GitHub URLs
- Verify PR is merged before publishing release — Check with
gh pr view
- Always publish GitHub release after merge — Every merged release PR needs a corresponding GitHub release
- Tag must match version in pyproject.toml — e.g., version
2026.01.21-2 = tag 2026.01.21-2
- Always merge master back into release after publish — Run
git merge origin/master --no-edit on release after every release PR merge. Without this, the version bump stays only on master, causing git diff --stat to show false differences and the next release merge to conflict.
- Never squash-merge release PRs — Release PRs to master MUST use "Create a merge commit". Squash merging breaks commit ancestry tracking.
Full End-to-End Example
Here's a complete example of releasing PR #2607:
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 "")
gh pr list --base release --state merged --limit 100 --json number,title,mergedAt \
--jq "[.[] | select(.mergedAt > \"${LAST_RELEASE_DATE}\")] | sort_by(.mergedAt) | .[] | \"#\\(.number): \\(.title)\""
date
git checkout -b releases/2026.01.21 origin/master
git merge origin/release --no-edit
sed -i '' 's/version = ".*"/version = "2026.01.21"/' pyproject.toml
uv lock
git add pyproject.toml uv.lock
git commit -m "Version bump to 2026.01.21"
git push -u origin releases/2026.01.21
gh pr create --base master \
--title "Release: 21st January 2026" \
--body "- https://github.com/DiversioTeam/Django4Lyfe/pull/2607"
gh pr view 2608 --json state
gh release create 2026.01.21 \
--title "January 21st 2026" \
--notes "- https://github.com/DiversioTeam/Django4Lyfe/pull/2607" \
--target master
git fetch origin
git checkout release
git merge origin/master --no-edit
git push origin release
gh release list -- 3
git diff -- origin/master origin/release
Quick Reference Commands
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 "") && \
gh pr list --base release --state merged --limit 100 --json number,title,mergedAt \
--jq "[.[] | select(.mergedAt > \"${LAST_RELEASE_DATE}\")] | sort_by(.mergedAt) | .[] | \"#\\(.number): \\(.title)\""
grep '^version' pyproject.toml
gh release list --limit 10
gh pr view <NUMBER> --json state,mergeable,mergeCommit
gh release view <TAG> --json body,tagName,name
Error Recovery
Merge conflict during release branch creation
git checkout --theirs uv.lock
uv lock
git add uv.lock
git add <resolved-files>
git commit
Wrong version bumped
uv lock
git add pyproject.toml uv.lock
git commit --amend -m "Version bump to correct-version"
git push --force-with-lease
PR created against wrong base
gh pr close <NUMBER>
gh pr create --base master ...
Converted and distributed by TomeVault — claim your Tome and manage your conversions.