CI: GitHub Action
Automate the whole release loop on GitHub: molt keeps a "Version Packages" pull request in sync with your accumulated changesets, and publishes to PyPI over OIDC the moment that PR merges.
Molt ships a composite GitHub Action that installs itself with uv and runs the release loop for you. Because it is the same molt you run locally, nothing about the automation is GitHub-specific magic -- the Action just wires molt's own commands into the two events that matter.
The two-phase loop
Every push to your base branch triggers one of two behaviors, and molt decides which:
- Version phase -- there are pending changesets. Molt opens (or updates) a release pull request titled "Version Packages". That PR contains exactly what
molt versionwould do: bumped manifests, rewritten changelogs, an updated lockfile. As more changesets merge, the same PR is kept up to date -- it is reused, never re-created, so its number and review history survive. - Publish phase -- there are no pending changesets but there are versions on disk that are not yet on PyPI. That is the state right after the release PR merges. Molt builds and publishes the changed packages, git-tags each one, and creates a GitHub Release per package with its changelog entry as the body.
So the human loop is simply: merge feature PRs (each carrying a changeset), watch the "Version Packages" PR accumulate, and merge it when you want to cut a release. Merging it is the release.
The release PR lives on a dedicated branch, molt/release/<base> (for a main base, molt/release/main). Molt owns that branch and force-updates it -- do not hand-edit it, your changes are replaced on the next push.
A copy-pasteable workflow
That is the entire release automation for a single package or a monorepo. In production, pin actions/checkout, astral-sh/setup-uv, and molt/action to full commit SHAs.
You also configure PyPI Trusted Publishing once, on PyPI itself, to trust this repository and workflow. After that there is no API token anywhere in the workflow -- see Publishing.
Understanding the permissions
The three grants map one-to-one onto the three things molt does:
id-token: writeis the important one. It lets the job mint the short-lived OIDC token that PyPI exchanges for a one-time upload token. This is what replaces a storedPYPI_API_TOKENsecret entirely.contents: writelets molt push the release branch, push the annotated tags after a successful publish, and create the GitHub Releases.pull-requests: writelets molt open and update the "Version Packages" PR.
Starting from permissions: {} at the workflow level and granting per job is the recommended posture: nothing has ambient write access, and the OIDC grant exists only where a publish can happen. For a stricter setup, split the version phase and the publish phase into separate jobs so id-token: write is scoped to the publish job alone.
Pinning molt
molt-version pins the exact molt the Action runs, and it defaults to the Action's own version. Pinning it is what stops the Action and the CLI from drifting apart between releases -- a failure mode worth designing out from the start. Because the Action installs molt with uvx from a warm uv cache, a pinned version resolves in seconds.
Molt is not tied to GitHub Actions
The Action is a convenience, not a dependency. Every phase above is a plain molt command, and molt publish -- including --filter -- runs the same everywhere. You can drive releases from GitLab CI, a Jenkins box, a cron job, or your laptop with nothing more than uv installed:
This is a deliberate contrast with changesets, whose publish flow is coupled to its own GitHub Action. Molt keeps the orchestration and the CLI as the same code path, so "run it in CI" and "run it by hand" are the same tool.
Forge-agnostic by design
Opening the release PR, attributing authors, and writing release notes all go through molt's forge seam rather than being hard-wired to GitHub. GitHub ships first and is the most polished, but the abstraction is there from day one, so other forges are additions rather than rewrites. See Forges for what a forge backend does and GitLab, Gitea & others for what is planned.
Windows and cross-platform notes
Molt is tested on Windows, macOS, and Linux from the first commit, so the CLI behaves identically wherever your CI runs -- paths resolve correctly and output stays ASCII-clean under Windows' cp1252 console. The release job itself typically runs on Linux, but the tool you are automating does not care which runner you pick.
Action outputs
The Action exposes outputs so later steps can react to a release:
published--"true"when at least one package was published this run.published_packages-- a JSON array of{ "name", "version" }for what went out.has_changesets--"true"when pending changesets exist (the version phase ran).pull_request_number-- the number of the created or updated "Version Packages" PR.
See also
- Publishing -- the OIDC upload flow the publish phase runs.
molt version-- what the "Version Packages" PR contains.- Forges -- the seam behind PR creation and release notes.
- The changesets model -- the add / version / publish loop this automates.