# Release process

_Black_ has had a lot of work done into standardizing and automating its release
process. This document sets out to explain how everything works and how to release
_Black_ using said automation.

## Release cadence

**We aim to release whatever is on `main` every 1-2 months.** This ensures merged
improvements and bugfixes are shipped to users reasonably quickly, while not massively
fracturing the user-base with too many versions. This also keeps the workload on
maintainers consistent and predictable.

If there's not much new on `main` to justify a release, it's acceptable to skip a
month's release. Ideally January releases should not be skipped because as per our
[stability policy](labels/stability-policy), the first release in a new calendar year
may make changes to the _stable_ style. While the policy applies to the first release
(instead of only January releases), confining changes to the stable style to January
will keep things predictable (and nicer) for users.

Unless there is a serious regression or bug that requires immediate patching, **there
should not be more than one release per month**. While version numbers are cheap,
releases require a maintainer to both commit to do the actual cutting of a release, but
also to be able to deal with the potential fallout post-release. Releasing more
frequently than monthly nets rapidly diminishing returns.

## Cutting a release

**You must have `write` permissions for the _Black_ repository to cut a release.**

The 10,000 foot view of the release process is that you prepare a release PR and then
publish a [GitHub Release]. This triggers [release automation](#release-workflows) that
builds all release artifacts and publishes them to the various platforms we publish to.

We now have a `scripts/release.py` script to help with cutting the release PRs.

- `python3 scripts/release.py --help` is your friend.
  - `release.py` has only been tested in Python 3.12 (so get with the times :D)

To cut a release:

1. Determine the release's version number
   - **_Black_ follows the [CalVer] versioning standard using the `YY.M.N` format**
     - So unless there already has been a release during this month, `N` should be `0`
   - Example: the first release in January, 2022 → `22.1.0`
   - `release.py` will calculate this and log to stderr for you copy paste pleasure
1. Double-check that no changelog entries since the last release were put in the wrong
   section (e.g., run `git diff origin/stable CHANGES.md`)
1. File a PR editing `CHANGES.md` and the docs to version the latest changes
   - Run `python3 scripts/release.py [--debug]` to generate most changes
1. If `release.py` fails manually edit; otherwise, yay, skip this step!
   1. Replace the `## Unreleased` header with the version number
   1. Remove any empty sections for the current release
   1. (_optional_) Read through and copy-edit the changelog (eg. by moving entries,
      fixing typos, or rephrasing entries)
   1. Update references to the latest version in
      {doc}`/integrations/source_version_control` and
      {doc}`/usage_and_configuration/the_basics`
   - Example PR: [GH-4563]
1. Once the release PR is merged, wait until all CI passes
   - If CI does not pass, **stop** and investigate the failure(s) as generally we'd want
     to fix failing CI before cutting a release
1. [Draft a new GitHub Release][new-release]
   1. Click `Choose a tag` and type in the version number, then select the
      `Create new tag: YY.M.N on publish` option that appears
   1. Verify that the new tag targets the `main` branch
   1. Make sure the release title is set to the version (`YY.M.N`), as otherwise the
      default title is the last commit's title
   1. Copy and paste the _raw changelog Markdown_ for the current release into the
      description box
1. Publish the GitHub Release, triggering [release automation](#release-workflows) that
   will handle the rest
1. Once CI is done add + PR a new empty template for the next release to CHANGES.md
   _(Template is able to be copy pasted from release.py should we fail)_
   1. `python3 scripts/release.py --add-changes-template|-a [--debug]`
   1. Should that fail, please return to copy + paste
1. At this point, you're basically done. It's good practice to go and [watch and verify
   that all the release workflows pass][black-actions], although you will receive a
   GitHub notification should something fail.
   - If something fails, don't panic. Please go read the respective workflow's logs and
     configuration file to reverse-engineer your way to a fix/solution.

Congratulations! You've successfully cut a new release of _Black_. Go and stand up and
take a break, you deserve it.

```{important}
Once the release artifacts reach PyPI, you may see new issues being filed indicating
regressions. While regressions are not great, they don't automatically mean a hotfix
release is warranted. Unless the regressions are serious and impact many users, a hotfix
release is probably unnecessary.

In the end, use your best judgement and ask other maintainers for their thoughts.
```

## Release workflows

All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore
configured using YAML files in the `.github/workflows` directory of the _Black_
repository.

They are triggered by the publication of a [GitHub Release].

Below are descriptions of our release workflows.

### Publish to PyPI

This is our main workflow. It builds an [sdist] and [wheels] to upload to PyPI where the
vast majority of users will download Black from. It's divided into three job groups:

#### sdist + pure wheel

This single job builds the sdist and pure Python wheel (i.e., a wheel that only contains
Python code) using [build] and then uploads them to PyPI using [twine]. These artifacts
are general-purpose and can be used on basically any platform supported by Python.

#### mypyc wheels (…)

We use [mypyc] to compile _Black_ into a CPython C extension for significantly improved
performance. Wheels built with mypyc are platform and Python version specific.
[Supported platforms are documented in the FAQ](labels/mypyc-support).

These matrix jobs use [cibuildwheel] which handles the complicated task of building C
extensions for many environments for us. Since building these wheels is slow, there are
multiple mypyc wheels jobs (hence the term "matrix") that build for a specific platform
(as noted in the job name in parentheses).

Like the previous job group, the built wheels are uploaded to PyPI using [twine].

#### Update stable branch

So this job doesn't _really_ belong here, but updating the `stable` branch after the
other PyPI jobs pass (they must pass for this job to start) makes the most sense. This
saves us from remembering to update the branch sometime after cutting the release.

- _Currently this workflow uses an API token associated with @ambv's PyPI account_

### Publish executables

This workflow builds native executables for multiple platforms using [PyInstaller]. This
allows people to download the executable for their platform and run _Black_ without a
[Python runtime](https://wiki.python.org/moin/PythonImplementations) installed.

The created binaries are stored on the associated GitHub Release for download over _IPv4
only_ (GitHub still does not have IPv6 access 😢).

### docker

This workflow uses the QEMU powered `buildx` feature of Docker to upload an `arm64` and
`amd64`/`x86_64` build of the official _Black_ Docker image™.

- _Currently this workflow uses an API Token associated with @cooperlees account_

```{note}
This also runs on each push to `main`.
```

[black-actions]: https://github.com/psf/black/actions
[build]: https://pypa-build.readthedocs.io/
[calver]: https://calver.org
[cibuildwheel]: https://cibuildwheel.readthedocs.io/
[gh-4563]: https://github.com/psf/black/pull/4563
[github actions]: https://github.com/features/actions
[github release]: https://github.com/psf/black/releases
[new-release]: https://github.com/psf/black/releases/new
[mypyc]: https://mypyc.readthedocs.io/
[mypyc-platform-support]:
  /faq.html#what-is-compiled-yes-no-all-about-in-the-version-output
[pyinstaller]: https://www.pyinstaller.org/
[sdist]:
  https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist
[twine]: https://github.com/features/actions
[wheels]: https://packaging.python.org/en/latest/glossary/#term-Wheel
