Skip to content

feat(ci): build promotable RC wheels and attach them to a GitHub pre-release - #198

Merged
pofallon merged 1 commit into
mainfrom
feat/rc-github-prerelease
Aug 31, 2026
Merged

pofallon merged 1 commit into
mainfrom
feat/rc-github-prerelease

Conversation

@pofallon

Copy link
Copy Markdown
Contributor

Option 2 from the pre-release testing discussion: close the promote-don't-rebuild gap without touching PyPI.

The gap

The standard's Tier 2 says the wheel you test is "the same wheel that would go to PyPI if the rc proves out — promote it by publishing the identical artifact, don't rebuild."

Ours could never be that. dev-build.yml appended a +g<sha> local segment unconditionally, including when an operator passed an explicit RC version — and PyPI rejects local versions outright. So 4.4.0rc1+g4a0e2aa, the wheel built yesterday, is structurally unpromotable, and merging the release PR would uv build a different artifact from a different commit.

Worth being precise about where the defect was: the constitution was already right. Principle IX scopes the local-segment rule to "a dev build", not to RCs. dev-build.yml was over-applying it. No constitution change needed.

Two modes

Input Version Can it leak to PyPI? Can it be promoted?
(none) X.Y.Z.dev<run>+g<sha> No — PyPI rejects local versions No, same reason. Validation only
version=X.Y.ZrcN X.Y.ZrcN verbatim Not in practice — resolvers skip pre-releases by default Yes — that is the point

An explicit version must match a PEP 440 pre-release/dev form. A final version like 4.4.0 is refused, so this workflow cannot produce something that passes for a real release. Verified against the shipped regex:

4.4.0rc1           ACCEPT      4.4.0              REFUSE
4.4.0b2            ACCEPT      4.4.0rc1+gdeadbee  REFUSE
4.4.0.dev5         ACCEPT      v4.4.0rc1          REFUSE

The GitHub pre-release

-f prerelease=true attaches the same wheel the build job produced — downloaded from the artifact, not rebuilt — to a GitHub pre-release, so any machine can install by URL with no gh auth:

uv tool install --force "remo-cli[web] @ https://github.com/get2knowio/remo/releases/download/rc-4.4.0rc1/remo_cli-4.4.0rc1-py3-none-any.whl"

Nothing here publishes to PyPI. The tag is rc-<version>, deliberately not v*release.yml triggers on v* and publishes to PyPI + GHCR. A guard in the step refuses any v-prefixed tag outright rather than relying on the naming convention holding.

contents: write is scoped to the new prerelease job; the ordinary dev-build path stays read-only under the top-level contents: read added in #195.

Notes

The release notes are built with a quoted heredoc plus placeholder substitution. An unquoted one would execute the markdown's backticks as command substitution — which is also what broke the first attempt at this step (the notes body escaped the YAML block scalar; both are fixed and the file is verified to parse).

Docs

CLAUDE.md / AGENTS.md: the Commands block now shows all three dispatch shapes and both install paths, and the Quality Gates note explains why an RC drops the local segment where a dev build keeps it.

Verification

  • Workflow parses; jobs build + prerelease; permissions contents: read / contents: write respectively; heredoc body dedents to column 0
  • Version guard exercised against all six cases above
  • uv run pytest tests/unit/test_docs_structure.py — 10 passed

Not yet exercised end-to-end — the first real run would be the 4.4.0rc1 pre-release, which has not been cut.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MzLwS8fijKdrVVh7oj9kNd

…release

Closes the gap between what dev-build.yml produced and what the pre-release
testing standard asks for. Tier 2 says the wheel you test is "the same wheel
that would go to PyPI if the rc proves out — promote it by publishing the
identical artifact, don't rebuild." Ours could never be that: dev-build.yml
appended a +g<sha> local segment unconditionally, including when the operator
passed an explicit RC version, and PyPI rejects local versions outright.

The constitution was already right — Principle IX scopes the local-segment rule
to "a dev build", not to RCs. The workflow was over-applying it.

Two modes now, and the difference is what the artifact may become:

  - No version input -> X.Y.Z.dev<run>+g<sha>. Un-uploadable by construction,
    so it can never leak and can never be promoted. Validation only.
  - version=X.Y.ZrcN -> stamped verbatim, no local segment, so it stays
    promotable. Resolvers skip pre-release versions by default, so it is still
    invisible to a plain `uv tool install remo-cli`.

An explicit version must match a PEP 440 pre-release/dev form; a final version
like 4.4.0 is refused, so this workflow cannot produce something that passes for
a real release.

New prerelease input attaches that same wheel (downloaded from the build job —
not rebuilt) to a GitHub pre-release tagged rc-<version>, so other machines
install by URL with no gh auth. The tag is deliberately not v*: release.yml
matches v* and publishes to PyPI + GHCR, and a guard in the step refuses any
v-prefixed tag outright. Nothing here touches PyPI.

contents: write is scoped to the new prerelease job; the common dev-build path
stays read-only under the top-level contents: read.

Docs: the Commands block and the Quality Gates note in CLAUDE.md/AGENTS.md now
cover both install paths and say why an RC drops the local segment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzLwS8fijKdrVVh7oj9kNd
@pofallon
pofallon merged commit 1192454 into main Aug 31, 2026
12 checks passed
@pofallon
pofallon deleted the feat/rc-github-prerelease branch August 31, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant