diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh index 13ce11d..8b99398 100755 --- a/.github/scripts/release.sh +++ b/.github/scripts/release.sh @@ -4,7 +4,7 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: .github/scripts/release.sh +Usage: .github/scripts/release.sh Example: .github/scripts/release.sh patch @@ -15,8 +15,9 @@ This script: 1) Adds a new release section under [Unreleased] in CHANGELOG.md 2) Bumps VERSION 3) Creates a commit - 4) Creates a tag - 5) Pushes the tag to origin + 4) Leaves the commit on the current feature branch for a PR + +After that PR merges, run publish on updated main to tag and push the merged commit. EOF } @@ -26,13 +27,44 @@ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || $# -ne 1 ]]; then fi bump_type="$1" +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" + +if [[ "$bump_type" == "publish" ]]; then + if [[ "$(git branch --show-current)" != "main" ]] || [[ -n "$(git status --porcelain)" ]]; then + echo "ERROR: publish requires a clean main checkout." >&2 + exit 1 + fi + git fetch origin main --tags + if [[ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]]; then + echo "ERROR: main must match origin/main before publishing." >&2 + exit 1 + fi + version="$(tr -d '[:space:]' < VERSION)" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || ! grep -Fq "## [$version] - " CHANGELOG.md; then + echo "ERROR: VERSION must match a versioned changelog entry." >&2 + exit 1 + fi + tag="v${version}" + if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "ERROR: tag ${tag} already exists; verify it before retrying." >&2 + exit 1 + fi + git tag "$tag" HEAD + git push origin "refs/tags/$tag" + echo "Published $tag at $(git rev-parse HEAD)" + exit 0 +fi + if [[ "$bump_type" != "major" && "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then echo "ERROR: argument must be one of: major, minor, patch (got: '$bump_type')." exit 1 fi -repo_root="$(git rev-parse --show-toplevel)" -cd "$repo_root" +if [[ "$(git branch --show-current)" == "main" ]] || [[ -z "$(git branch --show-current)" ]]; then + echo "ERROR: prepare the release on a feature branch for a PR." >&2 + exit 1 +fi if [[ ! -f CHANGELOG.md ]]; then echo "ERROR: CHANGELOG.md not found at repo root." @@ -176,10 +208,8 @@ printf '%s\n' "$version" > VERSION git add CHANGELOG.md VERSION git commit -m "chore(release): ${tag}" -git tag "$tag" -git push origin "$tag" echo "Release prepared." echo "- Version: ${effective_version} -> ${version} (VERSION file before bump: ${current_version})" -echo "- Tag created and pushed: ${tag}" -echo "- Commit created locally (not pushed): chore(release): ${tag}" \ No newline at end of file +echo "- Commit created locally: chore(release): ${tag}" +echo "- Open a PR to main, then run .github/scripts/release.sh publish on updated main." \ No newline at end of file diff --git a/.github/scripts/tests/release.sh b/.github/scripts/tests/release.sh new file mode 100644 index 0000000..ea55d31 --- /dev/null +++ b/.github/scripts/tests/release.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail +script="$(cd "$(dirname "$0")/.." && pwd)/release.sh" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +git init --bare -q "$tmp/remote.git" +git init -q -b main "$tmp/work" +cd "$tmp/work" +git config user.email test@example.com +git config user.name Test +git remote add origin "$tmp/remote.git" +printf '1.0.0\n' > VERSION +printf '# Changelog\n\n## [Unreleased]\n\n### Added\n\n- New behavior.\n\n## [1.0.0] - 2026-01-01\n' > CHANGELOG.md +git add VERSION CHANGELOG.md +git commit -qm initial +git push -q -u origin main +git checkout -qb release-test +bash "$script" minor +[[ "$(cat VERSION)" == '1.1.0' ]] +[[ -z "$(git tag -l v1.1.0)" ]] +if bash "$script" publish; then + echo 'Publishing from a feature branch should fail' >&2 + exit 1 +fi +git checkout -q main +git merge -q --ff-only release-test +git push -q origin main +bash "$script" publish +[[ "$(git rev-parse v1.1.0)" == "$(git rev-parse origin/main)" ]] +[[ "$(git ls-remote origin refs/tags/v1.1.0 | cut -f1)" == "$(git rev-parse HEAD)" ]] +if bash "$script" publish; then + echo 'Publishing an existing tag should fail' >&2 + exit 1 +fi diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml index e0f51ba..68fe6e3 100644 --- a/.github/workflows/test-actions.yml +++ b/.github/workflows/test-actions.yml @@ -40,3 +40,11 @@ jobs: persist-credentials: false - name: Verify compliant and divergent fixtures run: bash .github/tests/agent-drift.sh "$GITHUB_WORKSPACE/.agent-config-engine/scripts/check-project-agents.py" + + release-script-tests: + name: release script tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Verify prepare and publish + run: bash .github/scripts/tests/release.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e3429..041fa47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ All contributors (including maintainers) should update `CHANGELOG.md` when creat ### Changed +- Prepare release versions in a PR and publish the tag only after the release commit is merged. + - Agent rules: migrated `.cursor/rules/*.mdc` to `.claude/rules/*.md` (`globs` → `paths`) and removed the remaining Cursor-specific files and references. ## [4.7.0] - 2026-10-29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ffee0e..924f363 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -185,33 +185,11 @@ After opening the PR, maintainers will review and may request changes. Once appr Releases are made from `main`. -1. **Ensure you're on `main` and up to date** - - ```bash - git checkout main - git pull origin main - ``` - -2. **Run the release script** - -```bash - .github/scripts/release.sh patch -``` - -- Accepts exactly one bump type: `major`, `minor`, or `patch` -- Computes the next version from the current root `VERSION` file -- Moves current `[Unreleased]` entries under a new section `## [0.2.0] - YYYY-MM-DD` -- Bumps the root `VERSION` file -- Creates commit `chore(release): vX.Y.Z` -- Creates tag `vX.Y.Z` and pushes that tag to `origin` - -3. **Push commit to `main`** - - ```bash - git push origin main - ``` - -4. **Create a GitHub Release** (optional) from the tag and paste the changelog section for that version. +1. Create a feature branch from an up-to-date `main`. +2. Run `.github/scripts/release.sh minor` (or `patch` / `major`) on the feature branch. The script updates the changelog and version and commits them without creating a tag. +3. Push the branch, open a PR to `main`, and merge it after review and checks. +4. Update a clean local `main` to the merged commit, then run `.github/scripts/release.sh publish`. The script checks that `main` matches `origin/main`, creates the version tag on the merged commit, and pushes it. +5. Optionally create a GitHub Release from that tag and include the version's changelog section. ## License & Attribution