Installs into .claude/skills of the current project.
Are you the author of Ll Manage Release?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/brennontwilliams-ll-manage-release)
---
name: ll-manage-release
description: |
Manage releases - create git tags, generate changelogs, and manage GitHub releases.
Integrates with Issue Management to include completed issues in release notes.
Trigger keywords: "manage release", "create release", "new release", "tag release", "publish release", "make release", "bump version"
argument-hint: "[action] [version]"
arguments:
- name: action
description: "Action to perform (tag|changelog|release|bump|full). Omit for interactive mode."
required: false
- name: version
description: "Version string (e.g., v1.2.3 or 1.2.3) or bump level (patch|minor|major)"
required: false
- name: flags
description: "Optional flags: --dry-run, --push, --draft"
required: false
allowed-tools:
- Bash(gh:*, git:*)
- AskUserQuestion
- Task
- Read
- Glob
- Grep
---
# Manage Release
You are tasked with managing releases for this project. This includes creating git tags, generating changelogs from commits and completed issues, creating GitHub releases, and bumping version numbers.
## Configuration
Read settings from `.ll/ll-config.json`:
- **Issues base**: `{{config.issues.base_dir}}` (default: `.issues`)
Version is tracked in files like these (discovered per-project by Agent 3, not a fixed checklist):
- Manifest `version` fields — e.g. `pyproject.toml`, `package.json`, `Cargo.toml`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`
- A `__version__ = "X.Y.Z"` assignment, if the project has one
- Any other declaration Agent 3 finds under the project's actual layout
Changelog: `CHANGELOG.md` (follows [Keep a Changelog](https://keepachangelog.com/) format)
---
## Process
### 1. Check Prerequisites
```bash
# Verify gh CLI is authenticated
gh auth status
# Verify we're in a git repo
git rev-parse --git-dir
# Check for uncommitted changes
git status --porcelain
```
If `gh` is not authenticated, instruct the user to run `gh auth login` and stop.
If there are uncommitted changes, warn the user and ask whether to proceed or stop:
```yaml
questions:
- question: "There are uncommitted changes in the working tree. Proceed with release anyway?"
header: "Uncommitted"
multiSelect: false
options:
- label: "Proceed anyway"
description: "Continue with release despite uncommitted changes"
- label: "Stop"
description: "Abort release to commit or stash changes first"
```
### 2. Parse Arguments
```
ACTION="${action}" # tag|changelog|release|bump|full (optional)
VERSION="${version}" # vX.Y.Z, X.Y.Z, patch, minor, major (optional)
DRY_RUN=false # set true if flags contains "--dry-run"
PUSH=false # set true if flags contains "--push"
DRAFT=false # set true if flags contains "--draft"
```
### 3. Interactive Mode (No Arguments)
If `ACTION` is empty, use `AskUserQuestion` to gather preferences:
```yaml
questions:
- question: "Which release actions should be performed?"
header: "Actions"
multiSelect: true
options:
- label: "Full release (Recommended)"
description: "Run bump + changelog + tag + release in sequence"
- label: "Tag only"
description: "Create a git tag for the release version"
- label: "Changelog only"
description: "Generate changelog from commits and completed issues since last tag"
- label: "GitHub release only"
description: "Create a GitHub release with generated notes"
- question: "Which version bump level should be used?"
header: "Version"
multiSelect: false
options:
- label: "Auto-detect (Recommended)"
description: "Infer from conventional commits (feat→minor, fix→patch, BREAKING→major)"
- label: "Patch"
description: "Bug fixes and minor changes (x.y.Z)"
- label: "Minor"
description: "New features, backwards compatible (x.Y.0)"
- label: "Major"
description: "Breaking changes (X.0.0)"
```
Map user responses to action and version variables:
- "Full release" → `ACTION=full`
- "Tag only" → `ACTION=tag`
- "Changelog only" → `ACTION=changelog`
- "GitHub release only" → `ACTION=release`
- If multiple selected, execute each in order: bump → changelog → tag → release
- "Auto-detect" → `VERSION=auto`
- "Patch" / "Minor" / "Major" → `VERSION=patch|minor|major`
**After initial prompt**: Execute ALL selected actions without stopping for further confirmation.
---
### 4. Wave 1: Parallel Information Gathering
**IMPORTANT**: Spawn all 3 agents in a SINGLE message with multiple Task tool calls, each with `run_in_background: false`, and wait for all results in this same turn.
#### Agent 1: Git History Analysis
```
Use Task tool with subagent_type="Explore"
Prompt:
Analyze git history for release preparation.
1. List all existing tags:
git tag --list --sort=-version:refname
2. Identify the baseline tag for this release using smart detection:
# If HEAD is exactly at a tag (e.g., changelog run after tagging), use the tag before it
if git describe --exact-match HEAD >/dev/null 2>&1; then
CURRENT_TAG=$(git describe --exact-match HEAD)
PREV_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^")
else
PREV_TAG=$(git describe --tags --abbrev=0)
fi
3. Get commits since baseline tag (or all commits if no tags):
git log ${PREV_TAG}..HEAD --pretty=format:"%H|%s|%an|%ad" --date=short
4. Parse conventional commits and categorize:
- feat: → Features (minor bump)
- fix: → Bug Fixes (patch bump)
- refactor:/perf: → Improvements
- docs: → Documentation
- chore:/ci:/build: → Maintenance
- BREAKING CHANGE or !: → Breaking Changes (major bump)
Exclusions (ENH-2467): skip commits whose subject starts with `chore(issues):`
— these are `.issues/`-only housekeeping commits (issue status flips,
session-log appends) deliberately demoted by /ll:manage-issue. They must not
appear in ANY changelog section, including Maintenance. As defense-in-depth,
also skip any commit whose changed files are entirely under `.issues/`
(check with: git diff-tree --no-commit-id --name-only -r <sha>).
5. Suggest next version based on commit types:
- If any BREAKING CHANGE → major
- If any feat: → minor
- Otherwise → patch
Return: last tag, commit breakdown by type, suggested next version, full commit list.
```
#### Agent 2: Completed Issues Since Last Tag
```
Use Task tool with subagent_type="Explore"
Prompt:
Scan completed issues for release notes using frontmatter status fields.
1. Determine the baseline tag using smart detection (handles running before or after tagging):
if git describe --exact-match HEAD >/dev/null 2>&1; then
CURRENT_TAG=$(git describe --exact-match HEAD)
PREV_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^")
else
PREV_TAG=$(git describe --tags --abbrev=0)
fi
2. Get the full ISO timestamp of the baseline tag commit:
PREV_TIMESTAMP=$(git log -1 --format="%aI" "${PREV_TAG}")
3. List issues with status: done whose completed_at falls on/after the baseline
tag's date. `ll-issues list --json` emits `completed_at` as a day-granularity
ISO date (e.g. "2026-07-09") or null; compare on DATE, not raw strings —
completed_at day-values and the tag's tz-offset timestamp do not order
correctly under lexical `>=`:
ll-issues list --status done --json | python3 -c "
import sys, json
from datetime import date, datetime
data = json.load(sys.stdin)
prev_ts = '$(echo ${PREV_TIMESTAMP})'
prev_date = datetime.fromisoformat(prev_ts).date()
def _cad(v):
if not v:
return None
try:
return date.fromisoformat(v[:10])
except ValueError:
return None
results = [i for i in data if (_cad(i.get('completed_at')) or date.min) >= prev_date]
print(json.dumps(results))
"
4. For each issue in the result:
- Extract id, title, type (BUG/FEAT/ENH/EPIC) from the JSON data
- Extract github_issue from frontmatter (if present)
5. Categorize issues:
- Features: FEAT-* issues
- Bug Fixes: BUG-* issues
- Enhancements: ENH-* issues
6. Format each entry as:
- With github_issue: "ISSUE-ID: Title (#github_number)"
- Without github_issue: "ISSUE-ID: Title"
Return: categorized list of issues, count per category.
```
#### Agent 3: Version References
```
Use Task tool with subagent_type="Explore"
Prompt:
Find all version declarations in the project.
1. Glob (non-recursive, top-level only — never `**/`) for manifest files at
the repo root and at `{{config.project.src_dir}}`: `pyproject.toml`,
`package.json`, `Cargo.toml`, `.claude-plugin/plugin.json`,
`.claude-plugin/marketplace.json`. For each match, report its `version`
field(s) — some files (e.g. `marketplace.json`) may declare it more than once.
2. Grep for `^__version__\s*=` scoped to `{{config.project.src_dir}}` — do
not Glob for `__init__.py`, which matches every package in the tree.
3. Grep the project for the current version string and classify each hit as
a **declaration** (a manifest `version` field or `__version__ =`
assignment — already covered by steps 1–2) or **incidental** (e.g.
`CHANGELOG.md`, lockfiles, `node_modules/`, `.issues/`, docs). Report
incidental hits separately, for visibility only — they are never bump
targets.
For each declaration found, report:
- File path and line number
- Current version value
- The exact line content (for precise editing)
Return: list of version declarations with current values, plus a separate
list of incidental matches (informational only).
```
### 5. Wave 2: Synthesis and Execution
After all Wave 1 agents complete:
#### 5a. Merge Results
1. **Determine target version**:
- If `VERSION=auto`: use Wave 1 Agent 1's suggestion based on conventional commits
- If `VERSION=patch|minor|major`: calculate from current version (Wave 1 Agent 3)
- If `VERSION=vX.Y.Z` or `X.Y.Z`: use as-is (normalize to `vX.Y.Z` for tags, `X.Y.Z` for files)
2. **Build release notes** by merging:
- Completed issues (Agent 2) grouped by type
- Commits without associated issues (Agent 1) grouped by conventional commit type
3. **Empty-result guard** (do not ship a silent empty changelog): if Agent 1
returns 0 commits AND Agent 2 returns 0 issues, STOP and warn loudly —
report the baseline tag (`PREV_TAG`) and range used, and ask the user to
confirm before continuing. A double-zero almost always means a broken
baseline or a query regression (see BUG-942), not a genuinely empty release.
Also warn (but continue) if Agent 2 returns 0 issues while Agent 1 found
commits that reference issue IDs — the completed_at query may be stale.
#### 5b. Execute Actions
If `DRY_RUN` is true, show preview of each action instead of executing.
Execute actions in order: **bump → changelog → tag → release**
The changelog is written and committed **before** the tag is created, so the
tag points at a commit that contains its own `CHANGELOG.md` entry (and so the
GitHub release, whose notes are derived from that entry, is consistent with the
tagged tree).
##### Action: `bump`
Update version in exactly the declaration files Agent 3 found — never the
incidental matches:
```bash
# For each version declaration Agent 3 reported, use Edit tool to update the
# version string in place (e.g. version = "X.Y.Z" → version = "NEW_VERSION",
# or __version__ = "X.Y.Z" → __version__ = "NEW_VERSION").
```
After bumping, commit the version change, staging exactly the files you just
edited above (explicit paths — never `git add -A` or `git add -u`):
```bash
git add <declaration files edited above>
git commit -m "chore(release): bump version to NEW_VERSION"
```
##### Action: `changelog`
Generate a changelog entry following the [Keep a Changelog](https://keepachangelog.com/) format.
Build the entry:
```markdown
## [X.Y.Z] - YYYY-MM-DD
### Added
- **Feature name** - Description (FEAT-NNN)
- feat: commit message (abc1234)
### Fixed
- **Bug fix name** - Description (BUG-NNN)
- fix: commit message (abc1234)
### Changed
- **Enhancement name** - Description (ENH-NNN)
- refactor: commit message (abc1234)
### Other
- docs: commit message (abc1234)
- chore: commit message (abc1234)
```
Rules:
- Completed issues take priority over commits — if a commit is associated with an issue, only list the issue
- Include GitHub issue links where `github_issue` exists: `(FEAT-NNN, #42)`
- Omit empty sections (e.g., if no "Fixed" entries, don't include the heading)
- Add comparison link at bottom: `[X.Y.Z]: https://github.com/OWNER/REPO/compare/PREV_TAG...vX.Y.Z`
Insert the entry into `CHANGELOG.md`:
1. Read current `CHANGELOG.md`
2. Replace `## [Unreleased]` section content with the `### Planned` items (keep section header)
3. Insert new version section between `## [Unreleased]` and the previous version section
4. Add comparison link
Commit the changelog:
```bash
git add CHANGELOG.md
git commit -m "docs(release): add changelog for vX.Y.Z"
```
##### Pre-Release: Learning Test Audit
Before creating a tag, run the learning-test pre-release audit if `learning_tests.enabled` is true in `.ll/ll-config.json`:
```bash
python -c "
import sys, pathlib
from little_loops.learning_tests.release_gate import run_release_gate
sys.exit(run_release_gate(pathlib.Path.cwd()))
"
```
- If the script exits **1**, abort the release and display the warning table output above.
- If the script exits **0**, continue to tag creation.
- If `learning_tests.enabled` is false or absent, skip this step silently.
The audit behavior is controlled by `learning_tests.release_gate` in `.ll/ll-config.json`:
- `warn` (default) — prints a warning table but continues
- `block` — aborts the release with exit 1
##### Action: `tag`
Create an annotated git tag:
```bash
git tag -a vX.Y.Z -m "Release vX.Y.Z"
```
If `--push` flag is set:
```bash
git push origin vX.Y.Z
```
##### Action: `release`
Create a GitHub release:
```bash
# Write release notes to temp file
# (Same content as changelog entry, formatted for GitHub)
mkdir -p .loops/tmp
gh release create vX.Y.Z \
--title "vX.Y.Z" \
--notes-file .loops/tmp/ll-release-notes.md
```
If `--draft` flag is set, add `--draft` to the command.
If the tag hasn't been pushed yet:
```bash
git push origin vX.Y.Z
```
##### Action: `full`
Execute all actions in sequence: bump → changelog → (learning test audit) → tag → release
---
### 6. Dry-Run Output
When `--dry-run` is active, output:
```
=== DRY RUN: Release vX.Y.Z ===
Current version: A.B.C
Target version: X.Y.Z
Last tag: vA.B.C
Actions to perform:
[bump] Update version in N files
[tag] Create annotated tag vX.Y.Z
[changelog] Add changelog entry with N issues and M commits
[release] Create GitHub release vX.Y.Z
--- Changelog Preview ---
## [X.Y.Z] - YYYY-MM-DD
### Added
- ...
### Fixed
- ...
--- Version Files ---
<declaration file>:<line> → version = "X.Y.Z"
<declaration file>:<line> → version = "X.Y.Z"
... (one line per version declaration Agent 3 found — paths and count are
project-specific, not fixed)
=== END DRY RUN (no changes made) ===
```
### 7. Report Result
After successful execution, output:
```
Release vX.Y.Z completed successfully!
Tag: vX.Y.Z (annotated)
Changelog: CHANGELOG.md updated
Release: https://github.com/OWNER/REPO/releases/tag/vX.Y.Z
Version: X.Y.Z (updated in 3 files)
Issues included: N (F features, B bug fixes, E enhancements)
Commits included: M
```
---
## Arguments
$ARGUMENTS
- **action** (optional): Action to perform
- `tag` — Create an annotated git tag
- `changelog` — Generate changelog from commits and completed issues since last tag
- `release` — Create a GitHub release with generated notes
- `bump` — Update version strings in project files
- `full` — Run bump + changelog + tag + release in sequence
- If omitted, enters interactive mode
- **version** (optional): Version target
- `vX.Y.Z` or `X.Y.Z` — Use specific version
- `patch` — Bump patch version (x.y.Z)
- `minor` — Bump minor version (x.Y.0)
- `major` — Bump major version (X.0.0)
- If omitted, auto-detects from conventional commits
- **flags** (optional):
- `--dry-run` — Preview actions without executing
- `--push` — Push tag to remote after creation
- `--draft` — Create GitHub release as draft
---
## Examples
```bash
# Interactive mode — prompts for actions and version
/ll:manage-release
# Create a specific version tag
/ll:manage-release tag v1.5.0
# Generate changelog (auto-detect version from commits)
/ll:manage-release changelog
# Full release pipeline with specific version
/ll:manage-release full v1.5.0
# Full release with auto-detected version
/ll:manage-release full
# Bump version only
/ll:manage-release bump minor
# Preview what a release would do
/ll:manage-release full v1.5.0 --dry-run
# Create a draft GitHub release
/ll:manage-release release v1.5.0 --draft
# Tag and push to remote
/ll:manage-release tag v1.5.0 --push
```
---
## Error Handling
- **gh not installed**: Suggest installation via `brew install gh` (macOS) or platform docs
- **gh not authenticated**: Suggest `gh auth login`
- **Not a git repo**: Inform user and stop
- **Uncommitted changes**: Warn and ask to proceed or stop
- **Tag already exists**: Inform user, offer to use `--force` or choose different version
- **No commits since last tag**: Inform user there are no changes to release
- **Version parse failure**: Show current version and ask user to provide explicit version
- **GitHub release creation fails**: Show error, suggest checking `gh auth status` and repository permissions
---
## Integration
This command works well with:
- `/ll:commit` — Commit changes before releasing
- `/ll:check-code` — Ensure code quality before releasing
- `/ll:run-tests` — Verify tests pass before releasing
- `/ll:manage-issue` — Complete issues that should be in the release
- `/ll:sync-issues` — Sync issues with GitHub before including in release notes
- `/ll:open-pr` — Open PR for release branch if using release branches