From 1070c3c4bd66024d993b2679d8fcafb5564b1350 Mon Sep 17 00:00:00 2001 From: Paul O'Fallon <505519+pofallon@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:10:37 +0000 Subject: [PATCH 1/2] feat(skill): add /release skill to operationalize the two-lane release flow A user-invocable skill that drives remo's release process with a mandatory local "test before PyPI" gate and approval-before-publish guardrails: - stable lane: locate the release-please "chore(main): release X.Y.Z" PR, review the bump + CHANGELOG, run the validation gate on the PR head, then merge only on explicit approval (release-please tags -> release.yml publishes). - rc lane: compute X.Y.ZrcN, bump pyproject, run the validation gate, then either hand off a local wheel (no PyPI) or, only on explicit approval, tag vX.Y.Z-rcN and publish a prerelease. - Step V validation gate: full pytest + uv build + throwaway-venv smoke that the wheel's --version matches, before any tag/merge/publish. Guardrails: never push a tag/commit or merge a release PR without explicit in-turn approval; require a clean, up-to-date main; prefer local-only testing. Pairs with the release-please integration (#78). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HAPB5GA4NVUgKFsVFVJDxN --- .claude/skills/release/SKILL.md | 122 ++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 .claude/skills/release/SKILL.md diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 00000000..536e6ba9 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,122 @@ +--- +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 -- 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 --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@"`). + Nothing is committed, tagged, or published. + - **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--py3-none-any.whl +VENV="$(mktemp -d)/venv" +uv venv "$VENV" +uv pip install --python "$VENV/bin/python" ./dist/remo_cli--py3-none-any.whl +"$VENV/bin/remo" --version # MUST equal +"$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. From 9fe49f0c85c33b2e7abd83b350003f67c8312ef6 Mon Sep 17 00:00:00 2001 From: Paul O'Fallon <505519+pofallon@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:27:08 +0000 Subject: [PATCH 2/2] ci(dev-build): manual-dispatch workflow to publish RC/dev wheels as run artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build an installable wheel in clean CI and upload it as a run artifact you can download and test on another machine — no git tag, no PyPI, no reserved version: - .github/workflows/dev-build.yml: workflow_dispatch with an optional `version` input. Stamps pyproject.toml with the base version plus a PEP 440 LOCAL segment (+g), which PyPI rejects outright — so a dev build can never leak to PyPI and every build is uniquely identifiable. Uploads dist/* as the `remo-wheel` artifact (30-day retention). release.yml is untouched. gh workflow run dev-build.yml [-f version=2.3.0rc1] gh run download -n remo-wheel && uv tool install --force ./remo_cli-*.whl - /release skill: add a "CI dev build (cross-machine, no PyPI)" outcome to the RC lane alongside local-only and publish-to-PyPI. Version stamping validated locally (packaging.Version): blank -> X.Y.Z.dev, label -> X.Y.ZrcN, each with a +g local segment. actionlint clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HAPB5GA4NVUgKFsVFVJDxN --- .claude/skills/release/SKILL.md | 11 +++++ .github/workflows/dev-build.yml | 78 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 .github/workflows/dev-build.yml diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 536e6ba9..00566f3f 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -84,6 +84,17 @@ up to date (`git rev-parse HEAD` == `git rev-parse origin/main`). (`dist/*.whl`) or the git-install one-liner (`uv tool install --force "git+https://github.com/get2knowio/remo.git@"`). 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` 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 -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" diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml new file mode 100644 index 00000000..f46520ef --- /dev/null +++ b/.github/workflows/dev-build.yml @@ -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`), +# 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+g +# gh workflow run dev-build.yml -f version=2.3.0rc1 # -> 2.3.0rc1+g +# +# Then, on any machine: +# gh run download -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 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. `@ # v4`). + uses: actions/upload-artifact@v4 + with: + name: remo-wheel + path: dist/* + retention-days: 30 + if-no-files-found: error