CI: an MSRV that could not build, and a gate that floated

First remote CI run went red twice, neither from the change that triggered it.

The declared MSRV was false. Cargo.toml claimed 1.77 and all ten crates
inherited it, but the committed lockfile carries blake3 1.8.5, which ships
edition 2024 and cannot be parsed by any Cargo before 1.85. The job died in
fifteen seconds, before compiling a line. The claim had simply never been
tested -- there was no remote to test it.

Raised to 1.85, which is measured rather than picked: over the Linux-target
dependency closure minus the GUI crate, the highest declared rust-version is
constant_time_eq at 1.85.0, pulled in by blake3 itself. The comment on the
field says so, and says not to raise it again to make a build pass.

Raising it woke six clippy lints that MSRV-gating had suppressed: map_or(true,
f) is is_none_or(f) as of 1.82. Rewritten. Five of the six sites are covered --
verified by flipping each to is_some_and and confirming a named test dies. The
sixth, invariants.rs:1335, survives the flip: nothing constructs a Constant
tempo segment with end_tempo None, so that branch has no test that can see it.
The rewrite is safe by the standard library's own equivalence, but the gap is
real and is not created by this commit.

The blocking jobs no longer float on stable. CI ran 1.97.1; this machine has
1.95.0 with no rustup, so the second failure -- float_literal_f32_fallback on
a 2.0 literal in the GUI crate -- was invisible and unreproducible here. With
-D warnings and a floating toolchain, a Rust release turns the gate red with
no repo change, and a gate that fails for reasons the developer cannot
reproduce stops being a gate. All four blocking jobs now pin PINNED_STABLE;
the nightly soak keeps floating, so drift surfaces on a schedule instead of in
an unrelated push. The GUI literal is suffixed anyway, so the eventual
deliberate bump is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-22 15:05:04 -04:00
parent 5e465a1067
commit e6a118e1c6
6 changed files with 41 additions and 14 deletions

View File

@ -16,6 +16,16 @@ on:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
# Every blocking job pins an exact toolchain. With `-D warnings` and a
# floating `stable`, a Rust release turns this gate red with no repo change —
# which is exactly how it first failed, eight days after 1.97.1 added
# `float_literal_f32_fallback`. Worse, the developer machine cannot install
# arbitrary toolchains, so such a failure is unreproducible locally and the
# local gate stops meaning anything. Pinned here to the version developed
# against; bump it deliberately, as its own commit, with the lints it
# introduces fixed in that commit. The nightly soak floats on `stable` and is
# the drift detector.
PINNED_STABLE: "1.95.0"
jobs:
check:
@ -27,7 +37,7 @@ jobs:
- name: Install Rust (pinned MSRV)
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.77"
toolchain: "1.85"
components: rustfmt, clippy
- name: Cache cargo
@ -56,18 +66,20 @@ jobs:
RUSTDOCFLAGS: -D warnings
run: cargo doc --workspace --exclude epiphany-editor-gui --no-deps
# The demo GUI crate, on current stable (its eframe/resvg tree requires a newer
# toolchain than the workspace MSRV). Separate so a GUI build break is its own
# attributable signal, not a regression in the core crates.
# The demo GUI crate, on the pinned stable rather than the MSRV: its
# eframe/resvg tree pulls `image`, which declares 1.88 — three releases above
# the workspace floor. Separate so a GUI build break is its own attributable
# signal, not a regression in the core crates.
editor-gui:
name: editor GUI (current stable)
name: editor GUI (pinned stable)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (current stable)
- name: Install Rust (pinned stable)
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.PINNED_STABLE }}
components: clippy
- name: Install GUI build dependencies
@ -94,6 +106,8 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.PINNED_STABLE }}
- name: Cache cargo
uses: Swatinem/rust-cache@v2
@ -131,6 +145,8 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.PINNED_STABLE }}
- name: Cache cargo
uses: Swatinem/rust-cache@v2
@ -139,6 +155,10 @@ jobs:
run: cargo test -p epiphany-testkit --test prepass
# A longer soak, scheduled nightly, exploring a wider seed space at scale.
# This job deliberately floats on `stable` while every blocking job is pinned:
# it is where toolchain drift surfaces, on a schedule, instead of in a push
# that has nothing to do with it. A red soak against a green push means Rust
# moved, not that the change did.
soak:
name: conformance soak (nightly)
if: github.event_name == 'schedule'

View File

@ -26,7 +26,12 @@ members = [
[workspace.package]
edition = "2021"
rust-version = "1.77"
# 1.85 is the floor the dependency graph actually imposes, not a preference:
# blake3 and its `constant_time_eq` both ship edition 2024, which no Cargo
# before 1.85 can parse. The previous 1.77 claim was untestable until CI
# existed and was false when it ran — raise this only against a measured
# closure (`cargo metadata --filter-platform`), never to make a build pass.
rust-version = "1.85"
authors = ["Epiphany contributors"]
repository = "https://github.com/epiphany-notation/epiphany"

View File

@ -1332,7 +1332,7 @@ impl<'a> GraphIndex<'a> {
TempoShape::Constant => flag(
seg.end_tempo
.as_ref()
.map_or(true, |et| et == &seg.start_tempo),
.is_none_or(|et| et == &seg.start_tempo),
"constant tempo segment has end_tempo != start_tempo".to_string(),
),
TempoShape::Linear | TempoShape::Exponential | TempoShape::Curve => flag(

View File

@ -484,7 +484,9 @@ impl EditorApp {
ui.painter().rect_stroke(
shape_rect(&region.shape, &vm),
0.0,
egui::Stroke::new(2.0, egui::Color32::from_rgb(0, 120, 215)),
// Suffixed: `float_literal_f32_fallback` (Rust 1.97) rejects
// inferring `f32` here, and it becomes a hard error later.
egui::Stroke::new(2.0_f32, egui::Color32::from_rgb(0, 120, 215)),
);
}
}

View File

@ -1516,7 +1516,7 @@ fn stroke_fate(
} else {
0.0
};
if best.map_or(true, |(_, d)| distance < d) {
if best.is_none_or(|(_, d)| distance < d) {
best = Some((r, distance));
}
}
@ -1595,7 +1595,7 @@ fn curve_fate(
for (r, span) in region_spans.iter().enumerate() {
let Some((rlo, rhi)) = span else { continue };
let distance = interval_distance(lo, hi, (*rlo, *rhi));
if best.map_or(true, |(_, d)| distance < d) {
if best.is_none_or(|(_, d)| distance < d) {
best = Some((r, distance));
}
}

View File

@ -824,7 +824,7 @@ fn tempo_segment_shape_well_formed(segment: &TempoSegment) -> bool {
TempoShape::Constant => segment
.end_tempo
.as_ref()
.map_or(true, |end| end == &segment.start_tempo),
.is_none_or(|end| end == &segment.start_tempo),
TempoShape::Linear | TempoShape::Exponential | TempoShape::Curve => {
segment.end_tempo.is_some() && segment.end.is_some()
}
@ -6620,7 +6620,7 @@ impl<'a> Reducer<'a> {
(0u8, signed)
};
let key = (rank, distance, direction, *event);
if best.as_ref().map_or(true, |current| key < *current) {
if best.as_ref().is_none_or(|current| key < *current) {
best = Some(key);
}
}
@ -7425,7 +7425,7 @@ fn first_missing_vector_predecessor(
if !complete {
let candidate = OperationId::new(replica, expected);
if first_missing.map_or(true, |current| candidate < current) {
if first_missing.is_none_or(|current| candidate < current) {
first_missing = Some(candidate);
}
}