Skip to content

Support manual PyPI releases via workflow_dispatch - #292

Draft
anandhu-eng wants to merge 5 commits into
mainfrom
release-dispatch-workflow
Draft

Support manual PyPI releases via workflow_dispatch#292
anandhu-eng wants to merge 5 commits into
mainfrom
release-dispatch-workflow

Conversation

@anandhu-eng

@anandhu-eng anandhu-eng commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

New release flow:

mlcflow release flow

Diagram source: .github/images/release_flow.png

prepare turns a dispatch into a tag; publish is unchanged and still the only job that uploads. An App-token push re-triggers workflows, so prepare's tag push starts publish — the same job a terminal git tag -a v1.2.5 && git push origin v1.2.5 starts. Wheels therefore only ever reach PyPI from a tag ref, built by one job, however the release was started.

Inputs

  • bumpnone (release VERSION as it stands) / patch / minor / major
  • dry_run — resolve, validate and build only; nothing committed, tagged or published

Version resolution lives in .github/scripts/resolve_release_version.py so the decisions are testable outside CI, which is how the matrix above was verified.

Concurrency

prepare takes one repo-wide group; publish one per tag. Overlapping dispatches would otherwise both resolve against the pre-release state and collide on the push.

⚠️ Required before the dispatch path works

mlc-automations must be a bypass actor on the main ruleset (bypass_actors[].actor_type: Integration), or a bump != none dispatch will fail at the push to main. The ruleset currently has an empty bypass list. Tag-driven releases are unaffected and work without this.

🤖 Generated with Claude Code

build_wheels.yml could only release from a pushed tag. Add a dispatch path
so a release can be started from the Actions UI, optionally incrementing
VERSION itself, without changing how tag-driven releases work.

The workflow is now two jobs:

- prepare (workflow_dispatch only) resolves the version, optionally bumps
  VERSION, commits it to the default branch and pushes the tag as the
  mlc-automations GitHub App. An App-token push re-triggers workflows, so
  that tag push starts publish.
- publish (v* tag push only) builds, uploads to PyPI and cuts the GitHub
  Release, exactly as before.

Wheels therefore still only ever reach PyPI from a tag ref, built by one
job, however the release was started. Retrying a failed release is
GitHub's "Re-run jobs" on the failed run, not a second entry point.

Inputs: bump (none/patch/minor/major) and dry_run.

Version resolution moves to .github/scripts/resolve_release_version.py so
the decisions are testable outside CI. PyPI versions are immutable, so
every case that would resolve to an already-published version is refused
up front, before anything is built, committed, tagged or uploaded; there
is no override. prepare likewise refuses to create a tag that already
exists and never force-updates one.

Requires mlc-automations to be a bypass actor on the main ruleset for the
VERSION bump push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@anandhu-eng
anandhu-eng requested a review from a team as a code owner August 8, 2026 20:50
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

🤖 AI PR Review Summary

Adds a new Python script to resolve and verify release versions against PyPI, enforcing immutability of published versions and automating version bumps. Updates the GitHub Actions workflow to support both tag-driven and dispatch-driven release flows, with detailed documentation and inputs for version bumping and dry runs. The script and workflow improve release safety by preventing re-use of published versions and automating tagging and version management. Risks include ensuring the script's network calls to PyPI are reliable and that the workflow permissions and token usage are correctly configured to allow commits and tag pushes by the automation. The design is clear and well-documented, but the complexity of the release flow requires careful maintenance and testing.

Comment thread .github/scripts/resolve_release_version.py
Comment thread .github/scripts/resolve_release_version.py Outdated
Comment thread .github/scripts/resolve_release_version.py
Comment thread .github/scripts/resolve_release_version.py
Comment thread .github/workflows/build_wheels.yml
@anandhu-eng
anandhu-eng marked this pull request as draft August 8, 2026 20:54
anandhu-eng and others added 4 commits August 9, 2026 11:58
Two of the five automated review comments were actionable:

- Send a descriptive User-Agent (and Accept) on the PyPI metadata
  request instead of the default `Python-urllib/3.x`, which PyPI asks
  API consumers not to rely on.

- Replace VERSION through a sibling temp file and os.replace() rather
  than truncating it in place, so a failed or interrupted write leaves
  the original intact instead of handing a truncated VERSION to the
  commit that follows. The write is fsync'd, the temp file is cleaned
  up on failure, and the result is read back and re-parsed before the
  release proceeds. A write that cannot even be started now reports a
  ::error:: annotation like every other refusal, rather than a raw
  traceback.

The other three were declined: GITHUB_OUTPUT needs no locking (steps
in a job are sequential), a PyPI-consistency grace period in verify
would wait for a version that is supposed to be absent, and the
bump=none already-published guard the bot asked for is already there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two workflow_dispatch runs that overlap both read VERSION, PyPI and the
tag list before either has pushed, both resolve to the same version, and
both clear the tag guard. Nothing corrupt lands even without this — git
refuses the second writer (non-fast-forward on the VERSION push,
"already exists" on the tag push) and the tag step carries an implicit
success(), so a failed VERSION push cannot leave an orphan tag behind.
But the losing run fails on a raw git error rather than on one of the
release script's own refusals, and which of the two wins is arbitrary.

`prepare` now takes a single repo-wide concurrency group, so the second
dispatch resolves against the state the first one left and is refused by
the ordinary guards: bump=none hits the tag guard, a bump level hits
"VERSION is already ahead of the latest release".

`publish` is serialized per tag, so re-running a release cannot overlap
the run it retries and race it on the PyPI upload and the GitHub
Release. Keyed on the ref rather than a constant because distinct tags
publish distinct versions and share no mutable state.

Neither group cancels in progress: interrupting a release between the
VERSION push and the tag push is worse than queueing behind it.

This is the release-level race; $GITHUB_OUTPUT itself needs no locking,
as it lives in $RUNNER_TEMP and concurrent runs get separate runners.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release workflow carried more comment than code — 131 of its 314
lines. Step-by-step narration ("Step 2: Set up Python"), restatements of
the line below, and multi-paragraph rationale are gone; what remains is
the small set that stops someone reintroducing a bug: the errexit
exemption behind the ls-remote assignment, why ref_type == 'tag' is not
redundant with on.push.tags, why `build` alone suffices, and one- or
two-line notes on each concurrency group.

Now 48 comment lines of 231. Verified comment-only: parsing both the old
and new YAML and diffing the structures shows no functional difference.

Same pass over resolve_release_version.py, mostly the module docstring
and the two docstrings added in the last two commits. The env/output
tables stay — they document an interface nothing else records.

Recorded the rule in AGENTS.md ("Comment style") with the repo's own
before/after examples, and as a one-liner in .claude/skill.md's Don'ts.

Re-ran the full resolver matrix against live PyPI plus the write-failure
paths; unchanged. actionlint and autopep8 -a both clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both release paths, the guards and the point where each one stops
mutating are easier to see at a glance than to read. Referenced from the
PR description, which drops the prose the diagram replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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