Skip to main content

⚠️ Warning ⚠️
Simple Releaser is beta software and still being tested. Expect rough edges, breaking changes, and an occasional gremlin.

Simple Releaser

Simple Releaser is a configurable command-line tool for validating, building, tagging, and publishing Python package releases.

The PyPI distribution is named simple-releaser. Once installed, it can be invoked as either simple-releaser or release.

Simple Releaser grew out of a direct copy of the release workflow used by the QRtsy Project, which was itself just a copy of similar tooling that I've used in several other projects. This version of the tool has since been updated significantly to generalize it in order to accommodate different release workflows. While Simple Releaser is very configurable, it is deliberately designed to be simple and is unlikely to grow much beyond this initial vision. If you've got more complex needs, you may wish to consider some alternatives.

Project assumptions

Run Simple Releaser from a directory containing the releaser configuration (releaser.toml by default). This directory may also be the project root, or the releaser configuration may point at a separate project directory.

The project being released is expected to have:

  • Project configuration, including name and version, in pyproject.toml.
  • A changelog file named CHANGELOG.md, with Unreleased and version sections.
  • A Git repo with an origin remote.

When publishing a repository release locally, the project is also expected to have:

  • A repository hosted by Codeberg, GitHub, or GitLab.com.
  • Repository host tokens or overrides supplied through environment variables or a .env file beside the releaser configuration when needed.

Installation

A convenient way to install Simple Releaser as a standalone command-line tool is with pipx:

pipx install simple-releaser

Once installed, Simple Releaser can then be invoked as either:

release

or:

simple-releaser

Commands

The available CLI commands are:

release -h
usage: release [-h] [--dry-run] {init,show-config,clean,validate,build,tag,to_pypi,to_repo,run} ...

Validate, build, archive, and publish Python project releases.

A simple project release policy:
  - Validate that release is ready
  - Run any project-specific checks
  - Build package and archive it
  - Create release tag
  - Publish package to PyPI or TestPyPI
  - Publish release and assets to repository host

positional arguments:
  {init,show-config,clean,validate,build,tag,to_pypi,to_repo,run}
    init                initialize Simple Releaser configuration
    show-config         show resolved configuration and project information
    clean               check if project is a clean git repository
    validate            validate that a new release is ready
    build               build and archive the current version
    tag                 create and push the current release tag
    to_pypi             publish the archived distributions to PyPI or TestPyPI
    to_repo             publish the release to repository host
    run                 run the configured release process

options:
  -h, --help            show this help message and exit
  --dry-run             show what would be done without making changes

Dry run

Pass --dry-run before or after a command to perform the normal read-only preflight and describe the release actions without making changes. For example:

release run --dry-run
release --dry-run tag

Dry-run mode still reads project/repository metadata, checks that the Git working tree is clean, validates the version and changelog, and inspects existing Git tags. It does not run configured project commands, create the release archive, create or push Git tags, publish to PyPI/TestPyPI, or call repository-host APIs. Because the build is simulated, later dry-run tasks use descriptive artifact placeholders rather than requiring _releases/<version> to already exist.

The read-only show-config, clean, and validate commands behave the same with or without --dry-run.

Initialization

Run release init from the directory where the releaser configuration should live:

release init

If that directory contains the pyproject.toml for a project, Simple Releaser creates a minimal releaser.toml for that project. Otherwise it prompts for the project directory and then records the given project_path in the generated configuration. Relative paths are used when possible. An existing releaser configuration is never overwritten.

release init --dry-run performs the same project discovery but only reports where the configuration would be created.

Configuration

Simple Releaser requires a configuration file in the current working directory. By default it is named releaser.toml. To use a different filename, set the RELEASER_CONFIG environment variable.

When the directory also contains the project's pyproject.toml, no project path is needed:

tag_prefix = "v"

[commands]
run = "check build tag to_pypi to_repo"

If the project lives elsewhere, project_path is required and must identify a directory containing pyproject.toml. Relative paths are resolved from the directory containing the releaser configuration; absolute paths are also accepted. For example:

project_path = "../my-project"
repo_host = "github"
tag_prefix = "v"

[commands]
run = "check build tag to_pypi to_repo"

Set auto_confirm = true at the top level to allow PyPI or TestPyPI publication without an interactive confirmation prompt, for example in a deliberately unattended release workflow. It defaults to false.

Build artifacts are archived under _releases in the same directory as the releaser config file. If _releases is in the project directory, then Simple Releaser will automatically add a .gitignore file to exclude it from Git. In the unlikely event that excluding this folder from Git is not desired, set gitignore_releases = false at the top level to disable this feature.

Configurable commands

The [commands] section currently supports check, build, publish, and run. A value may be either a shell-like string or a TOML array of argument strings. The following forms are equivalent:

check = "uv run pytest"
check = ["uv", "run", "pytest"]

Quoting in a string may be used to preserve spaces within one argument:

check = 'python -c "print(1 + 1)"'

An empty string or empty array disables the task associated with that configured command. For example:

[commands]
check = ""
publish = ""

causes the check and to_pypi tasks to be skipped. The publish configuration name controls the to_pypi task.

Configuring check

The configured check command is the project's generic handoff to its own release-readiness policy. Tests, linters, type checks, documentation checks, project-specific validation, or any other prerequisite can live behind this one command. Since that policy is project-specific, check is disabled by default and must be customized for the project:

[commands]
check = ""

Simple Releaser deliberately does not try to model all of those project-specific tasks itself. A useful pattern is to point check at a project task runner, so the same check task can be used during development, in CI, and during a release without duplicating workflow configuration. Command strings are parsed into arguments but are not executed through a shell; compound shell logic is therefore also usually cleaner inside a task runner or project script.

The Simple Releaser project dogfoods its own tooling for releases of itself and uses Poe the Poet as the task runner for its check task. This is defined in the project's releaser.toml like so:

[commands]
check = "hatch run poe check"

See the project's pyproject.toml for the poe check task configuration to see what it runs as part of its pre-release project check.

See Project Checks and Task Runners for a discussion of various other Task Runners that can be used in similar ways.

Configuring build and publish

By default, Simple Releaser builds its release artifacts with build and publishes with twine:

[commands]
build = "{python} -m build --outdir {output}"
publish = "{python} -m twine upload -r {pypi} {wheel} {sdist}"

Custom build and publish commands may also use {python}, which expands to the absolute path of the Python interpreter running Simple Releaser. This lets an override deliberately use a tool installed alongside Simple Releaser rather than whichever python happens to be on PATH.

The default publisher uses Twine's normal authentication mechanisms, including keyring, .pypirc, and TWINE_* environment variables. PyPI and TestPyPI have separate accounts and tokens; repository-specific .pypirc or keyring entries are the clearest way to keep those credentials distinct. For repository-host settings, Simple Releaser checks the process environment first and then falls back to the .env file beside the releaser configuration.

The {output} placeholder in build is replaced with the temporary directory where the build command must write the wheel and source distribution. A non-empty custom build command must include {output}. Publish commands may use {release_dir}, {wheel}, and {sdist} to refer to the verified archived release and its two distribution files. Publish commands may also use {pypi}, which expands to the selected pypi or testpypi service name. A custom publish command does not have to use a placeholder when the command already knows how to locate the artifacts and which service it should publish to.

The default build and publish commands work with most standards-compliant build backends declared in the [build-system] section of a project's pyproject.toml configuration. Projects that prefer their project-management tool's native build and publish commands can override the defaults. Typical equivalents are:

# Poetry
build = "poetry build --clean --output {output}"
publish = "poetry publish --dist-dir {release_dir}"

# Hatch
build = "hatch build --clean {output}"
publish = "hatch publish --no-prompt {wheel} {sdist}"

# uv
build = "uv build --no-sources --out-dir {output}"
publish = "uv publish {wheel} {sdist}"

# PDM
build = "pdm build --dest {output}"
publish = "pdm publish --no-build --dest {release_dir}"

Configuring run

The run command is an ordered list of task names. Its default value is:

[commands]
run = "check build tag to_pypi to_repo"

The available run task names are check, clean, validate, build, tag, to_pypi, and to_repo.

  • clean is available for release flows that want an explicit clean-repository check at a particular point, but this is usually unnecessary because release validation already checks the working tree and tasks that require cleanliness enforce it themselves.
  • validate may also be included explicitly, but again this is usually unnecessary since validation is already automatically done before run begins.

Both of these are included as options for custom release plans where revalidation or additional clean checks might be useful.

Local and CI release strategies

The run sequence also determines where the release process stops.

The default sequence performs the complete release locally:

[commands]
run = "check build tag to_pypi to_repo"

For a project whose remote CI publishes after a release tag is pushed, omit the local publication tasks:

[commands]
run = "check build tag"

Here the local build acts as a packaging preflight. Simple Releaser verifies that the distributions can be built and archived before tag creates and pushes the release tag. The remote workflow may then rebuild the tagged source and publish its own artifacts.

Projects with expensive, platform-specific, or otherwise CI-authoritative builds may omit the local build as well:

[commands]
run = "check tag"

In this form, Simple Releaser runs the project's configured check command and then creates and pushes the release tag; the remote workflow is responsible for both building and publishing. This avoids a duplicate local build, but a packaging failure will not be discovered until after the release tag has been pushed.

The configured check in these examples remains the project's own quality-check command, such as its tests, linters, and type checks. It is separate from Simple Releaser's release validation, which the CLI performs before the run sequence begins.

Because tag pushes the tag to origin, it can serve directly as the handoff to a tag-triggered CI workflow. Simple Releaser does not need to know which CI system handles the tag or how that system builds and publishes the release. When a release tag already exists locally or on origin, Simple Releaser reuses it only if it resolves to the current project commit; a conflicting tag aborts the release instead of being overwritten.

For a concrete GitHub Actions workflow using PyPI Trusted Publishing, see GitHub Actions and PyPI Trusted Publishing.

Repository host

The repository host and repository identifier are normally derived from the configured project's Git origin. HTTPS and SSH origins on codeberg.org, gitea.com, github.com, and gitlab.com are recognized automatically. tag_prefix controls the text prepended to the project version when creating and locating Git tags; it defaults to "v". Set it to an empty string to use bare version tags such as 1.2.3.

For unusual arrangements where the Git origin is not the repository that receives releases, repo_host may explicitly select codeberg, forgejo, gitea, github, or gitlab; pair it with the corresponding repository override when the release repository identifier also differ.

repo_url may be used with repo_host to select a different instance of that host family. The repo_url is the web/base URL of the repository-host instance, not an API URL. Simple Releaser derives the appropriate API paths for Forgejo/Gitea-compatible instances, GitHub Enterprise Server, and GitLab self-managed. For self-hosted instances, Simple Releaser normally derives the instance URL from the Git origin when repo_host identifies the host family. Use repo_url when that inferred URL is not correct, such as an installation below a URL base path or one whose Git and web hostnames differ. For example:

repo_host = "gitlab"
repo_url = "https://git.example.org"

Repository overrides may be set as environment variables or in the .env file beside releaser.toml; an exported environment variable takes precedence over the same .env entry.

To publish to a Codeberg repository, set CODEBERG_TOKEN. Set CODEBERG_REPOSITORY=owner/repo to override the repository identifier derived from the Git origin.

To publish to a Forgejo repository, set FORGEJO_TOKEN and configure repo_host = "forgejo". Simple Releaser derives the instance URL from the Git origin unless repo_url overrides it. Set FORGEJO_REPOSITORY=owner/repo to override the derived repository identifier.

To publish to a Gitea repository, set GITEA_TOKEN. Set GITEA_REPOSITORY=owner/repo to override the derived repository identifier. For self-hosted Gitea, configure repo_host = "gitea"; use repo_url only when the instance URL cannot be inferred correctly from the Git origin.

To publish to a GitHub repository, set GITHUB_TOKEN. Set GITHUB_REPOSITORY=owner/repo to override the derived repository identifier. The GitHub token must have permission to create releases and upload release assets.

To publish to a GitLab.com repository, set GITLAB_TOKEN. Set GITLAB_REPOSITORY=namespace/repo to override the derived repository identifier. Nested namespaces such as group/subgroup/repo are supported. The GitLab token must have permission to create releases, upload project files, and create release asset links.

PyPI service

The to_pypi task publishes to production PyPI by default. To exercise the publishing workflow without affecting the production index, select TestPyPI:

pypi = "testpypi"

The accepted values are "pypi" and "testpypi". The selected service is shown by show-config, dry runs, confirmation prompts, publication output, and project/release links.

TestPyPI is a separate service from production PyPI and requires its own account and API token. With the default Twine publisher, the selected value is passed as Twine's named repository (pypi or testpypi), so Twine can use the corresponding .pypirc or keyring credentials.

Development

For development purposes, some project checks and a test runner are defined as Poe tasks:

poe lint
poe format-check
poe typecheck
poe test

# Run all of the above
poe check 

The test suite can also be run across the matrix of supported Python versions, which currently includes Python 3.11, 3.12, 3.13, and 3.14, using another Poe task:

poe test-matrix

A separate packaging smoke test creates isolated test environments, installs the Simple Releaser wheel and sdist package artifacts, and performs some basic checks to verify that the packages are actually installable. This test can be invoked with another Poe task:

poe package-smoke

Finally, a more thorough check meant for CI runs the linters, type checks, tests across all supported Python versions, and the packaging smoke test. It can also be invoked locally via a Poe task:

poe ci

Release files for simple-releaser 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for simple-releaser 0.1.0
File Size Uploaded
simple_releaser-0.1.0.tar.gz 58.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for simple-releaser 0.1.0
File Interpreter ABI Platform
simple_releaser-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.4 kB

Release files / simple_releaser-0.1.0.tar.gz

Download URL simple_releaser-0.1.0.tar.gz
Size 58.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e3398e9113af9f7f1611ddf44e182b9a0a5fc95bd32191a78ea546fbdddcc982
BLAKE2b-256 checksum
How to use checksums
83658452c2a8b757c7603d5082b9bad45a0cb169584bde6acc936ee6e6a21e07
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / simple_releaser-0.1.0-py3-none-any.whl

Download URL simple_releaser-0.1.0-py3-none-any.whl
Size 32.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a066f12f5f3ebe5106764f4bf9d866eff25a4f1e17bdebedd9b2b286b027f124
BLAKE2b-256 checksum
How to use checksums
23a895295c2a926ac248291c6b24e61e77236bd6dd93d56167be49151dd88cd8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page