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
133 changes: 133 additions & 0 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
name: release
description: Cut a remo release — drive the release-please stable release PR, or build/validate/publish a pre-release (RC) — with a mandatory local "test before PyPI" gate and explicit approval before anything is tagged, merged, or published.
argument-hint: "[stable | rc [X.Y.ZrcN]] — omit to be asked"
metadata:
author: remo
---

# Release

Operationalizes remo's two-lane release process (see
[CONTRIBUTING.md](../../../CONTRIBUTING.md) → Release Process). Pairs with the
release-please integration.

**Non-negotiable safety rules — apply in every mode:**

- **Publishing to PyPI is irreversible.** A version, once uploaded, can never be
replaced. Run the local build + smoke gate (Step V below) before any tag,
merge, or publish — no exceptions.
- **Never** `git push` a tag, `git push` a version-bump commit, or `gh pr merge`
a release PR **without explicit user approval in the current turn.** Approval
from an earlier task does not carry over.
- Work only from a **clean working tree** on an **up-to-date `main`**. If the
tree is dirty or `main` is behind `origin/main`, stop and surface it.
- Prefer testing **without** publishing. Only publish an RC to PyPI when the user
explicitly asks for a public prerelease.

## Step 0 — Determine the mode

From the argument, or by asking the user:

- **`stable`** — drive the release-please release PR to a published stable release.
- **`rc`** — build/validate a pre-release, optionally publishing it.

Then run `git fetch origin` and verify `git status` is clean and `main` is
up to date (`git rev-parse HEAD` == `git rev-parse origin/main`).

---

## Stable lane (release-please owns the version + tag)

1. `git checkout main && git pull --ff-only`.
2. Find the open release PR (release-please titles it `chore(main): release X.Y.Z`):
```bash
gh pr list --state open --json number,title,headRefName,url \
--search 'chore(main): release in:title'
```
If none exists, tell the user release-please has not opened one (no
releasable `feat:`/`fix:` commits since the last release) and **stop**.
3. Show the proposed bump + changelog for review:
```bash
gh pr diff <number> -- pyproject.toml CHANGELOG.md
```
4. **Run the validation gate (Step V) on the PR's head branch** so you build the
exact version that will publish.
5. **Only on explicit approval**, merge it (release-please then tags `vX.Y.Z`,
which triggers `release.yml` to publish to PyPI + GHCR):
```bash
gh pr merge <number> --squash
```
Before merging, confirm the `RELEASE_PLEASE_TOKEN` secret is set — without it
the tag will not trigger the publish. If it's missing, warn the user and let
them decide.
6. Return to `main`, `git pull --ff-only`, report the new tag, and offer to watch
the release CI (`gh run watch` / `gh pr checks`).

---

## RC lane (manual — release-please stays out)

1. `git checkout main && git pull --ff-only`.
2. Determine the RC version `X.Y.ZrcN` (PEP 440 form, **no separator**):
- `X.Y.Z` is the next target version (feat → minor, fix → patch over the last
stable tag).
- `N` increments from the last `vX.Y.Z-rcM` tag for the same `X.Y.Z`
(`git tag --list "vX.Y.Z-rc*"`), else `1`.
- Confirm the chosen version with the user.
3. Bump `pyproject.toml` `[project].version` to `X.Y.ZrcN` (optionally run
`uv lock` to keep the lockfile's version in step).
4. **Run the validation gate (Step V).**
5. Ask the user which outcome they want:
- **Local test only (default, no PyPI):** revert the bump
(`git checkout pyproject.toml uv.lock`), then hand off the built wheel
(`dist/*.whl`) or the git-install one-liner
(`uv tool install --force "git+https://github.com/get2knowio/remo.git@<branch>"`).
Nothing is committed, tagged, or published.
- **CI dev build for cross-machine testing (no PyPI):** trigger the
`dev-build.yml` workflow, which builds the wheel in clean CI and uploads it
as a run artifact. The stamped version carries a `+g<sha>` local segment, so
it is unique and can never reach PyPI. Nothing is committed or tagged.
```bash
gh workflow run dev-build.yml -f version=X.Y.ZrcN # omit -f for an auto dev version
gh run watch # wait for it to finish
# then, on ANY machine:
gh run download <run-id> -n remo-wheel -D ./dl
uv tool install --force ./dl/remo_cli-*.whl
```
- **Publish a prerelease (only on explicit approval):**
```bash
git commit -am "chore(release): X.Y.ZrcN"
git tag vX.Y.Z-rcN
git push origin main vX.Y.Z-rcN
```
`release.yml` detects the `rc` suffix, publishes the prerelease to PyPI +
GHCR, and never moves `latest`. Offer to watch CI.

---

## Step V — Validation gate (test the exact wheel before PyPI)

Run this on whatever ref will be released (the release-please PR head for stable,
or the bumped working tree for an RC). The published version comes from
`pyproject.toml` via `uv build`, so this builds the identical artifact:

```bash
uv run pytest -q # full suite must pass
uv build # -> dist/remo_cli-<version>-py3-none-any.whl
VENV="$(mktemp -d)/venv"
uv venv "$VENV"
uv pip install --python "$VENV/bin/python" ./dist/remo_cli-<version>-py3-none-any.whl
"$VENV/bin/remo" --version # MUST equal <version>
"$VENV/bin/remo" --help # sanity-check the CLI loads
```

Report the results. If tests fail, the wheel version doesn't match, or the CLI
doesn't load, **stop** and surface it — do not proceed to tag/merge/publish.

## Done when

- The requested lane completed through the point the user approved (validated
only; RC wheel handed off; RC published; or stable PR merged).
- No tag was pushed, commit was pushed, or PR merged without explicit approval.
- The outcome (and, if published, the immutable version) was reported clearly.
78 changes: 78 additions & 0 deletions .github/workflows/dev-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: dev-build

# Build an installable wheel and upload it as a run artifact you can download and
# test on ANY machine — no git tag, no PyPI, no version reserved.
#
# The stamped version always carries a PEP 440 *local* segment (`+g<shortsha>`),
# which PyPI rejects outright — so a dev build can never leak to PyPI, and every
# build is uniquely identifiable.
#
# Trigger manually:
# gh workflow run dev-build.yml # -> 2.2.0.dev<run>+g<sha>
# gh workflow run dev-build.yml -f version=2.3.0rc1 # -> 2.3.0rc1+g<sha>
#
# Then, on any machine:
# gh run download <run-id> -n remo-wheel -D ./dl
# uv tool install --force ./dl/remo_cli-*.whl

on:
workflow_dispatch:
inputs:
version:
description: "Full base version to stamp (e.g. 2.3.0rc1, 2.3.0). Blank = auto dev version from pyproject. A +g<sha> local segment is always appended."
required: false
default: ""

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7

- name: Compute dev version
id: ver
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
SHA=$(git rev-parse --short HEAD)
if [ -n "$INPUT_VERSION" ]; then
BASE="$INPUT_VERSION"
else
# Core X.Y.Z from pyproject, plus a per-run .dev segment.
CORE=$(grep -m1 '^version = ' pyproject.toml \
| sed -E 's/version = "([0-9]+\.[0-9]+\.[0-9]+).*"/\1/')
BASE="${CORE}.dev${GITHUB_RUN_NUMBER}"
fi
# Always append the local segment: unique + un-uploadable to PyPI.
VERSION="${BASE}+g${SHA}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building version: $VERSION"

- name: Stamp version into pyproject.toml
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
sed -i -E "0,/^version = \".*\"/s//version = \"${VERSION}\"/" pyproject.toml
grep -m1 '^version = ' pyproject.toml

- name: Build wheel + sdist
run: uv build

- name: Upload artifact
# NOTE: SHA-pin to match the repo's action-pinning convention once the
# v4 commit SHA is available (e.g. `@<sha> # v4`).
uses: actions/upload-artifact@v4
with:
name: remo-wheel
path: dist/*
retention-days: 30
if-no-files-found: error
Loading