diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b821405 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + generate_release_notes: true diff --git a/CLAUDE.md b/CLAUDE.md index c5ff692..9d5e6cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,6 +65,19 @@ not just this one. Every change we make must account for the experience of those - **Discuss trade-offs**: if a feature requires consumers to make non-trivial changes (new secrets, new workflow jobs, new permissions), flag it in the PR for a deliberate decision. +## Versioning & Releases + +We use semver tags (`v1.0.0`) alongside a floating major tag (`v1`) — consumers pin to `@v1`. +Only bump the major version for breaking changes (e.g. removed/renamed inputs, new required secrets). + +To release: + +```bash +./scripts/release.sh 1.0.0 +``` + +Always use the script — it handles both tags atomically. Never tag manually. + ## Branching Convention - Features: `feat/` diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..1811616 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 1.0.0" + exit 1 +fi + +VERSION="$1" + +# Strip leading 'v' if provided +VERSION="${VERSION#v}" + +# Validate semver format +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: '$VERSION' is not a valid semantic version (expected X.Y.Z)" + exit 1 +fi + +MAJOR="${VERSION%%.*}" +TAG="v${VERSION}" +MAJOR_TAG="v${MAJOR}" + +# Ensure we're on a clean working tree +if [ -n "$(git status --porcelain)" ]; then + echo "Error: working tree is not clean. Commit or stash changes first." + exit 1 +fi + +# Create the immutable semver tag +if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Error: tag '$TAG' already exists. Semver tags are immutable." + exit 1 +fi + +git tag "$TAG" +echo "Created tag $TAG" + +# Force-move the floating major tag +git tag -f "$MAJOR_TAG" +echo "Moved tag $MAJOR_TAG -> $(git rev-parse --short HEAD)" + +# Push both tags +git push origin "$TAG" +git push origin "$MAJOR_TAG" --force + +echo "" +echo "Release complete:" +echo " $TAG (immutable semver tag)" +echo " $MAJOR_TAG (floating major tag, force-updated)"