mirror of
https://github.com/github/spec-kit.git
synced 2026-03-18 19:33:09 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33fd13c304 | ||
|
|
658ab2a38c | ||
|
|
2c41d3627e | ||
|
|
b55d00beed | ||
|
|
525eae7f7e |
191
.github/workflows/RELEASE-PROCESS.md
vendored
Normal file
191
.github/workflows/RELEASE-PROCESS.md
vendored
Normal 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
161
.github/workflows/release-trigger.yml
vendored
Normal 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.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: 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.GITHUB_TOKEN }}
|
||||||
|
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"
|
||||||
52
.github/workflows/release.yml
vendored
52
.github/workflows/release.yml
vendored
@@ -2,68 +2,60 @@ name: Create Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
tags:
|
||||||
paths:
|
- 'v*'
|
||||||
- 'memory/**'
|
|
||||||
- 'scripts/**'
|
|
||||||
- 'src/**'
|
|
||||||
- 'templates/**'
|
|
||||||
- '.github/workflows/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Get latest tag
|
|
||||||
id: get_tag
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/get-next-version.sh
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
.github/workflows/scripts/get-next-version.sh
|
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Building release for $VERSION"
|
||||||
|
|
||||||
- name: Check if release already exists
|
- name: Check if release already exists
|
||||||
id: check_release
|
id: check_release
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/check-release-exists.sh
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create release package variants
|
- name: Create release package variants
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/create-release-packages.sh
|
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
|
- name: Generate release notes
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
id: release_notes
|
id: release_notes
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/generate-release-notes.sh
|
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
|
- name: Create GitHub Release
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/create-github-release.sh
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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
161
.github/workflows/scripts/simulate-release.sh
vendored
Executable 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 ""
|
||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
|
|||||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -7,6 +7,53 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.11] - 2026-03-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 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
|
## [0.1.6] - 2026-02-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.1.6"
|
version = "0.1.11"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -351,10 +351,19 @@ create_new_agent_file() {
|
|||||||
# Convert \n sequences to actual newlines
|
# Convert \n sequences to actual newlines
|
||||||
newline=$(printf '\n')
|
newline=$(printf '\n')
|
||||||
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
|
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
|
||||||
|
|
||||||
# Clean up backup files
|
# Clean up backup files
|
||||||
rm -f "$temp_file.bak" "$temp_file.bak2"
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,13 +501,24 @@ update_existing_agent_file() {
|
|||||||
changes_entries_added=true
|
changes_entries_added=true
|
||||||
fi
|
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
|
# Move temp file to target atomically
|
||||||
if ! mv "$temp_file" "$target_file"; then
|
if ! mv "$temp_file" "$target_file"; then
|
||||||
log_error "Failed to update target file"
|
log_error "Failed to update target file"
|
||||||
rm -f "$temp_file"
|
rm -f "$temp_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -258,6 +258,12 @@ function New-AgentFile {
|
|||||||
# Convert literal \n sequences introduced by Escape to real newlines
|
# Convert literal \n sequences introduced by Escape to real newlines
|
||||||
$content = $content -replace '\\n',[Environment]::NewLine
|
$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
|
$parent = Split-Path -Parent $TargetFile
|
||||||
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
|
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
|
||||||
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
|
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
|
||||||
@@ -334,6 +340,12 @@ function Update-ExistingAgentFile {
|
|||||||
$newTechEntries | ForEach-Object { $output.Add($_) }
|
$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
|
Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|||||||
263
tests/test_cursor_frontmatter.py
Normal file
263
tests/test_cursor_frontmatter.py
Normal 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"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user