Compare commits

..

6 Commits

Author SHA1 Message Date
github-actions[bot]
9dc09c0e66 chore: bump version to 0.1.12 2026-03-02 19:31:49 +00:00
Manfred Riem
78ed453e38 fix: use RELEASE_PAT so tag push triggers release workflow (#1736) 2026-03-02 13:31:18 -06:00
Manfred Riem
658ab2a38c fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
* fix: use release branch + PR instead of direct push to main

Bypass branch protection rules by pushing version bump to a
chore/release-vX.Y.Z branch, tagging that commit, then opening
an auto PR to merge back into main. The release workflow still
triggers immediately from the tag push.

* fix: remove --label automated from gh pr create (label does not exist)
2026-03-02 13:16:13 -06:00
Manfred Riem
2c41d3627e 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>
2026-03-02 12:52:13 -06:00
Fabián Silva
b55d00beed fix: prepend YAML frontmatter to Cursor .mdc files (#1699)
* fix: prepend YAML frontmatter to Cursor .mdc files for auto-inclusion

Cursor IDE requires YAML frontmatter with `alwaysApply: true` in .mdc
rule files for them to be automatically loaded. Without this frontmatter,
users must manually configure glob patterns for the rules to take effect.

This fix adds frontmatter generation to both the bash and PowerShell
update-agent-context scripts, handling three scenarios:
- New .mdc file creation (frontmatter prepended after template processing)
- Existing .mdc file update without frontmatter (frontmatter added)
- Existing .mdc file with frontmatter (no duplication)

Closes #669

🤖 Generated with [Claude Code](https://claude.com/code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address Copilot review suggestions

- Handle CRLF line endings in frontmatter detection (grep '^---' instead
  of '^---$')
- Fix double blank line after frontmatter in PowerShell New-AgentFile
- Remove unused tempfile import from tests
- Add encoding="utf-8" to all open() calls for cross-platform safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 08:20:00 -06:00
dependabot[bot]
525eae7f7e chore(deps): bump astral-sh/setup-uv from 6 to 7 (#1709)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 08:09:43 -06:00
10 changed files with 884 additions and 36 deletions

191
.github/workflows/RELEASE-PROCESS.md vendored Normal file
View File

@@ -0,0 +1,191 @@
# 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 to a `chore/release-vX.Y.Z` branch
- Create and push the git tag from that branch
- Open a PR to merge the version bump into `main`
- Trigger the release workflow automatically via the tag push
### 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 to a `chore/release-vX.Y.Z` branch
- Create and push the git tag from that branch
- Open a PR to merge the version bump into `main`
- Trigger the release workflow automatically via the tag push
## What Happens Next
Once the release trigger workflow completes:
1. A `chore/release-vX.Y.Z` branch is pushed with the version bump commit
2. The git tag is pushed, pointing to that commit
3. The **Release Workflow** is automatically triggered by the tag push
4. Release artifacts are built for all supported agents
5. A GitHub Release is created with all assets
6. A PR is opened to merge the version bump branch into `main`
> **Note**: Merge the auto-opened PR after the release is published to keep `main` in sync.
## 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. Create `chore/release-vX.Y.Z` branch
5. Update `pyproject.toml`
6. Update `CHANGELOG.md` from git commits
7. Commit changes
8. Push branch and tag
9. Open PR to merge version bump into `main`
### 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+.

161
.github/workflows/release-trigger.yml vendored Normal file
View File

@@ -0,0 +1,161 @@
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
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}
- 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: Create release branch
run: |
BRANCH="chore/release-${{ steps.version.outputs.tag }}"
git checkout -b "$BRANCH"
echo "branch=$BRANCH" >> $GITHUB_ENV
- 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"
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 "${{ env.branch }}"
git push origin "${{ steps.version.outputs.tag }}"
echo "Branch ${{ env.branch }} and tag ${{ steps.version.outputs.tag }} pushed"
- name: Open pull request
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
gh pr create \
--base main \
--head "${{ env.branch }}" \
--title "chore: bump version to ${{ steps.version.outputs.version }}" \
--body "Automated version bump to ${{ steps.version.outputs.version }}.
This PR was created by the Release Trigger workflow. The git tag \`${{ steps.version.outputs.tag }}\` has already been pushed and the release artifacts are being built.
Merge this PR to record the version bump and changelog update on \`main\`."
- name: Summary
run: |
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
echo "✅ PR opened to merge version bump into main"
echo "🚀 Release workflow is building artifacts from the tag"

View File

@@ -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

161
.github/workflows/scripts/simulate-release.sh vendored Executable file
View File

@@ -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 ""

View File

@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v6
@@ -36,7 +36,7 @@ jobs:
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6

View File

@@ -7,6 +7,54 @@ 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.12] - 2026-03-02
### Changed
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [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

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.1.6"
version = "0.1.12"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [

View File

@@ -351,10 +351,19 @@ create_new_agent_file() {
# Convert \n sequences to actual newlines
newline=$(printf '\n')
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
# Clean up backup files
rm -f "$temp_file.bak" "$temp_file.bak2"
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if [[ "$target_file" == *.mdc ]]; then
local frontmatter_file
frontmatter_file=$(mktemp) || return 1
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
cat "$temp_file" >> "$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
return 0
}
@@ -492,13 +501,24 @@ update_existing_agent_file() {
changes_entries_added=true
fi
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if [[ "$target_file" == *.mdc ]]; then
if ! head -1 "$temp_file" | grep -q '^---'; then
local frontmatter_file
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
cat "$temp_file" >> "$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
fi
# Move temp file to target atomically
if ! mv "$temp_file" "$target_file"; then
log_error "Failed to update target file"
rm -f "$temp_file"
return 1
fi
return 0
}
#==============================================================================

View File

@@ -258,6 +258,12 @@ function New-AgentFile {
# Convert literal \n sequences introduced by Escape to real newlines
$content = $content -replace '\\n',[Environment]::NewLine
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if ($TargetFile -match '\.mdc$') {
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine
$content = $frontmatter + $content
}
$parent = Split-Path -Parent $TargetFile
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
@@ -334,6 +340,12 @@ function Update-ExistingAgentFile {
$newTechEntries | ForEach-Object { $output.Add($_) }
}
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') {
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','')
$output.InsertRange(0, $frontmatter)
}
Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8
return $true
}

View File

@@ -0,0 +1,263 @@
"""
Tests for Cursor .mdc frontmatter generation (issue #669).
Verifies that update-agent-context.sh properly prepends YAML frontmatter
to .mdc files so that Cursor IDE auto-includes the rules.
"""
import os
import shutil
import subprocess
import textwrap
import pytest
SCRIPT_PATH = os.path.join(
os.path.dirname(__file__),
os.pardir,
"scripts",
"bash",
"update-agent-context.sh",
)
EXPECTED_FRONTMATTER_LINES = [
"---",
"description: Project Development Guidelines",
'globs: ["**/*"]',
"alwaysApply: true",
"---",
]
requires_git = pytest.mark.skipif(
shutil.which("git") is None,
reason="git is not installed",
)
class TestScriptFrontmatterPattern:
"""Static analysis — no git required."""
def test_create_new_has_mdc_frontmatter_logic(self):
"""create_new_agent_file() must contain .mdc frontmatter logic."""
with open(SCRIPT_PATH, encoding="utf-8") as f:
content = f.read()
assert 'if [[ "$target_file" == *.mdc ]]' in content
assert "alwaysApply: true" in content
def test_update_existing_has_mdc_frontmatter_logic(self):
"""update_existing_agent_file() must also handle .mdc frontmatter."""
with open(SCRIPT_PATH, encoding="utf-8") as f:
content = f.read()
# There should be two occurrences of the .mdc check — one per function
occurrences = content.count('if [[ "$target_file" == *.mdc ]]')
assert occurrences >= 2, (
f"Expected at least 2 .mdc frontmatter checks, found {occurrences}"
)
def test_powershell_script_has_mdc_frontmatter_logic(self):
"""PowerShell script must also handle .mdc frontmatter."""
ps_path = os.path.join(
os.path.dirname(__file__),
os.pardir,
"scripts",
"powershell",
"update-agent-context.ps1",
)
with open(ps_path, encoding="utf-8") as f:
content = f.read()
assert "alwaysApply: true" in content
occurrences = content.count(r"\.mdc$")
assert occurrences >= 2, (
f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}"
)
@requires_git
class TestCursorFrontmatterIntegration:
"""Integration tests using a real git repo."""
@pytest.fixture
def git_repo(self, tmp_path):
"""Create a minimal git repo with the spec-kit structure."""
repo = tmp_path / "repo"
repo.mkdir()
# Init git repo
subprocess.run(
["git", "init"], cwd=str(repo), capture_output=True, check=True
)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=str(repo),
capture_output=True,
check=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=str(repo),
capture_output=True,
check=True,
)
# Create .specify dir with config
specify_dir = repo / ".specify"
specify_dir.mkdir()
(specify_dir / "config.yaml").write_text(
textwrap.dedent("""\
project_type: webapp
language: python
framework: fastapi
database: N/A
""")
)
# Create template
templates_dir = specify_dir / "templates"
templates_dir.mkdir()
(templates_dir / "agent-file-template.md").write_text(
"# [PROJECT NAME] Development Guidelines\n\n"
"Auto-generated from all feature plans. Last updated: [DATE]\n\n"
"## Active Technologies\n\n"
"[EXTRACTED FROM ALL PLAN.MD FILES]\n\n"
"## Project Structure\n\n"
"[ACTUAL STRUCTURE FROM PLANS]\n\n"
"## Development Commands\n\n"
"[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n"
"## Coding Conventions\n\n"
"[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n"
"## Recent Changes\n\n"
"[LAST 3 FEATURES AND WHAT THEY ADDED]\n"
)
# Create initial commit
subprocess.run(
["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True
)
subprocess.run(
["git", "commit", "-m", "init"],
cwd=str(repo),
capture_output=True,
check=True,
)
# Create a feature branch so CURRENT_BRANCH detection works
subprocess.run(
["git", "checkout", "-b", "001-test-feature"],
cwd=str(repo),
capture_output=True,
check=True,
)
# Create a spec so the script detects the feature
spec_dir = repo / "specs" / "001-test-feature"
spec_dir.mkdir(parents=True)
(spec_dir / "plan.md").write_text(
"# Test Feature Plan\n\n"
"## Technology Stack\n\n"
"- Language: Python\n"
"- Framework: FastAPI\n"
)
return repo
def _run_update(self, repo, agent_type="cursor-agent"):
"""Run update-agent-context.sh for a specific agent type."""
script = os.path.abspath(SCRIPT_PATH)
result = subprocess.run(
["bash", script, agent_type],
cwd=str(repo),
capture_output=True,
text=True,
timeout=30,
)
return result
def test_new_mdc_file_has_frontmatter(self, git_repo):
"""Creating a new .mdc file must include YAML frontmatter."""
result = self._run_update(git_repo)
assert result.returncode == 0, f"Script failed: {result.stderr}"
mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc"
assert mdc_file.exists(), "Cursor .mdc file was not created"
content = mdc_file.read_text()
lines = content.splitlines()
# First line must be the opening ---
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
# Check all frontmatter lines are present
for expected in EXPECTED_FRONTMATTER_LINES:
assert expected in content, f"Missing frontmatter line: {expected}"
# Content after frontmatter should be the template content
assert "Development Guidelines" in content
def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo):
"""Updating an existing .mdc file that lacks frontmatter must add it."""
# First, create the file WITHOUT frontmatter (simulating pre-fix state)
cursor_dir = git_repo / ".cursor" / "rules"
cursor_dir.mkdir(parents=True, exist_ok=True)
mdc_file = cursor_dir / "specify-rules.mdc"
mdc_file.write_text(
"# repo Development Guidelines\n\n"
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
"## Active Technologies\n\n"
"- Python + FastAPI (main)\n\n"
"## Recent Changes\n\n"
"- main: Added Python + FastAPI\n"
)
result = self._run_update(git_repo)
assert result.returncode == 0, f"Script failed: {result.stderr}"
content = mdc_file.read_text()
lines = content.splitlines()
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
for expected in EXPECTED_FRONTMATTER_LINES:
assert expected in content, f"Missing frontmatter line: {expected}"
def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo):
"""Updating an .mdc file that already has frontmatter must not duplicate it."""
cursor_dir = git_repo / ".cursor" / "rules"
cursor_dir.mkdir(parents=True, exist_ok=True)
mdc_file = cursor_dir / "specify-rules.mdc"
frontmatter = (
"---\n"
"description: Project Development Guidelines\n"
'globs: ["**/*"]\n'
"alwaysApply: true\n"
"---\n\n"
)
body = (
"# repo Development Guidelines\n\n"
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
"## Active Technologies\n\n"
"- Python + FastAPI (main)\n\n"
"## Recent Changes\n\n"
"- main: Added Python + FastAPI\n"
)
mdc_file.write_text(frontmatter + body)
result = self._run_update(git_repo)
assert result.returncode == 0, f"Script failed: {result.stderr}"
content = mdc_file.read_text()
# Count occurrences of the frontmatter delimiter
assert content.count("alwaysApply: true") == 1, (
"Frontmatter was duplicated"
)
def test_non_mdc_file_has_no_frontmatter(self, git_repo):
"""Non-.mdc agent files (e.g., Claude) must NOT get frontmatter."""
result = self._run_update(git_repo, agent_type="claude")
assert result.returncode == 0, f"Script failed: {result.stderr}"
claude_file = git_repo / ".claude" / "CLAUDE.md"
if claude_file.exists():
content = claude_file.read_text()
assert not content.startswith("---"), (
"Non-mdc file should not have frontmatter"
)