Skip to main content
Releases are prepared with a script that a maintainer runs locally, then built and published by a GitHub Actions workflow:
  1. tools/prepare_release.py — Run first, on your own machine. Validates the version, checks out the matching release/YY.MM branch, checks that CI passed, makes sure no duplicate tag or release exists, syncs translations, then commits the updated .version and pushes the branch. CI then runs automatically on the resulting commit (for release/** branches). The CI check can be skipped with --skip-ci-check.
  2. .github/workflows/release.yml — Dispatch after CI passes on the prepared commit. Builds installers and wheels for all platforms (Linux x86/ARM, macOS Intel/ARM, Windows), and can optionally sign macOS/Windows artifacts, create a draft GitHub release, publish wheels to TestPyPI, and publish wheels to PyPI.
release.yml is workflow_dispatch and uses a release concurrency group, so two release builds cannot run simultaneously.

Prepare script requirements

The prepare script runs on your machine, so it needs:
  • A clean working tree. The script stops if there are uncommitted changes.
  • An authenticated gh CLI, with push access to ankitects/anki and to the two translation repos.
  • rsync on the PATH. It copies the translation templates.
  • A Python interpreter with the packaging module, which the project’s uv environment provides.
The script pushes translation template commits to the translation repos before it commits .version, so a failure after that point leaves those pushes in place. Re-running the script is safe: the translation step is a no-op when there is nothing new to copy.

Version format

Versions follow calendar versioning with PEP 440: YY.MM for stable releases (e.g. 26.04), with optional .patch (e.g. 26.04.1) and pre-release suffixes (b1, rc1, a1). Months must be zero-padded. Examples: 26.05b1 (beta), 26.05rc1 (release candidate), 26.05 (stable), 26.05.1 (patch).

Release branch workflow

All releases are cut from a release/YY.MM branch. The branch name uses only the major version (YY.MM), not the full pre-release suffix — betas, release candidates, and the stable release all come from the same branch.

Standard release

  1. Create a release branch from main:
  2. CI runs automatically on push to release/** branches.
  3. Prepare the release (updates .version on the branch):
  4. Verify on TestPyPI:
  5. Publish the full release:
  6. For subsequent pre-releases or the stable release from the same cycle, repeat steps 3-5 with the new version (e.g. 26.05b2, 26.05rc1, 26.05).
  7. After the stable release, merge the release branch back to main to pick up the .version bump and any cherry-picked fixes:
  8. Delete the release branch after the stable release is published.

Security and hotfix releases

For security fixes, an admin should first create a security advisory with a temporary private fork. Work on the fix in the private fork via the normal PR workflow. Do not open a public PR or publish the advisory until the fix is ready for release. Once the fix is ready:
  1. Create a release branch from the latest release tag:
  2. Cherry-pick the fix onto the release branch.
  3. Push the branch and wait for CI:
  4. Prepare and publish:
  5. Merge the release branch back to main.
  6. For security patches, publish the advisory and credit the reporter if applicable.

Release process overview

Release workflow jobs

Inputs

prepare_release.py: takes a version argument and an optional --skip-ci-check flag. The release branch is derived from the version, so there is no branch argument. release: takes a version (must match .version for public release operations) and five boolean inputs:
  • sign signs macOS and Windows artifacts.
  • draft-release creates the draft GitHub release.
  • publish-testpypi publishes wheels to TestPyPI.
  • publish-pypi publishes wheels to PyPI.
  • skip-ci-check skips the CI status check.
All booleans default to false. Non-release runs use the .version already in the repo, so builds work without a prepare step. For a normal public release, enable the first four booleans: sign=true, draft-release=true, publish-testpypi=true, and publish-pypi=true.

Environment gates

The release workflow uses GitHub environments as manual approval gates. Jobs that access signing credentials or publish artifacts require a reviewer to approve the deployment before they run:
  • release — Required when sign, draft-release, publish-testpypi, or publish-pypi is enabled. Protects code-signing secrets, the release token, and PyPI/TestPyPI trusted publishing/OIDC.
When sign is disabled, the macOS and Windows build jobs run without the release environment so they do not require approval and cannot access signing secrets.

Workflow input behavior

The release.yml workflow uses independent boolean inputs to control what gets signed and published:

Running releases with just

The release module defined in release.just wraps both the prepare script and the release.yml dispatches. release::prepare runs locally and takes no --ref. Every other recipe dispatches release.yml and requires an explicit --ref argument pointing to the release branch. Run just --list --list-submodules to see all available recipes and their arguments.

Testing the release workflow from a feature branch

release.yml can be dispatched from any branch for testing. The release guards only apply when draft-release or publish-pypi is enabled. To run a test build:
  1. Dispatch release.yml from your branch with all boolean inputs left false:
  2. The workflow reads .version from the branch as-is (the version input is ignored for non-release runs), so no prepare step is needed.
  3. All release guards (CI check, duplicate tag check) are skipped.
  4. Artifacts are uploaded to the workflow run but nothing is published or tagged.

Testing with code signing

To test the signing flow from a feature branch:
  1. In the repo’s Settings → Environments → release, temporarily add your branch to the allowed deployment branches.
  2. Dispatch the workflow:
  3. Approve the environment deployment when prompted.
  4. After testing, remove your branch from the environment’s allowed branches.
Note: workflow_dispatch workflows only appear in the GitHub Actions UI if the workflow file exists on the default branch. If release.yml is new or modified on your branch, use gh workflow run to trigger it — the UI dropdown won’t show it until it’s merged to main.

Important notes

  • The release workflow builds the exact commit at github.sha. It does not write .version — that is done by the prepare script. If you dispatch release before the prepare commit has been pushed, the build will use whatever .version was HEAD at dispatch time.
  • draft-release=true with sign=false is rejected — draft releases must use signed installer artifacts.
  • When publish-pypi=true, wheels are published to TestPyPI first, then to PyPI after the TestPyPI job succeeds. If draft-release=true is also set, PyPI publishing waits for the draft GitHub release to succeed too.
  • Pre-release versions (e.g. 26.05b1) are automatically marked as pre-releases on the GitHub draft release.

Announcements

  • Once a GitHub release draft is created, modify the generated changelog if necessary then click Publish release.
  • Create a forum topic on the Beta Testing category. For stable releases, lock the topic and ask users to report issues on a new topic.
  • For stable releases, update the version in ankitects/anki-landing-page (See example).

Audio package

Audio playback and recording is handled by the anki-audio package under qt/audio, which bundles mpv and lame for Windows and macOS. To update:
  1. Update the archive links in build/configure/src/audio.rs if you’re updating the Windows pre-built binaries to new versions, then run ./tools/ninja audio_wheel to test.
  2. Update the macOS build script at qt/audio/mpv.rb and test the Homebrew build locally by running the qt/audio/build.sh script.
  3. Bump the version at qt/audio/.version.
  4. Submit a PR with the changes.
  5. Once the PR is merged to main, run the .github/workflows/publish-audio-package.yml workflow to publish the package to PyPI:
  6. Once the package is published, update the pinned anki-audio version in qt/pyproject.toml to the new version.