Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .cursor/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -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. In non-interactive mode, `gh` requires an explicit merge strategy with `--auto` (use the repository default: usually **`--merge`** for a merge commit, or **`--squash`** / **`--rebase`** if that is what the repo uses).

```bash
gh pr merge <PR_NUMBER_OR_URL> --auto --merge
```

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 <PR_NUMBER_OR_URL> --watch` then retry `gh pr merge ... --auto --merge`, or merge manually.

## After merge

Merging the PR into `main` runs **Release on merge** (`.github/workflows/github_release_on_release_branch_merge.yml`), which creates a **GitHub Release** for tag `v{version}` from the merge commit. Because releases created with the default `GITHUB_TOKEN` do not trigger other workflows, **Publish to PyPI** (`publish.yml`) is also started via **`workflow_run`** when that release workflow finishes. Manual or API-created releases still match the `release: published` trigger on `publish.yml`.

## 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` |
68 changes: 68 additions & 0 deletions .github/workflows/github_release_on_release_branch_merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# When a release/* PR merges into main, create a GitHub release (tag vX.Y.Z).
# Events from GITHUB_TOKEN do not start other workflows; publish.yml is triggered via
# workflow_run when this workflow completes (see publish.yml).

name: Release on merge

on:
pull_request:
types: [closed]
branches:
- main

concurrency:
group: release-on-merge-${{ github.event.pull_request.number }}
cancel-in-progress: false

permissions:
contents: write

jobs:
github-release:
if: >-
github.event.pull_request.merged == true &&
startsWith(github.head_ref, 'release/') &&
github.event.pull_request.head.repo.full_name == github.repository
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
84 changes: 64 additions & 20 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,79 @@ on:
workflow_dispatch:
inputs:
test_pypi:
description: 'Publish to TestPyPI instead of PyPI'
description: "Publish to TestPyPI instead of PyPI"
required: false
default: false
type: boolean

# GitHub does not start new workflow runs for events caused by the default
# GITHUB_TOKEN (e.g. gh release create in another workflow). After
# "Release on merge" creates a release, trigger publish here instead.
workflow_run:
workflows: [Release on merge]
types: [completed]

permissions:
contents: read
id-token: write

jobs:
gate:
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.decide.outputs.publish }}
steps:
- uses: actions/checkout@v4
if: github.event_name == 'workflow_run'
with:
ref: main

- id: decide
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" != "workflow_run" ]]; then
echo "publish=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
if [[ "${{ github.event.workflow_run.conclusion }}" != "success" ]]; then
echo "publish=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
VERSION="$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)"
TAG="v${VERSION}"
if gh release view "${TAG}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "publish=true" >> "${GITHUB_OUTPUT}"
else
echo "No GitHub release ${TAG} yet (or release job was skipped); skipping publish."
echo "publish=false" >> "${GITHUB_OUTPUT}"
fi

build:
name: Build distribution
needs: gate
if: needs.gate.outputs.publish == 'true'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

with:
ref: ${{ github.event_name == 'workflow_run' && 'main' || github.event_name == 'release' && github.ref || 'main' }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: "3.x"

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build

- name: Build package
run: python -m build

- name: Store distribution packages
uses: actions/upload-artifact@v4
with:
Expand All @@ -42,24 +89,24 @@ jobs:

publish-to-pypi:
name: Publish to PyPI
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.test_pypi == false)
if: >-
github.event_name == 'workflow_run' ||
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.test_pypi == false)
needs: build
runs-on: ubuntu-latest

environment:
name: pypi
url: https://pypi.org/p/python-proxy-headers

permissions:
id-token: write # Required for trusted publishing


steps:
- name: Download distribution packages
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

Expand All @@ -68,21 +115,18 @@ jobs:
if: github.event_name == 'workflow_dispatch' && inputs.test_pypi == true
needs: build
runs-on: ubuntu-latest

environment:
name: testpypi
url: https://test.pypi.org/p/python-proxy-headers

permissions:
id-token: write


steps:
- name: Download distribution packages
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-proxy-headers"
version = "0.2.1"
version = "0.2.2"
authors = [
{ name="ProxyMesh", email="support@proxymesh.com" },
]
Expand Down
Loading