mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 11:27:21 -06:00
* egui037: name each control for what it sets, and order the eager fonts for the UI CodeRabbit review follow-ups on the migration: the Artboard X/Y fields, the CAF dropdowns, the channel Solidity field, the GPU backend/adapter dropdowns, the corner radius fields and the canvas extension colour all had an accessible name that named another control (or was shared by four). The eager craft-fonts registration now puts the UI Japanese face first, as the lazy loader does, so Japanese UI is not drawn in the manifest's Mincho face and shared Han characters do not take Japanese forms. Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * egui037: rustfmt the eager craft-font order --------- Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com> Co-authored-by: Codebuff <noreply@codebuff.com>
418 lines
19 KiB
YAML
418 lines
19 KiB
YAML
name: CI
|
|
|
|
# Quality gates for every push to main and every pull request. The Linux gate runs through the
|
|
# suite's shared gate workflow in craft-common-actions (`cargo xtask ci`); the Windows legs, the
|
|
# test variant and the corpus, web and fuzz jobs are PhotoCraft's own.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
# `labeled` so that adding `full-ci` to an open PR starts a run with Windows in it.
|
|
types: [opened, synchronize, reopened, labeled]
|
|
workflow_dispatch:
|
|
inputs:
|
|
fuzz:
|
|
description: "Run the fuzz job"
|
|
type: boolean
|
|
default: false
|
|
fuzz_seconds:
|
|
description: "Seconds per fuzz target"
|
|
default: "600"
|
|
|
|
# The token is only used to check out the code: no job writes to the repository.
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
RUST_BACKTRACE: 1
|
|
# No test or fuzz run may print a page: the engine refuses to spool, and if something ever
|
|
# bypasses that, lp fails loudly on a printer that does not exist.
|
|
PHOTOCRAFT_NO_PRINT: "1"
|
|
LPDEST: photocraft-no-such-printer
|
|
|
|
# A newer push to a PR branch cancels its older run. Every push to main gets a run of its own that
|
|
# is never cancelled, so each state of main is checked to the end. Adding any label but `full-ci`
|
|
# gets a group of its own, so it neither runs anything nor cancels a run.
|
|
concurrency:
|
|
group: ci-${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'full-ci') && github.ref || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
# Whether the Windows legs and the web build run. main, manual runs and PRs labelled `full-ci`
|
|
# get both. Other PRs get Windows when they touch code whose behaviour differs by platform: the
|
|
# app shell, UI, text and fonts, GPU, pen input, native saves, build scripts or the lockfile.
|
|
# Format changes must exercise the Windows-only publication barriers (#8034). macOS is tested
|
|
# only by release.yml.
|
|
platforms:
|
|
if: github.event.action != 'labeled' || github.event.label.name == 'full-ci'
|
|
timeout-minutes: 10
|
|
name: select platforms
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
windows: ${{ steps.pick.outputs.windows }}
|
|
web: ${{ steps.pick.outputs.web }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
- name: Git dependencies come from storytold
|
|
run: |
|
|
set -euo pipefail
|
|
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
|
# Source replacement, patches and path overrides in cargo config never show up
|
|
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
|
# Cargo still reads the legacy .cargo/config.
|
|
python3 - <<'PY'
|
|
import os, sys, tomllib
|
|
for path in (".cargo/config", ".cargo/config.toml"):
|
|
if not os.path.exists(path):
|
|
continue
|
|
config = tomllib.load(open(path, "rb"))
|
|
replaced = [name for name, source in config.get("source", {}).items()
|
|
if isinstance(source, dict) and "replace-with" in source]
|
|
if replaced or "patch" in config or "paths" in config:
|
|
print(f"{path} replaces, patches or overrides a source")
|
|
sys.exit(1)
|
|
PY
|
|
# Fails when Cargo.lock is out of date with the manifests.
|
|
cargo metadata --locked --format-version 1 > /dev/null
|
|
python3 - <<'PY'
|
|
import sys, tomllib
|
|
lock = tomllib.load(open("Cargo.lock", "rb"))
|
|
bad = sorted({p["source"] for p in lock.get("package", [])
|
|
if p.get("source", "").startswith("git+")
|
|
and not p["source"].startswith("git+https://github.com/storytold/")})
|
|
if bad:
|
|
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
|
sys.exit(1)
|
|
PY
|
|
- id: pick
|
|
env:
|
|
EVENT: ${{ github.event_name }}
|
|
BASE: ${{ github.event.pull_request.base.sha }}
|
|
FULL: ${{ contains(github.event.pull_request.labels.*.name, 'full-ci') }}
|
|
run: |
|
|
if [ "$EVENT" != pull_request ] || [ "$FULL" = true ]; then
|
|
echo "windows=true" >> "$GITHUB_OUTPUT"
|
|
echo "web=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# The browser build (a fat-LTO wasm build) runs only when something it compiles or
|
|
# packages changed: the web app and its packaging, crate sources and manifests (the web
|
|
# build compiles nearly every crate), the workspace manifests and this workflow.
|
|
if git diff --name-only "$BASE" HEAD | grep -Eq '^(apps/photocraft-web(-boot)?/|packaging/web/|packaging/env\.sh$|crates/[^/]+/(src/|Cargo\.toml$|build\.rs$)|Cargo\.(toml|lock)$|rust-toolchain(\.toml)?$|\.github/workflows/ci\.yml$)'; then
|
|
echo "web=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "web=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
if git diff --name-only "$BASE" HEAD | grep -Eq '^(apps/|crates/(ui-egui|text|gpu|tablet|heif|format)/|Cargo\.lock$|(.*/)?build\.rs$|\.github/workflows/ci\.yml$)'; then
|
|
echo "windows=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "windows=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# The Linux gate. Its rust-cache entry is the one `test (without heif)` restores.
|
|
gate:
|
|
if: github.event.action != 'labeled' || github.event.label.name == 'full-ci'
|
|
# Tracks @main so suite-wide CI fixes reach every app without a PR in each repository.
|
|
uses: storytold/craft-common-actions/.github/workflows/gate.yml@main
|
|
with:
|
|
# egui/winit/rfd.
|
|
packages: >-
|
|
libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev
|
|
libgtk-3-dev
|
|
# The tests build with the optional craft-fonts input (crates/text/build.rs), so the
|
|
# Japanese font tests that skip without it run. Same commit as release.yml.
|
|
craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83
|
|
# Cargo.toml's dev profile already keeps line tables only.
|
|
dev-debug: ""
|
|
env: |
|
|
PHOTOCRAFT_NO_PRINT=1
|
|
LPDEST=photocraft-no-such-printer
|
|
timeout-minutes: 100
|
|
# fmt, clippy, layers, scorecard, tests, panic_hunt, doctests and the wasm32 check, with
|
|
# `--locked --features heif`, then the missing-translations report (xtask/src/main.rs).
|
|
command: cargo xtask ci
|
|
|
|
# Windows tests run as two legs in parallel: the unit tests and the integration tests. As one job
|
|
# they took ~28 min on the Windows runner; the legs take ~13 and ~15. Splitting the integration
|
|
# tests over a third runner (nextest --partition) saved ~2 min of wall time but cost ~7
|
|
# runner-minutes and one more Windows slot per PR. They stay outside the shared gate for sccache,
|
|
# which needs its own action step.
|
|
windows:
|
|
needs: platforms
|
|
if: needs.platforms.outputs.windows == 'true'
|
|
timeout-minutes: 100
|
|
name: test (windows-latest, ${{ matrix.part }})
|
|
runs-on: windows-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
part: [unit, integration]
|
|
env:
|
|
# No debuginfo on Windows: writing PDBs for ~200 test binaries cost 2.2 of ~21 build minutes.
|
|
# A panic still names its file and line; only backtrace frames lose theirs (Linux keeps
|
|
# line-tables-only, so a backtrace with lines is one OS away).
|
|
CARGO_PROFILE_DEV_DEBUG: "0"
|
|
# The runner has no Japanese or Simplified Chinese system font, and egui 0.37 panics on a
|
|
# missing glyph: the localized UI tests need the same craft-fonts input the gate and FreeBSD
|
|
# build with (crates/text/build.rs). Same commit as the gate above and release.yml.
|
|
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
|
|
CRAFT_FONTS_REQUIRED: "1"
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Check out craft-fonts
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
repository: storytold/craft-fonts
|
|
ref: b076417b5880bd4154bd22f00695b1d28c6bea83
|
|
path: craft-fonts
|
|
persist-credentials: false
|
|
|
|
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
|
|
with:
|
|
components: clippy
|
|
|
|
# Only main saves caches; PRs restore main's, so the repo's 10 GB cache budget keeps main's.
|
|
# A failing test still saves: the cache holds compiled dependencies, not test results.
|
|
# Both legs share one key (same dependencies); the integration leg saves it, because it also
|
|
# runs Clippy and so leaves Clippy's dependency metadata in it.
|
|
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
|
with:
|
|
save-if: ${{ github.ref == 'refs/heads/main' && matrix.part == 'integration' }}
|
|
cache-on-failure: true
|
|
|
|
# sccache caches the workspace crates' compiled libraries, which rust-cache leaves out:
|
|
# warm, each Windows leg built 4.5 min faster. Like rust-cache, only main writes (PRs read
|
|
# main's entries); a full set is ~90 MB, so it barely touches the 10 GB budget (#2688).
|
|
# Test executables can't be cached, so a PR still links its tests.
|
|
- name: Install sccache
|
|
uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
|
with:
|
|
version: v0.18.0
|
|
|
|
- name: Use sccache
|
|
env:
|
|
MAIN: ${{ github.ref == 'refs/heads/main' }}
|
|
run: |
|
|
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
|
|
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
|
|
if [ "$MAIN" != true ]; then echo "SCCACHE_GHA_RW_MODE=READ_ONLY" >> "$GITHUB_ENV"; fi
|
|
shell: bash
|
|
|
|
- name: Clippy
|
|
if: matrix.part == 'integration'
|
|
run: cargo clippy --workspace --all-targets --locked --features heif -- -D warnings
|
|
|
|
- name: Dependency layering
|
|
if: matrix.part == 'integration'
|
|
run: cargo xtask layers
|
|
|
|
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29
|
|
with:
|
|
tool: cargo-nextest@0.9.149
|
|
|
|
# The unit leg runs `--lib --bins` and the integration leg `--test '*'` (every
|
|
# integration-test target); the feature selection stays the same, so both legs share the
|
|
# dependency cache.
|
|
- name: Test
|
|
env:
|
|
PART: ${{ matrix.part }}
|
|
run: |
|
|
case "$PART" in
|
|
unit) targets=(--lib --bins) ;;
|
|
*) targets=(--test '*') ;;
|
|
esac
|
|
cargo nextest run --profile ci --workspace --locked --features heif "${targets[@]}"
|
|
shell: bash
|
|
|
|
- name: Doctests
|
|
if: matrix.part == 'unit'
|
|
run: cargo test --workspace --locked --features heif --doc
|
|
|
|
# The same codecs without the optional decoder: HEIC must stay a clear "not in this build" error
|
|
# for distributors who leave it out. Runs beside the gate and restores the gate's dependency
|
|
# cache without saving one: the dependencies are the same, and the 10 GB cache budget is tight
|
|
# (#2688).
|
|
test-without-heif:
|
|
if: github.event.action != 'labeled' || github.event.label.name == 'full-ci'
|
|
timeout-minutes: 60
|
|
name: test (without heif)
|
|
# The gate's runner: its cache entry is keyed on the OS.
|
|
runs-on: ubuntu-24.04
|
|
env:
|
|
# The gate sets these; rust-cache hashes CARGO_* and RUST* variables into its key.
|
|
CARGO_INCREMENTAL: 0
|
|
CARGO_NET_RETRY: 10
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Linux build dependencies
|
|
uses: storytold/craft-common-actions/actions/install-apt-packages@main
|
|
with:
|
|
packages: >-
|
|
libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev
|
|
libgtk-3-dev
|
|
|
|
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
|
|
|
|
# gate.yml keys its cache `<cache-version>-rust-<hash of Cargo.toml>-gate`.
|
|
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
|
with:
|
|
prefix-key: v1-rust
|
|
shared-key: ${{ hashFiles('Cargo.toml') }}-gate
|
|
save-if: false
|
|
|
|
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29
|
|
with:
|
|
tool: cargo-nextest@0.9.149
|
|
|
|
- name: Test without heif
|
|
run: cargo nextest run -p photocraft-heif -p photocraft-codecs -p photocraft-io --locked --profile ci
|
|
|
|
corpus:
|
|
if: github.event.action != 'labeled' || github.event.label.name == 'full-ci'
|
|
timeout-minutes: 30
|
|
# Real-file corpus tests (cargo feature `corpus`), on every PR and on main. The corpora are
|
|
# pinned in xtask/src/corpus_pins.rs and sha256-verified (xtask/*.sha256) after both a cache
|
|
# restore and a download; a missing corpus fails the tests. The cache key changes only when a
|
|
# pin or manifest does, so normally this restores in seconds (fork PRs read the base cache).
|
|
name: corpus tests
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
# test-corpus builds with --release, but here without the release profile's thin LTO and
|
|
# single codegen unit: they ran LTO over the dependency graph for each of the ~95 corpus test
|
|
# binaries, so building took 23.5 of the job's 37 minutes while the tests ran for 1.5.
|
|
# Turning them off builds 2.7x faster; the code under test is the same.
|
|
CARGO_PROFILE_RELEASE_LTO: "false"
|
|
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16"
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
|
|
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
|
with:
|
|
key: corpus
|
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
cache-on-failure: true
|
|
- name: Restore test corpora
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: corpus/
|
|
key: corpus-${{ hashFiles('xtask/src/corpus_pins.rs', 'xtask/*.sha256') }}
|
|
- name: Fetch and verify corpora
|
|
run: cargo xtask corpus --all
|
|
- name: Corpus tests
|
|
run: cargo xtask test-corpus
|
|
|
|
web:
|
|
timeout-minutes: 60
|
|
name: web build and wasm size gate
|
|
# PRs run it only when web-relevant files change (see `platforms`); main always does.
|
|
needs: platforms
|
|
if: needs.platforms.outputs.web == 'true'
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
# Match release.yml so PR results reflect what would actually ship.
|
|
TRUNK_VERSION: 0.21.14
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
|
with:
|
|
key: ci-web
|
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
cache-on-failure: true
|
|
- name: Install trunk
|
|
run: |
|
|
curl --proto '=https' -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
|
|
| tar -xz -C "$HOME/.cargo/bin" trunk
|
|
trunk --version
|
|
# package.sh runs Trunk's release builds, validates relative URLs, cuts the wasm into
|
|
# parts, checks the 24 MiB per-file limit and exercises the packaging step release.yml uses.
|
|
- name: Build and verify browser release
|
|
run: packaging/web/package.sh
|
|
- name: Report wasm size
|
|
if: success()
|
|
run: |
|
|
{
|
|
echo "### Browser WebAssembly size"
|
|
echo
|
|
echo "| File | Bytes | MiB | Limit |"
|
|
echo "| --- | ---: | ---: | ---: |"
|
|
row() { echo "| $1 | $2 | $(awk -v n="$2" 'BEGIN { printf "%.2f", n / 1048576 }') | $3 |"; }
|
|
# The app's wasm as built; it ships gzipped in parts, so only the download grows with it.
|
|
for wasm in dist/web/*.wasm; do
|
|
[ -f "$wasm" ] || continue
|
|
row "$(basename "$wasm")" "$(wc -c < "$wasm" | tr -d ' ')" "none"
|
|
done
|
|
# What a host sees: the largest file of the packaged site.
|
|
largest=$(find target/web-package -type f -printf '%s %f\n' | sort -n | tail -1)
|
|
row "largest file in the site: ${largest#* }" "${largest%% *}" "24 MiB"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
fuzz:
|
|
timeout-minutes: 60
|
|
name: fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})
|
|
# Placeholder: manual trigger only until a nightly schedule is enabled.
|
|
if: github.event_name == 'workflow_dispatch' && inputs.fuzz
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target:
|
|
- { crate: psd, name: parse }
|
|
- { crate: psd, name: descriptor }
|
|
- { crate: psd, name: image_source_data }
|
|
- { crate: psd, name: grd }
|
|
- { crate: psd, name: ase }
|
|
- { crate: psd, name: aco }
|
|
- { crate: psd, name: patterns }
|
|
- { crate: psd, name: filter_effects }
|
|
- { crate: psd, name: metadata }
|
|
- { crate: psd, name: tagged }
|
|
- { crate: psd, name: resources }
|
|
- { crate: psd, name: asl }
|
|
- { crate: codecs, name: decode_any }
|
|
- { crate: codecs, name: decode_png }
|
|
- { crate: codecs, name: decode_jpeg }
|
|
- { crate: codecs, name: decode_tiff }
|
|
- { crate: codecs, name: decode_exr }
|
|
- { crate: codecs, name: decode_heif }
|
|
- { crate: affinity, name: preview }
|
|
- { crate: affinity, name: import_preview, args: "--features io" }
|
|
- { crate: format, name: load }
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # nightly
|
|
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
|
with:
|
|
workspaces: crates/${{ matrix.target.crate }}/fuzz
|
|
- name: Install cargo-fuzz
|
|
run: cargo install cargo-fuzz --locked
|
|
- name: Fuzz
|
|
working-directory: crates/${{ matrix.target.crate }}
|
|
env:
|
|
FUZZ_SECONDS: ${{ inputs.fuzz_seconds }}
|
|
run: cargo fuzz run ${{ matrix.target.name }} ${{ matrix.target.args }} -- -max_total_time="$FUZZ_SECONDS"
|
|
- name: Upload crash artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: fuzz-${{ matrix.target.crate }}-${{ matrix.target.name }}
|
|
path: crates/${{ matrix.target.crate }}/fuzz/artifacts
|