Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions .github/workflows/RELEASE-PROCESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Release Process

This document describes the automated release process for Spec Kit.

## Overview

The release process is split into two workflows to ensure version consistency:

1. **Release Trigger Workflow** (`release-trigger.yml`) - Manages versioning and triggers release
2. **Release Workflow** (`release.yml`) - Builds and publishes artifacts

This separation ensures that git tags always point to commits with the correct version in `pyproject.toml`.

## Before Creating a Release

**Important**: Write clear, descriptive commit messages!

### How CHANGELOG.md Works

The CHANGELOG is **automatically generated** from your git commit messages:

1. **During Development**: Write clear, descriptive commit messages:
```bash
git commit -m "feat: Add new authentication feature"
git commit -m "fix: Resolve timeout issue in API client (#123)"
git commit -m "docs: Update installation instructions"
```

2. **When Releasing**: The release trigger workflow automatically:
- Finds all commits since the last release tag
- Formats them as changelog entries
- Inserts them into CHANGELOG.md
- Commits the updated changelog before creating the new tag

### Commit Message Best Practices

Good commit messages make good changelogs:
- **Be descriptive**: "Add user authentication" not "Update files"
- **Reference issues/PRs**: Include `(#123)` for automated linking
- **Use conventional commits** (optional): `feat:`, `fix:`, `docs:`, `chore:`
- **Keep it concise**: One line is ideal, details go in commit body

**Example commits that become good changelog entries:**
```
fix: prepend YAML frontmatter to Cursor .mdc files (#1699)
feat: add generic agent support with customizable command directories (#1639)
docs: document dual-catalog system for extensions (#1689)
```

## Creating a Release

### Option 1: Auto-Increment (Recommended for patches)

1. Go to **Actions** β†’ **Release Trigger**
2. Click **Run workflow**
3. Leave the version field **empty**
4. Click **Run workflow**

The workflow will:
- Auto-increment the patch version (e.g., `0.1.10` β†’ `0.1.11`)
- Update `pyproject.toml`
- Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag
- Commit changes
- Create and push git tag
- Trigger the release workflow automatically

### Option 2: Manual Version (For major/minor bumps)

1. Go to **Actions** β†’ **Release Trigger**
2. Click **Run workflow**
3. Enter the desired version (e.g., `0.2.0` or `v0.2.0`)
4. Click **Run workflow**

The workflow will:
- Use your specified version
- Update `pyproject.toml`
- Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag
- Commit changes
- Create and push git tag
- Trigger the release workflow automatically

## What Happens Next

Once the release trigger workflow completes:

1. The git tag is pushed to GitHub
2. The **Release Workflow** is automatically triggered
3. Release artifacts are built for all supported agents
4. A GitHub Release is created with all assets
5. Release notes are generated from PR titles

## Workflow Details

### Release Trigger Workflow

**File**: `.github/workflows/release-trigger.yml`

**Trigger**: Manual (`workflow_dispatch`)

**Permissions Required**: `contents: write`

**Steps**:
1. Checkout repository
2. Determine version (manual or auto-increment)
3. Check if tag already exists (prevents duplicates)
4. Update `pyproject.toml`
5. Update `CHANGELOG.md`
6. Commit changes
7. Create and push tag

### Release Workflow

**File**: `.github/workflows/release.yml`

**Trigger**: Tag push (`v*`)

**Permissions Required**: `contents: write`

**Steps**:
1. Checkout repository at tag
2. Extract version from tag name
3. Check if release already exists
4. Build release package variants (all agents Γ— shell/powershell)
5. Generate release notes from commits
6. Create GitHub Release with all assets

## Version Constraints

- Tags must follow format: `v{MAJOR}.{MINOR}.{PATCH}`
- Example valid versions: `v0.1.11`, `v0.2.0`, `v1.0.0`
- Auto-increment only bumps patch version
- Cannot create duplicate tags (workflow will fail)

## Benefits of This Approach

βœ… **Version Consistency**: Git tags point to commits with matching `pyproject.toml` version

βœ… **Single Source of Truth**: Version set once, used everywhere

βœ… **Prevents Drift**: No more manual version synchronization needed

βœ… **Clean Separation**: Versioning logic separate from artifact building

βœ… **Flexibility**: Supports both auto-increment and manual versioning

## Troubleshooting

### No Commits Since Last Release

If you run the release trigger workflow when there are no new commits since the last tag:
- The workflow will still succeed
- The CHANGELOG will show "- Initial release" if it's the first release
- Or it will be empty if there are no commits
- Consider adding meaningful commits before releasing

**Best Practice**: Use descriptive commit messages - they become your changelog!

### Tag Already Exists

If you see "Error: Tag vX.Y.Z already exists!", you need to:
- Choose a different version number, or
- Delete the existing tag if it was created in error

### Release Workflow Didn't Trigger

Check that:
- The release trigger workflow completed successfully
- The tag was pushed (check repository tags)
- The release workflow is enabled in Actions settings

### Version Mismatch

If `pyproject.toml` doesn't match the latest tag:
- Run the release trigger workflow to sync versions
- Or manually update `pyproject.toml` and push changes before running the release trigger

## Legacy Behavior (Pre-v0.1.10)

Before this change, the release workflow:
- Created tags automatically on main branch pushes
- Updated `pyproject.toml` AFTER creating the tag
- Resulted in tags pointing to commits with outdated versions

This has been fixed in v0.1.10+.
141 changes: 141 additions & 0 deletions .github/workflows/release-trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: Release Trigger

on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.1.11). Leave empty to auto-increment patch version.'
required: false
type: string

jobs:
bump-version:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Determine version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
if [[ -n "$INPUT_VERSION" ]]; then
# Manual version specified - strip optional v prefix
VERSION="${INPUT_VERSION#v}"
# Validate strict semver format to prevent injection
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format '$VERSION'. Must be X.Y.Z (e.g. 1.2.3 or v1.2.3)"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
echo "Using manual version: $VERSION"
else
# Auto-increment patch version
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Latest tag: $LATEST_TAG"

# Extract version number and increment
VERSION=$(echo $LATEST_TAG | sed 's/v//')
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}

# Increment patch version
PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$PATCH"

echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Auto-incremented version: $NEW_VERSION"
fi

- name: Check if tag already exists
run: |
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
echo "Error: Tag ${{ steps.version.outputs.tag }} already exists!"
exit 1
fi

- name: Update pyproject.toml
run: |
sed -i "s/version = \".*\"/version = \"${{ steps.version.outputs.version }}\"/" pyproject.toml
echo "Updated pyproject.toml to version ${{ steps.version.outputs.version }}"

- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)

# Get the previous tag to compare commits
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

echo "Generating changelog from commits..."
if [[ -n "$PREVIOUS_TAG" ]]; then
echo "Changes since $PREVIOUS_TAG"

# Get commits since last tag, format as bullet points
# Extract PR numbers and format nicely
COMMITS=$(git log --oneline "$PREVIOUS_TAG"..HEAD --no-merges --pretty=format:"- %s" 2>/dev/null || echo "- Initial release")
else
echo "No previous tag found - this is the first release"
COMMITS="- Initial release"
fi

# Create new changelog entry
{
head -n 8 CHANGELOG.md
echo ""
echo "## [${{ steps.version.outputs.version }}] - $DATE"
echo ""
echo "### Changed"
echo ""
echo "$COMMITS"
echo ""
tail -n +9 CHANGELOG.md
} > CHANGELOG.md.tmp
mv CHANGELOG.md.tmp CHANGELOG.md

echo "βœ… Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
else
echo "No CHANGELOG.md found"
fi

- name: Commit version bump
run: |
if [ -f "CHANGELOG.md" ]; then
git add pyproject.toml CHANGELOG.md
else
git add pyproject.toml
fi

if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: bump version to ${{ steps.version.outputs.version }}"
echo "Changes committed"
fi
- name: Create and push tag
run: |
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
git push origin main
git push origin "${{ steps.version.outputs.tag }}"
echo "Tag ${{ steps.version.outputs.tag }} created and pushed"

- name: Summary
run: |
echo "βœ… Version bumped to ${{ steps.version.outputs.version }}"
echo "βœ… Tag ${{ steps.version.outputs.tag }} created and pushed"
echo "πŸš€ Release workflow will now build artifacts automatically"
Loading