Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Image Toolkit is a desktop app for culling a photo shoot quickly. Open a folder, flick through the thumbnails, look at any photo fitted to the window, and move on. RAW files are first-class: they are shown from the JPEG preview the camera embedded in them, so a folder of CR3 or NEF files browses about as fast as a folder of JPEGs.

It works directly on your folders. There is no import step, no catalog and no database, and version 0.1.0 never modifies your files.

What 0.1.0 does

  • Opens a folder, optionally with all its subfolders, and lists every supported image in a thumbnail grid that stays smooth with thousands of photos.
  • Decodes the thumbnails in the background, in parallel on all CPU cores but one, visible ones first.
  • Shows any photo fitted to the window, with the previous and next ones loaded ahead of time.
  • Is driven from the keyboard as much as with the mouse (see Keyboard shortcuts).
  • Speaks French (the default) and English, and remembers your preferences between sessions.

What comes next

Version 0.1.0 is the first step of a roadmap towards a complete culling tool: tags stored in XMP sidecars (0.2.0), built-in blur detection (0.3.0), deletion of the rejects (0.4.0), then zoom, comparison, lens corrections and export.

The blur detection comes from the author’s command-line tool blurdetector. Its scoring is being moved into Image Toolkit’s own code as a native feature, and further work on it happens here. The project also builds on rawcleaner, a command-line tool that deletes the RAW files left without an exported counterpart after culling.

Where to start

The source code is on GitLab, licensed under MIT OR Apache-2.0.

Installation

Prebuilt binaries

Every version is built by the project’s CI and published on the GitLab Releases page, one archive per platform:

PlatformArchiveContainsRequirements
Linuximage-toolkit-X.Y.Z-linux-x86_64.tar.gzimage-toolkit, x86-64glibc 2.31 or newer (Ubuntu 20.04, Debian 11 and later)
Windowsimage-toolkit-X.Y.Z-windows-x86_64.zipimage-toolkit.exe, x86-64Windows 10 or later
macOSimage-toolkit-X.Y.Z-macos-universal.zipImage Toolkit.app, universal (Apple Silicon and Intel)macOS 11 Big Sur or later

Each archive also holds README.md, the two license files and THIRD-PARTY-LICENSES.html, the list of the third-party licenses that apply to the binary (see Licenses below). A SHA256SUMS.txt file published with each release lets you check the downloads.

There is no installer: extract the archive and run the program. On macOS, move the .app to /Applications if you like.

The binaries are not signed with a paid certificate, so each operating system warns about them on first launch. The sections below explain how to get past that.

macOS

The app is ad-hoc signed but not notarized by Apple, so Gatekeeper blocks it the first time you open it.

  1. Open the app once. macOS refuses to open it and shows a warning; close the warning without moving the app to the Trash.
  2. Go to System Settings → Privacy & Security (System Preferences → Security & Privacy → General on macOS 12 and earlier), scroll down to the message about the app, and click Open Anyway.
  3. Confirm. From then on the app opens normally.

Alternatively, remove the quarantine flag that macOS put on the download, from the Terminal app. Replace the placeholder with the path of the app; dragging the app from the Finder onto the Terminal window types its path for you:

xattr -dr com.apple.quarantine /path/to/the/app

Windows

SmartScreen may show “Windows protected your PC” on first launch. Click More info, then Run anyway.

Linux

The program needs:

  • A graphics driver with Vulkan or OpenGL support. The interface is drawn on the GPU.
  • An X11 or Wayland session. Both are supported by the same binary; the client libraries are loaded at run time and are part of any desktop installation.
  • xdg-desktop-portal for the folder picker, together with a backend that provides the file chooser, for example xdg-desktop-portal-gtk, xdg-desktop-portal-gnome or xdg-desktop-portal-kde. GNOME and KDE Plasma install them by default. On minimal window-manager setups you may have to install them yourself; xdg-desktop-portal-wlr alone does not provide the file chooser. If no portal works, the folder picker falls back to zenity when it is installed; with neither, the Open folder… dialog cannot appear.

Building from source

With rustup installed:

git clone https://gitlab.com/julienbltt/image-toolkit.git
cd image-toolkit
cargo run --release

The repository pins its Rust version in rust-toolchain.toml, and rustup installs it on first use. The runtime requirements above apply to your own builds too. See Building from source for details.

Licenses

Image Toolkit is licensed under MIT OR Apache-2.0, at your option.

The release binaries include third-party code under other licenses. THIRD-PARTY-LICENSES.html, in every release archive, names every dependency compiled into the binary, with its license. In particular, rawler, which reads the RAW files, is licensed under the LGPL-2.1 and is statically linked; since the complete source code of Image Toolkit is public, you can rebuild it against a modified version of rawler, as the LGPL allows.

The lens corrections planned for 0.8.0 will add lensfun: the library is licensed under the LGPL-3.0 and its lens database under CC BY-SA 3.0.

Getting started

Language. The interface starts in French. To switch to English, open the language selector at the right end of the toolbar (it shows Français) and pick English. This guide uses the English labels; in French, Open folder… reads Ouvrir un dossier… and Include subfolders reads Inclure les sous-dossiers. See Preferences & language.

The window has three parts: the toolbar at the top, the thumbnail grid (or the viewer) in the middle, and the status bar at the bottom.

Opening a folder

Click Open folder… in the toolbar, or press Ctrl+O (Cmd+O on macOS), and pick a folder. You can also drop a folder from your file manager onto the window. Its path is then shown in the toolbar.

The folder is opened in place: nothing is imported or copied, and version 0.1.0 never modifies, moves or deletes your files.

Every file with a supported extension is listed, in file-name order (case-insensitive); other files are ignored. At the next launch, the last folder you opened is opened again. If it is not available (a memory card or a network share that is not connected), the app starts empty and tries again at the following launch.

Include subfolders

With the Include subfolders checkbox ticked, the scan descends into every subfolder as well, and the photos are sorted by their path relative to the opened folder. Ticking or unticking it rescans the open folder.

Two rules apply during the scan:

  • Hidden entries are skipped: files and folders whose name starts with a dot. Besides the usual hidden files, this drops the ._IMG_0001.CR3 companion files macOS writes on memory cards formatted in exFAT, which carry an image extension but contain no image.
  • Symbolic links to folders are not followed, so a link cannot make the scan loop forever. Symbolic links to files are listed.

An entry that cannot be read, such as a subfolder without read permission, is skipped and the scan goes on. The status bar then shows a warning count; hover it to see the list.

The thumbnail grid

The thumbnails fill in as they are decoded. Decoding runs in the background, in parallel on all CPU cores but one, which stays free for the interface. The visible thumbnails are decoded first, so the interface stays responsive while a large folder loads, and the grid stays smooth with thousands of photos.

  • The file name is shown under each thumbnail; hover a thumbnail to see its path inside the opened folder, which tells photos with the same name in different subfolders apart.
  • A spinner marks a thumbnail that is still loading.
  • A warning sign (⚠) marks a file that cannot be decoded. Hover it to read the error message. The most common cause is a RAW file without a usable embedded JPEG preview (see Supported formats).
  • The thumbnail size slider in the toolbar, left of the language selector, makes the thumbnails larger or smaller. It sets the smallest size: the columns then widen so that the grid fills the width of the window. Thumbnails are decoded again at a higher resolution when they grow, so they stay sharp on high-resolution screens.

Click a thumbnail to select it. Move the selection with the arrow keys, by a screenful with Page Up and Page Down, or jump to the first or last image with Home and End. The grid scrolls to keep the selection in view.

The viewer

Double-click a thumbnail, or press Enter or Space on the selected one, to open it in the viewer. The image is fitted to the window; images smaller than the window are not enlarged beyond their actual size.

  • ← and → show the previous and next images. The neighbours of the current image are loaded ahead of time, so stepping through a shoot rarely waits on decoding. When it does, the thumbnail is shown in the meantime.
  • Home and End jump to the first and last images.
  • Esc, Enter, Space or a double-click returns to the grid, with the selection on the last image you viewed.

For RAW files, the viewer shows the camera’s full-size embedded JPEG (see Supported formats).

The status bar

While a folder is being scanned, the status bar shows Scanning the folder…. Then it shows how many images the folder contains and, while images are being decoded, how many are still waiting. Only the thumbnails on screen and a few rows around them are queued, so that number stays small and disappears once the visible part of the grid is complete. In the viewer, the status bar also shows the position of the current image, for example 12 / 340.

What is not there yet

Tagging, filtering, user-chosen sorting, blur detection, zoom and deletion are planned for later versions; see the roadmap.

Keyboard shortcuts

On macOS, Cmd replaces Ctrl.

General

ShortcutAction
Ctrl+OOpen a folder

Thumbnail grid

ShortcutAction
← → ↑ ↓Move the selection
Page Up Page DownMove the selection by a screenful
HomeSelect the first image
EndSelect the last image
Enter or SpaceOpen the selected image in the viewer
Double-clickOpen that image in the viewer

Viewer

ShortcutAction
←Previous image
→Next image
HomeFirst image
EndLast image
Esc, Enter or SpaceBack to the grid
Double-clickBack to the grid

Supported formats

Files are recognized by their extension, case-insensitively (IMG_0001.CR3 and img_0001.cr3 are both listed). Files with any other extension are ignored.

RAW

BrandExtensions
Canoncr2 cr3
Fujifilmraf
Leicarwl
Nikonnef nrw
Sonyarw srf sr2
Cross-branddng, Adobe’s open RAW format, used by many brands

Canon’s older CRW format is not supported: the RAW library used here cannot extract its embedded preview.

Standard images

jpg jpeg png tif tiff bmp webp

How images are displayed

RAW files are displayed from the JPEG previews the camera embedded in them; nothing is demosaiced. Reading an embedded JPEG takes a fraction of the time a RAW development would, which is what makes browsing a RAW shoot fast.

  • CR3 (Canon). A CR3 file holds several JPEG renditions of the shot. Thumbnails use the 1620×1080 preview, which decodes in a few milliseconds; the viewer uses the full-size embedded JPEG.
  • Other RAW formats. The embedded preview is extracted with rawler and used for both the thumbnail and the viewer. Its resolution depends on the camera: often full size, sometimes smaller.
  • Standard images are decoded directly.

The orientation recorded by the camera (EXIF orientation) is applied, so portrait shots are shown upright. Images are also reduced to the size the screen needs: thumbnails to the size of their cell on screen (between 256 and 1024 pixels on the long edge), the viewer image to the resolution of your monitor (at most 4096 pixels).

Keep in mind what an embedded preview is: the camera’s own JPEG rendering of the shot, with its picture style and white balance as set at capture time. That is what you see in Image Toolkit, not a neutral development of the RAW data, and the level of detail is that of the preview the camera chose to embed.

When a file cannot be displayed

A file that cannot be read or decoded gets a warning sign (⚠) in the grid instead of a thumbnail; hover it to read the error message. The viewer shows the message in place of the image. The rest of the folder is not affected.

Since RAW files are only shown through their embedded preview, a RAW file without a usable embedded JPEG cannot be displayed. For example, CR3 files shot with Canon’s HDR PQ mode turned on carry HEIF previews instead of JPEG ones, which Image Toolkit does not decode.

Preferences & language

Interface language

The interface is available in French and English. It starts in French. To change it, open the language selector at the right end of the toolbar and pick a language. Each language is listed under its own name (Français, English), so you can find yours even in an interface you cannot read.

This documentation is in English only.

What is remembered

There is no separate preferences window: each setting is changed where it is used, and Image Toolkit remembers it for the next session.

SettingChanged withDefault
Interface languageThe language selector in the toolbarFrench
Last opened folderOpen folder… or dropping a folder on the window; it is opened again at launch when availableNone
Include subfoldersThe checkbox in the toolbarOff
Thumbnail sizeThe slider in the toolbarTowards the small end of the slider
Window size and positionResizing or moving the window1280 × 800

Where the settings are stored

The settings are saved automatically by eframe, the GUI framework the app is built on, in a file named app.ron:

PlatformLocation
Linux~/.local/share/image-toolkit/app.ron (or under $XDG_DATA_HOME when it is set)
macOS~/Library/Application Support/image-toolkit/app.ron
Windows%APPDATA%\image-toolkit\data\app.ron

Deleting that file restores the defaults. Your photos and folders are never written to.

Roadmap

Image Toolkit grows one feature set per version until 1.0. Everything on this page except 0.1.0 is planned, not available; the order and the contents may still change. The changelog records what each released version actually contains.

0.1.0: browse (current version)

Open a folder in place, thumbnail grid, single-image viewer, keyboard navigation, French and English interface. Files are never modified. See Getting started.

0.2.0: display modes

Three ways to look at a shoot, switched with the keyboard (as in Lightroom) or from the toolbar:

  • Grid (G): the thumbnail grid.
  • Loupe (E): one image fitted to the window, with a filmstrip of every image of the folder along the bottom; the current image is highlighted in it, and clicking a thumbnail shows that image.
  • Full screen (F): the system full screen, with the image alone on a black background; Esc or F returns to the previous mode.

0.3.0: tags, filters, sorting

  • Three tags: Favorite, Neutral and Rejected.
  • Tags stored in Adobe-style XMP sidecar files next to the photos (IMG_0001.xmp for IMG_0001.CR3). This is the first version that writes files to your folders.
  • Filters, sorting and multi-selection.

0.4.0: blur detection

The sharpness scoring of blurdetector, integrated into the app as a native feature. From this version on, blur detection evolves as part of Image Toolkit.

  • A sharpness score for every photo.
  • Adjustable parameters: threshold, grid size, top fraction, pre-blur and long edge.
  • Hide the blurry photos.
  • Reject all the blurry photos in one batch.

0.5.0: deletion

  • Delete the selected images.
  • Deleted files go to the operating system’s trash by default; permanent deletion is available as an option.
  • A confirmation lists every file that will be removed, sidecars included.

0.6.0: closer inspection

100% zoom to check focus, and an EXIF panel.

0.7.0: comparison

A sharpness map overlay showing where the image is sharp, and side-by-side comparison of 2 to 4 images.

0.8.0: lens corrections

Lens corrections applied on display, from the lensfun database: distortion and vignetting.

0.9.0: export

Corrected export from a 16-bit RAW development to JPEG, PNG or TIFF.

0.10.0: hardening

Hardening and performance work; chromatic aberration correction if time allows.

1.0.0: stable

The first stable release.

Changelog

All notable changes to this project are documented in this file. The format is based on Keep a Changelog and the project follows Semantic Versioning. A new version section pushed to main triggers the release pipeline.

[Unreleased]

[0.1.0] - 2026-10-22

First release: a fast photo viewer for culling, straight from RAW files. Files are only read; tagging, blur detection and deletion arrive in the next versions.

Added

  • Open a folder of photos in place (toolbar button, Ctrl/Cmd+O, or drag and drop a folder onto the window), optionally including its subfolders; hidden files, macOS ._* companion files and symbolic links to directories are skipped
  • Supported formats: Canon CR3 and CR2, Fujifilm RAF, Leica RWL, Nikon NEF and NRW, Sony ARW, SRF and SR2, DNG, plus JPEG, PNG, TIFF, BMP and WebP (extensions matched case-insensitively)
  • RAW files are shown from their embedded JPEG previews, never demosaiced: for CR3, the 1620×1080 preview feeds the thumbnails (≈ 10 ms each) and the full-size JPEG the viewer; orientation metadata is applied
  • Virtualized thumbnail grid that stays fluid with thousands of photos: thumbnails decoded in the background on every core except one, the visible rows first, sharp at any size on high-DPI screens, with a GPU memory budget; the thumbnail size slider sets the minimum size and the columns fill the window width
  • Single-image viewer, fit to the window without enlarging past 100 %, with the neighbouring images decoded ahead
  • Keyboard navigation: arrow keys, Page Up/Down, Home/End, Enter or Space to open the viewer, Left/Right in the viewer, Esc to go back to the grid; double-click opens the viewer and returns from it
  • Unreadable or corrupt files show a warning placeholder with the error as a tooltip instead of interrupting anything; unreadable subfolders are listed in the status bar
  • French and English interface (French by default); language, last folder, subfolder option, thumbnail size and window geometry are remembered
  • Release archives for Linux x86_64, Windows x86_64 and macOS universal (Apple Silicon and Intel, .app), with third-party license notices
  • mdBook documentation published on GitLab Pages with each release

Building from source

Toolchain

Install Rust with rustup. The repository pins its toolchain in rust-toolchain.toml: Rust 1.99.1 with the rustfmt and clippy components. Inside the checkout, rustup selects that version and downloads it on first use, so there is nothing to configure. If your rustup is set up not to install toolchains automatically, run rustup toolchain install once in the repository.

CI uses the same pinned version.

Running the app

git clone https://gitlab.com/julienbltt/image-toolkit.git
cd image-toolkit
cargo run --release

Always use release builds to actually use the app: image decoding is much slower in debug builds. The binary is written to target/release/image-toolkit (image-toolkit.exe on Windows).

The runtime requirements of the prebuilt binaries apply to your own builds too: on Linux, a Vulkan or OpenGL driver, an X11 or Wayland session, and xdg-desktop-portal for the folder picker.

Everyday commands

cargo check                                   # fast validation
cargo test                                    # unit tests
cargo clippy --all-targets -- -D warnings     # lints; CI fails on any warning
cargo fmt                                     # formatting (CI runs cargo fmt --check)

Cargo.lock is committed and CI builds with --locked: commit it whenever a dependency or the version changes.

The unit tests need no test data. A larger check decodes every photo of a local folder, kept out of the repository; it is ignored by default:

IMAGE_TOOLKIT_SAMPLES=/path/to/photos cargo test --release sample_corpus -- --ignored --nocapture

Coding rules

The coding guidelines are in rules.md at the repository root. In short: correctness and clarity first, no panicking shortcuts such as unwrap(), errors are handled rather than discarded, full-word names, and comments only to explain a non-obvious why.

Working on this documentation

This book lives in docs/ and is built with mdBook 0.5 (a later major version may reject the configuration):

cargo install mdbook --version 0.5.4 --locked
mdbook serve docs --open    # live preview, rebuilt on every change
mdbook build docs           # output in docs/book/ (ignored by git)

The Changelog page includes CHANGELOG.md from the repository root, so it is edited there. The published site is only rebuilt for releases (see Releasing), so build the book locally after changing it: a broken page would otherwise go unnoticed until the next release.

Architecture

This page is a high-level overview. The source code in src/ is the reference for the details; each module starts with a comment describing its role. AGENTS.md, at the root of the repository, keeps the detailed notes for contributors and coding agents: mechanisms, decisions and conventions.

Layout

Image Toolkit is a single Cargo crate (no workspace) that builds one binary, image-toolkit. The interface is built with egui through its eframe framework, drawn on the GPU with wgpu.

ModuleRole
main.rsCreates the window and starts the app.
app.rsThe interface: toolbar, thumbnail grid, viewer, status bar, keyboard handling, saved preferences.
library.rsScans the opened folder: which files are listed, in which order.
decode.rsTurns a file into pixels for display: embedded JPEG previews of RAW files (through rawler), standard images (through image), orientation, reduction to the displayed size.
loader.rsThe background decoding queue and its worker threads.
i18n.rsEvery interface text, in French and English.

How the pieces fit

  • The interface thread draws every frame and never decodes images itself.
  • The folder scan runs on a thread of its own, so opening a large tree does not freeze the window.
  • Decoding runs on a pool of worker threads, one per CPU core minus one. At every frame the interface tells the loader which images it needs, most urgent first: the visible thumbnails, then a few rows around them; in the viewer, the current image, then its neighbours. Workers always take the most urgent request. Requests that scrolled out of view before a worker reached them are dropped, which keeps a fast scroll through thousands of photos from queueing thousands of decodes.
  • The grid is virtualized: only the visible rows are laid out and drawn, and decoded thumbnails beyond a fixed budget are evicted, farthest from view first, and decoded again when needed.
  • Errors stay per file. A file that fails to decode, even one that makes rawler panic, becomes an error placeholder; it never stops the scan or the other decodes.

Files are only ever read in 0.1.0. Writing (XMP sidecars, deletion) arrives with the versions listed in the roadmap.

Blur detection

The blur detection planned for 0.4.0 comes from the author’s command-line tool blurdetector. Its code is being moved into this crate rather than pulled in as a dependency, so that from then on blur detection is developed here, as a native feature of Image Toolkit.

Translations

Every fixed interface text is a variant of one enumeration in i18n.rs, translated in both languages in the same place. A missing translation is therefore a compile error rather than a blank label. Texts that contain a number, such as the image count with its plural rules, are small functions in the same file.

Continuous integration

The GitLab CI pipeline is defined in .gitlab-ci.yml. This page describes what it does; the file itself is the reference for the details.

The project runs on GitLab.com’s Free tier, which allows 400 compute minutes a month on GitLab-hosted runners. That budget shapes the pipeline: everyday pushes only run the quick checks, and the three release builds run only when a version is actually released.

On every push

CheckWhat it does
Formatcargo fmt --check
Lintcargo clippy with -D warnings: any warning fails the pipeline
Testscargo test (headless: the tests that need a GPU or a local photo corpus are skipped)
DocumentationBuilds this book with mdBook 0.5.4; a page listed in SUMMARY.md but missing fails the build
Third-party licensesGenerates the license list with cargo-about and fails if a dependency uses a license not accepted in about.toml

Cargo commands run with --locked, so an out-of-date Cargo.lock fails the pipeline instead of being silently updated.

On a release

When a push to main brings a new version section in CHANGELOG.md, the pipeline also builds, publishes and tags the release. Whether a push is a release is only known once the release:detect job has read the changelog, so the builds live in a child pipeline (ci/release-pipeline.yml) that this job generates and that the release job starts once every check has passed: an ordinary push spends its minutes on the checks only. The trigger and the steps are described in Releasing.

On other branches and in merge requests, a manual builds job runs the same three builds without releasing anything, to try a change to the build before merging it.

The builds all run on Linux runners:

PlatformHow it is built
Linux x86-64Built with cargo-zigbuild against glibc 2.31, so the binary runs on distributions as old as Ubuntu 20.04 and Debian 11 rather than only on the runner’s own glibc.
Windows x86-64Cross-compiled with the mingw-w64 toolchain (x86_64-pc-windows-gnu target).
macOS universal2Cross-compiled for Apple Silicon and Intel in one binary with cargo-zigbuild, using the official cargo-zigbuild container image, which bundles the macOS 11.3 SDK. A GUI app links against Apple’s system frameworks (AppKit, Metal…), whose SDK stubs Zig alone does not provide. The binary is packaged as an ad-hoc signed .app bundle that runs on macOS 11 and later.

The ad-hoc signature is what lets the app run on Apple Silicon at all, which refuses unsigned native code. It is not a Developer ID signature, hence the Gatekeeper warning described in Installation.

Documentation deployment

This book is deployed to GitLab Pages by the pipeline of a release tag, so the published documentation always matches the latest released version rather than unreleased work on main. A manual job on main pipelines can republish it in between, for example to fix a typo. See Releasing.

Releasing

CHANGELOG.md drives the releases. It follows the Keep a Changelog format: a ## [Unreleased] section at the top, then one ## [X.Y.Z] - YYYY-MM-DD section per released version, newest first. There is no release command to run and no tag to push: writing a new version section and pushing it to main is the release.

Day to day

Record every user-visible change under ## [Unreleased], in the same commit as the change, grouped under the usual Keep a Changelog headings (### Added, ### Changed, ### Fixed, ### Removed…).

Pushes that do not add a version section only run the checks described in Continuous integration.

Cutting a release

  1. Rename the [Unreleased] section to the new version and today’s date, and add a fresh, empty [Unreleased] section above it:

    # Changelog
    
    ## [Unreleased]
    
    ## [0.2.0] - 2026-12-01
    
    ### Added
    - Three display modes: grid, loupe with a filmstrip, full screen
    
  2. Bump the version in Cargo.toml to the same number (version = "0.2.0"), then run cargo check so that Cargo.lock records it too. CI builds with --locked and fails on an outdated Cargo.lock.

  3. Commit both files and push to main, directly or by merging a merge request.

What CI does

The pipeline of a push to main reads the topmost version heading of CHANGELOG.md, ignoring [Unreleased]. That heading must be exactly ## [X.Y.Z] - YYYY-MM-DD: any other level-2 heading in its place (a typo in the date, missing brackets) fails the pipeline rather than being skipped. If the tag vX.Y.Z for that version does not exist yet, the push is a release, and the pipeline:

  1. Runs the usual checks.
  2. Checks that the version in Cargo.toml equals X.Y.Z and that X.Y.Z is greater than every existing vX.Y.Z tag, and fails otherwise.
  3. Builds Linux, Windows and macOS release binaries (see Continuous integration).
  4. Uploads the release archives to the project’s generic package registry.
  5. Creates the tag vX.Y.Z on the pushed commit and the GitLab Release, whose notes are the ## [X.Y.Z] section of the changelog and whose assets link to the uploaded archives.

The new tag starts a pipeline of its own, which deploys the documentation (see below).

When the tag already exists, the version is considered released and the push only runs the checks. That makes later pushes safe: they never release the same version twice.

When a release fails

The tag is created only after the builds and the uploads have succeeded. If any earlier step fails, for example on a version mismatch, nothing is tagged: fix the problem, push to main again, and the release runs again.

If the last job, release:publish, fails after the tag was created but before the GitLab Release exists, retry that job: it notices the tag on its own commit and creates the missing release. A new push would not help, because it treats a tagged version as released.

Do not create vX.Y.Z tags by hand. CI treats an existing tag as an already released version, so a hand-made tag would skip the builds and the GitLab Release.

Documentation

The documentation site at https://julienbltt.gitlab.io/image-toolkit/ is deployed by the pipeline of each release tag. It therefore describes the latest released version, not the work in progress on main.

To republish it between releases, for example after fixing a typo, run the manual documentation job from the latest pipeline on main. Keep in mind that it publishes the state of main, including any documentation of unreleased features.

Do not re-run the documentation job of an older tag’s pipeline: it would put that older version of the site back online.