Local-first release CLI tools: ship_bump, ship_pypi, ship_release_gh
Tiny, local-first release tools for modern Python projects.
fastship gives you the same workflow feel as the nbdev nbdev-bump-version, release-pypi, and release-gh commands — but for plain (non-notebook) Python projects.
pip install fastship
Create a new project:
ship-new my-project
cd my-project
pip install -e .[dev]
This creates a complete project with pyproject.toml, __version__, LICENSE, README, and everything wired for fastship.
ship-bumpBump a version part (0=major, 1=minor, 2=patch). For Rust projects (a Cargo.toml next to pyproject.toml) it bumps [package].version in Cargo.toml and runs maturin develop; otherwise it rewrites __version__ in your package __init__.py:
ship-bump --part 2
ship-bump --part 1
ship-bump --part 0
Decrement instead:
ship-bump --part 2 --unbump
ship-pypiBuild + upload to PyPI:
ship-pypi
Upload to a named repository in ~/.pypirc (e.g. testpypi):
ship-pypi --repository testpypi
Quiet mode:
ship-pypi --quiet
Fastship can also handle the repeated local tooling for PyO3 projects that use maturin and need Rust CLI binaries bundled into wheel scripts.
Create a new PyO3 project:
ship-rs-new my-project
cd my-project
pip install -e .[dev]
ship-rs-test
Use Cargo.toml as the version source:
[project]
name = "my_project"
dynamic = ["version"]
Commands:
ship-rs-new my-project # create a new maturin/PyO3 project
ship-rs-init # configure an existing maturin/PyO3 project
ship-rs-build # maturin build --release -o dist
ship-rs-test # build/install wheel, pytest -q
ship-bump # bump Cargo.toml version, then refresh the local editable install
ship-rs-release # tag v<version> and push branch + tags
ship-rs-init must be run from an existing maturin project with Cargo.toml. It sets [project].dynamic = ["version"], removes [project].version, and exposes __version__ from CARGO_PKG_VERSION when it finds the PyO3 module.
Generated CI runs the tests, then builds wheels with maturin-action across an OS matrix (manylinux: auto on Linux) and publishes to GitHub Releases and PyPI on v* tags. Any CLI tools are Python console scripts declared in [project.scripts]; there are no native Rust binaries to build.
ship-prCreate a PR from uncommitted or unpushed work, merge it immediately, and clean up:
ship-pr "Add new feature"
ship-pr "Fix bug" --label bug
ship-pr "Breaking change" --label breaking
This command:
enhancement)You must be on the default branch (usually main) with no unpulled changes.
ship-changelogGenerate or update CHANGELOG.md from closed GitHub issues since your last release:
ship-changelog
This is useful when you want to edit the changelog separately (e.g., in an editor or Claude Code) before releasing.
If you already have a CHANGELOG.md, it must include <!-- do not remove --> near the top so fastship knows where to insert the next release notes.
ship-ghThis is an interactive helper:
CHANGELOG.md from closed GitHub issues since your last GitHub release$EDITOR (defaults to nano) so you can edit the changeloggit commit -am release, git push__version__ship-gh
If you’ve already prepared the changelog (e.g., via ship-changelog), skip the changelog step:
ship-gh --no_changelog
This still opens CHANGELOG.md in your editor for final review before the release is created.
ship-gh looks for a token in this order:
FASTSHIP_TOKEN./token file in your repo rootGITHUB_TOKENThe token must have permission to create releases (typically repo scope for classic PATs, or appropriate fine-grained permissions).
ship-releaseFull release workflow assuming changelog is ready:
ship-changelog # generate changelog, edit as needed
ship-release # release to GitHub + PyPI, bump version, push
This runs:
ship-gh --no_changelog (open CHANGELOG.md for final review, commit if needed, push, create GitHub release)ship-pypi (upload to PyPI)ship-bump (bump patch version)ship-pypi does not bump your version for you — keep it explicit and boring.ship-gh requires that your project has a git origin remote pointing at GitHub (or use --repo OWNER/REPO).To add fastship to an existing project:
__init__.pyIn your package’s main __init__.py:
__version__ = "0.0.1"
pyproject.toml[project]
name = "my-project"
dynamic = ["version"]
[tool.setuptools.dynamic]
version = { attr = "my_project.__version__" }
Keep __version__ = "x.y.z" as a simple literal (don’t compute it). ship-bump will rewrite this line near the top of the file to keep builds happy.
Fastship infers your package name from [project].name (changing - to _). To override the release branch:
[tool.fastship]
branch = "main" # defaults to current git branch