diff --git a/.cursor/skills/release/SKILL.md b/.cursor/skills/release/SKILL.md new file mode 100644 index 0000000..c3c3486 --- /dev/null +++ b/.cursor/skills/release/SKILL.md @@ -0,0 +1,90 @@ +--- +name: release +description: >- + Prepares a version bump in pyproject.toml, opens a PR from branch release/VERSION + toward main with auto-merge, and coordinates with CI that publishes a GitHub Release + when that branch merges. Use when the user invokes /release, /release VERSION, asks + for a release PR, version bump, or release automation. +--- + +# Release (`/release` and optional VERSION) + +## When this applies + +- User message starts with **`/release`** or **`/release VERSION`** (VERSION optional). +- User asks to cut a release, bump the package version, or open a release PR with auto-merge. + +## Preconditions + +- Working tree clean (`git status`); stash or commit unrelated work first. +- `gh` CLI authenticated (`gh auth status`). +- Remote `origin` is GitHub. +- Repository allows **auto-merge** (Settings → General → Pull Requests → Allow auto-merge). If auto-merge is unavailable, open the PR anyway and tell the user to merge manually after checks pass. + +## Version selection + +1. Read the current version from `pyproject.toml` under `[project]` → `version` (PEP 440 / semver `MAJOR.MINOR.PATCH`). +2. If **VERSION was provided**: set the new version to that string (must match `^\d+\.\d+\.\d+` unless the project already uses a different scheme—then follow existing `pyproject.toml` format). +3. If **VERSION was omitted**: bump the **patch** segment only (e.g. `0.2.1` → `0.2.2`). If the current value is not `x.y.z`, stop and ask the user for an explicit VERSION. + +## Git identity (this repo) + +Configure once if needed: + +```bash +git config user.email "cursor@proxymesh.com" +git config user.name "Cursor" +``` + +## Steps + +1. **Sync main** + + ```bash + git fetch origin main + ``` + +2. **Compute** `NEW_VERSION` (per rules above). **Branch name** is `release/${NEW_VERSION}` (no `v` prefix in the branch name). + +3. **Create branch from latest main** + + ```bash + git checkout -B "release/${NEW_VERSION}" origin/main + ``` + +4. **Edit** `pyproject.toml`: set `version = "NEW_VERSION"` in `[project]`. + +5. **Commit and push** (never push to `main`; push only the release branch) + + ```bash + git add pyproject.toml + git commit -m "chore: bump version to ${NEW_VERSION}" + git push -u origin "release/${NEW_VERSION}" + ``` + +6. **Open PR** into `main` with a short body (no Cursor boilerplate). Example: + + ```bash + gh pr create --base main --head "release/${NEW_VERSION}" \ + --title "Release ${NEW_VERSION}" \ + --body "Bumps the package version to ${NEW_VERSION} for release." + ``` + +7. **Enable auto-merge** after the PR exists (merge method: repository default—omit `--merge` / `--squash` / `--rebase` unless the user specified one). + + ```bash + gh pr merge --auto + ``` + + If `--auto` fails (permissions, auto-merge disabled, or pending checks), leave the PR open and report the error; the user can merge manually after CI passes. You can poll with `gh pr checks --watch` then retry `gh pr merge ... --auto`, or merge manually. + +## After merge + +Merging the PR into `main` triggers `.github/workflows/github_release_on_release_branch_merge.yml`, which creates a **GitHub Release** for tag `v{version}` from the merge commit. That **published** release event runs the existing PyPI **publish** workflow. + +## Quick reference + +| Input | Result | +|--------------------|---------------------------------------------| +| `/release` | Patch bump, branch `release/x.y.(z+1)` | +| `/release 1.4.0` | Version `1.4.0`, branch `release/1.4.0` | diff --git a/.github/workflows/github_release_on_release_branch_merge.yml b/.github/workflows/github_release_on_release_branch_merge.yml new file mode 100644 index 0000000..848804b --- /dev/null +++ b/.github/workflows/github_release_on_release_branch_merge.yml @@ -0,0 +1,60 @@ +# When a PR from release/* is merged into main, create a GitHub Release (tag vX.Y.Z). +# The existing publish.yml workflow runs on release: published and uploads to PyPI. + +name: GitHub Release on release branch merge + +on: + pull_request: + types: [closed] + branches: + - main + +permissions: + contents: write + +jobs: + create-release: + if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') + runs-on: ubuntu-latest + steps: + - name: Checkout merge commit + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Read version from pyproject.toml + id: meta + run: | + python3 <<'PY' + import os + import tomllib + + with open("pyproject.toml", "rb") as f: + data = tomllib.load(f) + version = data["project"]["version"] + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as out: + out.write(f"version={version}\n") + PY + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + VERSION="${{ steps.meta.outputs.version }}" + TAG="v${VERSION}" + MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" + if gh release view "${TAG}" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Release ${TAG} already exists; skipping." + exit 0 + fi + gh release create "${TAG}" \ + --repo "${{ github.repository }}" \ + --target "${MERGE_SHA}" \ + --title "${TAG}" \ + --generate-notes