Metadata-Version: 2.5
Name: simple-releaser
Version: 0.1.0
Summary: A command-line tool for preparing and publishing Python package releases.
Project-URL: repository, https://codeberg.org/newbery/simple-releaser
Project-URL: issues, https://codeberg.org/newbery/simple-releaser/issues
Author-email: Ricardo Newbery <ric@digitalmarbles.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Software Distribution
Requires-Python: >=3.11
Requires-Dist: build<2.0.0,>=1.6.0
Requires-Dist: markdown-it-py<5.0.0,>=4.0.0
Requires-Dist: packaging<27.0,>=25.0
Requires-Dist: python-dotenv<2.0.0,>=1.1.0
Requires-Dist: twine<8.0.0,>=7.0.0
Requires-Dist: urllib3<3.0.0,>=2.8.0
Description-Content-Type: text/markdown

> ⚠️ **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](https://codeberg.org/newbery/qrtsy), 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](https://codeberg.org/newbery/simple-releaser/src/branch/master/docs/alternative-release-tools.md).


## 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`](https://pipx.pypa.io/):

```console
pipx install simple-releaser
```

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

```console
release
```

or:

```console
simple-releaser
```


## Commands

The available CLI commands are:

```console
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:

```console
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:

```console
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:

```toml
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:

```toml
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:

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

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

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

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

```toml
[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:

```toml
[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](https://en.wikipedia.org/wiki/Eating_your_own_dog_food)
its own tooling for releases of itself and uses [Poe the Poet](https://poethepoet.natn.io/)
as the task runner for its `check` task. This is defined in the project's
`releaser.toml` like so:

```toml
[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](https://codeberg.org/newbery/simple-releaser/src/branch/master/docs/task-runners.md)
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](https://build.pypa.io)
and publishes with [twine](https://twine.readthedocs.io):

```toml
[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:

```toml
# 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:

```toml
[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:

```toml
[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:

```toml
[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:

```toml
[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](https://codeberg.org/newbery/simple-releaser/src/branch/master/docs/github-trusted-publishing.md).


### 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:

```toml
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:

```toml
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:

```console
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:

```console
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:

```console
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:

```console
poe ci
```
