diff --git a/.github/workflows/RELEASE-PROCESS.md b/.github/workflows/RELEASE-PROCESS.md new file mode 100644 index 0000000000..6d0aaa8594 --- /dev/null +++ b/.github/workflows/RELEASE-PROCESS.md @@ -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+. diff --git a/.github/workflows/release-trigger.yml b/.github/workflows/release-trigger.yml new file mode 100644 index 0000000000..dd16152c50 --- /dev/null +++ b/.github/workflows/release-trigger.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39e0d8531a..2e29592cc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,68 +2,60 @@ name: Create Release on: push: - branches: [ main ] - paths: - - 'memory/**' - - 'scripts/**' - - 'src/**' - - 'templates/**' - - '.github/workflows/**' - workflow_dispatch: + tags: + - 'v*' jobs: release: runs-on: ubuntu-latest permissions: contents: write - pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - name: Get latest tag - id: get_tag + + - name: Extract version from tag + id: version run: | - chmod +x .github/workflows/scripts/get-next-version.sh - .github/workflows/scripts/get-next-version.sh + VERSION=${GITHUB_REF#refs/tags/} + echo "tag=$VERSION" >> $GITHUB_OUTPUT + echo "Building release for $VERSION" + - name: Check if release already exists id: check_release run: | chmod +x .github/workflows/scripts/check-release-exists.sh - .github/workflows/scripts/check-release-exists.sh ${{ steps.get_tag.outputs.new_version }} + .github/workflows/scripts/check-release-exists.sh ${{ steps.version.outputs.tag }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create release package variants if: steps.check_release.outputs.exists == 'false' run: | chmod +x .github/workflows/scripts/create-release-packages.sh - .github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }} + .github/workflows/scripts/create-release-packages.sh ${{ steps.version.outputs.tag }} + - name: Generate release notes if: steps.check_release.outputs.exists == 'false' id: release_notes run: | chmod +x .github/workflows/scripts/generate-release-notes.sh - .github/workflows/scripts/generate-release-notes.sh ${{ steps.get_tag.outputs.new_version }} ${{ steps.get_tag.outputs.latest_tag }} + # Get the previous tag for changelog generation + PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${{ steps.version.outputs.tag }}^ 2>/dev/null || echo "") + # Default to v0.0.0 if no previous tag is found (e.g., first release) + if [ -z "$PREVIOUS_TAG" ]; then + PREVIOUS_TAG="v0.0.0" + fi + .github/workflows/scripts/generate-release-notes.sh ${{ steps.version.outputs.tag }} "$PREVIOUS_TAG" + - name: Create GitHub Release if: steps.check_release.outputs.exists == 'false' run: | chmod +x .github/workflows/scripts/create-github-release.sh - .github/workflows/scripts/create-github-release.sh ${{ steps.get_tag.outputs.new_version }} + .github/workflows/scripts/create-github-release.sh ${{ steps.version.outputs.tag }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Update version in pyproject.toml (for release artifacts only) - if: steps.check_release.outputs.exists == 'false' - run: | - chmod +x .github/workflows/scripts/update-version.sh - .github/workflows/scripts/update-version.sh ${{ steps.get_tag.outputs.new_version }} - - name: Commit version bump to main - if: steps.check_release.outputs.exists == 'false' - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add pyproject.toml - git diff --cached --quiet || git commit -m "chore: bump version to ${{ steps.get_tag.outputs.new_version }} [skip ci]" - git push diff --git a/.github/workflows/scripts/simulate-release.sh b/.github/workflows/scripts/simulate-release.sh new file mode 100755 index 0000000000..a3960d0317 --- /dev/null +++ b/.github/workflows/scripts/simulate-release.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +set -euo pipefail + +# simulate-release.sh +# Simulate the release process locally without pushing to GitHub +# Usage: simulate-release.sh [version] +# If version is omitted, auto-increments patch version + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🧪 Simulating Release Process Locally${NC}" +echo "======================================" +echo "" + +# Step 1: Determine version +if [[ -n "${1:-}" ]]; then + VERSION="${1#v}" + TAG="v$VERSION" + echo -e "${GREEN}📝 Using manual version: $VERSION${NC}" +else + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo -e "${BLUE}Latest tag: $LATEST_TAG${NC}" + + 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} + + PATCH=$((PATCH + 1)) + VERSION="$MAJOR.$MINOR.$PATCH" + TAG="v$VERSION" + echo -e "${GREEN}📝 Auto-incremented to: $VERSION${NC}" +fi + +echo "" + +# Step 2: Check if tag exists +if git rev-parse "$TAG" >/dev/null 2>&1; then + echo -e "${RED}❌ Error: Tag $TAG already exists!${NC}" + echo " Please use a different version or delete the tag first." + exit 1 +fi +echo -e "${GREEN}✓ Tag $TAG is available${NC}" + +# Step 3: Backup current state +echo "" +echo -e "${YELLOW}💾 Creating backup of current state...${NC}" +BACKUP_DIR=$(mktemp -d) +cp pyproject.toml "$BACKUP_DIR/pyproject.toml.bak" +cp CHANGELOG.md "$BACKUP_DIR/CHANGELOG.md.bak" +echo -e "${GREEN}✓ Backup created at: $BACKUP_DIR${NC}" + +# Step 4: Update pyproject.toml +echo "" +echo -e "${YELLOW}📝 Updating pyproject.toml...${NC}" +sed -i.tmp "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml +rm -f pyproject.toml.tmp +echo -e "${GREEN}✓ Updated pyproject.toml to version $VERSION${NC}" + +# Step 5: Update CHANGELOG.md +echo "" +echo -e "${YELLOW}📝 Updating CHANGELOG.md...${NC}" +DATE=$(date +%Y-%m-%d) + +# Get the previous tag to compare commits +PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + +if [[ -n "$PREVIOUS_TAG" ]]; then + echo " Generating changelog from commits since $PREVIOUS_TAG" + # Get commits since last tag, format as bullet points + 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 temp file with new entry +{ + head -n 8 CHANGELOG.md + echo "" + echo "## [$VERSION] - $DATE" + echo "" + echo "### Changed" + echo "" + echo "$COMMITS" + echo "" + tail -n +9 CHANGELOG.md +} > CHANGELOG.md.tmp +mv CHANGELOG.md.tmp CHANGELOG.md +echo -e "${GREEN}✓ Updated CHANGELOG.md with commits since $PREVIOUS_TAG${NC}" + +# Step 6: Show what would be committed +echo "" +echo -e "${YELLOW}📋 Changes that would be committed:${NC}" +git diff pyproject.toml CHANGELOG.md + +# Step 7: Create temporary tag (no push) +echo "" +echo -e "${YELLOW}🏷️ Creating temporary local tag...${NC}" +git tag -a "$TAG" -m "Simulated release $TAG" 2>/dev/null || true +echo -e "${GREEN}✓ Tag $TAG created locally${NC}" + +# Step 8: Simulate release artifact creation +echo "" +echo -e "${YELLOW}📦 Simulating release package creation...${NC}" +echo " (High-level simulation only; packaging script is not executed)" +echo "" + +# Check if script exists and is executable +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -x "$SCRIPT_DIR/create-release-packages.sh" ]]; then + echo -e "${BLUE}In a real release, the following command would be run to create packages:${NC}" + echo " $SCRIPT_DIR/create-release-packages.sh \"$TAG\"" + echo "" + echo "This simulation does not enumerate individual package files to avoid" + echo "drifting from the actual behavior of create-release-packages.sh." +else + echo -e "${RED}⚠️ create-release-packages.sh not found or not executable${NC}" +fi + +# Step 9: Simulate release notes generation +echo "" +echo -e "${YELLOW}📄 Simulating release notes generation...${NC}" +echo "" +PREVIOUS_TAG=$(git describe --tags --abbrev=0 $TAG^ 2>/dev/null || echo "") +if [[ -n "$PREVIOUS_TAG" ]]; then + echo -e "${BLUE}Changes since $PREVIOUS_TAG:${NC}" + git log --oneline "$PREVIOUS_TAG".."$TAG" | head -n 10 + echo "" +else + echo -e "${BLUE}No previous tag found - this would be the first release${NC}" +fi + +# Step 10: Summary +echo "" +echo -e "${GREEN}🎉 Simulation Complete!${NC}" +echo "======================================" +echo "" +echo -e "${BLUE}Summary:${NC}" +echo " Version: $VERSION" +echo " Tag: $TAG" +echo " Backup: $BACKUP_DIR" +echo "" +echo -e "${YELLOW}⚠️ SIMULATION ONLY - NO CHANGES PUSHED${NC}" +echo "" +echo -e "${BLUE}Next steps:${NC}" +echo " 1. Review the changes above" +echo " 2. To keep changes: git add pyproject.toml CHANGELOG.md && git commit" +echo " 3. To discard changes: git checkout pyproject.toml CHANGELOG.md && git tag -d $TAG" +echo " 4. To restore from backup: cp $BACKUP_DIR/* ." +echo "" +echo -e "${BLUE}To run the actual release:${NC}" +echo " Go to: https://github.com/github/spec-kit/actions/workflows/release-trigger.yml" +echo " Click 'Run workflow' and enter version: $VERSION" +echo "" diff --git a/CHANGELOG.md b/CHANGELOG.md index 741ce5d0cb..c38c0f226b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,45 @@ Recent changes to the Specify CLI and templates are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.10] - 2026-03-02 + +### Fixed + +- **Version Sync Issue (#1721)**: Fixed version mismatch between `pyproject.toml` and git release tags + - Split release process into two workflows: `release-trigger.yml` for version management and `release.yml` for artifact building + - Version bump now happens BEFORE tag creation, ensuring tags point to commits with correct version + - Supports both manual version specification and auto-increment (patch version) + - Git tags now accurately reflect the version in `pyproject.toml` at that commit + - Prevents confusion when installing from source + +## [0.1.9] - 2026-02-28 + +### Changed + +- Updated dependency: bumped astral-sh/setup-uv from 6 to 7 + +## [0.1.8] - 2026-02-28 + +### Changed + +- Updated dependency: bumped actions/setup-python from 5 to 6 + +## [0.1.7] - 2026-02-27 + +### Changed + +- Updated outdated GitHub Actions versions +- Documented dual-catalog system for extensions + +### Fixed + +- Fixed version command in documentation + +### Added + +- Added Cleanup Extension to README +- Added retrospective extension to community catalog + ## [0.1.6] - 2026-02-23 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 5f6a2eb7ab..d0fc64b03e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.1.6" +version = "0.1.10" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [