All checks were successful
PR Check — Rust / rust (pull_request) Successful in 4m14s
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>
350 lines
18 KiB
YAML
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
|