mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 10:53:08 +00:00
Compare commits
1 Commits
v0.1.12
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f237803a2 |
191
.github/workflows/RELEASE-PROCESS.md
vendored
191
.github/workflows/RELEASE-PROCESS.md
vendored
@@ -1,191 +0,0 @@
|
|||||||
# 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+.
|
|
||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch all history for git info
|
fetch-depth: 0 # Fetch all history for git info
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run markdownlint-cli2
|
- name: Run markdownlint-cli2
|
||||||
uses: DavidAnson/markdownlint-cli2-action@v19
|
uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
|
|||||||
161
.github/workflows/release-trigger.yml
vendored
161
.github/workflows/release-trigger.yml
vendored
@@ -1,161 +0,0 @@
|
|||||||
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"
|
|
||||||
54
.github/workflows/release.yml
vendored
54
.github/workflows/release.yml
vendored
@@ -2,60 +2,68 @@ name: Create Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
branches: [ main ]
|
||||||
- 'v*'
|
paths:
|
||||||
|
- '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@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get latest tag
|
||||||
- name: Extract version from tag
|
id: get_tag
|
||||||
id: version
|
|
||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
chmod +x .github/workflows/scripts/get-next-version.sh
|
||||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
.github/workflows/scripts/get-next-version.sh
|
||||||
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.version.outputs.tag }}
|
.github/workflows/scripts/check-release-exists.sh ${{ steps.get_tag.outputs.new_version }}
|
||||||
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.version.outputs.tag }}
|
.github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }}
|
||||||
|
|
||||||
- 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
|
||||||
# Get the previous tag for changelog generation
|
.github/workflows/scripts/generate-release-notes.sh ${{ steps.get_tag.outputs.new_version }} ${{ steps.get_tag.outputs.latest_tag }}
|
||||||
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.version.outputs.tag }}
|
.github/workflows/scripts/create-github-release.sh ${{ steps.get_tag.outputs.new_version }}
|
||||||
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
161
.github/workflows/scripts/simulate-release.sh
vendored
@@ -1,161 +0,0 @@
|
|||||||
#!/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 ""
|
|
||||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
|
|
||||||
@@ -36,10 +36,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
|||||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -7,54 +7,6 @@ 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.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
|
## [0.1.6] - 2026-02-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -456,20 +456,18 @@ Users install with:
|
|||||||
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 3: Community Reference Catalog
|
### Option 3: Extension Catalog (Future)
|
||||||
|
|
||||||
Submit to the community catalog for public discovery:
|
Submit to official catalog:
|
||||||
|
|
||||||
1. **Fork** spec-kit repository
|
1. **Fork** spec-kit repository
|
||||||
2. **Add entry** to `extensions/catalog.community.json`
|
2. **Add entry** to `extensions/catalog.json`
|
||||||
3. **Update** `extensions/README.md` with your extension
|
3. **Create PR**
|
||||||
4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md)
|
4. **After merge**, users can install with:
|
||||||
5. **After merge**, your extension becomes available:
|
|
||||||
- Users can browse `catalog.community.json` to discover your extension
|
|
||||||
- Users copy the entry to their own `catalog.json`
|
|
||||||
- Users install with: `specify extension add my-ext` (from their catalog)
|
|
||||||
|
|
||||||
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed submission instructions.
|
```bash
|
||||||
|
specify extension add my-ext # No URL needed!
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -129,32 +129,26 @@ specify extension add --from https://github.com/your-org/spec-kit-your-extension
|
|||||||
|
|
||||||
## Submit to Catalog
|
## Submit to Catalog
|
||||||
|
|
||||||
### Understanding the Catalogs
|
|
||||||
|
|
||||||
Spec Kit uses a dual-catalog system. For details about how catalogs work, see the main [Extensions README](README.md#extension-catalogs).
|
|
||||||
|
|
||||||
**For extension publishing**: All community extensions should be added to `catalog.community.json`. Users browse this catalog and copy extensions they trust into their own `catalog.json`.
|
|
||||||
|
|
||||||
### 1. Fork the spec-kit Repository
|
### 1. Fork the spec-kit Repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Fork on GitHub
|
# Fork on GitHub
|
||||||
# https://github.com/github/spec-kit/fork
|
# https://github.com/statsperform/spec-kit/fork
|
||||||
|
|
||||||
# Clone your fork
|
# Clone your fork
|
||||||
git clone https://github.com/YOUR-USERNAME/spec-kit.git
|
git clone https://github.com/YOUR-USERNAME/spec-kit.git
|
||||||
cd spec-kit
|
cd spec-kit
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Add Extension to Community Catalog
|
### 2. Add Extension to Catalog
|
||||||
|
|
||||||
Edit `extensions/catalog.community.json` and add your extension:
|
Edit `extensions/catalog.json` and add your extension:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-01-28T15:54:00Z",
|
"updated_at": "2026-01-28T15:54:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
"catalog_url": "https://raw.githubusercontent.com/statsperform/spec-kit/main/extensions/catalog.json",
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"your-extension": {
|
"your-extension": {
|
||||||
"name": "Your Extension Name",
|
"name": "Your Extension Name",
|
||||||
@@ -204,25 +198,15 @@ Edit `extensions/catalog.community.json` and add your extension:
|
|||||||
- Use current timestamp for `created_at` and `updated_at`
|
- Use current timestamp for `created_at` and `updated_at`
|
||||||
- Update the top-level `updated_at` to current time
|
- Update the top-level `updated_at` to current time
|
||||||
|
|
||||||
### 3. Update Extensions README
|
### 3. Submit Pull Request
|
||||||
|
|
||||||
Add your extension to the Available Extensions table in `extensions/README.md`:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
| Your Extension Name | Brief description of what it does | [repo-name](https://github.com/your-org/spec-kit-your-extension) |
|
|
||||||
```
|
|
||||||
|
|
||||||
Insert your extension in alphabetical order in the table.
|
|
||||||
|
|
||||||
### 4. Submit Pull Request
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a branch
|
# Create a branch
|
||||||
git checkout -b add-your-extension
|
git checkout -b add-your-extension
|
||||||
|
|
||||||
# Commit your changes
|
# Commit your changes
|
||||||
git add extensions/catalog.community.json extensions/README.md
|
git add extensions/catalog.json
|
||||||
git commit -m "Add your-extension to community catalog
|
git commit -m "Add your-extension to catalog
|
||||||
|
|
||||||
- Extension ID: your-extension
|
- Extension ID: your-extension
|
||||||
- Version: 1.0.0
|
- Version: 1.0.0
|
||||||
@@ -234,7 +218,7 @@ git commit -m "Add your-extension to community catalog
|
|||||||
git push origin add-your-extension
|
git push origin add-your-extension
|
||||||
|
|
||||||
# Create Pull Request on GitHub
|
# Create Pull Request on GitHub
|
||||||
# https://github.com/github/spec-kit/compare
|
# https://github.com/statsperform/spec-kit/compare
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pull Request Template**:
|
**Pull Request Template**:
|
||||||
@@ -259,8 +243,6 @@ Brief description of what your extension does.
|
|||||||
- [x] Extension tested on real project
|
- [x] Extension tested on real project
|
||||||
- [x] All commands working
|
- [x] All commands working
|
||||||
- [x] No security vulnerabilities
|
- [x] No security vulnerabilities
|
||||||
- [x] Added to extensions/catalog.community.json
|
|
||||||
- [x] Added to extensions/README.md Available Extensions table
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
Tested on:
|
Tested on:
|
||||||
|
|||||||
@@ -76,15 +76,13 @@ vim .specify/extensions/jira/jira-config.yml
|
|||||||
|
|
||||||
## Finding Extensions
|
## Finding Extensions
|
||||||
|
|
||||||
**Note**: By default, `specify extension search` uses your organization's catalog (`catalog.json`). If the catalog is empty, you won't see any results. See [Extension Catalogs](#extension-catalogs) to learn how to populate your catalog from the community reference catalog.
|
|
||||||
|
|
||||||
### Browse All Extensions
|
### Browse All Extensions
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension search
|
specify extension search
|
||||||
```
|
```
|
||||||
|
|
||||||
Shows all extensions in your organization's catalog.
|
Shows all available extensions in the catalog.
|
||||||
|
|
||||||
### Search by Keyword
|
### Search by Keyword
|
||||||
|
|
||||||
@@ -417,15 +415,11 @@ export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Extension Catalogs
|
|
||||||
|
|
||||||
For information about how Spec Kit's dual-catalog system works (`catalog.json` vs `catalog.community.json`), see the main [Extensions README](README.md#extension-catalogs).
|
|
||||||
|
|
||||||
## Organization Catalog Customization
|
## Organization Catalog Customization
|
||||||
|
|
||||||
### Why Customize Your Catalog
|
### Why the Default Catalog is Empty
|
||||||
|
|
||||||
Organizations customize their `catalog.json` to:
|
The default spec-kit catalog ships empty by design. This allows organizations to:
|
||||||
|
|
||||||
- **Control available extensions** - Curate which extensions your team can install
|
- **Control available extensions** - Curate which extensions your team can install
|
||||||
- **Host private extensions** - Internal tools that shouldn't be public
|
- **Host private extensions** - Internal tools that shouldn't be public
|
||||||
|
|||||||
@@ -1,74 +1,8 @@
|
|||||||
# Spec Kit Extensions
|
# Spec Kit Community Extensions
|
||||||
|
|
||||||
Extension system for [Spec Kit](https://github.com/github/spec-kit) - add new functionality without bloating the core framework.
|
Community-contributed extensions for [Spec Kit](https://github.com/github/spec-kit).
|
||||||
|
|
||||||
## Extension Catalogs
|
## Available Extensions
|
||||||
|
|
||||||
Spec Kit provides two catalog files with different purposes:
|
|
||||||
|
|
||||||
### Your Catalog (`catalog.json`)
|
|
||||||
|
|
||||||
- **Purpose**: Default upstream catalog of extensions used by the Spec Kit CLI
|
|
||||||
- **Default State**: Empty by design in the upstream project - you or your organization populate a fork/copy with extensions you trust
|
|
||||||
- **Location (upstream)**: `extensions/catalog.json` in the GitHub-hosted spec-kit repo
|
|
||||||
- **CLI Default**: The `specify extension` commands use the upstream catalog URL by default, unless overridden
|
|
||||||
- **Org Catalog**: Point `SPECKIT_CATALOG_URL` at your organization's fork or hosted catalog JSON to use it instead of the upstream default
|
|
||||||
- **Customization**: Copy entries from the community catalog into your org catalog, or add your own extensions directly
|
|
||||||
|
|
||||||
**Example override:**
|
|
||||||
```bash
|
|
||||||
# Override the default upstream catalog with your organization's catalog
|
|
||||||
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
|
|
||||||
specify extension search # Now uses your organization's catalog instead of the upstream default
|
|
||||||
```
|
|
||||||
|
|
||||||
### Community Reference Catalog (`catalog.community.json`)
|
|
||||||
|
|
||||||
- **Purpose**: Browse available community-contributed extensions
|
|
||||||
- **Status**: Active - contains extensions submitted by the community
|
|
||||||
- **Location**: `extensions/catalog.community.json`
|
|
||||||
- **Usage**: Reference catalog for discovering available extensions
|
|
||||||
- **Submission**: Open to community contributions via Pull Request
|
|
||||||
|
|
||||||
**How It Works:**
|
|
||||||
|
|
||||||
## Making Extensions Available
|
|
||||||
|
|
||||||
You control which extensions your team can discover and install:
|
|
||||||
|
|
||||||
### Option 1: Curated Catalog (Recommended for Organizations)
|
|
||||||
|
|
||||||
Populate your `catalog.json` with approved extensions:
|
|
||||||
|
|
||||||
1. **Discover** extensions from various sources:
|
|
||||||
- Browse `catalog.community.json` for community extensions
|
|
||||||
- Find private/internal extensions in your organization's repos
|
|
||||||
- Discover extensions from trusted third parties
|
|
||||||
2. **Review** extensions and choose which ones you want to make available
|
|
||||||
3. **Add** those extension entries to your own `catalog.json`
|
|
||||||
4. **Team members** can now discover and install them:
|
|
||||||
- `specify extension search` shows your curated catalog
|
|
||||||
- `specify extension add <name>` installs from your catalog
|
|
||||||
|
|
||||||
**Benefits**: Full control over available extensions, team consistency, organizational approval workflow
|
|
||||||
|
|
||||||
**Example**: Copy an entry from `catalog.community.json` to your `catalog.json`, then your team can discover and install it by name.
|
|
||||||
|
|
||||||
### Option 2: Direct URLs (For Ad-hoc Use)
|
|
||||||
|
|
||||||
Skip catalog curation - team members install directly using URLs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**: Quick for one-off testing or private extensions
|
|
||||||
|
|
||||||
**Tradeoff**: Extensions installed this way won't appear in `specify extension search` for other team members unless you also add them to your `catalog.json`.
|
|
||||||
|
|
||||||
## Available Community Extensions
|
|
||||||
|
|
||||||
The following community-contributed extensions are available in [`catalog.community.json`](catalog.community.json):
|
|
||||||
|
|
||||||
| Extension | Purpose | URL |
|
| Extension | Purpose | URL |
|
||||||
|-----------|---------|-----|
|
|-----------|---------|-----|
|
||||||
@@ -77,43 +11,4 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
|
|
||||||
## Adding Your Extension
|
## Adding Your Extension
|
||||||
|
|
||||||
### Submission Process
|
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for instructions on how to submit your extension to the community catalog.
|
||||||
|
|
||||||
To add your extension to the community catalog:
|
|
||||||
|
|
||||||
1. **Prepare your extension** following the [Extension Development Guide](EXTENSION-DEVELOPMENT-GUIDE.md)
|
|
||||||
2. **Create a GitHub release** for your extension
|
|
||||||
3. **Submit a Pull Request** that:
|
|
||||||
- Adds your extension to `extensions/catalog.community.json`
|
|
||||||
- Updates this README with your extension in the Available Extensions table
|
|
||||||
4. **Wait for review** - maintainers will review and merge if criteria are met
|
|
||||||
|
|
||||||
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed step-by-step instructions.
|
|
||||||
|
|
||||||
### Submission Checklist
|
|
||||||
|
|
||||||
Before submitting, ensure:
|
|
||||||
|
|
||||||
- ✅ Valid `extension.yml` manifest
|
|
||||||
- ✅ Complete README with installation and usage instructions
|
|
||||||
- ✅ LICENSE file included
|
|
||||||
- ✅ GitHub release created with semantic version (e.g., v1.0.0)
|
|
||||||
- ✅ Extension tested on a real project
|
|
||||||
- ✅ All commands working as documented
|
|
||||||
|
|
||||||
## Installing Extensions
|
|
||||||
Once extensions are available (either in your catalog or via direct URL), install them:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From your curated catalog (by name)
|
|
||||||
specify extension search # See what's in your catalog
|
|
||||||
specify extension add <extension-name> # Install by name
|
|
||||||
|
|
||||||
# Direct from URL (bypasses catalog)
|
|
||||||
specify extension add --from https://github.com/<org>/<repo>/archive/refs/tags/<version>.zip
|
|
||||||
|
|
||||||
# List installed extensions
|
|
||||||
specify extension list
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information, see the [Extension User Guide](EXTENSION-USER-GUIDE.md).
|
|
||||||
|
|||||||
@@ -858,41 +858,11 @@ def should_execute_hook(hook: dict, config: dict) -> bool:
|
|||||||
|
|
||||||
## Extension Discovery & Catalog
|
## Extension Discovery & Catalog
|
||||||
|
|
||||||
### Dual Catalog System
|
### Central Catalog
|
||||||
|
|
||||||
Spec Kit uses two catalog files with different purposes:
|
|
||||||
|
|
||||||
#### User Catalog (`catalog.json`)
|
|
||||||
|
|
||||||
**URL**: `https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json`
|
**URL**: `https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json`
|
||||||
|
|
||||||
- **Purpose**: Organization's curated catalog of approved extensions
|
**Format**:
|
||||||
- **Default State**: Empty by design - users populate with extensions they trust
|
|
||||||
- **Usage**: Default catalog used by `specify extension` CLI commands
|
|
||||||
- **Control**: Organizations maintain their own fork/version for their teams
|
|
||||||
|
|
||||||
#### Community Reference Catalog (`catalog.community.json`)
|
|
||||||
|
|
||||||
**URL**: `https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json`
|
|
||||||
|
|
||||||
- **Purpose**: Reference catalog of available community-contributed extensions
|
|
||||||
- **Verification**: Community extensions may have `verified: false` initially
|
|
||||||
- **Status**: Active - open for community contributions
|
|
||||||
- **Submission**: Via Pull Request following the Extension Publishing Guide
|
|
||||||
- **Usage**: Browse to discover extensions, then copy to your `catalog.json`
|
|
||||||
|
|
||||||
**How It Works:**
|
|
||||||
|
|
||||||
1. **Discover**: Browse `catalog.community.json` to find available extensions
|
|
||||||
2. **Review**: Evaluate extensions for security, quality, and organizational fit
|
|
||||||
3. **Curate**: Copy approved extension entries from community catalog to your `catalog.json`
|
|
||||||
4. **Install**: Use `specify extension add <name>` (pulls from your curated catalog)
|
|
||||||
|
|
||||||
This approach gives organizations full control over which extensions are available to their teams while maintaining a shared community resource for discovery.
|
|
||||||
|
|
||||||
### Catalog Format
|
|
||||||
|
|
||||||
**Format** (same for both catalogs):
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -961,52 +931,25 @@ specify extension info jira
|
|||||||
|
|
||||||
### Custom Catalogs
|
### Custom Catalogs
|
||||||
|
|
||||||
**⚠️ FUTURE FEATURE - NOT YET IMPLEMENTED**
|
Organizations can host private catalogs:
|
||||||
|
|
||||||
The following catalog management commands are proposed design concepts but are not yet available in the current implementation:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add custom catalog (FUTURE - NOT AVAILABLE)
|
# Add custom catalog
|
||||||
specify extension add-catalog https://internal.company.com/spec-kit/catalog.json
|
specify extension add-catalog https://internal.company.com/spec-kit/catalog.json
|
||||||
|
|
||||||
# Set as default (FUTURE - NOT AVAILABLE)
|
# Set as default
|
||||||
specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json
|
specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json
|
||||||
|
|
||||||
# List catalogs (FUTURE - NOT AVAILABLE)
|
# List catalogs
|
||||||
specify extension catalogs
|
specify extension catalogs
|
||||||
```
|
```
|
||||||
|
|
||||||
**Proposed catalog priority** (future design):
|
**Catalog priority**:
|
||||||
|
|
||||||
1. Project-specific catalog (`.specify/extension-catalogs.yml`) - *not implemented*
|
1. Project-specific catalog (`.specify/extension-catalogs.yml`)
|
||||||
2. User-level catalog (`~/.specify/extension-catalogs.yml`) - *not implemented*
|
2. User-level catalog (`~/.specify/extension-catalogs.yml`)
|
||||||
3. Default GitHub catalog
|
3. Default GitHub catalog
|
||||||
|
|
||||||
#### Current Implementation: SPECKIT_CATALOG_URL
|
|
||||||
|
|
||||||
**The currently available method** for using custom catalogs is the `SPECKIT_CATALOG_URL` environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Point to your organization's catalog
|
|
||||||
export SPECKIT_CATALOG_URL="https://internal.company.com/spec-kit/catalog.json"
|
|
||||||
|
|
||||||
# All extension commands now use your custom catalog
|
|
||||||
specify extension search # Uses custom catalog
|
|
||||||
specify extension add jira # Installs from custom catalog
|
|
||||||
```
|
|
||||||
|
|
||||||
**Requirements:**
|
|
||||||
- URL must use HTTPS (HTTP only allowed for localhost testing)
|
|
||||||
- Catalog must follow the standard catalog.json schema
|
|
||||||
- Must be publicly accessible or accessible within your network
|
|
||||||
|
|
||||||
**Example for testing:**
|
|
||||||
```bash
|
|
||||||
# Test with localhost during development
|
|
||||||
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
|
||||||
specify extension search
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.1.12"
|
version = "0.1.6"
|
||||||
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,19 +351,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,24 +492,13 @@ 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,12 +258,6 @@ 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
|
||||||
@@ -340,12 +334,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
"""
|
|
||||||
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