Merge fix/ci-arch-container: pinned Arch CI + clippy/test fixes
All checks were successful
dev release / build (push) Successful in 2m9s

Replaces the from-source libadwaita build (repeatedly broken by
version drift) with a pinned Arch container that gets everything
prebuilt via pacman, adds a fast-fail check.yml for feature/fix
branches, and fixes the clippy/test debt that check.yml surfaced now
that it's enforced for the first time.
This commit is contained in:
Breadway 2026-08-04 17:54:47 +08:00
commit 96a954cc15
19 changed files with 198 additions and 171 deletions

View file

@ -0,0 +1,24 @@
name: check
# Fast-fail lint/test on short-lived work branches, before it ever reaches
# main and triggers a dev-track release build.
on:
push:
branches: ['feature/**', 'fix/**']
jobs:
check:
runs-on: [self-hosted, hestia]
steps:
- name: checkout
run: |
set -euo pipefail
rm -rf src && mkdir src
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
- name: clippy
run: cd src && bash ci/build.sh cargo clippy --workspace --all-targets --locked -- -D warnings
- name: test
run: cd src && bash ci/build.sh cargo test --workspace --locked

View file

@ -15,27 +15,8 @@ jobs:
git clone --branch main --depth 1 \ git clone --branch main --depth 1 \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
- name: build inside container - name: build
run: | run: cd src && bash ci/build.sh cargo build --release --locked
docker run --rm \
-v "$(pwd)/src:/workspace" \
-w /workspace \
fedora:41 \
bash -c "
set -euo pipefail
dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel
# Build Libadwaita 1.7.0 from source inside container (skipping tests & docs)
git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita
meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false
ninja -C /tmp/libadwaita/build
ninja -C /tmp/libadwaita/build install
# Install Rust and build
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
source \$HOME/.cargo/env
cargo build --release --locked
"
- name: compute dev version - name: compute dev version
run: | run: |

View file

@ -2,11 +2,10 @@ name: beta (rc) release
on: on:
push: push:
tags: ['v*'] tags: ['v*-rc.*']
jobs: jobs:
build: build:
if: ${{ contains(github.ref_name, '-rc.') }}
runs-on: [self-hosted, hestia] runs-on: [self-hosted, hestia]
steps: steps:
- name: checkout - name: checkout
@ -16,27 +15,8 @@ jobs:
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
- name: build inside container - name: build
run: | run: cd src && bash ci/build.sh cargo build --release --locked
docker run --rm \
-v "$(pwd)/src:/workspace" \
-w /workspace \
fedora:41 \
bash -c "
set -euo pipefail
dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel
# Build Libadwaita 1.7.0 from source inside container (skipping tests & docs)
git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita
meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false
ninja -C /tmp/libadwaita/build
ninja -C /tmp/libadwaita/build install
# Install Rust and build
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
source \$HOME/.cargo/env
cargo build --release --locked
"
- name: prepare artifacts - name: prepare artifacts
run: | run: |

View file

@ -2,11 +2,11 @@ name: release
on: on:
push: push:
tags: ["v*"] tags: ['v*']
tags-ignore: ['v*-rc.*']
jobs: jobs:
build: build:
if: ${{ !contains(github.ref_name, '-rc.') }}
runs-on: [self-hosted, hestia] runs-on: [self-hosted, hestia]
steps: steps:
- name: checkout - name: checkout
@ -16,27 +16,8 @@ jobs:
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
- name: build inside container - name: build
run: | run: cd src && bash ci/build.sh cargo build --release --locked
docker run --rm \
-v "$(pwd)/src:/workspace" \
-w /workspace \
fedora:41 \
bash -c "
set -euo pipefail
dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel
# Build Libadwaita 1.7.0 from source inside container (skipping tests & docs)
git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita
meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false
ninja -C /tmp/libadwaita/build
ninja -C /tmp/libadwaita/build install
# Install Rust and build
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
source \$HOME/.cargo/env
cargo build --release --locked
"
- name: prepare artifacts - name: prepare artifacts
run: | run: |

View file

@ -69,13 +69,21 @@ cargo test --release --workspace
## CI ## CI
- `check.yml` — clippy + test, triggered on push to `feature/**`/`fix/**`.
Fast-fail before anything reaches `main`.
- `dev-release.yml` — triggered on push to `main`. - `dev-release.yml` — triggered on push to `main`.
- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push. - `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push.
- `release.yml` — triggered on any other `v*` tag push, cuts the actual - `release.yml` — triggered on any other `v*` tag push, cuts the actual
stable release. stable release.
All CI runs on a self-hosted runner; nothing runs automatically on plain All of these build inside a pinned Arch Linux container (`ci/Containerfile`,
commits or PRs beyond the track builds above. See run via `ci/build.sh`) on a self-hosted runner — not the runner host's
native environment. Arch's repos carry current `gtk4`/`libadwaita`/
`gtk4-layer-shell` as prebuilt packages, so there's no from-source library
build to go stale. The image is rebuilt (and re-cached by Docker) only when
`ci/Containerfile` changes, so a plain push doesn't refetch or recompile
the toolchain. Nothing runs automatically on plain commits or PRs beyond
the jobs listed above. See
[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md) [bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md)
for the full policy, including how a new product gets wired onto these tracks. for the full policy, including how a new product gets wired onto these tracks.

View file

@ -108,13 +108,15 @@ mod args {
// ── AppState ────────────────────────────────────────────────────────────────── // ── AppState ──────────────────────────────────────────────────────────────────
type ErrorLog = Rc<RefCell<Vec<(chrono::DateTime<Local>, String)>>>;
/// Shared UI state, cheap to clone (all fields are Rc/Arc). /// Shared UI state, cheap to clone (all fields are Rc/Arc).
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
store: Arc<Store>, store: Arc<Store>,
notes: Rc<RefCell<Vec<Note>>>, notes: Rc<RefCell<Vec<Note>>>,
cfg: Rc<RefCell<Config>>, cfg: Rc<RefCell<Config>>,
errors: Rc<RefCell<Vec<(chrono::DateTime<Local>, String)>>>, errors: ErrorLog,
active_view: Rc<RefCell<String>>, active_view: Rc<RefCell<String>>,
stack: gtk4::Stack, stack: gtk4::Stack,
window: gtk4::ApplicationWindow, window: gtk4::ApplicationWindow,
@ -579,7 +581,7 @@ fn build_note_list(
.build(); .build();
let mut sorted: Vec<Note> = notes.iter().filter(|n| !n.done).cloned().collect(); let mut sorted: Vec<Note> = notes.iter().filter(|n| !n.done).cloned().collect();
sorted.sort_by(|a, b| b.created.cmp(&a.created)); sorted.sort_by_key(|n| std::cmp::Reverse(n.created));
if sorted.is_empty() { if sorted.is_empty() {
let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone())); let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone()));

View file

@ -35,7 +35,7 @@ pub fn build(entries: &[(DateTime<chrono::Local>, String)]) -> gtk4::ScrolledWin
.build(); .build();
let time_label = gtk4::Label::builder() let time_label = gtk4::Label::builder()
.label(&ts.format("%H:%M:%S").to_string()) .label(ts.format("%H:%M:%S").to_string())
.width_chars(10) .width_chars(10)
.xalign(0.0) .xalign(0.0)
.css_classes(["dim-label"]) .css_classes(["dim-label"])

View file

@ -73,7 +73,7 @@ impl OllamaClient {
let classification: OllamaClassification = extract_json(&ollama_resp.response) let classification: OllamaClassification = extract_json(&ollama_resp.response)
.ok_or_else(|| anyhow::anyhow!( .ok_or_else(|| anyhow::anyhow!(
"no JSON object found in response — raw: {:?}", "no JSON object found in response — raw: {:?}",
&ollama_resp.response ollama_resp.response
))?; ))?;
let note_type = classification let note_type = classification

View file

@ -135,7 +135,7 @@ impl Default for RemindersConfig {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CalendarConfig { pub struct CalendarConfig {
#[serde(default = "default_calendar_enabled")] #[serde(default = "default_calendar_enabled")]
pub enabled: bool, pub enabled: bool,
@ -150,17 +150,6 @@ pub struct CalendarConfig {
pub password: String, pub password: String,
} }
impl Default for CalendarConfig {
fn default() -> Self {
CalendarConfig {
enabled: false,
url: String::new(),
username: String::new(),
password: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config { pub struct Config {
#[serde(default)] #[serde(default)]

View file

@ -324,6 +324,67 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu
} }
} }
fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType {
let lower = text.to_lowercase();
if has_rrule || has_time {
return NoteType::Reminder;
}
if lower.contains("buy ")
|| lower.contains("pick up")
|| lower.contains("clean ")
|| lower.starts_with("call ")
|| lower.starts_with("email ")
|| lower.starts_with("fix ")
|| lower.starts_with("check ")
|| lower.starts_with("finish ")
|| lower.starts_with("write ")
|| lower.starts_with("update ")
|| lower.starts_with("prepare ")
|| lower.starts_with("schedule ")
|| lower.starts_with("organize ")
|| lower.starts_with("deploy ")
|| lower.starts_with("install ")
|| lower.starts_with("send ")
|| lower.starts_with("submit ")
|| lower.starts_with("create ")
|| lower.starts_with("setup ")
|| lower.starts_with("restore ")
|| lower.starts_with("archive ")
|| lower.starts_with("export ")
|| lower.starts_with("import ")
|| lower.starts_with("approve ")
|| lower.starts_with("configure ")
|| lower.starts_with("refactor ")
|| lower.starts_with("review ")
{
return NoteType::Todo;
}
if lower.starts_with("what if ")
|| lower.starts_with("idea:")
|| lower.contains("could ")
|| lower.contains("maybe ")
|| lower.starts_with("should we ")
{
return NoteType::Idea;
}
if lower.starts_with("why ")
|| lower.starts_with("how ")
|| (lower.starts_with("what ") && !lower.starts_with("what if "))
|| lower.starts_with("when ")
|| lower.starts_with("where ")
|| lower.starts_with("who ")
|| lower.starts_with("will ")
|| lower.starts_with("is ")
|| lower.starts_with("are ")
|| lower.starts_with("did ")
|| lower.starts_with("does ")
|| lower.ends_with('?')
{
return NoteType::Question;
}
NoteType::Note
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -490,7 +551,7 @@ mod tests {
let r = p("take a break in 30 minutes"); let r = p("take a break in 30 minutes");
let t = r.time.unwrap(); let t = r.time.unwrap();
let delta = (t - before).num_seconds(); let delta = (t - before).num_seconds();
assert!(delta >= 29 * 60 && delta <= 31 * 60, "delta was {}s", delta); assert!((29 * 60..=31 * 60).contains(&delta), "delta was {}s", delta);
} }
#[test] #[test]
@ -498,7 +559,7 @@ mod tests {
let before = Utc::now(); let before = Utc::now();
let r = p("ping in 1 minute"); let r = p("ping in 1 minute");
let delta = (r.time.unwrap() - before).num_seconds(); let delta = (r.time.unwrap() - before).num_seconds();
assert!(delta >= 55 && delta <= 65, "delta was {}s", delta); assert!((55..=65).contains(&delta), "delta was {}s", delta);
} }
#[test] #[test]
@ -506,7 +567,7 @@ mod tests {
let before = Utc::now(); let before = Utc::now();
let r = p("review PR in 2 hours"); let r = p("review PR in 2 hours");
let delta_min = (r.time.unwrap() - before).num_minutes(); let delta_min = (r.time.unwrap() - before).num_minutes();
assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min); assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min);
} }
#[test] #[test]
@ -514,7 +575,7 @@ mod tests {
let before = Utc::now(); let before = Utc::now();
let r = p("follow up in 3 days"); let r = p("follow up in 3 days");
let delta_h = (r.time.unwrap() - before).num_hours(); let delta_h = (r.time.unwrap() - before).num_hours();
assert!(delta_h >= 71 && delta_h <= 73, "delta was {}h", delta_h); assert!((71..=73).contains(&delta_h), "delta was {}h", delta_h);
} }
// ---- Time extraction: tomorrow ---- // ---- Time extraction: tomorrow ----
@ -717,7 +778,7 @@ mod tests {
let before = Utc::now(); let before = Utc::now();
let r = p("check on the server in an hour"); let r = p("check on the server in an hour");
let delta_min = (r.time.unwrap() - before).num_minutes(); let delta_min = (r.time.unwrap() - before).num_minutes();
assert!(delta_min >= 59 && delta_min <= 61, "delta was {}min", delta_min); assert!((59..=61).contains(&delta_min), "delta was {}min", delta_min);
} }
#[test] #[test]
@ -737,7 +798,7 @@ mod tests {
let before = Utc::now(); let before = Utc::now();
let r = p("in a couple of hours remind me to check the oven"); let r = p("in a couple of hours remind me to check the oven");
let delta_min = (r.time.unwrap() - before).num_minutes(); let delta_min = (r.time.unwrap() - before).num_minutes();
assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min); assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min);
} }
#[test] #[test]
@ -758,7 +819,7 @@ mod tests {
let before = Utc::now(); let before = Utc::now();
let r = p("in half an hour submit the report"); let r = p("in half an hour submit the report");
let delta_min = (r.time.unwrap() - before).num_minutes(); let delta_min = (r.time.unwrap() - before).num_minutes();
assert!(delta_min >= 29 && delta_min <= 31, "delta was {}min", delta_min); assert!((29..=31).contains(&delta_min), "delta was {}min", delta_min);
} }
// ---- Tonight / this evening ---- // ---- Tonight / this evening ----
@ -847,64 +908,3 @@ mod tests {
assert!(rule.as_str().contains("BYHOUR=16"), "rule: {}", rule.as_str()); assert!(rule.as_str().contains("BYHOUR=16"), "rule: {}", rule.as_str());
} }
} }
fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType {
let lower = text.to_lowercase();
if has_rrule || has_time {
return NoteType::Reminder;
}
if lower.contains("buy ")
|| lower.contains("pick up")
|| lower.contains("clean ")
|| lower.starts_with("call ")
|| lower.starts_with("email ")
|| lower.starts_with("fix ")
|| lower.starts_with("check ")
|| lower.starts_with("finish ")
|| lower.starts_with("write ")
|| lower.starts_with("update ")
|| lower.starts_with("prepare ")
|| lower.starts_with("schedule ")
|| lower.starts_with("organize ")
|| lower.starts_with("deploy ")
|| lower.starts_with("install ")
|| lower.starts_with("send ")
|| lower.starts_with("submit ")
|| lower.starts_with("create ")
|| lower.starts_with("setup ")
|| lower.starts_with("restore ")
|| lower.starts_with("archive ")
|| lower.starts_with("export ")
|| lower.starts_with("import ")
|| lower.starts_with("approve ")
|| lower.starts_with("configure ")
|| lower.starts_with("refactor ")
|| lower.starts_with("review ")
{
return NoteType::Todo;
}
if lower.starts_with("what if ")
|| lower.starts_with("idea:")
|| lower.contains("could ")
|| lower.contains("maybe ")
|| lower.starts_with("should we ")
{
return NoteType::Idea;
}
if lower.starts_with("why ")
|| lower.starts_with("how ")
|| (lower.starts_with("what ") && !lower.starts_with("what if "))
|| lower.starts_with("when ")
|| lower.starts_with("where ")
|| lower.starts_with("who ")
|| lower.starts_with("will ")
|| lower.starts_with("is ")
|| lower.starts_with("are ")
|| lower.starts_with("did ")
|| lower.starts_with("does ")
|| lower.ends_with('?')
{
return NoteType::Question;
}
NoteType::Note
}

View file

@ -176,7 +176,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O
} else { } else {
(now.date_naive() + chrono::Duration::days(1)).and_time(fire_time) (now.date_naive() + chrono::Duration::days(1)).and_time(fire_time)
}; };
return Some(local_naive_to_utc(naive)); Some(local_naive_to_utc(naive))
} }
"WEEKLY" => { "WEEKLY" => {
use chrono::Datelike; use chrono::Datelike;
@ -204,7 +204,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O
}; };
let target_date = let target_date =
(now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time); (now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time);
return Some(local_naive_to_utc(target_date)); Some(local_naive_to_utc(target_date))
} }
_ => None, _ => None,
} }

View file

@ -118,11 +118,11 @@ impl Store {
Ok(()) Ok(())
} }
fn rewrite_notes<F>(&self, mut f: F) -> Result<()> fn rewrite_notes<F>(&self, f: F) -> Result<()>
where where
F: FnMut(Note) -> Note, F: FnMut(Note) -> Note,
{ {
let notes: Vec<Note> = self.load_all()?.into_iter().map(|n| f(n)).collect(); let notes: Vec<Note> = self.load_all()?.into_iter().map(f).collect();
self.write_all(&self.notes_path, &notes) self.write_all(&self.notes_path, &notes)
} }
@ -145,7 +145,7 @@ impl Store {
let notes = self.load_all()?; let notes = self.load_all()?;
let (to_archive, keep): (Vec<Note>, Vec<Note>) = notes let (to_archive, keep): (Vec<Note>, Vec<Note>) = notes
.into_iter() .into_iter()
.partition(|n| n.done && n.completed.map_or(false, |c| c < cutoff)); .partition(|n| n.done && n.completed.is_some_and(|c| c < cutoff));
if to_archive.is_empty() { if to_archive.is_empty() {
return Ok(0); return Ok(0);

View file

@ -278,7 +278,7 @@ mod tests {
#[test] #[test]
fn css_defines_bg_color() { fn css_defines_bg_color() {
let css = build_css(&Palette::default(), None); let css = build_css(&Palette::default(), None);
assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]); assert!(css.contains("@define-color bg #0c0c0c"), "css missing bg: {}", &css[..300]);
} }
#[test] #[test]
@ -323,9 +323,11 @@ mod tests {
#[test] #[test]
fn css_reflects_custom_palette_colors() { fn css_reflects_custom_palette_colors() {
let mut p = Palette::default(); let p = Palette {
p.background = "#deadbe".into(); background: "#deadbe".into(),
p.color4 = "#cafe00".into(); color4: "#cafe00".into(),
..Default::default()
};
let css = build_css(&p, None); let css = build_css(&p, None);
assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]); assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]);
assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]); assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]);

View file

@ -15,6 +15,9 @@ pub enum NoteType {
} }
impl NoteType { impl NoteType {
// Not std::str::FromStr — infallible, returns Self directly rather than
// Result, and used across 30+ call sites as NoteType::from_str(..).
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self { pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"todo" => NoteType::Todo, "todo" => NoteType::Todo,

View file

@ -279,15 +279,19 @@ fn resolved_ort_dylib_empty_returns_none() {
#[test] #[test]
fn resolved_ort_dylib_whitespace_only_returns_none() { fn resolved_ort_dylib_whitespace_only_returns_none() {
let mut m = ModelConfig::default(); let m = ModelConfig {
m.ort_dylib_path = " ".into(); ort_dylib_path: " ".into(),
..Default::default()
};
assert!(m.resolved_ort_dylib_path().is_none()); assert!(m.resolved_ort_dylib_path().is_none());
} }
#[test] #[test]
fn resolved_ort_dylib_set_returns_some() { fn resolved_ort_dylib_set_returns_some() {
let mut m = ModelConfig::default(); let m = ModelConfig {
m.ort_dylib_path = "/usr/lib/libonnxruntime.so".into(); ort_dylib_path: "/usr/lib/libonnxruntime.so".into(),
..Default::default()
};
assert_eq!( assert_eq!(
m.resolved_ort_dylib_path().unwrap().to_str().unwrap(), m.resolved_ort_dylib_path().unwrap().to_str().unwrap(),
"/usr/lib/libonnxruntime.so" "/usr/lib/libonnxruntime.so"

View file

@ -389,7 +389,7 @@ fn cmd_show(index: usize, corpus_path: &Path, tier: &TierArg) -> Result<()> {
println!(); println!();
let sep = "".repeat(62); let sep = "".repeat(62);
println!("{:<14} {:<26} {}", "field", "expected", "actual"); println!("{:<14} {:<26} actual", "field", "expected");
println!("{sep}"); println!("{sep}");
println!( println!(
"{:<14} {:<26} {}", "{:<14} {:<26} {}",

View file

@ -468,7 +468,7 @@ fn build_reminder_window(
let local: chrono::DateTime<chrono::Local> = t.into(); let local: chrono::DateTime<chrono::Local> = t.into();
header.append( header.append(
&gtk4::Label::builder() &gtk4::Label::builder()
.label(&local.format("%H:%M").to_string()) .label(local.format("%H:%M").to_string())
.css_classes(["reminder-time"]) .css_classes(["reminder-time"])
.build(), .build(),
); );

22
ci/Containerfile Normal file
View file

@ -0,0 +1,22 @@
# Pinned CI build environment for breadpad.
#
# Arch instead of Fedora because breadpad targets BOS/Arch only, and Arch's
# repos already carry current libadwaita/gtk4/gtk4-layer-shell as prebuilt
# packages — no from-source libadwaita build needed (that from-source build
# was the repeated CI breakage: rust dep mismatches, libadwaita ABI
# mismatches, and finally a removed meson option).
#
# Base image pinned by digest, package set frozen at build time: this image
# only changes when someone deliberately rebuilds it, not on every push.
FROM archlinux@sha256:fae033b815a16f930325c2697e620362be4d2e5d739a301b10ad1fc9c8643a06
RUN pacman -Syu --noconfirm --needed \
base-devel \
git \
pkgconf \
rust \
gtk4 \
libadwaita \
gtk4-layer-shell \
graphene \
&& pacman -Scc --noconfirm

31
ci/build.sh Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Builds (or reuses, via docker's own layer cache) the pinned Arch CI image
# from ci/Containerfile, then runs the given cargo command inside it against
# this repo checkout.
#
# Cargo's registry/git caches and CARGO_TARGET_DIR are persisted in named
# docker volumes so they survive across runs even though the repo checkout
# itself (a fresh --depth 1 clone per workflow run) does not.
#
# Usage: ci/build.sh cargo build --release --locked
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
docker build -t breadpad-ci:archlinux -f ci/Containerfile ci
docker run --rm \
-v "${ROOT}:/workspace" \
-v breadpad-cargo-registry:/root/.cargo/registry \
-v breadpad-cargo-git:/root/.cargo/git \
-v breadpad-cargo-target:/cargo-target \
-w /workspace \
-e CARGO_TARGET_DIR=/cargo-target \
breadpad-ci:archlinux \
bash -c '
set -euo pipefail
"$@"
mkdir -p /workspace/target
cp -a /cargo-target/. /workspace/target/
' bash "$@"