diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2c0ce78 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +jobs: + release: + name: Create GitHub release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Derive version from tag + id: version + run: | + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#v}" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Verify tag matches plugin.json version + run: | + MANIFEST_VERSION="$(jq -r '.version' .claude-plugin/plugin.json)" + if [ "$MANIFEST_VERSION" != "${{ steps.version.outputs.version }}" ]; then + echo "::error::Tag ${{ steps.version.outputs.tag }} does not match plugin.json version ${MANIFEST_VERSION}. Bump plugin.json or retag." + exit 1 + fi + echo "Manifest version ${MANIFEST_VERSION} matches tag." + + - name: Extract release notes from CHANGELOG + id: notes + run: | + VERSION="${{ steps.version.outputs.version }}" + # Print the body of the [VERSION] section, stopping at the next ## heading. + awk -v ver="$VERSION" ' + $0 ~ "^## \\[" ver "\\]" { capture = 1; next } + capture && /^## \[/ { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + if [ ! -s release-notes.md ]; then + echo "::error::No CHANGELOG section found for [${VERSION}]. Add a '## [${VERSION}] - ' entry before tagging." + exit 1 + fi + echo "Extracted release notes for ${VERSION}:" + cat release-notes.md + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "${{ steps.version.outputs.tag }}" \ + --notes-file release-notes.md \ + --latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eaa4f0..33f0cd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,11 +26,26 @@ The `main` branch is protected; all changes go through pull requests. 1. **Fork and branch.** Create a feature branch from `main`: `git checkout -b feat/` or `fix/`. 2. **Make your change.** Keep PRs focused on one concern. If you're touching hook scripts, follow the existing `set -euo pipefail` and library-sourcing pattern. -3. **Run the tests.** `bats tests/` from the repo root. All 69 tests must pass. Add tests for any new behavior. +3. **Run the tests.** `bats tests/` from the repo root. All tests must pass. Add tests for any new behavior. 4. **Update docs.** If you add or change a subagent, hook, or command, update the matching file under `docs/reference/`. 5. **Update CHANGELOG.md** under the `[Unreleased]` heading. 6. **Open a PR** with a clear title (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`) and a Summary + Test plan section. +## Releasing + +Releases are cut by pushing a version tag. The `Release` workflow (`.github/workflows/release.yml`) does the rest. + +1. **Land the version bump.** A merged PR should already have bumped `.claude-plugin/plugin.json` to the new `X.Y.Z` and moved its CHANGELOG entries from `[Unreleased]` into a `## [X.Y.Z] - ` section. Follow [SemVer](https://semver.org): MINOR for new backward-compatible features, PATCH for fixes. +2. **Tag `main` and push the tag:** + ```bash + git checkout main && git pull + git tag -a vX.Y.Z -m "vX.Y.Z - " + git push origin vX.Y.Z + ``` +3. **The workflow takes over.** On a `v*.*.*` tag push it: verifies the tag matches `plugin.json` (fails loudly if not), extracts the `## [X.Y.Z]` section from `CHANGELOG.md` as the release notes, and creates the GitHub release marked `--latest`. + +A merged PR alone does NOT publish a release: the marketplace serves released versions, so an untagged bump never reaches installs. Always push the tag. + ## Local development Install dependencies (macOS):