feat(ci): build promotable RC wheels and attach them to a GitHub pre-release - #198
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.ymlappended a+g<sha>local segment unconditionally, including when an operator passed an explicit RC version — and PyPI rejects local versions outright. So4.4.0rc1+g4a0e2aa, the wheel built yesterday, is structurally unpromotable, and merging the release PR woulduv builda 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.ymlwas over-applying it. No constitution change needed.Two modes
X.Y.Z.dev<run>+g<sha>version=X.Y.ZrcNX.Y.ZrcNverbatimAn explicit version must match a PEP 440 pre-release/dev form. A final version like
4.4.0is refused, so this workflow cannot produce something that passes for a real release. Verified against the shipped regex:The GitHub pre-release
-f prerelease=trueattaches 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 noghauth: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 notv*—release.ymltriggers onv*and publishes to PyPI + GHCR. A guard in the step refuses anyv-prefixed tag outright rather than relying on the naming convention holding.contents: writeis scoped to the newprereleasejob; the ordinary dev-build path stays read-only under the top-levelcontents: readadded 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
build+prerelease; permissionscontents: read/contents: writerespectively; heredoc body dedents to column 0uv run pytest tests/unit/test_docs_structure.py— 10 passedNot 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