Simpl-Resultat/.forgejo/workflows/check-rust.yml
le king fu c5efc38e96
All checks were successful
PR Check — Rust / rust (pull_request) Successful in 4m14s
fix(deps): build the updater without its zip feature
Clears RUSTSEC-2026-0367, RUSTSEC-2026-0368 and RUSTSEC-2026-0369 on
zip 4.6.1, which turned the daily audit.yml red at run 558 (2026-10-17)
without any lockfile change. zip was compiled only for Windows, through
the default `zip` feature of tauri-plugin-updater 2.10.0. It is fixed
only in zip >= 9.0.0, and tauri-plugin-updater 2.14.0 still requires
zip ^4, so no cargo update can reach a fix.

That feature only unpacks archived update packages (.nsis.zip,
.msi.zip, .AppImage.tar.gz, produced by createUpdaterArtifacts
"v1Compatible"); this app ships createUpdaterArtifacts: true, whose
packages are never archives. The updater is now declared without its
default features, keeping rustls-tls. The lockfile loses zip, arbitrary
and derive_arbitrary (666 -> 663 packages, nothing added), and tar
leaves the Linux graph.

tar was the canary of the suppression guard in check-rust.yml. It is
replaced by webkit2gtk, Tauri's Linux webview, which keeps the property
that made tar useful: a dependency present on x86_64-unknown-linux-gnu
only, so finding it also proves that --target resolves conditional
edges. ADR 0018, architecture.md, qa-update-cycle.md and the release
skill are updated to match.

Verified locally: cargo-audit 0.22.2 exits 1 on the old lock with the
three IDs and 0 on the new one (663 crates, the same 12 allowed
warnings); the suppression guard is green, and red with the canary
swapped for tar or zip; windows-sys guard 9/9; cargo check and cargo
test (267 passed) on Linux; cargo xwin check for x86_64-pc-windows-msvc
is green on main (zip compiled) and on this branch (zip gone).

Refs #455

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:37:48 -04:00

350 lines
18 KiB
YAML

name: PR Check — Rust
# Rust half of the former check.yml (split in #232).
#
# Runs on PRs that change Rust, plus once a week on main to refresh the shared
# cache (see the note on the triggers). The runner has capacity 1 — jobs queue
# instead of running in parallel, so every minute spent here is a minute the
# next PR waits.
#
# No `branches:` filter on purpose. `branches: [main]` never matched a PR
# stacked on top of another feature branch, which is what /autopilot produces:
# 4 of the 5 PRs in the feature-gating milestone ran no CI at all.
on:
pull_request:
paths:
- 'src-tauri/**'
- '.forgejo/workflows/check-rust.yml'
# Whole directory, not just audit.toml: a later .cargo/config.toml
# (rustflags, linker, target dir) would change the Rust build, and an
# exact-path entry would let it skip Rust CI unnoticed.
- '.cargo/**'
# The weekly run is what feeds the cache to pull requests (#234). The runner
# (forgejo-runner v12.7.0) isolates what a pull_request run writes to that
# PR's ref: later pushes to the same PR read it back, other PRs never see it.
# A PR run falls back to the shared, non-isolated entries only when nothing
# matches in its own scope. Scheduled and dispatched runs write to the shared
# scope, which makes them this workflow's only writers of it (a merged PR's
# `closed` event would be another, but this workflow does not subscribe to
# it).
#
# The runner deletes an entry unused for 7 days or created more than 30 days
# ago, and keeps only the newest entry per key, whatever its scope; the next
# Monday run saves a fresh one. Cold runs that do not mean #234 is back (a
# timeout would):
# - a new stable Rust (the toolchain floats on `stable`) or a change to this
# job's `env:`: the key hashes rustc and the CARGO*, RUST*, CC*, CFLAGS*,
# CXX* and CMAKE* variables. Cold until the next Monday;
# - a PR run that saves under the shared entry's key evicts it for every PR
# until the next Monday. It saves when its own scope holds an older entry
# for another Cargo.lock, served before the shared one: a PR that bumped a
# dependency, then went back to main's lockfile. Two open PRs on the same
# lockfile, different from main's, evict each other the same way;
# - PR runs once an entry that only got exact hits, hence never re-saved,
# reaches 30 days;
# - after a week without any cache request, the Monday restore can fail with
# a 404: the lookup does not refresh the entry, and the cleanup it triggers
# may delete it (forgejo/runner#1741). The run then saves a fresh entry.
# workflow_dispatch re-seeds by hand.
schedule:
# Mondays 06:30 UTC, after audit.yml (06:00 UTC).
- cron: '30 6 * * 1'
workflow_dispatch:
# What this key does on Forgejo 8.0.3 / runner v12.7.0, as measured on
# 2026-10-02 (la-compagnie-maximus docs/forgejo-ops.md, sections "Le schéma du
# workflow est validé par le runner" and "Un push annule le run du push
# précédent"):
# - it passes the runner's schema validation, but what it adds has not been
# isolated;
# - without it, Forgejo cancels a `push` run when a newer one of the same
# workflow is created on the same ref, whether the older run's job was still
# queued or already running;
# - no pull_request (or pull_request_sync) job has been cancelled by a later
# run: on a PR, a force-push does not cancel the obsolete run.
# Kept anyway (#376): harmless here, and it keeps its GitHub Actions meaning
# should these workflows run where it is honoured (GitHub, a later Forgejo).
# There, a group shared with the frontend workflow would make the two cancel
# each other on a PR that touches both, hence the distinct group.
concurrency:
group: ci-rust-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
rust:
runs-on: ubuntu
container: ubuntu:22.04
env:
PATH: /root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
CARGO_TERM_COLOR: always
# rust-cache keeps the dependencies and drops this crate's own artifacts
# before saving, so incremental data would never be reused — pure
# overhead — and test debug info would only make the cached target/
# bigger.
CARGO_INCREMENTAL: 0
CARGO_PROFILE_TEST_DEBUG: 0
steps:
- name: Install system dependencies, Node.js and Rust
run: |
apt-get update
# zstd: the cache archives are compressed with it when present,
# faster and smaller than the gzip fallback.
apt-get install -y --no-install-recommends \
curl wget git ca-certificates build-essential pkg-config \
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libssl-dev \
libdbus-1-dev zstd
# Node.js runs the JavaScript actions: actions/checkout (node20) and
# Swatinem/rust-cache (node24 since v2.9). The runner starts them
# with whatever `node` the container has, so install the newest
# runtime they declare. taiki-e/install-action is a composite action.
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
# Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
node --version
rustc --version
cargo --version
- name: Checkout
uses: https://github.com/actions/checkout@v4
# Caches ~/.cargo (registry, git, bin) and src-tauri/target, keyed on the
# rustc version, the job's CARGO*/RUST* environment, the dependencies
# declared in the Cargo manifests and lockfile (not the app's own
# version), and this job. Saved only when the job succeeds and the key
# was not an exact hit. Served by
# the runner's cache proxy on port 28100, which ufw opens to the Docker
# job networks only; until 2026-10-03 every restore and save timed out
# there (#234, la-compagnie-maximus docs/forgejo-ops.md). What a PR run
# saves stays private to that PR: see the note on the triggers.
- name: Rust cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
- name: cargo check
run: cargo check --manifest-path src-tauri/Cargo.toml --all-targets
# Anti-rot guard for the suppressions in .cargo/audit.toml (#310). Each
# entry there is justified by its crate being absent from every shipped
# target's graph — a property of today's resolved graph, not a permanent
# one. rsa is currently reachable from nothing: its only parent in the
# lockfile is sqlx-mysql, which this SQLite project never compiles. If a
# dependency change ever pulls it into a shipped target, the suppression
# would silently hide a live advisory and the daily audit would stay
# green: the inverse of the permanent red #310 exists to kill.
#
# Such a change would itself touch src-tauri, which is exactly what
# triggers this workflow. Runs after cargo check so the registry index is
# already warm, and --locked so cargo tree cannot rewrite the lockfile the
# audit was taken against.
#
# CRATES must mirror the crates named in .cargo/audit.toml. Adding an
# entry there without adding its crate here leaves it unguarded.
#
# Every check echoes its result, including the passing ones. A guard that
# is silent on success cannot be told apart in the log from a guard that
# never ran — which is the same silent-skip failure mode this step exists
# to catch, one level up.
- name: Verify suppressed advisories are still unreachable
run: |
set -u
CRATES="rsa"
TARGETS="x86_64-unknown-linux-gnu x86_64-pc-windows-msvc"
rc=0
checks=0
for crate in $CRATES; do
for target in $TARGETS; do
# An absent crate exits 0 with empty stdout ("nothing to print"
# goes to stderr). A non-zero exit means cargo tree itself failed
# — treat that as a failure rather than as proof of absence.
out=$(cargo tree --manifest-path src-tauri/Cargo.toml --locked \
-i "$crate" --target "$target" 2>/dev/null) || {
echo "cargo tree failed for $crate / $target — cannot verify the suppression"
rc=1
continue
}
if [ -n "$out" ]; then
echo "FAIL: $crate is now compiled for $target — its .cargo/audit.toml suppression is no longer justified (see #310)."
rc=1
else
echo "ok: $crate absent from $target"
fi
checks=$((checks + 1))
done
done
# Canary: a crate known to be present on the Linux target, and only
# there. If this stops being found, the loop above is broken and its
# silence means nothing. webkit2gtk is Tauri's Linux webview (wry): a
# Tauri v2 Linux build cannot drop it, and as a Linux-only dependency
# it also proves that --target resolves target-conditional edges, which
# the per-target check above relies on. It replaced tar, a Linux-only
# dependency of the updater until #455 built it without its zip
# feature.
canary=$(cargo tree --manifest-path src-tauri/Cargo.toml --locked \
-i webkit2gtk --target x86_64-unknown-linux-gnu 2>/dev/null) || true
if [ -z "$canary" ]; then
echo "FAIL: canary 'webkit2gtk' was not found although it is a known dependency. The guard is not proving anything."
rc=1
else
echo "ok: canary 'webkit2gtk' found for x86_64-unknown-linux-gnu"
fi
echo "Suppression guard: $checks checks, exit $rc"
exit $rc
# Pin guard for windows-sys (#345). Seven crates in the lockfile accept
# windows-sys through a wide bound, so cargo may point each of them at
# any version the graph already holds. All seven point at 0.61.2, the
# state every Windows release has shipped with since v0.14.0. An update
# such as `cargo update -p hyper` re-resolves the seven edges down to
# 0.48/0.52/0.59, and cargo reports only the packages it bumped, never
# the edges: the Windows build then compiles five windows-sys versions
# instead of three, and nothing says so. Which updates do it cannot be
# told from their names (#348), so the step guards the resolved version.
# docs/adr/0023-windows-sys-pinning.md holds the measurements, the exit
# condition and what to do when this step fails.
#
# awk reads Cargo.lock: mawk is part of the ubuntu:22.04 base image,
# while python3 is only here as a transitive dependency of the apt
# packages installed above (run 517), which nothing guarantees. Its exit
# code is tested apart from its output: an empty output already fails the
# checks below (version set, missing blocks, canary), but an awk that
# fails after printing every fact would pass without that test. As in the
# step above, every check echoes its result, including the passing ones,
# and a canary must be read and checked, or the loop's silence proves
# nothing.
#
# No cargo call, so neither the warm index nor --locked matters here: the
# step reads the lockfile as cargo check left it, which is the committed
# one unless cargo had to update it.
- name: Verify windows-sys pinning
run: |
set -u
# The guarded list. Why 0.61.2, why these versions, and when either
# may change: docs/adr/0023-windows-sys-pinning.md. Edit CRATES, PIN
# or EXPECTED only on the proof that ADR asks for, never to turn this
# step green.
CRATES="colored dirs-sys errno rustix rustls-platform-verifier tempfile winapi-util"
PIN="0.61.2"
# Every windows-sys version in the lockfile, all targets included,
# as of 2026-10-10.
EXPECTED="0.45.0 0.48.0 0.52.0 0.59.0 0.60.2 0.61.2"
CANARY="tempfile"
LOCK="src-tauri/Cargo.lock"
ADR="docs/adr/0023-windows-sys-pinning.md"
rc=0
checks=0
canary_checked=0
# One fact per line: "wsys <version>" for each windows-sys package,
# and "block <crate> <version> [<target>...]" for each [[package]]
# block of a crate in CRATES, with one <target> per windows-sys
# edge: the version the edge names, or "-" for a bare edge (cargo
# drops the version when the lockfile holds a single windows-sys).
# A " (registry+...)" source suffix on an edge is dropped.
facts=$(awk -v crates="$CRATES" '
function flush( i, line) {
if (inpkg && name == "windows-sys") print "wsys", version
if (inpkg && (name in wanted)) {
line = "block " name " " version
for (i = 1; i <= n; i++) line = line " " target[i]
print line
}
inpkg = 0; name = ""; version = ""; n = 0
}
BEGIN { k = split(crates, list, " "); for (i = 1; i <= k; i++) wanted[list[i]] = 1 }
/^\[\[package\]\]$/ { flush(); inpkg = 1; next }
/^\[/ { flush(); next }
inpkg && /^name = "/ { name = $3; gsub(/"/, "", name) }
inpkg && /^version = "/ { version = $3; gsub(/"/, "", version) }
inpkg && /^[ \t]*"windows-sys[ "]/ {
s = $0; sub(/^[ \t]*"/, "", s); sub(/".*$/, "", s)
m = split(s, f, " "); target[++n] = (m >= 2 ? f[2] : "-")
}
END { flush() }
' "$LOCK") && awk_rc=0 || awk_rc=$?
if [ "$awk_rc" -ne 0 ]; then
echo "FAIL: awk exited $awk_rc reading $LOCK, so nothing was checked."
echo "windows-sys pinning: $checks checks, exit 1"
exit 1
fi
# The exact set of windows-sys versions, duplicates kept: a version
# that appears or disappears is a decision, not a side effect.
found=$(printf '%s\n' "$facts" | awk '$1 == "wsys" { print $2 }' | sort | paste -sd ' ' -)
want=$(printf '%s\n' $EXPECTED | sort | paste -sd ' ' -)
checks=$((checks + 1))
if [ "$found" = "$want" ]; then
echo "ok: windows-sys versions in the lockfile: $found"
else
echo "FAIL: windows-sys versions in the lockfile are \"$found\", expected exactly \"$want\". A version was added or removed: follow $ADR before editing EXPECTED."
rc=1
fi
# Every edge of every block of each guarded crate must name PIN. A
# bare edge passes only when the lockfile's one windows-sys is PIN.
for crate in $CRATES; do
blocks=$(printf '%s\n' "$facts" | awk -v c="$crate" '$1 == "block" && $2 == c')
if [ -z "$blocks" ]; then
checks=$((checks + 1))
echo "FAIL: no [[package]] block named $crate in $LOCK (renamed, or gone from the graph?). See $ADR."
rc=1
continue
fi
while read -r kind name version targets; do
if [ -z "$targets" ]; then
checks=$((checks + 1))
echo "FAIL: $crate $version has no windows-sys edge left. See $ADR."
rc=1
continue
fi
for target in $targets; do
checks=$((checks + 1))
if [ "$target" = "$PIN" ]; then
echo "ok: $crate $version -> windows-sys $PIN"
elif [ "$target" != "-" ]; then
echo "FAIL: $crate $version -> windows-sys $target, pinned at $PIN. See $ADR."
rc=1
elif [ "$found" = "$PIN" ]; then
echo "ok: $crate $version -> windows-sys (bare edge: the lockfile's only windows-sys is $PIN)"
else
echo "FAIL: $crate $version -> windows-sys with no version, while the lockfile holds \"$found\". See $ADR."
rc=1
fi
if [ "$crate" = "$CANARY" ]; then
canary_checked=1
fi
done
done <<<"$blocks"
done
# Canary: the loop must have read and checked an edge of a crate
# known to carry one. Otherwise its silence proves nothing.
checks=$((checks + 1))
if [ "$canary_checked" -eq 1 ]; then
echo "ok: canary '$CANARY' found, its windows-sys edge read and checked above"
else
echo "FAIL: canary '$CANARY' was not read and checked. The guard is not proving anything."
rc=1
fi
echo "windows-sys pinning: $checks checks, exit $rc"
exit $rc
- name: cargo test
run: cargo test --manifest-path src-tauri/Cargo.toml --all-targets
# Prebuilt binary. `cargo install --locked cargo-audit` recompiled the
# tool from source on every single run (~4m40s). No continue-on-error:
# a tooling failure should fail the job rather than be swallowed.
- name: Install cargo-audit
uses: https://github.com/taiki-e/install-action@v2
with:
tool: cargo-audit
# Advisories are informational — they can land on unrelated crates and
# would otherwise stall unrelated work — so the step is non-blocking.
# It no longer hides real failures behind `|| true` though. Daily
# RustSec coverage outside Rust PRs lives in audit.yml.
- name: cargo audit
continue-on-error: true
run: cargo audit --file src-tauri/Cargo.lock