Skip to content

Commit 2c41d36

Browse files
mnriemCopilot
andauthored
fix: Split release process to sync pyproject.toml version with git tags (#1732)
* fix: split release process to sync pyproject.toml version with git tags (#1721) - Split release workflow into two: release-trigger.yml and release.yml - release-trigger.yml: Updates pyproject.toml, generates changelog from commits, creates tag - release.yml: Triggered by tag push, builds artifacts, creates GitHub release - Ensures git tags point to commits with correct version in pyproject.toml - Auto-generates changelog from commit messages since last tag - Supports manual version input or auto-increment patch version - Added simulate-release.sh for local testing without pushing - Added comprehensive RELEASE-PROCESS.md documentation - Updated pyproject.toml to v0.1.10 to sync with latest release This fixes the version mismatch issue where tags pointed to commits with outdated pyproject.toml versions, preventing confusion when installing from source. * Update .github/workflows/RELEASE-PROCESS.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/workflows/scripts/simulate-release.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/workflows/release.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/workflows/release-trigger.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: harden release-trigger against shell injection and fix stale docs - Pass workflow_dispatch version input via env: instead of direct interpolation into shell script, preventing potential injection attacks - Validate version input against strict semver regex before use - Fix RELEASE-PROCESS.md Option 2 still referencing [Unreleased] section handling that no longer exists in the workflow --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b55d00b commit 2c41d36

File tree

6 files changed

+548
-31
lines changed

6 files changed

+548
-31
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Release Process
2+
3+
This document describes the automated release process for Spec Kit.
4+
5+
## Overview
6+
7+
The release process is split into two workflows to ensure version consistency:
8+
9+
1. **Release Trigger Workflow** (`release-trigger.yml`) - Manages versioning and triggers release
10+
2. **Release Workflow** (`release.yml`) - Builds and publishes artifacts
11+
12+
This separation ensures that git tags always point to commits with the correct version in `pyproject.toml`.
13+
14+
## Before Creating a Release
15+
16+
**Important**: Write clear, descriptive commit messages!
17+
18+
### How CHANGELOG.md Works
19+
20+
The CHANGELOG is **automatically generated** from your git commit messages:
21+
22+
1. **During Development**: Write clear, descriptive commit messages:
23+
```bash
24+
git commit -m "feat: Add new authentication feature"
25+
git commit -m "fix: Resolve timeout issue in API client (#123)"
26+
git commit -m "docs: Update installation instructions"
27+
```
28+
29+
2. **When Releasing**: The release trigger workflow automatically:
30+
- Finds all commits since the last release tag
31+
- Formats them as changelog entries
32+
- Inserts them into CHANGELOG.md
33+
- Commits the updated changelog before creating the new tag
34+
35+
### Commit Message Best Practices
36+
37+
Good commit messages make good changelogs:
38+
- **Be descriptive**: "Add user authentication" not "Update files"
39+
- **Reference issues/PRs**: Include `(#123)` for automated linking
40+
- **Use conventional commits** (optional): `feat:`, `fix:`, `docs:`, `chore:`
41+
- **Keep it concise**: One line is ideal, details go in commit body
42+
43+
**Example commits that become good changelog entries:**
44+
```
45+
fix: prepend YAML frontmatter to Cursor .mdc files (#1699)
46+
feat: add generic agent support with customizable command directories (#1639)
47+
docs: document dual-catalog system for extensions (#1689)
48+
```
49+
50+
## Creating a Release
51+
52+
### Option 1: Auto-Increment (Recommended for patches)
53+
54+
1. Go to **Actions****Release Trigger**
55+
2. Click **Run workflow**
56+
3. Leave the version field **empty**
57+
4. Click **Run workflow**
58+
59+
The workflow will:
60+
- Auto-increment the patch version (e.g., `0.1.10``0.1.11`)
61+
- Update `pyproject.toml`
62+
- Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag
63+
- Commit changes
64+
- Create and push git tag
65+
- Trigger the release workflow automatically
66+
67+
### Option 2: Manual Version (For major/minor bumps)
68+
69+
1. Go to **Actions****Release Trigger**
70+
2. Click **Run workflow**
71+
3. Enter the desired version (e.g., `0.2.0` or `v0.2.0`)
72+
4. Click **Run workflow**
73+
74+
The workflow will:
75+
- Use your specified version
76+
- Update `pyproject.toml`
77+
- Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag
78+
- Commit changes
79+
- Create and push git tag
80+
- Trigger the release workflow automatically
81+
82+
## What Happens Next
83+
84+
Once the release trigger workflow completes:
85+
86+
1. The git tag is pushed to GitHub
87+
2. The **Release Workflow** is automatically triggered
88+
3. Release artifacts are built for all supported agents
89+
4. A GitHub Release is created with all assets
90+
5. Release notes are generated from PR titles
91+
92+
## Workflow Details
93+
94+
### Release Trigger Workflow
95+
96+
**File**: `.github/workflows/release-trigger.yml`
97+
98+
**Trigger**: Manual (`workflow_dispatch`)
99+
100+
**Permissions Required**: `contents: write`
101+
102+
**Steps**:
103+
1. Checkout repository
104+
2. Determine version (manual or auto-increment)
105+
3. Check if tag already exists (prevents duplicates)
106+
4. Update `pyproject.toml`
107+
5. Update `CHANGELOG.md`
108+
6. Commit changes
109+
7. Create and push tag
110+
111+
### Release Workflow
112+
113+
**File**: `.github/workflows/release.yml`
114+
115+
**Trigger**: Tag push (`v*`)
116+
117+
**Permissions Required**: `contents: write`
118+
119+
**Steps**:
120+
1. Checkout repository at tag
121+
2. Extract version from tag name
122+
3. Check if release already exists
123+
4. Build release package variants (all agents × shell/powershell)
124+
5. Generate release notes from commits
125+
6. Create GitHub Release with all assets
126+
127+
## Version Constraints
128+
129+
- Tags must follow format: `v{MAJOR}.{MINOR}.{PATCH}`
130+
- Example valid versions: `v0.1.11`, `v0.2.0`, `v1.0.0`
131+
- Auto-increment only bumps patch version
132+
- Cannot create duplicate tags (workflow will fail)
133+
134+
## Benefits of This Approach
135+
136+
**Version Consistency**: Git tags point to commits with matching `pyproject.toml` version
137+
138+
**Single Source of Truth**: Version set once, used everywhere
139+
140+
**Prevents Drift**: No more manual version synchronization needed
141+
142+
**Clean Separation**: Versioning logic separate from artifact building
143+
144+
**Flexibility**: Supports both auto-increment and manual versioning
145+
146+
## Troubleshooting
147+
148+
### No Commits Since Last Release
149+
150+
If you run the release trigger workflow when there are no new commits since the last tag:
151+
- The workflow will still succeed
152+
- The CHANGELOG will show "- Initial release" if it's the first release
153+
- Or it will be empty if there are no commits
154+
- Consider adding meaningful commits before releasing
155+
156+
**Best Practice**: Use descriptive commit messages - they become your changelog!
157+
158+
### Tag Already Exists
159+
160+
If you see "Error: Tag vX.Y.Z already exists!", you need to:
161+
- Choose a different version number, or
162+
- Delete the existing tag if it was created in error
163+
164+
### Release Workflow Didn't Trigger
165+
166+
Check that:
167+
- The release trigger workflow completed successfully
168+
- The tag was pushed (check repository tags)
169+
- The release workflow is enabled in Actions settings
170+
171+
### Version Mismatch
172+
173+
If `pyproject.toml` doesn't match the latest tag:
174+
- Run the release trigger workflow to sync versions
175+
- Or manually update `pyproject.toml` and push changes before running the release trigger
176+
177+
## Legacy Behavior (Pre-v0.1.10)
178+
179+
Before this change, the release workflow:
180+
- Created tags automatically on main branch pushes
181+
- Updated `pyproject.toml` AFTER creating the tag
182+
- Resulted in tags pointing to commits with outdated versions
183+
184+
This has been fixed in v0.1.10+.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: Release Trigger
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Version to release (e.g., 0.1.11). Leave empty to auto-increment patch version.'
8+
required: false
9+
type: string
10+
11+
jobs:
12+
bump-version:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: write
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v6
19+
with:
20+
fetch-depth: 0
21+
token: ${{ secrets.GITHUB_TOKEN }}
22+
23+
- name: Configure Git
24+
run: |
25+
git config user.name "github-actions[bot]"
26+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
27+
28+
- name: Determine version
29+
id: version
30+
env:
31+
INPUT_VERSION: ${{ github.event.inputs.version }}
32+
run: |
33+
if [[ -n "$INPUT_VERSION" ]]; then
34+
# Manual version specified - strip optional v prefix
35+
VERSION="${INPUT_VERSION#v}"
36+
# Validate strict semver format to prevent injection
37+
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
38+
echo "Error: Invalid version format '$VERSION'. Must be X.Y.Z (e.g. 1.2.3 or v1.2.3)"
39+
exit 1
40+
fi
41+
echo "version=$VERSION" >> $GITHUB_OUTPUT
42+
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
43+
echo "Using manual version: $VERSION"
44+
else
45+
# Auto-increment patch version
46+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
47+
echo "Latest tag: $LATEST_TAG"
48+
49+
# Extract version number and increment
50+
VERSION=$(echo $LATEST_TAG | sed 's/v//')
51+
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
52+
MAJOR=${VERSION_PARTS[0]:-0}
53+
MINOR=${VERSION_PARTS[1]:-0}
54+
PATCH=${VERSION_PARTS[2]:-0}
55+
56+
# Increment patch version
57+
PATCH=$((PATCH + 1))
58+
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
59+
60+
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
61+
echo "tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
62+
echo "Auto-incremented version: $NEW_VERSION"
63+
fi
64+
65+
- name: Check if tag already exists
66+
run: |
67+
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
68+
echo "Error: Tag ${{ steps.version.outputs.tag }} already exists!"
69+
exit 1
70+
fi
71+
72+
- name: Update pyproject.toml
73+
run: |
74+
sed -i "s/version = \".*\"/version = \"${{ steps.version.outputs.version }}\"/" pyproject.toml
75+
echo "Updated pyproject.toml to version ${{ steps.version.outputs.version }}"
76+
77+
- name: Update CHANGELOG.md
78+
run: |
79+
if [ -f "CHANGELOG.md" ]; then
80+
DATE=$(date +%Y-%m-%d)
81+
82+
# Get the previous tag to compare commits
83+
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
84+
85+
echo "Generating changelog from commits..."
86+
if [[ -n "$PREVIOUS_TAG" ]]; then
87+
echo "Changes since $PREVIOUS_TAG"
88+
89+
# Get commits since last tag, format as bullet points
90+
# Extract PR numbers and format nicely
91+
COMMITS=$(git log --oneline "$PREVIOUS_TAG"..HEAD --no-merges --pretty=format:"- %s" 2>/dev/null || echo "- Initial release")
92+
else
93+
echo "No previous tag found - this is the first release"
94+
COMMITS="- Initial release"
95+
fi
96+
97+
# Create new changelog entry
98+
{
99+
head -n 8 CHANGELOG.md
100+
echo ""
101+
echo "## [${{ steps.version.outputs.version }}] - $DATE"
102+
echo ""
103+
echo "### Changed"
104+
echo ""
105+
echo "$COMMITS"
106+
echo ""
107+
tail -n +9 CHANGELOG.md
108+
} > CHANGELOG.md.tmp
109+
mv CHANGELOG.md.tmp CHANGELOG.md
110+
111+
echo "✅ Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
112+
else
113+
echo "No CHANGELOG.md found"
114+
fi
115+
116+
- name: Commit version bump
117+
run: |
118+
if [ -f "CHANGELOG.md" ]; then
119+
git add pyproject.toml CHANGELOG.md
120+
else
121+
git add pyproject.toml
122+
fi
123+
124+
if git diff --cached --quiet; then
125+
echo "No changes to commit"
126+
else
127+
git commit -m "chore: bump version to ${{ steps.version.outputs.version }}"
128+
echo "Changes committed"
129+
fi
130+
- name: Create and push tag
131+
run: |
132+
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
133+
git push origin main
134+
git push origin "${{ steps.version.outputs.tag }}"
135+
echo "Tag ${{ steps.version.outputs.tag }} created and pushed"
136+
137+
- name: Summary
138+
run: |
139+
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
140+
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
141+
echo "🚀 Release workflow will now build artifacts automatically"

0 commit comments

Comments
 (0)