anki Documentation

Anki is a smart spaced repetition flashcard program

32,065 stars Rust Rust 37 files · ~28,604 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy anki on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name anki --restart always ankitects/anki:latest

Technical Documentation: ankitects/anki

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

Anki



This repo contains the source code for the computer version of
Anki.

About

Anki is a spaced repetition program. Please see the website to learn more.

Getting Started

Contributing

Want to contribute to Anki? Check out the Contribution Guidelines.

For more information on building and developing, please see Development.

Contributors

The following people have contributed to Anki: CONTRIBUTORS

Anki Betas

If you'd like to try development builds of Anki but don't feel comfortable
building the code, please see Anki betas.

License

Anki's license: LICENSE


Overview (`cargo/README.md`)

This folder contains:

  • a list of Rust crate licenses, which is checked/updated with ./ninja [check|fix]:minilints
  • a nightly toolchain definition for formatting

Overview (`docs-site/README.md`)

Documentation Website

This directory contains Mintlify docs served at https://anki.mintlify.app/

License

Unless stated otherwise, any documentation changes you submit will be licensed under the CC BY-SA 4 license.

Translations

To help translate the website to your language, please see
Translating the Documentation.


Overview (`ftl/README.md`)

Files related to Anki's translations.

Please see https://translating.ankiweb.net/anki/developers


Overview (`proto/README.md`)

Protobuf files defining the interface the frontend and backend components use to talk to each other,
and how Anki stores some of the data inside its SQLite database. These files are used to generate Rust,
Python and TypeScript bindings.


Overview (`pylib/README.md`)

Anki

Core Python library for Anki, the spaced repetition flashcard program.

About

Anki is a spaced repetition program that helps you remember things efficiently. This package contains the Python layer of Anki's core: it wraps the Rust backend (rslib) and exposes the primary API used by the desktop app and add-ons alike.

It provides access to:

  • Collection — open, read, and write an Anki .anki2 database
  • Notes & Cards — create, update, and query notes and cards
  • Decks & Models — manage deck configurations and note types
  • Scheduler — the FSRS/SM-2 scheduling algorithms
  • Media — media file management and sync
  • Import / Export — support for .apkg, .colpkg, and other formats
  • Sync — synchronisation with AnkiWeb
  • Hooks — event system for extending behaviour

Installation

bash
pip install anki

Note: anki is the headless library. If you want the full desktop application, install aqt instead, which depends on this package.

Add-on development

If you are building an Anki add-on, this is the package that gives you access to the collection and scheduling internals. See the Add-on Guide for full documentation.

Contributing

Want to contribute? Check out the Contribution Guidelines and the Development Guide.

License

AGPL-3.0-or-later


Overview (`qt/README.md`)

aqt — Anki Qt GUI

aqt is the Qt-based desktop interface for Anki, the spaced repetition flashcard program.

It provides all the visual components of the Anki desktop app: the deck browser, card editor, reviewer, browser, add-on manager, and more. Under the hood it uses PyQt6 and communicates with Anki's core logic via the anki package.

Running

Once installed, Anki can be launched from the command line:

bash
anki

Add-on development

If you are building an Anki add-on, this is the package that exposes the GUI hooks and Qt widgets you need. See the Add-on Guide for full documentation.

Source code

https://github.com/ankitects/anki


Overview (`ts/README.md`)

Anki's TypeScript and Sass dependencies. Some TS/JS code is also
stored separately in ../qt/aqt/data/web/.

To update all dependencies:

./update.sh

To add a new dev dependency, use something like:

./add.sh -D @rollup/plugin-alias


Overview (`docs/docker/README.md`)

Building and running Anki in Docker

This is an example Dockerfile contributed by an Anki user, which shows how Anki
can be both built and run from within a container. It works by streaming the GUI
over an X11 socket.

Building and running Anki within a container has the advantage of fully isolating
the build products and runtime dependencies from the rest of your system, but it is
a somewhat niche approach, with some downsides such as an inability to display natively
on Wayland, and a lack of integration with desktop icons/filetypes. But even if you
do not use this Dockerfile as-is, you may find it useful as a reference.

Anki's Linux CI runs on GitHub Actions. You can use, as references, the setup-anki
to see the lists of packages CI installs, and ci.yml to see the build steps that follow.

Build the Docker image

For best results, enable BuildKit (export DOCKER_BUILDKIT=1).

When in this current directory, one can build the Docker image like this:

bash
docker build --tag anki --file Dockerfile ../../

When this is done, run docker image ls to see that the image has been created.

If one wants to build from the project's root directory, use this command:

bash
docker build --tag anki --file docs/docker/Dockerfile .

Run the Docker image

Anki starts a graphical user interface, and this requires some extra setup on the user's
end. These instructions were tested on Linux (Debian 11) and will have to be adapted for
other operating systems.

To allow the Docker container to pull up a graphical user interface, we need to run the
following:

bash
xhost +local:root

Once done using Anki, undo this with

bash
xhost -local:root

Then, we will construct our docker run command:

bash
docker run --rm -it \
    --name anki \
    --volume $HOME/.local/share:$HOME/.local/share:rw \
    --volume /etc/passwd:/etc/passwd:ro \
    --user $(id -u):$(id -g) \
    --volume /tmp/.X11-unix:/tmp/.X11-unix:rw \
    --env DISPLAY=$DISPLAY \
    anki

Here is a breakdown of some of the arguments:

  • Mount the current user's ~/.local/share directory onto the container. Anki saves things
    into this directory, and if we don't mount it, we will lose any changes once the
    container exits. We mount this as read-write (rw) because we want to make changes here.

    bash
    --volume $HOME/.local/share:$HOME/.local/share:rw
  • Mount /etc/passwd so we can enter the container as ourselves. We mount this as
    read-only because we definitely do not want to modify this.

    bash
    --volume /etc/passwd:/etc/passwd:ro
  • Enter the container with our user ID and group ID, so we stay as ourselves.

    bash
    --user $(id -u):$(id -g)
  • Mount the X11 directory that allows us to open displays.

    bash
    --volume /tmp/.X11-unix:/tmp/.X11-unix:rw
  • Pass the DISPLAY variable to the container, so it knows where to display graphics.

    bash
    --env DISPLAY=$DISPLAY

Running Dockerized Anki easily from the command line

One can create a shell function that executes the docker run command. Then one can
simply run anki on the command line, and Anki will open in Docker. Make sure to change
the image name to whatever you used when building Anki.

bash
anki() {
    docker run --rm -it \
        --name anki \
        --volume $HOME/.local/share:$HOME/.local/share:rw \
        --volume /etc/passwd:/etc/passwd:ro \
        --user $(id -u):$(id -g) \
        --volume /tmp/.X11-unix:/tmp/.X11-unix:rw \
        --env DISPLAY=$DISPLAY \
        anki "$@"
}

Overview (`docs/syncserver/README.md`)

Building and running Anki sync server in Docker

This is an example Dockerfile contributed by an Anki user, which shows how you can run a self-hosted sync server,
similar to what AnkiWeb.net offers.

Building and running the sync server within a container has the advantage of fully isolating
the build products and runtime dependencies from the rest of your system.

Requirements

Aspect Dockerfile Dockerfile.distroless
Shell & Tools ✅ Includes shell and tools ❌ Minimal, no shell or tools
Debugging ✅ Easier debugging with shell and tools ❌ Harder to debug due to minimal environment
Health Checks ✅ Supports complex health checks ❌ Health checks need to be simple or directly executable
Image Size ❌ Larger image size ✅ Smaller image size
Customization ✅ Easier to customize with additional packages ❌ Limited customization options
Attack Surface ❌ Larger attack surface due to more installed packages ✅ Reduced attack surface
Libraries ✅ More libraries available ❌ Limited libraries
Start-up Time ❌ Slower start-up time due to larger image size ✅ Faster start-up time
Tool Compatibility ✅ Compatible with more tools and libraries ❌ Compatibility limitations with certain tools
Maintenance ❌ Higher maintenance due to larger image and dependencies ✅ Lower maintenance with minimal base image
Custom uid/gid ✅ It's possible to pass in PUID and PGID ❌ PUID and PGID are not supported

Building image

To proceed with building, you must specify the Anki version you want, by replacing <version> with something like 24.11 and <Dockerfile> with the chosen Dockerfile (e.g., Dockerfile or Dockerfile.distroless)

The --platform option specifies which systems the image will be able to run on after it has been built. This is helpful in scenarios where you are building the image on one architecture (eg: an M series Mac) but running the image on another architecture (eg: x86 Linux server).

  • linux/amd64 is for x86 linux systems (Images built with this option will work on Windows as well through WSL2 which is included with Docker Desktop)
  • linux/arm64 is for ARM systems (eg: M series Macs)

On Linux systems, you will have to configure a custom builder which supports ARM builds first as shown below. You can then run the Mac build command in the next step.

bash
# Configuring multiplatform builder and activating it
docker buildx create --name multiplatform-builder --driver docker-container --driver-opt default-load=true --bootstrap --use

On M-series Macs running Docker Desktop, you can build for both platforms using the default builder by running the command below:

bash
# Execute this command from this directory
docker buildx build -f <Dockerfile> --platform linux/amd64,linux/arm64 --no-cache --build-arg ANKI_VERSION=<version> -t anki-sync-server .

If the build fails with Resource Exhausted:.... cannot allocate memory, go to Docker Desktop > Settings > Resources and increase the memory limit.

If you are unsure of what platform your system is, just run the command below without the platform option.

bash
# Execute this command from this directory
docker buildx build -f <Dockerfile> --no-cache --build-arg ANKI_VERSION=<version> -t anki-sync-server .

Run container

Once done with build, you can proceed with running this image with the following command:

bash
# this will create anki server
docker run -d \
    -e "SYNC_USER1=admin:admin" \
    -p 8080:8080 \
    --mount type=volume,src=anki-sync-server-data,dst=/anki_data \
    --name anki-sync-server \
    anki-sync-server

If the image you are using was built with Dockerfile you can specify the
PUID and PGID env variables for the user and group id of the process that
will run the anki-sync-server process. This is valuable when you want the files
written and read from the /anki_data volume to belong to a particular
user/group e.g. to access it from the host or another container. Note the the
ids chosen for PUID and PGID must not already be in use inside the
container (1000 and above is fine). For example add -e "PUID=1050" and -e "PGID=1050" to the above command.

If you want to have multiple Anki users that can sync their devices, you can
specify multiple SYNC_USER as follows:

bash
# this will create anki server with multiple users
docker run -d \
    -e "SYNC_USER1=admin:admin" \
    -e "SYNC_USER2=admin2:admin2" \
    -p 8080:8080 \
    --mount type=volume,src=anki-sync-server-data,dst=/anki_data \
    --name anki-sync-server \
    anki-sync-server

Moreover, you can pass additional env vars mentioned
here. Note that SYNC_BASE and
SYNC_PORT will be ignored. In the first case for safety reasons, to avoid
accidentally placing data outside the volume and the second for simplicity
since the internal port of the container does not matter given that you can
change the external one.

Upgrading

If your image was built after January 2025 then you can just build a new image
and start a new container with the same configuration as the previous
container. Everything should work as expected.

If the image you were running was built before January 2025 then it did not
contain a volume, meaning all syncserver data was stored inside the container.
If you discard the container, for example because you want to build a new
container using an updated image, then your syncserver data will be lost.

The easiest way of working around this is by ensuring at least one of your
devices is fully in sync with your syncserver before upgrading the Docker
container. Then after upgrading the container when you try to sync your device
it will tell you that the server has no data. You will then be given the option
of uploading all local data from the device to syncserver.


Overview (`qt/audio/README.md`)

Build scripts for bundled mpv/lame binaries.

Credit

The macOS build scripts are adapted from iina/deps-buildscripts.


Overview (`qt/icons/README.md`)

Source files used to produce some of the svg/png files.


Overview (`qt/installer/linux-template/README.md`)

This template is adapted from briefcase-linux-system-template for building a zip with a standalone build of Python and custom install scripts.


Overview (`ts/lib/generated/README.md`)

Files in this folder get combined with generated files in out/ts/lib/generated/


Overview (`qt/installer/linux-template/{{ cookiecutter.format }}/{{ cookiecutter.app_name }}/README.md`)

Installing Anki

Running directly

To run without installing, change to this folder in a terminal, and run the
following command:

./anki

Installing

To install system wide, run 'sudo ./install.sh'

To remove in the future, run 'sudo /usr/local/share/anki/uninstall.sh'.

Audio

To play and record audio, mpv and lame must be installed. If mpv is not
installed or too old, Anki will try to fall back on using mplayer.

Problems

If Anki fails to start, please run it from a terminal to see what errors it
outputs, and then post on our support site.


Index (`docs/index.md`)

{include} ../readme.md
{toctree}
---
maxdepth: 2
hidden:
---

development
build
contributing
dependabot
unit-testing
testing-coverage
sonarcloud
architecture
protobuf

Auto-generated API References

{toctree}
---
maxdepth: 1
hidden:
---

api-python
api-rust

Api Aqt Modules (`docs/api-aqt-modules.md`)

aqt

The pages below are generated from Python docstrings in qt/aqt.

{toctree}
:maxdepth: 2
:glob:

autoapi/aqt/*/index
autoapi/aqt/*/*/index
autoapi/aqt/*/*/*/index

Api Python Modules (`docs/api-python-modules.md`)

anki

The pages below are generated from Python docstrings in pylib/anki.

{toctree}
:maxdepth: 2
:glob:

autoapi/anki/*/index
autoapi/anki/*/*/index

Api Python (`docs/api-python.md`)

Python API

{toctree}
:maxdepth: 2

api-python-modules
api-aqt-modules

Api Rust (`docs/api-rust.md`)

Rust API

Anki's Rust API documentation is generated from doc comments in the rslib/
crates using cargo doc.

To browse the docs locally:

text
cargo doc --open

The main crates are:

  • anki (rslib/) - Core library
  • anki_io (rslib/io/) - I/O utilities

Architecture (`docs/architecture.md`)

Anki Architecture

Very brief notes for now.

Backend/GUI

At the highest level, Anki is logically separated into two parts.

A neat visualization of the file layout is available here:
https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=ankitects%2Fanki
(or go to https://githubnext.com/projects/repo-visualization#explore-for-yourself and enter ankitects/anki).

Library (rslib & pylib)

The Python library (pylib) exports "backend" methods - opening collections,
fetching and answering cards, and so on. It is used by Anki’s GUI, and can also
be included in command line programs to access Anki decks without the GUI.

The library is accessible in Python with "import anki". Its code lives in
the pylib/anki/ folder.

These days, the majority of backend logic lives in a Rust library (rslib, located in rslib/). Calls to pylib proxy requests to rslib, and return the results.

pylib contains a private Python module called rsbridge (pylib/rsbridge/) that wraps the Rust code, making it accessible in Python.

GUI (aqt & ts)

Anki's GUI is a mix of Qt (via the PyQt Python bindings for Qt), and
TypeScript/HTML/CSS. The Qt code lives in qt/aqt/, and is importable in Python
with "import aqt". The web code is split between qt/aqt/data/web/ and ts/,
with the majority of new code being placed in the latter, and copied into the
former at build time.

Protobuf

Anki uses Protocol Buffers to define backend methods, and the storage format of
some items in a collection file. The definitions live in proto/anki/.

The Python/Rust bridge uses them to pass data back and forth, and some of the
TypeScript code also makes use of them, allowing data to be communicated in a
type-safe manner between the different languages.

At the moment, the protobuf is not considered public API. Some pylib methods
expose a protobuf object directly to callers, but when they do so, they use a
type alias, so callers outside pylib should never need to import a generated
_pb2.py file.


Build (`docs/build.md`)

The build system

Basic use

Basic use is described in development.md.

Architecture

The build/ folder is made up of 4 packages:

  • build/configure defines the actions and inputs/outputs of the build graph -
    this is where you add new build steps or modify existing ones. The defined
    actions are converted at build time to a build.ninja file that Ninja executes.
  • build/ninja_gen is a library for writing a build.ninja file, and includes
    various rules like "build a Rust crate" or "run a command".
  • build/archives is a helper to download/checksum/extract a dependency as part
    of the build process.
  • build/runner serves a number of purposes:
    • it's the entrypoint to the build process, taking care of generating
      the build file and then invoking Ninja
    • it wraps executable invocations in the build file, swallowing their output
      if they exit successfully
    • it provides a few helpers for multi-step processes that can't be easily
      described in a cross-platform manner thanks to differences on Windows.

Tracing build problems

If you run into trouble with the build process:

  • You can see the executed commands with e.g. ./ninja pylib -v
  • You can see the output of successful commands by defining N2_OUTPUT_SUCCESS=1
  • You can see what's triggering a rebuild of a target with e.g.
    ./ninja qt/anki -d explain.
  • You can browse the build graph via e.g. ./ninja -- -t browse wheels
  • You can profile build performance with
    https://discourse.cmake.org/t/profiling-build-performance/2443/3.

Packaging considerations

See this page.


Contributing (`docs/contributing.md`)

Contributing Code

For info on contributing things other than code, such as translations, decks
and add-ons, please see the contribution guide.

Help wanted

If you'd like to contribute but don't know what to work on, please take a look
at the issues tab of the Anki repo
on GitHub.

Larger changes

Before starting work on larger changes, especially ones that aren't listed on the
issue tracker, please reach out on the forums before you begin work, so we can let
you know whether they're likely to be accepted or not. When you spent a bunch of time
on a PR that ends up getting rejected, it's no fun for either you or us.

Consider an Add-on First

Before submitting a PR for a new feature, please consider whether it could be
implemented as an add-on instead. We aim to keep the core Anki codebase lean
and maintainable. Many great ideas are better served as add-ons, where they
can iterate faster and serve specific user needs without affecting all users.

See the Add-on API documentation for
guidance on building add-ons.

Pull Request Description

When opening a pull request, fill in the provided template
(.github/pull_request_template.md). It covers the required fields: linked
issue, summary/motivation, how to test, and UI evidence for visual changes.

Linked Issues

Every pull request, except hotfixes and dependency updates, must be linked
to an existing open issue. If no issue exists, please open one to describe
the problem before submitting a PR. This helps us ensure we're solving the
right problems and prevents wasted effort on both sides.

PRs without a linked issue may be automatically closed after a short period.

Dependency updates

Dependabot PRs do not need a linked issue. Maintainers should follow
Dependabot updates when reviewing or landing them.

Refactoring

Please avoid PRs that focus on refactoring. Every PR has a cost to review, and a chance
of introducing accidental regressions, and often these costs are not worth it for
slightly more elegant code.

That's not to say there's no value in refactoring. But such changes are usually better done
in a PR that happens to be working in the same area - for example, making small changes
to the code as part of fixing a bug, or a larger refactor when introducing a new feature.

Type hints

Most of Anki's Python code now has type hints, which improve code completion,
and make it easier to discover errors during development. When adding new
code, please make sure you add type hints as well, or the tests will fail.

Qt's stubs are not perfect, so you may sometimes need to use cast(), or silence
a type error. When connecting signals, there's a qconnect() helper in aqt.utils
that can be used to work around the type warnings without obscuring other errors
such as a mistyped variable.

In cases where you have two modules that reference each other, you can fix the
import cycle by using fully qualified names in the types, and enabling
annotations. For example, instead of

text
from aqt.browser import Browser

def myfunc(b: Browser) -> None:
  pass

use the following instead:

text
from __future__ import annotations

import aqt

def myfunc(b: aqt.browser.Browser) -> None:
  pass

Hooks

If you're writing an add-on and would like to extend a function that doesn't
currently have a hook, a pull request that adds the required hooks would be
welcome. If you could mention your use case in the pull request, that would be
appreciated.

The hooks try to follow one of two formats:

[subject] [verb] - eg, note_type_added, card_will_render

[module] [verb] [subject] - eg, browser_did_change_row, editor_did_update_tags

The qt code tends to use the second form, as the hooks tend to focus on
particular screens. The pylib code tends to use the first form, as the focus
is usually subjects like cards, notes, etc.

Using "did change" instead of the past tense "changed" can seem awkward, but
makes it consistent with "will", and is similar to the naming style used in
iOS's libraries.

In most cases, hooks are better added in the GUI code than in pylib.

The hook code is automatically generated using the definitions in
pylib/tools/genhooks.py and qt/tools/genhooks_gui.py. Adding a new definition
in one of those files will update the generated files.

If you want to change an existing hook to, for example, receive an additional
argument, you must leave the existing hook unchanged to preserve backwards
compatibility. Create a new definition for your hook with a similar name and
include the properties replaces="name_of_old_hook" and
replaced_hook_args=["..."] in the definition of the new hook. If the old hook
has a legacy hook, you must not add the legacy hook to the definition of the
new hook.

Translations

For information on adding new translatable strings to Anki, please see
the translation guide.

For information on translating the documentation, please see
translating the manual.

Tests Must Pass

Please make sure 'ninja check' completes successfully before submitting code.

pre-commit is used to run that check from a Git hook.
It is configured in .pre-commit-config.yaml at the repository root. After
installing the dev dependencies (for example uv sync --group dev), run one of:

text
uv run pre-commit install --hook-type pre-push
text
python3 -m pre_commit install --hook-type pre-push

(pre-commit alone only works if that executable is on your PATH, for example
after pip install --user pre-commit or with your virtual environment activated.)

The bundled hook runs ./ninja check on pre-push (not on every commit),
because the full check suite can take a long time. You can still run
./ninja check manually at any time.

You may need to ensure your usual shell PATH is visible to the hook (for
example if tools such as ninja are installed outside standard locations),
because pre-commit does not use a login shell and a different path can cause
extra rebuilds.

New or changed behavior must be covered by tests. Exceptions where tests are not
required: version bumps, documentation-only changes, translation updates, dependency
updates, and CI/build housekeeping (chore).

Code Style

Please use standard Python snake_case variable names and functions in newly
introduced code. Because add-ons often rely on existing function names, if
renaming an existing function, please add a legacy alias to the old function.

Do One Thing

A patch or pull request should be the minimum necessary to address one issue.
Please don't make a pull request for a bunch of unrelated changes, as they are
difficult to review and will be rejected - split them up into separate
requests instead.

AI-Assisted Contributions

Using AI tools to help write or review code is permitted. However, you must
understand every change you submit. If you cannot explain what your changes
do and how they interact with the rest of the system, your PR will be closed.

Please review AI-generated code carefully before submitting. PRs that appear
to have been submitted without human review — e.g., irrelevant code, duplicate
logic, or comments that don't match the implementation — may be closed without
further discussion.

License

Please add yourself to the CONTRIBUTORS file in your first pull request.


Dependabot (`docs/dependabot.md`)

Dependabot updates

Config lives in .github/dependabot.yml.
This page is for maintainers: how PRs are grouped, which ones to merge,
and how to clear Rust advisories that Dependabot cannot fix on its own.

Goals

Version updates were accumulating faster than anyone reviewed them. Our solution is to fix grouping and slow the schedule, not turn updates off.

Security updates stay individual. Each advisory needs its own review (false positives are common, so we need to know which CVE actually matters). GitHub also does not apply open-pull-requests-limit to security PRs.

What gets opened

Four ecosystems: Cargo, npm (Yarn), Python (uv), GitHub Actions.

Kind When Shape What to do
Security As soon as GitHub publishes an advisory. Ignores the schedule. One PR per advisory Review and land promptly, or close if it does not apply
Minor / patch Quarterly One grouped PR per ecosystem (*-minor-patch) Merge if CI is green
Major Quarterly, after a 30-day cooldown One grouped PR per ecosystem (*-major) Do not merge. Treat as a notification. If a bump is wanted, open a human PR for that package
GitHub Actions Quarterly One grouped PR (actions) Merge if CI is green

open-pull-requests-limit is 2 for Cargo / npm / Python (one slot for minor/patch, one for major) and 1 for Actions. That limit applies only to version updates. Security PRs are unlimited.

Cooldown is 7 days for ordinary version bumps (a short supply-chain window) and 30 days for majors (semver-major-days). Security updates skip cooldown.

Dependabot PRs are exempt from the linked-issue requirement.

Reviewing a PR

  1. Read the title / group name and put it in the table above;
  2. CI must be green. Dependency PRs do not need new tests, but format, cargo deny, and the existing suites still run;
  3. npm: if CI fails with YN0028 / yarn install --immutable, Dependabot updated package.json and left yarn.lock stale. Check out the branch, run Yarn so the lockfile matches, and push;
  4. Close older individual version PRs that a newer grouped PR already covers. Dependabot often does this itself when it opens a replacement.

Security PRs

Land these first. Confirm the advisory actually affects this repo: Dependabot matches on version, not on whether we call the vulnerable API.

If the bump is a major, or CI fails, do not force the Dependabot PR through. Open a normal PR with whatever code or lockfile changes are required.

If we cannot bump yet, say so in the PR and follow the Rust ignore path below when cargo-deny is the failing check.

Minor / patch grouped PRs

Low risk. Merge when CI is green. If one crate in the group breaks the build, do not merge the bundle, we should split or skip that crate.

Major grouped PRs

Majors often need code changes. The grouped PR exists so we are not flooded with one PR per package. It is not meant to be merged as-is. Pick the package we actually want, open a dedicated PR, and leave the rest.

GitHub Actions

Actions are pinned by SHA. Dependabot security alerts are not generated for SHA-pinned actions but Dependabot still opens the quarterly grouped version PR and bumps those SHAs to the latest release. So there is no immediate security PR for a vulnerable action. That is an accepted trade-off: SHA pins stop tag-rewrite supply-chain attacks, at the cost of losing action alerts.

Rust advisories (`cargo-deny`)

CI runs cargo deny check on pushes and on PRs that touch Cargo.lock, Cargo.toml, or .deny.toml. Failures often look like Dependabot work but are not something Dependabot can finish, especially when the crate sits behind a git dependency.

Worked example: RUSTSEC-2026-0258 needed a reqwest bump inside the ankitects/linkcheck fork, not a one-line change in this repo.

  1. Open the RUSTSEC-… advisory and note the crate and patched versions.
  2. See whether Dependabot already opened a security PR that bumps a crate we depend on directly. If CI is green, merge it.
  3. If the crate is transitive through a git dep (linkcheck, percent-encoding-iri, …):
    • Land the bump in the fork.
    • Point Cargo.toml at the new rev.
    • Run cargo update -p <crate> (and any other crates the advisory names) so Cargo.lock moves.
  4. Run cargo deny check locally before pushing.
  5. Only if a bump is not feasible, add the id to .deny.toml [advisories] ignore with a comment stating why and what the alternative is. Existing ignores follow that pattern. Do not silence an advisory without a comment.

Dependabot cannot update git rev pins. Those bumps are always manual.

Changing the config

  • Do not group security updates;
  • Do not put majors in the minor/patch group;
  • open-pull-requests-limit cannot cap security PRs;
  • Cooldown keys are default-days and semver-major-days.

Development (`docs/development.md`)

Anki development

Packaged betas

For non-developers who want to try beta versions, the easiest way is to use a
packaged version - please see:

https://betas.ankiweb.net/

Pre-built Python wheels

Pre-built Python packages are available on PyPI. They are useful if you wish to:

  • Run Anki from a local Python installation without building it yourself
  • Get code completion when developing add-ons
  • Make command line scripts that modify .anki2 files via Anki's Python libraries

You will need the 64 bit version of Python 3.10 or later installed. You can install
Python from python.org or from your distro.

For further instructions, please see https://betas.ankiweb.net/#via-pypipip. Note that
in the provided commands, --pre tells pip to fetch alpha/beta versions. If you remove
--pre, it will download the latest stable version instead.

Building from source

Clone the git repo into a folder of your choosing. The folder path must not
contain spaces, and should not be too long if you are on Windows.

On all platforms, you will need to install:

  • Rustup (https://rustup.rs/). The Rust version pinned in rust-toolchain.toml
    will be automatically downloaded if not yet installed. If removing that file
    to use a distro-provided Rust, newer Rust versions will typically work for
    building but may fail tests; older Rust versions may not work at all.
  • N2 or Ninja. N2 gives better status output. You can install it with tools/install-n2,
    or bash tools\install-n2 on Windows. If you want to use Ninja, it can be downloaded
    from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
    placed on your path, or from your distro/homebrew if it's 1.10+.
    • On Windows, if you have WSL installed, it may conflict with MSYS2 bash. If you are getting an error, try running C:\msys64\usr\bin\bash.exe tools/install-n2 instead.
  • (Optional) just command runner.
    Install with brew install just or uv tool install rust-just.
    We are experimenting with just as the official tool for running
    Anki-specific commands, and it will likely become the source of truth
    in the future.

Platform-specific requirements:

Running Anki during development

From the top level of Anki's source folder:

text
./run

(.\run on Windows)

This will build Anki and run it in place.

The first build will take a while, as it downloads and builds a bunch of
dependencies. When the build is complete, Anki will automatically start.

If Anki fails to start, you may need to install extra libraries.

Running tests/checks

To run all tests at once, from the top-level folder:

text
./ninja check

(tools\ninja check on Windows).

You can also run specific checks. For example, if you see during the checks
that check:svelte:editor is failing, you can use ./ninja check:svelte:editor
to re-run that check, or ./ninja check:svelte to re-run all Svelte checks.

Fixing formatting

When formatting issues are reported, they can be fixed with

text
./ninja format
text
./ninja fix

Fixing clippy issues

text
cargo clippy --fix

Excluding your own untracked files from formatting and checks

If you want to add files or folders to the project tree that should be excluded from version tracking and not be matched by formatters and checks, place them in an extra folder and they will automatically be ignored.

Optimized builds

The ./run command will create a non-optimized build by default. This is faster
to compile, but will mean Anki will run slower.

To run Anki in optimized mode, use:

text
./tools/runopt

Or set RELEASE=1 or RELEASE=2. The latter will further optimize the output, but make
the build much slower.

Building redistributable wheels

The ./run method described in the platform-specific instructions is a shortcut
for starting Anki directly from the build folder. For regular study, it's recommended
you build Python wheels and then install them into your own python venv. This is also
a good idea if you wish to install extra tools from PyPi that Anki's build process
does not use.

To build wheels on Mac/Linux:

text
./tools/build

(on Windows, .\tools\build.bat)

The generated wheels are in out/wheels. You can then install them by copying the paths into a pip install command.
Follow the steps on the beta site, but replace the
pip install --upgrade --pre aqt line with something like:

text
/my/pyenv/bin/pip install --upgrade out/wheels/*.whl

(On Windows you'll need to list out the filenames manually instead of using a wildcard).

Cleaning up build files

Apart from submodule checkouts, most build files go into the out/ folder (and
node_modules on Windows). You can delete that folder for a clean build, or
to free space.

Cargo, yarn and pip all cache downloads of dependencies in a shared cache that
other builds on your system may use as well. If you wish to clear up those caches,
they can be found in ~/.rustup, ~/.cargo and ~/.cache/{yarn,pip}. On
Windows, Yarn cache can be found in %LOCALAPPDATA%\Yarn.

If you invoke Rust outside of the build scripts (eg by running cargo, or
with Rust Analyzer), output files will go into target/ unless you have
overridden the default output location.

Storage and Performance

Most editors use rust-analyzer for Rust support, which runs cargo check in the
background whenever you save a file, so that problems are surfaced before you try to
build. This is useful, but on lower-end machines, it can noticeably increase disk
usage and CPU load. If that's a problem for you, consider disabling "check on save"
(e.g. set rust-analyzer.checkOnSave to false in VS Code) as a trade-off — you'll
lose real-time error checking, but rust-analyzer can still be used for code completion,
go-to-definition, etc.

If you chose to disable "check on save" and don't run cargo commands manually, you
may delete the target/ folder (if any) as that's no longer used.

IDEs

Please see this separate page for setting up an editor/IDE.

Making changes to the build

See this page

Generating documentation

Build and view the documentation site:

text
just docs

For Rust API docs:

text
just docs-rust

Environmental Variables

If ANKIDEV is set before starting Anki, some extra log messages will be printed on stdout,
and automatic backups will be disabled - so please don't use this except on a test profile.
It is automatically enabled when using ./run.

If TRACESQL is set, all SQL statements will be printed as they are executed.

If LOGTERM is set before starting Anki, warnings and error messages that are normally placed
in the collection2.log file will also be printed on stdout.

If ANKI_PROFILE_CODE is set, Python profiling data will be written on exit.

Installer

Run tools/build-installer to build the installer.

Depending on your operating system, this produces a file under out/installer/dist:

  • An MSI installer on Windows.
  • A .dmg file on macOS.
  • A tarball on Linux.

Issues During Building

If you are experiencing issues building the installer, make sure that all dependencies
are installed. See Building from source for more info.

Releasing

See Releasing.

Mixing development and study

You may wish to create a separate profile with File>Switch Profile for use
during development. You can pass the arguments "-p [profile name]" when starting
Anki to load a specific profile.

If you're using PyCharm:

  • right click on the "run" file in the root of the PyCharm Anki folder
  • click "Edit 'run'..." - in Script options and enter:
    "-p [dev profile name]" without the quotes
  • click "Ok"

E2e Testing (`docs/e2e-testing.md`)

End-to-End Testing with Playwright

Playwright drives a real headless Anki instance via its mediasrv HTTP API.
Tests live in ts/tests/e2e/ and are entirely separate from the Vitest unit tests.

Prerequisites

Build Anki at least once before running e2e tests:

shell
just build

That's it. just test-e2e automatically installs Playwright's Chromium browser
into out/playwright-browsers/ on the first run (idempotent on subsequent runs).

Running tests

Managed mode (CI-style)

Playwright starts and stops a throwaway Anki instance automatically:

shell
just test-e2e

The first run can be slow (~60 s) because Anki must fully initialise before tests start.

Start Anki once in a separate terminal, then reuse it across multiple test runs:

shell
# Terminal 1 — keep running
./run

# Terminal 2 — fast iteration
ANKI_E2E_REUSE_SERVER=1 just test-e2e

Interactive UI mode

Open Playwright's browser UI to inspect each test step with snapshots:

shell
ANKI_E2E_REUSE_SERVER=1 just test-e2e --ui

Writing tests

Add test files to ts/tests/e2e/ with the .test.ts suffix and import from
./fixtures instead of directly from @playwright/test:

typescript
import { expect, test } from "./fixtures";

test("my feature works", async ({ page }) => {
    await page.goto("/some-anki-page");
    await expect(page.locator("#some-element")).toBeVisible();
});

fixtures.ts re-exports expect and a pre-configured test object. Add
shared fixtures there as new features require them.

Calling the Anki API from tests

Anki's /_anki/ endpoints accept and return protobuf-encoded binary payloads
(Content-Type: application/binary). Use page.request.post with
Buffer.from(protoMsg.toBinary()) and decode the response with the matching
generated type from ts/lib/generated/.

Accessing Anki pages

Anki's mediasrv serves the following pages over HTTP (non-exhaustive):

URL pattern Description
/graphs Statistics graphs (SvelteKit)
/deck-options/[deckId] Deck options (SvelteKit)
/congrats Post-study screen (SvelteKit)
/card-info/[cardId] Card info (SvelteKit)
/editor/?mode=[mode] Editor (SvelteKit)
/congrats Congrats page (SvelteKit)
/favicon.ico Mediasrv liveness probe

CI

The e2e tests run as part of the check-linux job in .github/workflows/ci.yml,
after the regular build and test steps. Screenshots and traces from failed runs
are uploaded as artifacts and kept for 7 days.

See also


Editing (`docs/editing.md`)

Editing/IDEs

Visual Studio Code is recommended, since it provides decent support for all the languages
Anki uses. To set up the recommended workspace settings for VS Code, please see below.

For editing Python, PyCharm/IntelliJ's type checking/completion is a bit nicer than
VS Code, but VS Code has improved considerably in a short span of time.

There are a few steps you'll want to take before you start using an IDE.

Initial Setup

Python Environment

For code completion of external Python modules, you can use the venv that is
generated as part of the build process. After building Anki, the venv will be in
out/pyenv. In VS Code, use ctrl/cmd+shift+p, then 'python: select
interpreter'.

Rust

You'll need Rust to be installed, which is required as part of the build process.

Build First

Code completion partly depends on files that are generated as part of the
regular build process, so for things to work correctly, use './run' or
'tools/build' prior to using code completion.

Visual Studio Code

To start off with some default workspace settings that are optimized for Anki
development, please head to the project root and then run:

text
mkdir .vscode && cd .vscode
ln -sf ../.vscode.dist/* .

Once the workspace settings are set up, open the root of the repo in VS Code to
see and install a number of recommended extensions.

PyCharm/IntelliJ

Setting up Python environment

To make PyCharm recognize anki and aqt imports, you need to add source paths to Settings > Project Structure.
You can copy the provided .idea.dist directory to set up the paths automatically:

text
mkdir .idea && cd .idea
ln -sf ../.idea.dist/* .

You also need to add a new Python interpreter under Settings > Python > Interpreter pointing to the Python executable under out/pyenv (available after building Anki).


Language Bridge (`docs/language_bridge.md`)

Language Bridge

Anki's codebase uses three layers.

  1. The web frontend, created in Svelte and typescript,
  2. The Python layer and
  3. The core Rust layer.

Each layer can can makes RPC (Remote Procedure Call) to the layers below it. While it should be avoided, Python can also invoke Typescript functions. The Rust layers never make calls to the other layers. Note that it can make RPC to AnkiWeb and other servers, which is out of scope of this document.

In this document we'll provide examples of bridge between languages, explaining:

  • where the RPC is declared,
  • where it is called (with the appropriate imports) and
  • where it is implemented.

Imitating those examples should allow you to make call and create new RPCs.

Declaring RPCs

Let's consider the method NewDeck of DecksServices. It's declared in decks.proto as rpc NewDeck(generic.Empty) returns (Deck);. This means this methods takes no argument (technically, an argument containing no information), and returns a Deck.

Read protobuf to learn more about how those input and output types are defined.

If the RPC implementation is in Python, it should be declared in the service frontend.proto's FrontendService. RPCs declared in any other services are implemented in Rust.

Making a Remote Procedure Call

In this section we'll consider how to make Remote Procedure Call (RPC) from languages used in Anki. Languages used for AnkiDroid and AnkiMobile are out of scope of this document.

Making a RPC from Python

Python can invoke the NewDeck method with col._backend.new_deck(). This python method takes no argument and returns a Deck value.

However, most Python code should not call this method directly. Instead it should call col.decks.new_deck(). Generally speaking, all back-end functions called from Python should be called through a helper method defined in pylib/anki/. The _backend part is an implementation detail that most callers should ignore. This is especially important because add-ons should expect a relatively stable API independent of the implementation details of the RPC.

Invoking method from TypeScript

Let's consider the method rpc GetCsvMetadata(CsvMetadataRequest) returns (CsvMetadata); from ImportExportService..

It's used in the TypeScript class ImportCsvState, as an asynchronous function. It's argument is a single javascript object, whose keys are as in CsvMetadataRequest and it returns a CsvMetadata.

The method was imported with import { getCsvMetadata } from "@generated/backend"; and the types were imported with import type { CsvMetadata } from "@generated/anki/import_export_pb";. Note that it was not necessary to import the input type given that it's simply an untyped javascript object.

Implementation

Let's now look at implementations of those RPCs.

Implementation in Rust

The method NewDeck is implemented in Rust's DecksService as fn new_deck(&mut self) -> error::Result<anki_proto::decks::Deck>. It should be noted that the method name was changed from Pascal case to snake case, and the rps's argument of type generic.Empty is ignored.

Implementation in Python

Let's consider the implementation of the method DeckOptionsRequireClose. It's defined as def deck_options_require_close() -> bytes:. In this case, there should be a returned value. However, it'll be ignored, so returning b"" is perfectly fine.

Note that the incoming HTTP request is not processed on the main thread. In order to do any work with the GUI, we should call aqt.mw.taskman.run_on_main.

Invoking a TypeScript method from Python

This case should be avoided if possible, as we generally should avoid
calls to the upper layer. Contrary to the previous cases, we don't use
protobuf.

Calling a TS function.

Let's take as Example export function getTypedAnswer(): string | null. It's an exported function, and its return type can be encoded in JSON.

It's called in the Reviewer class through self.web.evalWithCallback("getTypedAnswer();", self._onTypedAnswer). The result is then sent to _onTypedAnswer.

If no return value is needed, web.eval would have been sufficient.

Calling a Svelte method

Let's now consider the case where the method we want to call is implemented in a Svelte library. Let's take as example deckOptionsPendingChanges. We define it with:

js
globalThis.anki || = {};
globalThis.anki.methodName = async (): Promise<void>=>{body}

Note that if the function is asynchronous, you can't directly send the
result to a callback. Instead your function will have to call a post
method that will be sent to Python or Rust.

This method is called in deckoptions.py with self.web.eval("anki.deckOptionsPendingChanges();".


Linux (`docs/linux.md`)

Linux-specific notes

Requirements

These instructions are written for Debian/Ubuntu; adjust for your distribution.
Some extra notes have been provided by a forum member, though some of the things
mentioned there no longer apply:
https://forums.ankiweb.net/t/guide-how-to-build-and-run-anki-from-source-with-xubuntu-20-04/12865

You can see a full list of buildtime and runtime requirements by looking at the apt-get install step in the setup-anki composite action,
used by the workflows under .github/workflows/.

Ensure some basic tools are installed:

text
$ sudo apt install bash grep findutils curl gcc gcc-12 g++ make git rsync
  • The 'find' utility is 'findutils' on Debian.

Missing Libraries

If you get errors during build or startup, try starting with

QT_DEBUG_PLUGINS=1 ./run

It will likely complain about missing libraries, which you can install with
your package manager. Some of the libraries that might be required on Debian
for example:

text
sudo apt install libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
  libxcb-randr0 libxcb-render-util0 libxkbfile1

The libraries that might be required on Arch Linux:

text
sudo pacman -S nss libxkbfile

On some distros such as Fedora, you may need to install the
libxcrypt-compat package if you get an error like this:

text
error while loading shared libraries: libcrypt.so.1: cannot open shared object file: No such file or directory

Audio

To play and record audio during development, install mpv and lame.

Glibc and Qt

Anki requires a recent glibc.

If you are using a distro that uses musl, Anki will not work.

You can use your system's Qt libraries if they are Qt 6.2 or later, if
you wish. After installing the system libraries (eg:
'sudo apt install python3-pyqt6.qt{quick,webengine} python3-venv pyqt6-dev-tools'),
find the place they are installed (eg '/usr/lib/python3/dist-packages'). On modern Ubuntu, you'll
also need 'sudo apt remove python3-protobuf'. Then before running any commands like './run', tell Anki where
the packages can be found:

text
export PYTHONPATH=/usr/lib/python3/dist-packages
export PYTHON_BINARY=/usr/bin/python3

Packaging considerations

Python, node and protoc are downloaded as part of the build. You can optionally define
PYTHON_BINARY, NODE_BINARY, YARN_BINARY and/or PROTOC_BINARY to use locally-installed versions instead.

If rust-toolchain.toml is removed, newer Rust versions can be used. Older versions
may or may not compile the code.

To build Anki fully offline, set the following environment variables:

  • OFFLINE_BUILD: If set, the build does not run tools that may access
    the network.

  • NODE_BINARY, YARN_BINARY and PROTOC_BINARY must also be set.

With OFFLINE_BUILD defined, manual intervention is required for the
offline build to succeed. The following conditions must be met:

  1. All required dependencies (node, Python, rust, yarn, etc.) must be
    present in the build environment.

  2. The offline repositories for the translation files must be
    copied/linked to ftl/qt-repo and ftl/core-repo.

  3. The Python pseudo venv must be set up:

    text
    mkdir out/pyenv/bin
    ln -s /path/to/python out/pyenv/bin/python
    ln -s /path/to/protoc-gen-mypy out/pyenv/bin/protoc-gen-mypy

    Optionally, set up your environment to generate Sphinx documentation:

    text
    ln -s /path/to/sphinx-apidoc out/pyenv/bin/sphinx-apidoc
    ln -s /path/to/sphinx-build out/pyenv/bin/sphinx-build

    Note that the PYTHON_BINARY environment variable need not be set,
    since it is only used when OFFLINE_BUILD is unset to automatically
    create a network-dependent Python venv.

  4. Create the offline cache for yarn and use its own environment
    variable YARN_CACHE_FOLDER to it:

    text
    YARN_CACHE_FOLDER=/path/to/the/yarn/cache
    /path/to/yarn install --ignore-scripts

You are now ready to build wheels and Sphinx documentation fully
offline.

More

For info on running tests, building wheels and so on, please see Development.


Mac (`docs/mac.md`)

Mac-specific notes

Requirements

Xcode:

Install the latest XCode from the App Store. Open it at least once
so it installs the command line tools.

Git/rsync

Install via Homebrew or similar tool.

More

For info on running tests, building wheels and so on, please see Development.


Ninja (`docs/ninja.md`)

Ninja

Brief notes for people used to the existing Bazel build system:

  • Put the ninja binary on your path: https://github.com/ninja-build/ninja/releases/tag/v1.11.1
    (on Windows, if you have it installed in msys, make sure the native binary occurs earlier on the path)

  • Ensure Rust is installed via rustup: https://rustup.rs/

  • Remove the .bazel and node_modules folders from your existing checkout

  • Run with ./run

  • Run tests with './ninja check' (tools\ninja on Windows)

  • Format files with './ninja format'

  • Fix eslint/copyright issues with './ninja fix'

  • Targets are hierarchical, so './ninja check:jest:deck-options' will run
    the Jest tests for ts/deck-options, and './ninja check:jest' will run all
    Jest tests.


Protobuf (`docs/protobuf.md`)

Protocol Buffers

ProtoBuf is a format used both to save data in storage and transmit
data between services. You can think of it as similar to JSON with
schemas, given that you can use basic types, list and records. Except
that it's usually transmitted and saved in an efficient byteform and
not in a human readable way.

Protocol Buffers

Anki uses different implementations of Protocol Buffers
and each has its own peculiarities. This document highlights some aspects relevant
to Anki and hopefully helps to avoid some common pitfalls.

For information about Protobuf's types and syntax, please see the official language guide.

General Notes

Names

Generated code follows the naming conventions of the targeted language. So to access
the message field foo_bar you need to use fooBar in Typescript and the
namespace created by the message FooBar is called foo_bar in Rust.

Optional Values

In Python and Typescript, unset optional values will contain the type's default
value rather than None, null or undefined. Here's an example:

protobuf
message Foo {
  optional string name = 1;
  optional int32 number = 2;
}
python
message = Foo()
assert message.number == 0
assert message name == ""

In Python, we can use the message's HasField() method to check whether a field is
actually set:

python
message = Foo(name="")
assert message.HasField("name")
assert not message.HasField("number")

In Typescript, this is even less ergonomic and it can be easier to avoid using
the default values in active fields. E.g. the CsvMetadata message uses 1-based
indices instead of optional 0-based ones to avoid ambiguity when an index is 0.

Oneofs

All fields in a oneof are implicitly optional, so the caveats above
apply just as much to a message like this:

protobuf
message Foo {
    oneof bar {
      string name = 1;
      int32 number = 2;
    }
}

In addition to HasField(), WhichOneof() can be used to get the name of the set
field:

python
message = Foo(name="")
assert message.WhichOneof("bar") == "name"

Backwards Compatibility

The official language guide
makes a lot of notes about backwards compatibility, but as Anki usually doesn't
use Protobuf to communicate between different clients, things like shuffling around
field numbers are usually not a concern.

However, there are some messages, like Deck, which get stored in the database.
If these are modified in an incompatible way, this can lead to serious issues if
clients with a different protocol try to read them. Such modifications are only
safe to make as part of a schema upgrade, because schema 11 (the targeted schema
when choosing Downgrade), does not make use of Protobuf messages.

Field Numbers

Field numbers larger than 15 need an additional byte to encode, so repeated fields
should preferably be assigned a number between 1 and 15. If a message contains
reserved fields, this is usually to accommodate potential future repeated fields.

Implementation-Specific Notes

Python

Protobuf has an official Python implementation with an extensive reference.

Typescript

Anki uses protobuf-es, which offers
some documentation.

Rust

Anki uses the prost crate.
Its documentation has some useful hints, but for working with the generated code,
there is a better option: From within anki/rslib run cargo doc --open --document-private-items.
Inside the pb module you will find all generated Rust types and their implementations.

  • Given an enum field Foo foo = 1;, message.foo is an i32. Use the accessor
    message.foo() instead to avoid having to manually convert to a Foo.
  • Protobuf does not guarantee any oneof field to be set or an enum field to contain
    a valid variant, so the Rust code needs to deal with a lot of Options. As we
    don't expect other parts of Anki to send invalid messages, using an InvalidInput
    error or unwrap_or_default() is usually fine.

Releasing (`docs/releasing.md`)

Releasing

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:

    text
    git checkout -b release/26.05 main
    git push origin release/26.05
  2. CI runs automatically on push to release/** branches.

  3. Prepare the release (updates .version on the branch):

    text
    just release::prepare --version 26.05b1
  4. Verify on TestPyPI:

    text
    just release::testpypi --ref release/26.05
  5. Publish the full release:

    text
    just release::public --ref release/26.05
  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. Merge the release branch back to main. When a stable release is published,
    .github/workflows/sync-release.yml opens a PR from release/26.05 to
    main, which brings in the .version bump, the translation updates, and any
    cherry-picked fixes. Review the PR and merge it.

  8. Delete the release branch after the merge PR is merged.

  9. Submit the Flathub update. When a stable release is published,
    .github/workflows/sync-release.yml makes a patch that moves
    flathub/net.ankiweb.Anki
    to the new version. The workflow uploads the patch as an artifact, and
    comments on the release PR with a report and the commands to open the
    Flathub PR. Fix the problems in the report's "Needs attention" section, if
    there are any. See
    updating-anki.md
    for the manual steps.

  10. Approve the website update. When a stable release is published, the
    update-website job in .github/workflows/sync-release.yml sets the
    download version in
    ankitects/anki-landing-page
    to the new version, then pushes the change. This job uses the release
    environment, so a reviewer must approve it before it runs.

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:

    text
    git checkout -b release/26.05 26.05
  2. Cherry-pick the fix onto the release branch.

  3. Push the branch and wait for CI:

    text
    git push origin release/26.05
  4. Prepare and publish:

    text
    just release::prepare --version 26.05.1
    just release::public --ref release/26.05
  5. Merge the release branch back to main.

  6. For security patches, publish the advisory and credit the reporter if
    applicable.

Release process overview

{mermaid}
flowchart LR
    A["<b>prepare_release.py</b><br/><i>local</i><br/>validate version<br/>check out release branch<br/>check CI<br/>check duplicate tag<br/>sync translations<br/>update .version<br/>push to branch"] --> B["<b>CI (ci.yml)</b><br/>runs automatically<br/>on release/** branches"]
    B --> C["<b>release.yml</b><br/>build all platforms<br/>optionally sign macOS/Windows<br/>optionally create draft GitHub release<br/>optionally publish to TestPyPI/PyPI"]

    style A fill:#2d333b,stroke:#539bf5,color:#adbac7
    style B fill:#2d333b,stroke:#e5c07b,color:#adbac7
    style C fill:#2d333b,stroke:#7ee787,color:#adbac7

Release workflow jobs

text
/* Detailed source-code truncated for AI context efficiency. */

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. The update-website job in
    sync-release.yml also uses it, to protect the landing page deploy key.

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:

Input Effect
sign Signs macOS and Windows artifacts. Requires the release environment. When false, those jobs upload unsigned artifacts and do not access signing secrets.
draft-release Creates a draft GitHub release with generated release notes and installer artifacts. Requires sign=true, the release environment, passing CI (unless skipped), no duplicate tag/release, and version matching .version.
publish-testpypi Publishes wheels to TestPyPI. Requires the release environment.
publish-pypi Publishes wheels to PyPI. Requires the release environment, passing CI (unless skipped), and version matching .version. It also runs and waits for the TestPyPI publish job first. It does not require signing unless draft-release=true.
skip-ci-check Skips the CI status check. Useful for hotfix releases.
version For draft-release or publish-pypi: must match .version. For build-only, signed-only, or TestPyPI-only runs: ignored (.version from the branch is used automatically).

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:
    text
    just release::build --ref <your-branch>
  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:
    text
    just release::sign --ref <your-branch>
  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.

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. On macOS, mpv, lame and all their libraries are built from source. Update the pinned versions and checksums in qt/audio/macos/versions.env, then test locally by running qt/audio/build.sh and qt/audio/macos/test-in-vm.sh.
  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:
    text
    gh workflow run publish-audio-package.yml -f sign-macos=true -f publish-testpypi=true -f publish-pypi=true
  6. Once the package is published, update the pinned anki-audio version in qt/pyproject.toml to the new version.

Sonarcloud (`docs/sonarcloud.md`)

SonarCloud analysis

SonarCloud provides informative static-analysis and coverage results. Its
Quality Gate is not a required merge check, so findings guide review without
blocking maintainers. Automatic Analysis must remain disabled because this
project uses CI-based analysis.

For the test commands and coverage thresholds enforced by CI, see
Testing and Coverage.

Analysis matrix

Event Static analysis Coverage in CI Coverage in SonarCloud
Internal pull request Yes Yes Yes
Push to main or release/** Yes Yes Yes
Fork pull request Yes Yes No

Fork pull requests still run the complete test coverage and coverage-regression
checks in the unprivileged CI workflow. Their reports are not imported by the
privileged SonarCloud workflow, so SonarCloud does not calculate a coverage
metric for those pull requests.

The fork scan still reports static bugs, vulnerabilities, security hotspots,
and code smells. Scanner-side Clippy and software composition analysis are
disabled; Clippy remains part of the regular CI checks.

How it runs

.github/workflows/sonar.yml runs after the complete CI workflow succeeds.
Keeping it separate prevents SONAR_TOKEN from being available to jobs that
build or test contributor code.

  1. The workflow validates the pull request number, state, head SHA,
    repositories, and branches against the GitHub API. Invalid or stale context
    fails closed instead of becoming a branch analysis.
  2. It checks out the exact commit tested by CI. Fork source is read only by the
    scanner and is never built, installed, or used as tooling.
  3. It replaces sonar-project.properties with the default branch's trusted
    copy, preventing a fork from changing the server or enabling build tools.
    The analysed version comes from the same trusted checkout of .version, so
    that new code is measured since the previous release rather than since a
    constant version.
  4. For a pull request, it fetches the target branch from the trusted upstream
    repository and pins it locally under the name passed as
    sonar.pullrequest.base. SonarCloud derives the changed-file set from git,
    so without the correct upstream base reference it may attribute unrelated
    changes to the pull request. A fork's local base reference may be outdated.
    The step logs the analysed commit, base repository and branch, base commit,
    and merge-base, and fails closed if the reference cannot be resolved.
  5. For internal changes, it downloads the coverage artifact from the triggering
    CI run. For forks, report paths are cleared and all sources are excluded only
    from the coverage calculation.
  6. Only the scanner step receives SONAR_TOKEN.

Trust boundary

This workflow has access to the base repository's `SONAR_TOKEN`. Never add a build, test, package install, PR-provided configuration, local Action from the PR, or restored CI cache to it. In particular, do not invoke `just`, `cargo`, `npm`, `pip`, or repository scripts in this workflow.
  • Keep Automatic Analysis disabled and SonarCloud checks non-required.
  • Restrict SONAR_TOKEN to analysis of this project.
  • Keep scanner-side Clippy and software composition analysis disabled.
  • Do not import artifacts produced by a fork into the privileged scan.

The scanner necessarily parses untrusted source when analyzing a fork. These
constraints prevent direct execution of contributor code and reduce the token's
exposure, but they do not eliminate vulnerabilities in third-party Actions or
analyzers.


Testing Coverage (`docs/testing-coverage.md`)

Testing and Coverage

CI runs Rust, Python, and TypeScript tests plus lint/type checks. Coverage is
orchestrated in justfile with direct CLI calls, using ninja only to prepare
generated build artifacts that the test commands need.

Quick reference

sh
just test                        # run all tests (no coverage)
just test --coverage             # run all tests + enforce coverage thresholds
just test --coverage --html      # same + generate HTML reports under out/coverage/

just test-rust                   # Rust only
just test-rust --coverage
just test-rust --coverage --html

just test-py                     # Python (pylib + qt) only
just test-py --coverage
just test-py --coverage --html

just test-ts                     # TypeScript/Svelte Vitest only
just test-ts --coverage
just test-ts --coverage --html

HTML reports are written under out/coverage/ (gitignored).

Coverage tools and thresholds

Stack Test runner Coverage tool Minimum
Rust workspace cargo nextest via cargo-llvm-cov cargo-llvm-cov 60%
Python pylib/anki pytest pylib/tests coverage.py 63%
Python qt/aqt pytest qt/tests coverage.py 20%
TypeScript/Svelte vitest run Vitest V8 5%

Linux pull requests run just test --coverage in CI. macOS and Windows
jobs run just test (no coverage enforcement) for now.

Notes

  • Rust — cargo-llvm-cov is installed on demand into out/bin/ to avoid
    polluting the global cargo install. Coverage runs rebuild the workspace with
    instrumentation, so they are slower than plain just test-rust.
    Windows ARM64 (aarch64-pc-windows-msvc) is not supported: the Rust
    compiler produces malformed .profraw files on that target
    (rust-lang/rust#150123,
    cargo-llvm-cov#436).
    just test-rust --coverage exits with a clear message on ARM64 Windows;
    use just test-rust (no coverage) or rely on CI (Linux) for enforcement.
  • Python — coverage is split across two suites (pylib and qt) because
    they have different PYTHONPATH setups and test folders.
  • TypeScript — coverage is measured only over code reachable through
    Vitest's module graph. Svelte component rendering behavior is not covered.

SonarCloud

SonarCloud runs automatically after successful CI pushes and pull requests. It
runs static analysis across Python, TypeScript, and Rust. For internal pull
requests and trusted branch pushes, it also aggregates the coverage reports
generated by the three stacks.

  • Dashboard: https://sonarcloud.io/project/overview?id=ankitects_anki
  • Internal coverage: uses the same LCOV/XML reports produced by just test --coverage
  • Fork coverage: remains enforced in CI but is not imported into SonarCloud
  • Analysis results: SonarCloud reports quality gate status on each analyzed PR

The quality gate is configured as non-blocking — a failing gate does not prevent
merging, but the results are visible on the PR.

See SonarCloud analysis for the analysis matrix and the fork trust
boundary.

Gaps and future improvements

  • Raise thresholds gradually as the test suite grows and CI timings stabilise.
  • Exclude generated files from coverage denominators where appropriate.
  • Publish out/coverage/ as a CI artifact so reviewers can browse HTML
    reports directly from a PR.
  • Consider diff/changed-file coverage once baselines are stable — it is a
    better enforcement mechanism for incremental improvement than whole-repo
    thresholds.
  • Add component or browser tests for Svelte UI surfaces if Svelte coverage
    is intended to cover rendered component behaviour.

See also


Unit Testing (`docs/unit-testing.md`)

Writing Unit Tests for Anki

This guide helps maintainers, contributors, and coding assistants write unit and
component tests for Anki. It is independent of any particular feature, module, or
bug. Use it together with the behavior being changed, the surrounding production
code, and nearby tests.

This guide assumes basic familiarity with automated tests. If testing is new to you,
start with one of the introductory resources in References,
then return here for Anki-specific guidance.

Quick summary

When adding a test:

  1. Describe what should happen from the user's point of view.
  2. Add the test close to the code responsible for that behavior.
  3. Consider normal input, errors, edge cases, and known regressions.
  4. Prefer the smallest and fastest test that gives enough confidence.
  5. Give the test a name that explains the behavior being checked.
  6. Avoid dependencies on real time, network access, user data, or test order.
  7. For a bug fix, confirm that the test fails before the fix when practical.
  8. Run the relevant just test-* recipe and then just check.

Purpose

A useful test gives fast, trustworthy evidence about behavior that matters. It also
acts as executable documentation: a reader should be able to understand the
scenario, action, and expected result without reverse-engineering the implementation.

A good unit test is:

  • Behavioral: it verifies an observable result, state transition, error, or
    required interaction.
  • Focused: it has one reason to fail. Multiple assertions are fine when they
    describe one behavior.
  • Deterministic: the same code and inputs produce the same result regardless of
    test order, wall-clock time, locale, network access, or machine speed.
  • Isolated: it does not depend on a user's profile, another test's state, or an
    unavailable external service.
  • Fast: it is cheap enough to run repeatedly while editing.
  • Readable: its name and test data explain the expected behavior and the failure.
  • Sensitive: a real regression in the behavior makes it fail; it does not keep
    passing when the expected behavior is broken.
  • Stable: a behavior-preserving refactor should usually leave it unchanged.

Code coverage is useful for finding untested code, but a covered line is not
necessarily a tested behavior. Do not add weak assertions merely to increase a
coverage number.

Choose the smallest useful test boundary

Use the lowest-cost test that can prove the behavior:

Boundary Use it for Do not use it for
Pure unit Calculations, parsing, validation, conversion, and deterministic state transitions Framework wiring or cross-process behavior
Unit with collaborators A small group of fast, in-process collaborators Network, real subprocesses, or a user's persistent data
UI component A Qt component's visible state, signals/events, and interaction; for Svelte, extracted logic and stores Whole application workflows
Integration Database mappings, protobuf/language bridges, filesystem contracts, and framework integration that a unit test cannot prove Repeating every unit-level branch
End-to-end A small number of critical workflows through a real Anki instance and browser Exhaustive validation and edge cases

Most scenarios should be below the end-to-end layer. If a higher-level test discovers
a defect, add a lower-level regression test when that level can reproduce the defect.
Keep the higher-level test only when it proves additional behavior.

For Svelte specifically, "UI component" means extracted logic and stores: the
repository does not wire rendered-component tests into the unit suite (see
Svelte, TypeScript, and JavaScript).

Anki spans several implementation layers. Test a rule at the layer that owns it:

  • Business rules implemented in Rust should normally be tested in Rust.
  • Python library tests should cover Python-owned behavior, compatibility surfaces,
    orchestration, and adaptations instead of duplicating Rust assertions.
  • Python/Qt tests should cover GUI-owned state, signals, callbacks, and backend
    orchestration. Move framework-independent logic into a form that can be tested
    without constructing the full GUI when practical.
  • TypeScript/JavaScript tests should cover web-owned logic, stores, DOM helpers, and
    component behavior. Use end-to-end tests only when a real browser/Anki boundary is
    essential.
  • Generated protobuf code does not need tests. Test custom conversion, validation,
    or bridge behavior at its owning layer; use an integration test if correctness
    depends on both sides of the bridge.

Choose scenarios from expected behavior

Before writing test code, describe what should happen in plain English. Identify the
inputs, results a caller or user can observe, side effects, errors, and facts that
must remain true. Then select the smallest set of scenarios that distinguishes a
correct implementation from a plausible bug.

Every meaningful behavior change should consider these categories:

Category Question Generic example
Happy path Does valid, representative input produce the intended result? An accepted value is normalized and stored.
Unhappy/error path Is invalid input rejected or recovered from as specified? A malformed value returns the expected error and does not mutate state.
Boundary What happens at empty, minimum, maximum, transition, and Unicode cases? An empty label uses the defined fallback; the maximum allowed value remains valid.
State/invariant What must remain true before and after the action? A failed update leaves the previous value and unrelated records unchanged.
Regression What is the smallest input that reproduces a reported defect? The exact shape that once crashed now produces the specified result.

Do not assume that every function needs one test from every category. Include a
category when the expected behavior or implementation has that failure mode.

Use equivalence classes rather than enumerating inputs that exercise the same rule.
Use parameterization when several compact input/output pairs express one behavior;
use separate tests when cases require different setup or should produce distinct
failure explanations.

Structure and naming

Prefer a visible Arrange–Act–Assert flow:

  1. Arrange only the state relevant to the scenario.
  2. Act once on the behavior under test.
  3. Assert the expected outcome, including important non-effects.

The phases do not require comments when the structure is already obvious. Avoid
hiding the act or the important assertion inside a helper.

Name a test after the unit under test and the expected behavior, not an
implementation method. Follow the convention of the tests already in that file;
newer tests in the codebase put the condition last:

text
<unit>_<expected_outcome>[_when_<condition>]

Real examples from the codebase:

  • Rust: answer_easy_graduates_new_card_to_review_queue,
    bury_leaves_suspended_card_untouched,
    add_or_update_notetype_preserves_usn_when_flag_is_set
  • Python/Qt: test_update_collection_skips_backend_when_unchanged,
    test_is_audio_file_rejects_no_extension

Vitest names the behavior in the test(...) description string rather than in an
identifier, e.g. test("event handler attributes are stripped").

Names such as test_helper, works, basic, or case_1 do not explain the
expected behavior.

Assertions should be precise enough to diagnose the defect. Prefer comparing the
meaningful value or structured result over a generic truthiness check. Assert an
error's type or variant and, when stable and user-relevant, its message. Avoid
asserting incidental formatting or a complete object when only one field defines the
behavior.

Test data, setup, and cleanup

  • Use the smallest realistic data and give it semantic names such as
    existing_record and invalid_name.
  • Prefer builders, factories, and fixtures already used by nearby tests.
  • Keep test-specific setup in the test. Extract a helper only when it removes noise
    without concealing the scenario.
  • Use temporary directories, temporary collections/databases, and in-memory values.
    Never read or modify a user's Anki profile.
  • Restore patched globals, environment variables, timers, DOM state, and callbacks.
    Prefer framework cleanup facilities that run even after an assertion fails.
  • Tests must pass independently, in any order, and when repeated.

Test doubles and boundaries

Prefer real, lightweight, deterministic collaborators. Use a stub, fake, spy, or mock
to control an awkward boundary such as time, randomness, network, subprocess,
clipboard, native dialog, or backend call, and patch it where the code looks it up.
Return realistic values and errors.

Assert interactions only when they affect expected behavior; avoid incidental calls
and ordering. If every collaborator is mocked and only calls are asserted, the test
probably describes the implementation rather than useful behavior.

Time, randomness, concurrency, and asynchronous behavior

  • Inject or freeze time when the exact time affects behavior. Do not depend on the
    current date, local timezone, or rollover hour.
  • Seed or replace randomness and assert invariants rather than one accidental random
    result.
  • Never use an unconditional sleep to wait for correctness. Await the promise,
    callback, signal, task, or condition with a bounded safety timeout.
  • Avoid exact performance timings in unit tests. Put performance claims in a
    benchmark with appropriate tolerance and environment control.
  • Assume files and test cases may run in parallel. Do not share mutable globals,
    fixed ports, or fixed temporary paths.

Regression tests

A bug fix should normally include a test that would have caught the bug.

  1. Reduce the report to the smallest representative input and state.
  2. Place the test at the lowest layer that reproduces the defect and proves the
    intended behavior.
  3. When practical, run it before the fix or against the buggy revision and confirm
    that it fails for the expected reason.
  4. Apply the fix and confirm that the new test passes.
  5. Run the relevant suite and check adjacent scenarios for the same class of defect.
  6. Keep the regression test permanently. Name it after the behavior, not an issue
    number; add the issue link in a short comment only when it provides essential
    context that the test cannot express.

Do not encode the buggy implementation in the expected result. A regression test
should describe lasting expected behavior so that future refactoring can preserve it.

Test-first workflow

Writing the test before the implementation is encouraged when the expected behavior
is clear. A test-first cycle can help verify that the test is sensitive to the defect:

  1. Write a focused test for the intended behavior.
  2. Confirm that it fails for the expected reason.
  3. Make the smallest production change that satisfies the expected behavior.
  4. Refactor while keeping the test suite green.

Test-first development is not mandatory. For exploratory work or behavior that is
not yet understood, investigate and describe the expected behavior before adding the
test. For bug fixes, a failing regression test before the fix is strongly recommended
whenever practical.

Additional risk areas

The scenario categories above apply to all behavior. Also consider risks specific to
Anki:

  • serialization or conversion code written by the project;
  • permission, path, escaping, and untrusted-input boundaries;
  • callbacks, signals, events, and async completion when they are part of the API;
  • compatibility behavior intentionally supported for add-ons or older data.

What not to test

Do not spend test maintenance on:

  • private implementation details with no observable contract;
  • trivial code, compiler/type-checker guarantees, generated accessors, or third-party
    behavior;
  • the same rule exhaustively at every language layer;
  • snapshots so broad that reviewers cannot tell whether a change is correct;
  • real network services, user profiles, home-directory files, or machine-specific
    executables;
  • random input without a reproducible seed and reported failing case;
  • internal call counts used only because mocking makes them easy to observe; or
  • assertions that only prove the code ran, returned something truthy, or did not
    throw when a more specific result exists.

UI testing scope and cost

Qt and Svelte tests have a higher setup and maintenance cost than tests for pure
logic. Do not attempt to test every widget, component, property, or markup detail. A
UI test should protect behavior whose value and risk justify that cost.

Prefer testing:

  • logic, view models, stores, state transitions, and error handling;
  • interactive states, signals, events, callbacks, and backend requests;
  • accessibility roles, labels, focus, and keyboard behavior when they are part of
    the user contract; and
  • regressions in stable, user-visible behavior.

Avoid unit tests for styling, exact geometry, incidental DOM structure or CSS/Sass
class names, animation timing, platform-native rendering, generated UI code,
framework internals, and large UI snapshots.

When possible, extract logic from the Qt widget or Svelte component and test it as
ordinary Python or TypeScript/JavaScript. Keep only a small wiring test at the UI
layer when it provides additional confidence.

Check presentation during UI review, or with visual regression if appearance is a
stable product contract and the project intentionally adopts such tooling. Volatile
code is not automatically exempt: test its stable contract when one exists. If both
implementation and expected presentation are temporary, omitting a brittle UI test
may be the better trade-off.

Stack-specific guidance

The examples below are illustrative pseudocode. Names such as State, Model, and
normalize_value are not Anki APIs and must not be copied without inspecting the
actual code.

Python library

Python library tests live under pylib/tests/ and use pytest.

  • Name files/functions test_* and use plain assert for diagnostic diffs.
  • Use pytest.raises() for expected errors; match a stable message only when the
    message is part of the contract or improves precision.
  • Use pytest.mark.parametrize() for compact equivalent cases.
  • Prefer typed fixtures, tmp_path, monkeypatch, and existing collection helpers;
    close resources through fixtures or context managers.
  • Mock the Rust backend only when testing Python-owned orchestration or an otherwise
    impractical failure. Do not mock it merely to make a domain-rule test appear unit
    sized.

Generic example:

python
import pytest


@pytest.mark.parametrize(
    ("raw_value", "expected"),
    [(" valid ", "valid"), ("", "default")],
)
def test_normalization_returns_the_documented_value(
    raw_value: str, expected: str
) -> None:
    assert normalize_value(raw_value) == expected


def test_invalid_value_is_rejected_without_mutating_state() -> None:
    state = State(value="existing")

    with pytest.raises(ValueError, match="invalid value"):
        state.update("invalid")

    assert state.value == "existing"

Python/Qt

Python/Qt tests live under qt/tests/ and also use pytest.

  • Test framework-independent decisions as ordinary Python first.
  • Construct the smallest useful Qt object; do not launch a complete Anki window for
    a unit test.
  • Trigger public actions or realistic input, then assert meaningful state, signals,
    callbacks, or backend requests. Use QSignalSpy or a callback recorder when signal
    arguments/count are the contract.
  • Await the event loop through a condition or signal with a bounded timeout; avoid
    fixed sleeps.
  • Replace native dialogs, clipboard access, web requests, audio, and backend
    operations at their boundary.
  • Keep QApplication, main-window state, and global hooks isolated and cleaned up.

Generic example:

python
from unittest.mock import MagicMock


def test_submit_with_invalid_input_shows_error_without_saving() -> None:
    save = MagicMock()
    dialog = make_minimal_dialog(save=save)
    dialog.form.value.setText("invalid")

    dialog.accept()

    assert dialog.form.error.isVisible()
    save.assert_not_called()

Rust

Rust unit tests normally live beside the code in a #[cfg(test)] module. Follow the
nearby crate's existing helpers and conventions.

  • Prefer inputs and assertions over extensive mocking.
  • Use assert_eq!/assert_ne! for diagnostic diffs and include a message when the
    invariant is otherwise unclear.
  • Match the expected error variant or structured value instead of only calling
    is_err() when the distinction matters.
  • Cover variants and transitions with distinct behavior, not properties guaranteed
    by the type system.
  • Use a collection only when persistence is part of the unit; use pure domain values
    otherwise.
  • Use small table-driven loops for one contract and include the input in assertion
    messages.
  • unwrap() and expect() may be used in setup and when an unexpected error
    should fail the test; do not use them to inspect the error path being tested.

Generic example:

rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_input_uses_the_default() {
        assert_eq!(normalize_value(""), "default");
    }

    #[test]
    fn invalid_input_preserves_existing_state() {
        let mut state = State::new("existing");

        let error = state.update("invalid").unwrap_err();

        assert!(matches!(error, UpdateError::InvalidValue));
        assert_eq!(state.value(), "existing");
    }
}

Svelte, TypeScript, and JavaScript

Web unit tests use Vitest. The current configuration discovers colocated
*.test.ts, *.spec.ts, *.test.js, and *.spec.js files, excluding
ts/tests/e2e/.

  • Test TypeScript/JavaScript logic and Svelte stores without a DOM unless rendering
    and interaction prove an additional contract.
  • Add // @vitest-environment jsdom only when the test needs DOM APIs, and restore
    any document/global state after the test.
  • Use vi.fn()/vi.spyOn() at external boundaries, fake timers for timer-driven
    behavior, and restore both after the test.
  • Await promises/reactive updates instead of real time. For rendered behavior,
    interact through the public UI and query by accessible role, label, or visible text
    rather than internal state.
  • Prefer focused value/DOM assertions. Use snapshots only for small, stable output
    whose entire structure is intentionally reviewed.
  • The repository does not currently configure a dedicated Svelte component-rendering
    harness in the unit suite. If a behavior cannot be tested through extracted logic
    or the existing DOM setup, agree on the appropriate harness or place the essential
    browser contract in the existing Playwright suite rather than inventing a private
    test setup.

Generic TypeScript example:

typescript
import { expect, test, vi } from "vitest";

test("invalid input reports the error without saving", async () => {
    const save = vi.fn();
    const model = new Model({ save });

    const result = await model.submit("invalid");

    expect(result).toEqual({ ok: false, reason: "invalid-value" });
    expect(save).not.toHaveBeenCalled();
});

For a Svelte component, the equivalent contract is expressed as a user action and a
visible/accessibility result: enter invalid input, activate Submit, observe the error,
and verify that saving did not occur. Do not call a component's private handler
directly.

Running and validating tests

Use the repository's just recipes:

sh
just test-rust  # Rust
just test-py    # Python library and Python/Qt
just test-ts    # TypeScript/JavaScript/Svelte unit tests
just test       # every stack (Rust, Python, TS)
just check      # required final formatting, build, lint, and test validation

During development, run the relevant stack frequently. Before considering a code
change complete, run just check. See Testing and Coverage
for coverage commands and End-to-End Testing when the
selected boundary requires Playwright.

Review checklist

A test is ready when the answer to each relevant question is yes:

  • Does the test name explain the behavior being checked?
  • Does the test clearly show when that behavior works and when it does not?
  • Were the relevant scenario categories considered at the lowest useful layer,
    without duplication?
  • Is Arrange–Act–Assert clear, with minimal data and doubles only at meaningful
    boundaries?
  • Is it deterministic, independent, cleaned up, and stable under behavior-preserving
    refactoring?
  • For a bug fix, did the regression test fail before and pass after the fix when
    practical?
  • Do the relevant just test recipe and just check pass?

Instructions for coding assistants

When this document is supplied to a coding assistant, the assistant must also inspect
the production behavior, issue/diff, neighboring tests, available helpers, and test
runner configuration. This guide does not replace repository context.

Before editing, it should follow the quick summary, describe the expected behavior,
identify the responsible layer, list the scenarios and selected test level, then
follow local naming, fixtures, builders, and file placement. It must not invent
APIs, dependencies, fixtures, or runner capabilities; change production visibility
solely to reach private implementation; or weaken an assertion to make it pass. It
should finish by reporting what was tested, intentionally omitted, and not validated.

References and rationale

This guide adapts these references to Anki's architecture:


Windows (`docs/windows.md`)

Windows

Minimum Requirements

  • 64-bit Windows 10 (version 1703 or newer)
  • Administrator access to install software

Install Rust

  1. Visit the official Rust website (https://rustup.rs/)
  2. Download and install rustup
  3. During installation, accept the default options

[!NOTE]
The Rust installer will offer to automatically download and install the required
MSVC build tools. If you prefer a minimal install without the full Visual
Studio IDE, do the following before installing Rust:

  1. Install Visual Studio Build Tools via winget:
    text
    winget install Microsoft.VisualStudio.BuildTools
  2. Open the Visual Studio Installer, select Build Tools, click
    Modify, then under Individual components, install:
    • MSVC Build Tools for x64/x86 (Latest)
    • Windows 11 SDK (or Windows 10 SDK if you're on Windows 10)

Then proceed with the Rust installation above.

[!IMPORTANT]
Windows ARM users: After installing rustup, run the following command in a terminal, inside the project folder:

bash
rustup target add x86_64-pc-windows-msvc

Install MSYS2

  1. Visit the MSYS2 website
  2. Install it in the default location
  3. After installation, open the MSYS2 UCRT64 terminal
  4. Run the following command:
bash
pacman -S git rsync

Configure the PATH environment variable

  1. Search for and open Edit the system environment variables in the Start menu
  2. Click Environment Variables
  3. Edit the Path variable under System Variables
  4. Add the following path then reboot your computer: C:\msys64\usr\bin

[!NOTE]
If you have native Windows apps relying on Git, e.g. the PowerShell extension
posh-git, you may want to install
Git for Windows and put it on the path instead,
as msys Git may cause issues with them. You'll need to make sure rsync is
available some other way.

Choose a good source code location

Anki's source files do not need to be in a specific location, but it's best to
avoid long paths, as they can cause problems. Spaces in the path may cause
problems.

More

For info on running tests, building wheels and so on, please see
Development.


--- METRICS ---