From c08f8c2f79353c8174aa62963e4999ebd7452e08 Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:18:10 +0000 Subject: [PATCH 01/11] docs(specs): add ITL-545 release branch argument design --- ...-itl-545-release-branch-argument-design.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 superpowers/specs/2026-07-21-itl-545-release-branch-argument-design.md diff --git a/superpowers/specs/2026-07-21-itl-545-release-branch-argument-design.md b/superpowers/specs/2026-07-21-itl-545-release-branch-argument-design.md new file mode 100644 index 00000000..8f40b770 --- /dev/null +++ b/superpowers/specs/2026-07-21-itl-545-release-branch-argument-design.md @@ -0,0 +1,110 @@ +# Release Cutting: Optional Branch Argument + +**Issue:** ITL-545 +**Date:** 2026-07-21 +**Status:** Approved + +## Overview + +The release workflow (`release.yml`) currently accepts only a `version` input and +implicitly cuts the release from `main` (via GitHub Actions' default dispatch ref). This +design adds an optional `branch` input that defaults to `main`, enabling release-cutting +from maintenance branches, hotfix branches, or any other named branch without merging to +`main` first. + +## Goals & Success Criteria + +- `release.yml` accepts a `branch` input alongside `version`; omitting it behaves + identically to today (cuts from `main`) +- The release tag is created at the tip of the specified branch +- Tests run against the specified branch +- A typo in `branch` fails immediately (before tests run) with a clear error message +- `RELEASING.md` documents the new parameter with a hotfix example + +## Design + +### New `branch` input + +Added to `workflow_dispatch.inputs`: + +```yaml +branch: + description: 'Branch to cut the release from (defaults to main)' + required: false + type: string + default: main +``` + +When omitted, `${{ inputs.branch }}` evaluates to `"main"` throughout the workflow — +identical behavior to today. + +### New `validate-branch` pre-flight job + +A dedicated job that runs before `test` and `license-check`. Uses the built-in +`GITHUB_TOKEN` to query the GitHub REST API — no checkout required. Fails immediately +with a clear error message if the branch does not exist. + +```yaml +validate-branch: + name: Validate release branch + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check branch exists on origin + env: + GH_TOKEN: ${{ github.token }} + run: | + BRANCH="${{ inputs.branch }}" + if ! gh api "repos/${{ github.repository }}/branches/${BRANCH}" --silent 2>/dev/null; then + echo "::error::Branch '${BRANCH}' does not exist. Check for typos." + exit 1 + fi + echo "Branch '${BRANCH}' confirmed." +``` + +### Updated job graph + +``` +validate-branch ─┬─ test ──────────┐ + └─ license-check ─┤─ build ─┬─ publish-testpypi ─ publish-pypi ─┐ + └─ linear-sync ─────────────────────┴─ linear-complete +``` + +### Per-job changes + +| Job | Change | +|-----|--------| +| `validate-branch` | New job — validates branch exists via GitHub API | +| `test` | Add `needs: [validate-branch]`; add `ref: ${{ inputs.branch }}` to `actions/checkout` | +| `license-check` | Add `needs: [validate-branch]` | +| `build` | Add `ref: ${{ inputs.branch }}` to `actions/checkout` — tag is created at tip of release branch | +| `publish-testpypi` | No change (downloads pre-built artifact, no checkout) | +| `publish-pypi` | No change (downloads pre-built artifact, no checkout) | +| `linear-sync` | No change (operates on the version tag, branch-agnostic) | +| `linear-complete` | No change (operates on the version tag, branch-agnostic) | + +### Branch validation approach + +- **No branch-pattern gating** — any existing branch may be used; policy enforcement is + out of scope for this ticket. +- Validation is purely existence-checking: if the branch exists on origin, the workflow + proceeds. If not, it fails before a single test minute is consumed. + +## Documentation Changes + +`RELEASING.md` is updated in two places: + +1. **Step 2** of "Cutting a Release" — mention the optional `Branch` field in the + workflow dispatch UI. + +2. **New "Cutting a Hotfix Release" section** — explains the `branch` parameter and + walks through a concrete hotfix example (e.g. `hotfix/0.1.x` → `v0.1.1`). + +## Out of Scope + +- Branch-pattern protection (e.g. only `main`, `release/*`, `hotfix/*`) +- Surfacing the release branch in Slack announcements (ITL-521) +- Changes to `release-sync.yml` (it watches pushes to `main` independently) +- Changes to `_license-check.yml` (its checkout uses the dispatch ref, which is + acceptable — the license check validates installed dependencies, not branch-specific + source) From 0d4417dc8dec601124894f7b89b27cf932e65f7e Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:05:36 +0000 Subject: [PATCH 02/11] docs(plans): add ITL-545 release branch argument implementation plan --- ...6-07-21-itl-545-release-branch-argument.md | 423 ++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 superpowers/plans/2026-07-21-itl-545-release-branch-argument.md diff --git a/superpowers/plans/2026-07-21-itl-545-release-branch-argument.md b/superpowers/plans/2026-07-21-itl-545-release-branch-argument.md new file mode 100644 index 00000000..951a45e8 --- /dev/null +++ b/superpowers/plans/2026-07-21-itl-545-release-branch-argument.md @@ -0,0 +1,423 @@ +# Release Branch Argument Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use sensei:subagent-driven-development (recommended) or sensei:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an optional `branch` input (default `main`) to the GitHub Actions release workflow so releases can be cut from any branch, with a pre-flight validation step that fails fast on invalid branch names. + +**Architecture:** Two files change. `release.yml` gets a new `branch` workflow input, a new `validate-branch` pre-flight job, and updated checkout steps in `test` and `build`. `RELEASING.md` gets updated docs and a new hotfix-release example. + +**Tech Stack:** GitHub Actions YAML, `gh` CLI (pre-installed on ubuntu-latest runners), PyYAML (for local syntax validation), Bash + +--- + +## File Map + +| Action | File | What changes | +|--------|------|--------------| +| Modify | `.github/workflows/release.yml` | Add `branch` input; add `validate-branch` job; update `test`, `license-check`, `build` jobs | +| Modify | `RELEASING.md` | Update Step 2; add "Cutting a Hotfix Release" section | + +--- + +### Task 1: Create the feature branch + +- [ ] **Step 1: Check out the feature branch** + +```bash +git checkout -b eywalker/itl-545-release-cutting-allow-optional-branch-argument-defaults-to +``` + +- [ ] **Step 2: Verify you are on the correct branch** + +```bash +git branch --show-current +``` + +Expected output: +``` +eywalker/itl-545-release-cutting-allow-optional-branch-argument-defaults-to +``` + +--- + +### Task 2: Add `branch` input to `release.yml` + +**Files:** +- Modify: `.github/workflows/release.yml:1-9` + +The `workflow_dispatch.inputs` block currently has only `version`. Add `branch` immediately after it. + +- [ ] **Step 1: Edit the `on:` block** + +Replace the current `on:` block (lines 1–9): + +```yaml +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 0.1.0 or v0.1.0 — leading v is stripped automatically)' + required: true + type: string +``` + +With: + +```yaml +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 0.1.0 or v0.1.0 — leading v is stripped automatically)' + required: true + type: string + branch: + description: 'Branch to cut the release from (defaults to main)' + required: false + type: string + default: main +``` + +- [ ] **Step 2: Validate YAML syntax** + +```bash +uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('YAML OK')" +``` + +Expected output: +``` +YAML OK +``` + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci(release): add optional branch input (defaults to main)" +``` + +--- + +### Task 3: Add `validate-branch` pre-flight job + +**Files:** +- Modify: `.github/workflows/release.yml` — insert new job before `test` + +The new job uses the built-in `GITHUB_TOKEN` (exposed as `GH_TOKEN` env var) to query the GitHub API. No checkout needed. `gh` is pre-installed on all `ubuntu-latest` runners. + +- [ ] **Step 1: Insert the `validate-branch` job** + +In `.github/workflows/release.yml`, after the closing `type: string` line of the `branch` input and before the `jobs:` key, the file structure is: + +```yaml +jobs: + test: + name: Test ... +``` + +Add the `validate-branch` job as the **first** job under `jobs:`: + +```yaml +jobs: + validate-branch: + name: Validate release branch + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check branch exists on origin + env: + GH_TOKEN: ${{ github.token }} + run: | + BRANCH="${{ inputs.branch }}" + if ! gh api "repos/${{ github.repository }}/branches/${BRANCH}" --silent 2>/dev/null; then + echo "::error::Branch '${BRANCH}' does not exist. Check for typos." + exit 1 + fi + echo "Branch '${BRANCH}' confirmed." + + test: + name: Test ... +``` + +- [ ] **Step 2: Validate YAML syntax** + +```bash +uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('YAML OK')" +``` + +Expected output: +``` +YAML OK +``` + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci(release): add validate-branch pre-flight job" +``` + +--- + +### Task 4: Wire `branch` into `test` and `license-check` jobs + +**Files:** +- Modify: `.github/workflows/release.yml` — `test` and `license-check` jobs + +Both jobs must now depend on `validate-branch`. The `test` job's checkout must use `ref: ${{ inputs.branch }}` so tests run against the release branch. + +- [ ] **Step 1: Update the `test` job** + +Find the `test` job. It currently starts with: + +```yaml + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest +``` + +And its checkout step is: + +```yaml + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 # required: hatch-vcs needs full tag history +``` + +Make two changes: + +1. Add `needs: [validate-branch]` between `name:` and `runs-on:`: + +```yaml + test: + name: Test (Python ${{ matrix.python-version }}) + needs: [validate-branch] + runs-on: ubuntu-latest +``` + +2. Add `ref: ${{ inputs.branch }}` to the checkout `with:` block: + +```yaml + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 # required: hatch-vcs needs full tag history + ref: ${{ inputs.branch }} +``` + +- [ ] **Step 2: Update the `license-check` job** + +Find the `license-check` job. It currently reads: + +```yaml + license-check: + uses: ./.github/workflows/_license-check.yml +``` + +Add `needs: [validate-branch]`: + +```yaml + license-check: + needs: [validate-branch] + uses: ./.github/workflows/_license-check.yml +``` + +- [ ] **Step 3: Validate YAML syntax** + +```bash +uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('YAML OK')" +``` + +Expected output: +``` +YAML OK +``` + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci(release): wire branch input into test and license-check jobs" +``` + +--- + +### Task 5: Wire `branch` into `build` job + +**Files:** +- Modify: `.github/workflows/release.yml` — `build` job's checkout step + +The `build` job creates the git tag and pushes it. Its checkout must use `ref: ${{ inputs.branch }}` so the tag is created at the tip of the release branch, not at whatever commit triggered the dispatch. + +- [ ] **Step 1: Update the `build` job checkout** + +Find the checkout step inside `build`. It currently reads: + +```yaml + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 # required: hatch-vcs reads tag to set version +``` + +Add `ref: ${{ inputs.branch }}`: + +```yaml + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 # required: hatch-vcs reads tag to set version + ref: ${{ inputs.branch }} +``` + +- [ ] **Step 2: Validate YAML syntax** + +```bash +uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('YAML OK')" +``` + +Expected output: +``` +YAML OK +``` + +- [ ] **Step 3: Verify the full job graph** + +Confirm the structure of `release.yml` looks like this (just the job names and `needs:` lines — spot-check): + +```bash +uv run python -c " +import yaml +wf = yaml.safe_load(open('.github/workflows/release.yml')) +for name, job in wf['jobs'].items(): + needs = job.get('needs', []) + print(f'{name}: needs={needs}') +" +``` + +Expected output: +``` +validate-branch: needs=[] +test: needs=['validate-branch'] +license-check: needs=['validate-branch'] +build: needs=['test', 'license-check'] +publish-testpypi: needs=['build'] +publish-pypi: needs=['build', 'publish-testpypi'] +linear-sync: needs=['build'] +linear-complete: needs=['build', 'publish-pypi', 'linear-sync'] +``` + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci(release): wire branch input into build checkout" +``` + +--- + +### Task 6: Update `RELEASING.md` + +**Files:** +- Modify: `RELEASING.md` + +Two edits: + +1. **Step 2 of "Cutting a Release"** — add a note about the optional branch field. +2. **New "Cutting a Hotfix Release" section** — insert after the existing "Pre-releases" section. + +- [ ] **Step 1: Update Step 2 of "Cutting a Release"** + +Find this paragraph in `RELEASING.md`: + +```markdown +2. **Trigger the release workflow** — go to + **[Actions → Release → Run workflow](https://github.com/nauticalab/orcapod-python/actions/workflows/release.yml)** + in the GitHub UI, enter the version (e.g. `0.1.0`), and click **Run workflow**. + + A leading `v` is stripped automatically — `v0.1.0` and `0.1.0` both work. +``` + +Replace it with: + +```markdown +2. **Trigger the release workflow** — go to + **[Actions → Release → Run workflow](https://github.com/nauticalab/orcapod-python/actions/workflows/release.yml)** + in the GitHub UI, enter the version (e.g. `0.1.0`), optionally enter a branch name + (defaults to `main`), and click **Run workflow**. + + A leading `v` is stripped automatically — `v0.1.0` and `0.1.0` both work. +``` + +- [ ] **Step 2: Add "Cutting a Hotfix Release" section** + +Find the "Pre-releases" section header in `RELEASING.md`: + +```markdown +## Pre-releases +``` + +Insert the following new section **after** the entire "Pre-releases" section (i.e., after the last paragraph of "Pre-releases", before "## Tag Format"): + +```markdown +## Cutting a Hotfix Release + +To release from a branch other than `main` — for example, a maintenance branch carrying +a critical patch — enter the branch name in the **Branch** field when triggering the +workflow. + +**Example:** cutting `v0.1.1` from `hotfix/0.1.x`: + +1. **Trigger the release workflow** — go to + **[Actions → Release → Run workflow](https://github.com/nauticalab/orcapod-python/actions/workflows/release.yml)** +2. Set **Version** to `0.1.1` +3. Set **Branch** to `hotfix/0.1.x` +4. Click **Run workflow** + +The workflow will confirm the branch exists, run tests against it, create the `v0.1.1` +tag at its tip, and publish to PyPI — identical to a normal release. + +``` + +- [ ] **Step 3: Commit** + +```bash +git add RELEASING.md +git commit -m "docs(releasing): document optional branch argument and hotfix example" +``` + +--- + +### Task 7: Push branch and open PR + +- [ ] **Step 1: Push the feature branch** + +```bash +git push -u origin eywalker/itl-545-release-cutting-allow-optional-branch-argument-defaults-to +``` + +- [ ] **Step 2: Open the PR** + +```bash +gh pr create \ + --title "ci(release): allow optional branch argument (defaults to main)" \ + --base main \ + --body "$(cat <<'EOF' +## Summary + +- Adds an optional `branch` input to the `release.yml` workflow dispatch (defaults to `main`) +- Adds a `validate-branch` pre-flight job that fails fast with a clear error if the branch doesn't exist +- Wires `ref: ${{ inputs.branch }}` into the `test` and `build` checkout steps so tests and tag creation target the release branch +- Updates `RELEASING.md` with the new parameter and a hotfix release example + +Closes ITL-545 + +## Test plan + +- [ ] Trigger the release workflow without specifying a branch — confirm behavior is identical to today (cuts from `main`) +- [ ] Trigger the release workflow with a valid non-main branch name — confirm the tag is created at the tip of that branch +- [ ] Trigger the release workflow with a non-existent branch name — confirm `validate-branch` fails immediately with the error message `Branch '...' does not exist. Check for typos.` and no other jobs run +EOF +)" +``` From fda2e31c3d7c552c1a7fa61a7369b5a49c53bcbc Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:16:18 +0000 Subject: [PATCH 03/11] ci(release): add optional branch input (defaults to main) --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12c7b077..ba67e950 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,11 @@ on: description: 'Release version (e.g. 0.1.0 or v0.1.0 — leading v is stripped automatically)' required: true type: string + branch: + description: 'Branch to cut the release from (defaults to main)' + required: false + type: string + default: main jobs: test: From ddf8212e4c913d69181dd62a550b42667907f557 Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:27:28 +0000 Subject: [PATCH 04/11] ci(release): add validate-branch pre-flight job --- .github/workflows/release.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba67e950..f936e7ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,22 @@ on: default: main jobs: + validate-branch: + name: Validate release branch + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check branch exists on origin + env: + GH_TOKEN: ${{ github.token }} + run: | + BRANCH="${{ inputs.branch }}" + if ! gh api "repos/${{ github.repository }}/branches/${BRANCH}" --silent 2>/dev/null; then + echo "::error::Branch '${BRANCH}' does not exist. Check for typos." + exit 1 + fi + echo "Branch '${BRANCH}' confirmed." + test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest From 1f769225e595c91ddc8648b95048e7e15ad40fb3 Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:33:00 +0000 Subject: [PATCH 05/11] ci(release): fix script injection in validate-branch via env var --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f936e7ed..f1b7b604 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,8 +22,8 @@ jobs: - name: Check branch exists on origin env: GH_TOKEN: ${{ github.token }} + BRANCH: ${{ inputs.branch }} run: | - BRANCH="${{ inputs.branch }}" if ! gh api "repos/${{ github.repository }}/branches/${BRANCH}" --silent 2>/dev/null; then echo "::error::Branch '${BRANCH}' does not exist. Check for typos." exit 1 From 4e78393a0ef5fc99b0d1a9a09e4191f2dcd7c66f Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:34:11 +0000 Subject: [PATCH 06/11] ci(release): wire branch input into test and license-check jobs --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1b7b604..b1fee101 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,7 @@ jobs: test: name: Test (Python ${{ matrix.python-version }}) + needs: [validate-branch] runs-on: ubuntu-latest timeout-minutes: 30 strategy: @@ -42,6 +43,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 # required: hatch-vcs needs full tag history + ref: ${{ inputs.branch }} - name: Install uv uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 @@ -56,6 +58,7 @@ jobs: run: uv run --python ${{ matrix.python-version }} pytest -m "not postgres" --tb=short -q license-check: + needs: [validate-branch] uses: ./.github/workflows/_license-check.yml build: From 24b756957cf133d058fe334768575a156aaa1b7b Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:36:20 +0000 Subject: [PATCH 07/11] ci(release): wire branch input into build checkout --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1fee101..f7e065d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,6 +81,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 # required: hatch-vcs reads tag to set version + ref: ${{ inputs.branch }} - name: Configure git identity run: | From 6b17f39688c1eb048a884a424b39cdd5d8256dc2 Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:40:30 +0000 Subject: [PATCH 08/11] docs(releasing): document optional branch argument and hotfix example Co-Authored-By: Claude Sonnet 4.6 --- RELEASING.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index c7b0875d..053511fb 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -16,7 +16,8 @@ Releases are triggered from the GitHub Actions UI — no manual `git tag` step r 2. **Trigger the release workflow** — go to **[Actions → Release → Run workflow](https://github.com/nauticalab/orcapod-python/actions/workflows/release.yml)** - in the GitHub UI, enter the version (e.g. `0.1.0`), and click **Run workflow**. + in the GitHub UI, enter the version (e.g. `0.1.0`), optionally enter a branch name + (defaults to `main`), and click **Run workflow**. A leading `v` is stripped automatically — `v0.1.0` and `0.1.0` both work. @@ -42,6 +43,23 @@ workflow input. PyPI handles the stable vs pre-release distinction natively: - `pip install orcapod` — installs the latest **stable** release only - `pip install --pre orcapod` — installs the latest release including pre-releases +## Cutting a Hotfix Release + +To release from a branch other than `main` — for example, a maintenance branch carrying +a critical patch — enter the branch name in the **Branch** field when triggering the +workflow. + +**Example:** cutting `v0.1.1` from `hotfix/0.1.x`: + +1. **Trigger the release workflow** — go to + **[Actions → Release → Run workflow](https://github.com/nauticalab/orcapod-python/actions/workflows/release.yml)** +2. Set **Version** to `0.1.1` +3. Set **Branch** to `hotfix/0.1.x` +4. Click **Run workflow** + +The workflow will confirm the branch exists, run tests against it, create the `v0.1.1` +tag at its tip, and publish to PyPI — identical to a normal release. + ## Tag Format | Release type | Tag format | Example | From 6c081df50cfbec175757ba39f6a2d8a807eca19c Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:48:12 +0000 Subject: [PATCH 09/11] docs(releasing): update job graph to include validate-branch --- RELEASING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 053511fb..41941b1c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -24,12 +24,12 @@ Releases are triggered from the GitHub Actions UI — no manual `git tag` step r 3. **CI takes over** — the workflow runs the following jobs automatically: ``` - test ──────────┐ - license-check ────┤─ build ─┬─ publish-testpypi ─ publish-pypi ─┐ - └─ linear-sync ─────────────────────┴─ linear-complete + validate-branch ─┬─ test ──────────┐ + └─ license-check ──┤─ build ─┬─ publish-testpypi ─ publish-pypi ─┐ + └─ linear-sync ──────────────────────┴─ linear-complete ``` - - Pre-flight: tests on Python 3.11 and 3.12, plus license check (in parallel) + - Pre-flight: branch validation, then tests on Python 3.11 and 3.12 and license check (parallel after validation) - Build: normalises version, creates local tag, builds wheel + sdist, pushes tag to origin - TestPyPI: publishes to test.pypi.org first as a staging step - PyPI: publishes to pypi.org and creates a GitHub Release with generated release notes From 255d054ca7bf50d298686c07ee3f18f7328ab988 Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:08:39 +0000 Subject: [PATCH 10/11] fix(release): url-encode branch in gh api call; clarify workflow-from dropdown in docs --- .github/workflows/release.yml | 3 ++- RELEASING.md | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7e065d5..69bfb5ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,8 @@ jobs: GH_TOKEN: ${{ github.token }} BRANCH: ${{ inputs.branch }} run: | - if ! gh api "repos/${{ github.repository }}/branches/${BRANCH}" --silent 2>/dev/null; then + ENCODED_BRANCH="${BRANCH//\//%2F}" + if ! gh api "repos/${{ github.repository }}/branches/${ENCODED_BRANCH}" --silent 2>/dev/null; then echo "::error::Branch '${BRANCH}' does not exist. Check for typos." exit 1 fi diff --git a/RELEASING.md b/RELEASING.md index 41941b1c..7ff01603 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -53,9 +53,11 @@ workflow. 1. **Trigger the release workflow** — go to **[Actions → Release → Run workflow](https://github.com/nauticalab/orcapod-python/actions/workflows/release.yml)** -2. Set **Version** to `0.1.1` -3. Set **Branch** to `hotfix/0.1.x` -4. Click **Run workflow** +2. In the **Use workflow from** dropdown, keep `main` selected (so the workflow version + includes the `Branch` field — do not select `hotfix/0.1.x` here) +3. Set **Version** to `0.1.1` +4. Set **Branch** to `hotfix/0.1.x` +5. Click **Run workflow** The workflow will confirm the branch exists, run tests against it, create the `v0.1.1` tag at its tip, and publish to PyPI — identical to a normal release. From 492530ef665645ce6d9bcdcf275c9477dcc8d94e Mon Sep 17 00:00:00 2001 From: "agent-kurodo[bot]" <268466204+agent-kurodo[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:40:47 +0000 Subject: [PATCH 11/11] ci(release): wire branch through to license-check reusable workflow --- .github/workflows/_license-check.yml | 7 +++++++ .github/workflows/release.yml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/_license-check.yml b/.github/workflows/_license-check.yml index eeade0c4..769a7fab 100644 --- a/.github/workflows/_license-check.yml +++ b/.github/workflows/_license-check.yml @@ -2,6 +2,12 @@ name: License check on: workflow_call: + inputs: + branch: + description: 'Branch to check out' + required: false + type: string + default: main jobs: license-check: @@ -11,6 +17,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # required: hatch-vcs runs during uv sync + ref: ${{ inputs.branch }} - name: Install uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69bfb5ac..afc4c6dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,6 +61,8 @@ jobs: license-check: needs: [validate-branch] uses: ./.github/workflows/_license-check.yml + with: + branch: ${{ inputs.branch }} build: name: Build distribution