Getting started
Put It Out There (piot) is a polyglot release orchestrator. One putitoutthere.toml describes your packages; the CLI computes a release plan from git + a commit trailer and publishes to crates.io, PyPI, and npm.
Does piot fit your library?
piot is a good fit if you can answer yes to most of these:
- [ ] Your artifacts publish to some combination of crates.io, PyPI, and npm — piot only covers those three registries.
- [ ] You use (or are willing to use) OIDC trusted publishing on each registry. Long-lived tokens work as a fallback, but OIDC is the happy path.
- [ ] You build your artifacts in GitHub Actions. The scaffolded workflow is a GitHub Actions workflow; piot's Action and OIDC flows assume that runtime.
- [ ] You're comfortable with one tag per package — defaults to
{name}-v{version}, or pick your own template viatag_format. - [ ] Your release trigger is up to you: merge to
main, scheduled cron, or manual dispatch all work. See the nightly release recipe for the cron shape. - [ ] The default bump is
patchwhenever a package's files change. Opt into explicitminor/majorbumps (orskip) via a commit trailer — trailers are optional.
piot is probably not the right tool if:
- You need a registry piot doesn't cover (Maven, NuGet, Docker Hub, internal registries, etc.).
- You publish from CI systems other than GitHub Actions.
- You want piot to run the compile itself. piot emits the build-job matrix (with per-target
runneroverrides you declare in config), butmaturin build/napi build/cargo buildstill live in your workflow's build step. - You want standalone binary archives attached to GitHub Releases with a curl-installable tarball. That's
cargo-dist's /goreleaser's lane; compose with them, don't replace them with piot. - You need changelog generation. Delegate to
release-pleaseor similar. - You want automatic tag rollback on partial-publish failures. piot deliberately doesn't do this — crates.io is immutable, so deletion isn't safe. Instead piot runs a completeness-check before anything ships.
See Known gaps for the full enumeration of non-goals and limitations, or Design commitments for the policy these are derived from.
Migrating? Read this first
If you already publish to crates.io / PyPI / npm and you're switching to piot, two things trip up most migrations:
The caller workflow filename is load-bearing for OIDC. crates.io and npm pin the caller workflow filename in the OIDC trust policy's JWT claim. If your current trusted publisher is registered against (say)
patch-release.ymlandputitoutthere initwrites a newrelease.yml, publish fails with HTTP 400. Declare the expected workflow inputitoutthere.tomlsodoctorcatches the drift before cutover:toml[package.trust_policy] workflow = "release.yml" environment = "release"With the block in place,
doctordiffs the declared workflow against the local file and (in CI) againstGITHUB_WORKFLOW_REF. See Authentication → Declaring trust-policy expectations.Tags are per package, not shared. piot tags each package independently as
{name}-v{version}. Anything reading a single sharedv{version}tag today (install scripts, doc links, release notes scripts) needs updating. Single-package repos often wanttag_format = "v{version}"instead, to keep the existing timeline — see Configuration.Dynamic-version
pyproject.toml. If your PyPI package uses[project].dynamic = ["version"]with hatch-vcs / setuptools-scm, piot skips the pyproject rewrite (the build backend owns the computation). You need to pass the planned version to the build backend via an env var, or you'll ship<pkg>-X.Y.Z.devN.tar.gzinstead of<pkg>-X.Y.Z.tar.gz. See dynamic versions for the recipe.
Pick your library shape
Worked end-to-end examples for the common shapes. Pick the one that matches your repo:
- Single-package Python library — one
pyproject.toml, publishing to PyPI. Covers static-version and dynamic-version (hatch-vcs/setuptools-scm) setups. - Polyglot Rust library (Rust crate + PyO3 wheel + napi npm) — one Rust core, three artifacts (crates.io, PyPI via
maturin, npm vianapi-rs).
More shapes will live under Library shapes as they're written.
Install
npx putitoutthere initScaffolds:
putitoutthere.toml— declare your packages..github/workflows/release.yml— plan → build → publish pipeline..github/workflows/putitoutthere-check.yml— PR dry-run check.putitoutthere/AGENTS.md— the trailer convention your LLM agent will follow.
Migrating an existing workflow? Run init in a scratch directory first
piot's Action surface is command: plan and command: publish — there is no command: build. The middle job is your workflow's job. The 3-job structure plan → build → publish (with actions/upload-artifact between them, keyed off matrix.artifact_name) is part of piot's contract; the publish-side completeness check assumes it.
If you're hand-porting from a 2-job inline-build-publish workflow, run npx putitoutthere init in a temp directory first, treat the emitted release.yml as the canonical reference, and copy in only the diff you care about. See Concepts → piot's surface and the artifact contract.
Minimum config
[putitoutthere]
version = 1
[[package]]
name = "my-crate"
kind = "crates"
path = "."
paths = ["src/**", "Cargo.toml"]
first_version = "0.1.0"Release a version
Merge to main. A patch release ships automatically. To bump minor or major:
release: minorin the merge commit body. See the trailer guide for the full grammar.
Further reading
- Concepts — cascade, trailer, plan/build/publish, and what piot does / doesn't cover.
- Configuration — every field in
putitoutthere.toml. - CLI reference.