From 5ad26e98b06b50f8afb6a198622aaf7c6bb1c2e3 Mon Sep 17 00:00:00 2001 From: Cursor Date: Mon, 6 Apr 2026 16:09:41 +0000 Subject: [PATCH 1/4] chore: bump version to 0.2.2 Made-with: Cursor --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 37f0b3e..80ff510 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" }, ] From daa5132eb40585cc59c74af34574788e73e8ccf4 Mon Sep 17 00:00:00 2001 From: Cursor Date: Mon, 6 Apr 2026 16:07:45 +0000 Subject: [PATCH 2/4] chore: add release skill and GitHub Release workflow for release/* merges Made-with: Cursor --- .cursor/skills/release/SKILL.md | 90 +++++++++++++++++++ ...github_release_on_release_branch_merge.yml | 60 +++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 .cursor/skills/release/SKILL.md create mode 100644 .github/workflows/github_release_on_release_branch_merge.yml 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 From bf333520e4208ec7f1688e15269a2f0a2fbd22a3 Mon Sep 17 00:00:00 2001 From: Cursor Date: Mon, 6 Apr 2026 16:18:37 +0000 Subject: [PATCH 3/4] docs(skill): require merge strategy with gh pr merge --auto Made-with: Cursor --- .cursor/skills/release/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.cursor/skills/release/SKILL.md b/.cursor/skills/release/SKILL.md index c3c3486..29a6dbf 100644 --- a/.cursor/skills/release/SKILL.md +++ b/.cursor/skills/release/SKILL.md @@ -70,13 +70,13 @@ git config user.name "Cursor" --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). +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 --auto + gh pr merge --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 --watch` then retry `gh pr merge ... --auto`, or merge manually. + 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 --merge`, or merge manually. ## After merge From 1cfa3e4d2eec07e44bd8d735643e7f2b8cda1854 Mon Sep 17 00:00:00 2001 From: Cursor Date: Mon, 6 Apr 2026 16:42:59 +0000 Subject: [PATCH 4/4] ci: chain publish to PyPI after Release on merge via workflow_run Align with javascript-proxy-headers: add a gate job that runs after the release workflow completes, verify the GitHub release tag exists, then build and publish. Keep release: published for manually created releases. Rename the release workflow to "Release on merge", add concurrency and fork safety, and document the token limitation in the release skill. Made-with: Cursor --- .cursor/skills/release/SKILL.md | 2 +- ...github_release_on_release_branch_merge.yml | 18 ++-- .github/workflows/publish.yml | 84 ++++++++++++++----- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/.cursor/skills/release/SKILL.md b/.cursor/skills/release/SKILL.md index 29a6dbf..7fcb7e7 100644 --- a/.cursor/skills/release/SKILL.md +++ b/.cursor/skills/release/SKILL.md @@ -80,7 +80,7 @@ git config user.name "Cursor" ## 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. +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 diff --git a/.github/workflows/github_release_on_release_branch_merge.yml b/.github/workflows/github_release_on_release_branch_merge.yml index 848804b..d76074b 100644 --- a/.github/workflows/github_release_on_release_branch_merge.yml +++ b/.github/workflows/github_release_on_release_branch_merge.yml @@ -1,7 +1,8 @@ -# 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. +# 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: GitHub Release on release branch merge +name: Release on merge on: pull_request: @@ -9,12 +10,19 @@ on: branches: - main +concurrency: + group: release-on-merge-${{ github.event.pull_request.number }} + cancel-in-progress: false + permissions: contents: write jobs: - create-release: - if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') + 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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 10b4e45..6308a91 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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: @@ -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 @@ -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: