Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: CI

# Every job runs a mise task from mise.toml — the same tasks the git hooks
# (hk.pkl) and developers run locally. Never inline a command here.

on:
push:
branches:
Expand All @@ -18,7 +21,10 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check
- uses: jdx/mise-action@v4
env:
GITHUB_TOKEN: ${{ github.token }}
- run: mise run fmt-check

clippy:
name: Clippy
Expand All @@ -28,8 +34,11 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: jdx/mise-action@v4
env:
GITHUB_TOKEN: ${{ github.token }}
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: mise run lint

test:
name: Test
Expand All @@ -38,7 +47,20 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
- uses: jdx/mise-action@v4
env:
GITHUB_TOKEN: ${{ github.token }}
- run: mise run test

tooling:
name: Tooling config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v4
env:
GITHUB_TOKEN: ${{ github.token }}
- run: mise run validate-hooks

commits:
name: Conventional commits
Expand All @@ -48,8 +70,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: cargo-bins/cargo-binstall@main
- name: Install convco
run: cargo binstall -y convco
- uses: jdx/mise-action@v4
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Check commits
run: convco check ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}
run: mise run commits "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
42 changes: 18 additions & 24 deletions .github/workflows/determinism.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
name: Determinism

# docs/verification.md: export the same (raw + sidecar) on two architectures and diff
# bytes. Runs the full workspace test suite (golden-file and regression
# tests) on both, then byte-hashes focale-cli renders of the committed
# fixture in every export format and compares across architectures.
# docs/verification.md: export the same (raw + sidecar) on two architectures and
# diff bytes. Runs the full workspace test suite (golden-file and regression
# tests) on both, then byte-hashes focale-cli renders of the committed fixture
# in every export format and compares across architectures.
#
# Both steps are mise tasks from mise.toml; the render/hash step lives in
# scripts/determinism-hashes.sh so it can be reproduced locally with
# `mise run determinism`.

on:
push:
Expand All @@ -29,28 +33,15 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: jdx/mise-action@v4
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Workspace tests (golden + regression suites)
run: cargo test --workspace
run: mise run test
- name: Render fixture in every format and hash
run: |
set -euo pipefail
RAW=crates/focale-core/tests/fixtures/synthetic.dng
SIDECAR=crates/focale-cli/tests/fixtures/determinism.fcl
OUT=hashes-${{ matrix.arch }}.txt
: > "$OUT"
echo "working $(cargo run -q -p focale-cli -- hash "$RAW" --sidecar "$SIDECAR")" >> "$OUT"
for fmt in tiff16 png16 png8 jpeg jxl avif; do
hash=$(cargo run -q -p focale-cli -- render "$RAW" --sidecar "$SIDECAR" \
--format "$fmt" --gamut display-p3 --hash --out "/tmp/out.$fmt")
echo "$fmt $hash" >> "$OUT"
done
hash=$(cargo run -q -p focale-cli -- render "$RAW" --sidecar "$SIDECAR" \
--format avif --gamut rec2020 --hdr pq --hash --out /tmp/out-hdr.avif)
echo "avif-hdr-pq $hash" >> "$OUT"
hash=$(cargo run -q -p focale-cli -- render "$RAW" --sidecar "$SIDECAR" \
--format jxl --gamut rec2020 --hdr hlg --hash --out /tmp/out-hdr.jxl)
echo "jxl-hdr-hlg $hash" >> "$OUT"
cat "$OUT"
run: mise run determinism
env:
HASHES_OUT: hashes-${{ matrix.arch }}.txt
- uses: actions/upload-artifact@v4
with:
name: hashes-${{ matrix.arch }}
Expand All @@ -65,6 +56,9 @@ jobs:
with:
pattern: hashes-*
merge-multiple: true
# The one workflow step that does not go through a mise task: this job
# deliberately has no checkout (it only needs the two artifacts), so there
# is no mise.toml here to run a task from.
- name: Diff
run: |
set -euo pipefail
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
**/*.rs.bk
*.pdb

# === Tooling ===
# mise's local, uncommitted config override.
mise.local.toml
.mise.local.toml
# Default output of `mise run determinism`.
/hashes*.txt

# === Linux ===
*~
.fuse_hidden*
Expand Down
20 changes: 14 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@ changing processing code.

## Quality

Validate changes with `just check` (exactly what CI runs), or individually:
Validate changes with `mise run check` — the four gate jobs CI runs on every push.
CI additionally validates the commit range on pull requests (`mise run commits`) and
runs the two-architecture Determinism workflow. Individually:

```bash
cargo test --workspace # correctness
cargo fmt --check # formatting
cargo clippy --workspace --all-targets -- -D warnings # lint
mise run test # correctness (cargo test --workspace)
mise run fmt-check # formatting (cargo fmt --check)
mise run lint # lint (cargo clippy --workspace --all-targets -- -D warnings)
mise run validate-hooks # hk.pkl parses (hk validate)
```

`mise.toml` is the single source of truth for these commands: the git hooks
(`hk.pkl`) and every GitHub Actions job invoke the same tasks. Never inline a
command in a hook or a workflow — add or change the task instead.

- All `pub` items need doc comments where non-obvious; processing code documents its
algorithm source (paper/reference implementation).
- Floating-point code on the export path must not depend on evaluation order that the
Expand All @@ -42,8 +49,9 @@ cargo clippy --workspace --all-targets -- -D warnings # lint
## Commits

Commits MUST follow [Conventional Commits](https://www.conventionalcommits.org/)
(`feat:`, `fix:`, `chore:`, …) — enforced by `convco` at commit-msg, pre-push, and in
CI on pull requests. Merge commits are exempt.
(`feat:`, `fix:`, `chore:`, …) — enforced by `convco` (via `mise run commit-msg` and
`mise run commits`) at commit-msg, pre-push, and in CI on pull requests. Merge commits
are exempt.

## Releases

Expand Down
67 changes: 48 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ A: If you have another tool you need to further process your images, your best b
### Getting Started

```bash
just run # launch the desktop app (Wayland/X11)
mise run run # launch the desktop app (Wayland/X11)
scripts/fetch-models.sh # optional: download the AI segmentation models
cargo run -p focale-cli -- render photo.ARW --format tiff16 # headless export
```
Expand All @@ -94,33 +94,62 @@ bit-identically on any machine, forever. See the docs index at `docs/README.md`
### Prerequisites

- [Rust (rustup)](https://rustup.rs) — toolchain (pinned via `rust-toolchain.toml`)
- [just](https://github.com/casey/just) — command runner
- [Lefthook](https://github.com/evilmartians/lefthook) — git hooks manager (`lefthook install` after cloning)
- [convco](https://github.com/convco/convco) — conventional-commit checker used by hooks
- cmake + a C++ toolchain — builds the vendored libjxl for JPEG XL export
- [mise](https://mise.jdx.dev) — task runner; it also pins and installs `hk` and `convco`
- cmake + a C++ toolchain — builds the vendored libjxl for JPEG XL export (not managed by mise)

After cloning:

```bash
mise install # fetch the pinned tools (hk, convco)
mise run hooks # install the git hooks
```

`mise.toml` pins [hk](https://hk.jdx.dev) (git hooks) and
[convco](https://github.com/convco/convco) (conventional-commit checker), so neither
needs installing by hand. `mise run hooks` installs the hooks into this clone only,
and they run through `mise x` — so whatever launches git must have `mise` on its
`PATH`. Committing from a GUI client with a trimmed environment needs mise's shim
directory added to that client's `PATH`.

## Commands

| Command | Description |
| ------------ | -------------------------------------------- |
| `just check` | Run everything CI runs (format, lint, tests) |
| `just test` | Run the test suite |
| `just fmt` | Format code |
| `just lint` | Clippy with warnings denied |
| `just run` | Launch the desktop app |
`mise.toml` is the single source of truth for every command: the git hooks and
every CI job invoke these same tasks. `mise tasks ls` lists them all.

| Command | Description |
| --------------------- | -------------------------------------------- |
| `mise run check` | Run every gate CI runs (format, lint, tests, hk.pkl) |
| `mise run test` | Run the test suite |
| `mise run fmt` | Format code |
| `mise run lint` | Clippy with warnings denied |
| `mise run run` | Launch the desktop app |
| `mise run commits` | Validate conventional commits in a range |
| `mise run determinism`| Render the determinism fixture and hash it |

## Git Hooks

This project uses Lefthook. Pre-commit auto-formats staged Rust files; commit-msg
validates the message is a conventional commit; pre-push runs the full CI check suite
(format, clippy, tests, commit-range check) so pushes never fail CI.
This project uses [hk](https://hk.jdx.dev); `mise run hooks` installs them.
`hk.pkl` decides only *when* each task runs — the commands themselves come from
`mise.toml`.

- **pre-commit** formats staged Rust files. hk stashes unstaged work first, so the
hook sees the staged content and only the files you staged get formatted and
re-staged — work you deliberately left out of the commit is preserved untouched.
- **commit-msg** validates the message is a conventional commit; merge and rebase
commits are exempt.
- **pre-push** runs the full CI check suite (format, clippy, tests, commit-range
check, hk.pkl validation) on every push, so pushes should not fail CI. The
Determinism workflow is the one exception — it is a two-architecture comparison
CI alone can make.

## CI/CD

GitHub Actions runs format checks, clippy, tests on pushes to `master` and pull
requests, plus conventional-commit validation on pull requests. A separate
Determinism workflow renders the committed (raw + sidecar) fixture on x86_64 and
aarch64 in every export format and fails if any byte differs (`docs/verification.md`).
GitHub Actions runs format checks, clippy, tests and an `hk.pkl` validation on pushes
to `master` and pull requests, plus conventional-commit validation on pull requests.
Every job invokes the same `mise run <task>` a developer runs locally, so CI and the
git hooks cannot drift apart. A separate Determinism workflow renders the committed
(raw + sidecar) fixture on x86_64 and aarch64 in every export format and fails if any
byte differs (`docs/verification.md`).

## Releases & Changelog

Expand Down
3 changes: 3 additions & 0 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ check; write them before adding the next dependency, not after.
- CI job matrix: `ubuntu-24.04` (x86_64) and `ubuntu-24.04-arm` (aarch64) render
the committed fixture set and compare SHA-256 of output bytes; any divergence
fails.
- The render/hash step is `scripts/determinism-hashes.sh`, driven by the
`mise run determinism` task, so CI runs exactly what a developer can run
locally on one architecture.
- Golden-file suites: (a) sidecar bytes for a canonical edit state, (b) frozen
sidecars + frozen output hashes per pipeline version (regression), (c) colour
transform vectors per gamut/format.
Expand Down
62 changes: 62 additions & 0 deletions hk.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/// Git hooks for Focale.
///
/// Every command lives in `mise.toml` — this file only decides when each task
/// runs and against which files. Install the hooks with `mise run hooks`.
///
/// Keep the version below in sync with `hk` in `mise.toml`.
amends "package://github.com/jdx/hk/releases/download/v2.0.1/hk@2.0.1#/Config.pkl"

min_hk_version = "2.0.1"
default_branch = "master"

hooks {
["pre-commit"] {
fix = true
// Run against the staged content, not the dirty worktree: hk saves unstaged
// work, formats, stages only the files it selected, then restores the rest.
stash = "git"
steps {
["fmt"] {
glob = "**/*.rs"
fix = "mise run fmt"
}
}
}

["commit-msg"] {
steps {
// convco rather than hk's check_conventional_commit builtin: hk can only
// validate a single message, and pre-push and CI need a *range* checker,
// so convco is the one implementation of the rule. Merge and rebase
// commits are exempt; the task handles that.
["conventional-commit"] {
check = "mise run commit-msg '{{commit_msg_file}}'"
}
}
}

["pre-push"] {
steps {
// All three are deliberately unglobbed. Each runs a workspace-wide cargo
// command that ignores hk's file selection, so a glob would only gate
// whether the step runs at all — and a push touching just Cargo.toml,
// rust-toolchain.toml or rustfmt.toml would then skip a gate CI still
// enforces. Matches the lefthook config this replaces.
["fmt-check"] {
check = "mise run fmt-check"
}
["lint"] {
check = "mise run lint"
}
["test"] {
check = "mise run test"
}
["validate-hooks"] {
check = "mise run validate-hooks"
}
["commits"] {
check = "mise run commits"
}
}
}
}
24 changes: 0 additions & 24 deletions justfile

This file was deleted.

Loading
Loading