Skip to content

Commit d03e7e9

Browse files
dtomsclaude
andcommitted
feat: AppFolio spec-kit fork — specify-af-cli with bundled extensions
Single commit containing all AppFolio fork changes on top of upstream v0.6.1. Designed to sit as one commit for clean upstream merges. - Rename package to specify-af-cli with specify-af binary - Bundle af-appfolio extension (auto-installed on init) - Add upgrade command to sync extensions to bundled versions - Fix version resolution via packages_distributions() - Rewire catalog URL to appfolio/spec-kit af-main branch - Add release-please workflow with af-v* tag convention - Add FORK.md documenting changes and maintenance procedures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2499c9a commit d03e7e9

31 files changed

Lines changed: 1042 additions & 19 deletions

.github/workflows/af-release.yml

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
name: AF Release
2+
3+
on:
4+
push:
5+
branches:
6+
- af-main
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
release:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v6
18+
with:
19+
fetch-depth: 0
20+
fetch-tags: true
21+
token: ${{ secrets.GITHUB_TOKEN }}
22+
23+
- name: Setup Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Compute next version
29+
id: version
30+
run: |
31+
# Read current version from pyproject.toml
32+
CURRENT=$(python3 -c "
33+
import tomllib
34+
with open('pyproject.toml', 'rb') as f:
35+
print(tomllib.load(f)['project']['version'])
36+
")
37+
echo "current=$CURRENT" >> $GITHUB_OUTPUT
38+
39+
# Find latest af-v* tag
40+
LATEST_TAG=$(git tag -l 'af-v*' --sort=-version:refname | head -n 1)
41+
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
42+
43+
# Bump fourth segment
44+
IFS='.' read -ra PARTS <<< "$CURRENT"
45+
if [ ${#PARTS[@]} -le 3 ]; then
46+
NEXT="${CURRENT}.1"
47+
else
48+
LAST=${PARTS[3]}
49+
NEXT="${PARTS[0]}.${PARTS[1]}.${PARTS[2]}.$((LAST + 1))"
50+
fi
51+
echo "next=$NEXT" >> $GITHUB_OUTPUT
52+
53+
# Check if this version is already tagged
54+
if git tag -l "af-v${NEXT}" | grep -q .; then
55+
echo "skip=true" >> $GITHUB_OUTPUT
56+
else
57+
echo "skip=false" >> $GITHUB_OUTPUT
58+
fi
59+
60+
- name: Generate changelog
61+
if: steps.version.outputs.skip == 'false'
62+
id: changelog
63+
run: |
64+
LATEST_TAG="${{ steps.version.outputs.latest_tag }}"
65+
NEXT="${{ steps.version.outputs.next }}"
66+
67+
if [ -z "$LATEST_TAG" ]; then
68+
# First release — use commits since the upstream base tag
69+
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges v0.6.1..HEAD)
70+
else
71+
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges "${LATEST_TAG}..HEAD")
72+
fi
73+
74+
DATE=$(date +%Y-%m-%d)
75+
ENTRY="## ${NEXT} (${DATE})"$'\n\n'"${COMMITS}"
76+
77+
# Write to file for later use (avoids multiline env var issues)
78+
echo "$ENTRY" > /tmp/changelog_entry.txt
79+
echo "date=$DATE" >> $GITHUB_OUTPUT
80+
81+
- name: Check for existing release PR
82+
if: steps.version.outputs.skip == 'false'
83+
id: existing_pr
84+
run: |
85+
PR_NUM=$(gh pr list --repo ${{ github.repository }} \
86+
--head "af-release" --base "af-main" \
87+
--json number --jq '.[0].number // empty')
88+
echo "number=${PR_NUM}" >> $GITHUB_OUTPUT
89+
env:
90+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
91+
92+
- name: Create or update release branch
93+
if: steps.version.outputs.skip == 'false'
94+
run: |
95+
NEXT="${{ steps.version.outputs.next }}"
96+
97+
git config user.name "github-actions[bot]"
98+
git config user.email "github-actions[bot]@users.noreply.github.com"
99+
100+
# Create/reset release branch from af-main
101+
git checkout -B af-release
102+
103+
# Bump version in pyproject.toml
104+
python3 -c "
105+
import re
106+
with open('pyproject.toml', 'r') as f:
107+
content = f.read()
108+
content = re.sub(
109+
r'(version\s*=\s*\")([^\"]+)(\")',
110+
lambda m: m.group(1) + '${NEXT}' + m.group(3),
111+
content,
112+
count=1
113+
)
114+
with open('pyproject.toml', 'w') as f:
115+
f.write(content)
116+
"
117+
118+
# Prepend changelog to FORK.md at the marker
119+
python3 << 'PYEOF'
120+
marker = "<!-- release-please writes changelog entries below this line -->"
121+
with open("/tmp/changelog_entry.txt") as f:
122+
changelog = f.read().strip()
123+
with open("FORK.md", "r") as f:
124+
content = f.read()
125+
if marker in content:
126+
content = content.replace(marker, marker + "\n\n" + changelog)
127+
else:
128+
content += "\n\n" + changelog
129+
with open("FORK.md", "w") as f:
130+
f.write(content)
131+
PYEOF
132+
133+
git add pyproject.toml FORK.md
134+
git commit -m "chore: release af-v${NEXT}"
135+
git push -f origin af-release
136+
137+
- name: Create or update PR
138+
if: steps.version.outputs.skip == 'false'
139+
run: |
140+
NEXT="${{ steps.version.outputs.next }}"
141+
EXISTING="${{ steps.existing_pr.outputs.number }}"
142+
CHANGELOG=$(cat /tmp/changelog_entry.txt)
143+
144+
BODY="$(cat <<EOF
145+
## Release af-v${NEXT}
146+
147+
${CHANGELOG}
148+
149+
---
150+
Merging this PR will trigger a tagged release with extension ZIPs.
151+
EOF
152+
)"
153+
154+
if [ -n "$EXISTING" ]; then
155+
gh pr edit "$EXISTING" \
156+
--title "chore: release af-v${NEXT}" \
157+
--body "$BODY"
158+
echo "Updated PR #${EXISTING}"
159+
else
160+
gh pr create \
161+
--title "chore: release af-v${NEXT}" \
162+
--body "$BODY" \
163+
--head af-release \
164+
--base af-main
165+
fi
166+
env:
167+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: AF Tag Release
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches:
7+
- af-main
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
tag:
14+
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'af-release')
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v6
19+
with:
20+
token: ${{ secrets.GITHUB_TOKEN }}
21+
22+
- name: Read version and tag
23+
run: |
24+
VERSION=$(python3 -c "
25+
import tomllib
26+
with open('pyproject.toml', 'rb') as f:
27+
print(tomllib.load(f)['project']['version'])
28+
")
29+
TAG="af-v${VERSION}"
30+
31+
echo "Tagging ${TAG}"
32+
git tag "$TAG"
33+
git push origin "$TAG"

.github/workflows/release.yml

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Create Release
33
on:
44
push:
55
tags:
6-
- 'v*'
6+
- 'af-v*'
77

88
jobs:
99
release:
@@ -42,10 +42,10 @@ jobs:
4242
if: steps.check_release.outputs.exists == 'false'
4343
run: |
4444
VERSION="${{ steps.version.outputs.tag }}"
45-
VERSION_NO_V=${VERSION#v}
45+
VERSION_NO_PREFIX=${VERSION#af-v}
4646
4747
# Find previous tag
48-
PREVIOUS_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${VERSION}$" | head -n 1)
48+
PREVIOUS_TAG=$(git tag -l 'af-v*' --sort=-version:refname | grep -v "^${VERSION}$" | head -n 1)
4949
if [ -z "$PREVIOUS_TAG" ]; then
5050
PREVIOUS_TAG=""
5151
fi
@@ -66,8 +66,8 @@ jobs:
6666
## Install
6767
6868
\`\`\`bash
69-
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@${VERSION}
70-
specify init my-project
69+
uv tool install specify-af-cli --from git+https://github.com/appfolio/spec-kit.git@${VERSION}
70+
specify-af init my-project
7171
\`\`\`
7272
7373
NOTES_EOF
@@ -76,14 +76,30 @@ jobs:
7676
echo "" >> release_notes.md
7777
echo "$COMMITS" >> release_notes.md
7878
79+
- name: Build extension ZIPs
80+
if: steps.check_release.outputs.exists == 'false'
81+
run: |
82+
mkdir -p dist/extensions
83+
for ext_dir in extensions/af-*/; do
84+
ext_name=$(basename "$ext_dir")
85+
if [ -f "$ext_dir/extension.yml" ]; then
86+
(cd extensions && zip -r "../dist/extensions/${ext_name}.zip" "$ext_name")
87+
echo "Built ${ext_name}.zip"
88+
fi
89+
done
90+
7991
- name: Create GitHub Release
8092
if: steps.check_release.outputs.exists == 'false'
8193
run: |
8294
VERSION="${{ steps.version.outputs.tag }}"
83-
VERSION_NO_V=${VERSION#v}
95+
VERSION_NO_PREFIX=${VERSION#af-v}
96+
ASSETS=""
97+
for zip in dist/extensions/*.zip; do
98+
[ -f "$zip" ] && ASSETS="$ASSETS $zip"
99+
done
84100
gh release create "$VERSION" \
85-
--title "Spec Kit - $VERSION_NO_V" \
86-
--notes-file release_notes.md
101+
--title "Specify AF CLI - $VERSION_NO_PREFIX" \
102+
--notes-file release_notes.md \
103+
$ASSETS
87104
env:
88105
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
89-

FORK.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# AppFolio Spec Kit Fork (specify-af-cli)
2+
3+
This is AppFolio's fork of [github/spec-kit](https://github.com/github/spec-kit). It bundles AppFolio-specific extensions into the CLI for zero-config developer setup.
4+
5+
## What's Changed from Upstream
6+
7+
| Area | Change |
8+
|------|--------|
9+
| Package name | `specify-cli``specify-af-cli` |
10+
| Binary name | `specify``specify-af` |
11+
| Catalog URL | Points at `appfolio/spec-kit` `af-main` branch |
12+
| Init flow | Auto-installs bundled AF extensions after scaffold |
13+
| New command | `specify-af upgrade` — syncs AF extensions to bundled versions |
14+
| Version resolution | Uses `packages_distributions()` instead of hardcoded package name |
15+
| Release workflow | release-please with `af-v*` tags on `af-main` |
16+
17+
## Conflict-Prone Files
18+
19+
When merging upstream releases, expect conflicts in these files:
20+
21+
- `pyproject.toml` — keep AF name, console script, and force-include entries
22+
- `src/specify_cli/__init__.py` — keep version fix (`_get_distribution_name`), init hook (`install_af_extensions`), upgrade command, panel title
23+
- `src/specify_cli/extensions.py` — keep AF catalog URL
24+
- `src/specify_cli/presets.py` — keep AF package name in error message
25+
- `extensions/catalog.json` — keep AF entries and catalog URL
26+
- `.github/workflows/release.yml` — keep `af-v*` tag filter, AF install URL, extension ZIP step
27+
28+
## How to Maintain
29+
30+
### Local Development Setup
31+
32+
```bash
33+
git clone git@github.com:appfolio/spec-kit.git
34+
cd spec-kit
35+
git checkout af-main
36+
uv tool install . --force
37+
specify-af version
38+
```
39+
40+
### Making Changes
41+
42+
```bash
43+
git checkout -b feat/my-feature af-main
44+
# Make changes (use Conventional Commits: feat:, fix:, chore:, etc.)
45+
uv tool install . --force --reinstall
46+
cd /path/to/test-project && specify-af init --here --ai claude
47+
git push -u origin feat/my-feature
48+
# Open PR targeting af-main
49+
```
50+
51+
### Adding a New AF Extension
52+
53+
1. Create `extensions/af-<name>/` with `extension.yml`, `README.md`, `commands/`
54+
2. Add `"af-<name>"` to `AF_EXTENSION_IDS` in `src/specify_cli/af_init.py`
55+
3. Add `"extensions/af-<name>" = "specify_cli/core_pack/extensions/af-<name>"` to `force-include` in `pyproject.toml`
56+
4. Add entry to `extensions/catalog.json`
57+
58+
### Integrating an Upstream Release
59+
60+
```bash
61+
git fetch upstream --tags
62+
git checkout af-main
63+
git merge v<upstream-version>
64+
# Resolve conflicts (see Conflict-Prone Files above)
65+
# Test: uv tool install . --force --reinstall && specify-af init --here
66+
```
67+
68+
If integration work is needed:
69+
```bash
70+
git checkout -b feat/integrate-v<version>
71+
# Fix, test, open PR to af-main
72+
```
73+
74+
## Release History
75+
76+
<!-- release-please writes changelog entries below this line -->

extensions/af-appfolio/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# AppFolio Extension for Spec Kit
2+
3+
AppFolio lifecycle hooks and commands for Spec Kit. This extension provides hook points at every stage of the spec-kit workflow, allowing AppFolio-specific behavior to be injected before and after each operation.
4+
5+
## Hook Points
6+
7+
| Lifecycle Phase | Before | After |
8+
|----------------|--------|-------|
9+
| specify | `before_specify` | `after_specify` |
10+
| plan | `before_plan` | `after_plan` |
11+
| tasks | `before_tasks` | `after_tasks` |
12+
| implement | `before_implement` | `after_implement` |
13+
| analyze | `before_analyze` | `after_analyze` |
14+
| checklist | `before_checklist` | `after_checklist` |
15+
| clarify | `before_clarify` | `after_clarify` |
16+
| constitution | `before_constitution` | `after_constitution` |
17+
| taskstoissues | `before_taskstoissues` | `after_taskstoissues` |
18+
19+
## Installation
20+
21+
This extension is bundled with `af-specify-cli` and installed automatically during `af-specify init`.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
description: "AppFolio after-analyze lifecycle hook"
3+
---
4+
5+
# AppFolio: after-analyze
6+
7+
This is an internal lifecycle hook. It is designed to be triggered automatically by spec-kit at the appropriate point in the workflow.
8+
9+
If you are executing this because a user typed this command directly (not because spec-kit fired it as a hook), STOP. Reply with:
10+
11+
> This is an internal lifecycle hook and cannot be run directly. It runs automatically during the spec-kit workflow.
12+
13+
Do not proceed further.
14+
15+
## Hook Logic
16+
17+
<!-- TODO: Add instructions for what should happen at this lifecycle point -->

0 commit comments

Comments
 (0)