Compare commits
5 Commits
560470b86b
...
v0.44.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 48ad6b6618 | |||
| 81ac4a5383 | |||
| f51ee7b234 | |||
| d593b61af8 | |||
| fc87b13e5b |
@@ -2,6 +2,19 @@
|
||||
# locally, run on every master push and pull request. Until this workflow
|
||||
# existed, nothing in CI ran the test suite at all — a direct push to
|
||||
# master was entirely unguarded.
|
||||
#
|
||||
# Build caching (2026-07-13): the Gitea actions cache never restored on
|
||||
# this instance — every run back through run 597 logged "No cache found"
|
||||
# even for exact keys saved successfully ("Cache saved successfully") by
|
||||
# a run an hour earlier, including master→master restores. Until the
|
||||
# cache server on the runner host is fixed, Swatinem/rust-cache is pure
|
||||
# overhead here. `rust-host` is a HOST executor (its filesystem persists
|
||||
# between runs — ~/.cargo and the rustup toolchain already carry over),
|
||||
# so we get warm builds by pointing CARGO_TARGET_DIR at a persistent
|
||||
# path on the runner instead of tarring gigabytes through a cache API
|
||||
# that never returns them. Concurrent runs are safe: cargo serialises
|
||||
# on the target-dir lock.
|
||||
|
||||
name: Test
|
||||
|
||||
on:
|
||||
@@ -28,8 +41,27 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Seconds-long formatting gate in its own job so a rustfmt slip fails
|
||||
# here instead of after a 35-minute cold build (run 600 spent its
|
||||
# whole build budget to report an unformatted file).
|
||||
fmt:
|
||||
runs-on: rust-host
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust 1.95.0
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.95.0
|
||||
components: rustfmt
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt --check
|
||||
|
||||
test:
|
||||
runs-on: rust-host
|
||||
needs: fmt
|
||||
|
||||
# Full debuginfo made the solitaire_engine test-binary link peak past the
|
||||
# runner's memory — ld was OOM-killed (signal 9) on runs 447 and 486.
|
||||
@@ -40,10 +72,19 @@ jobs:
|
||||
# test binaries concurrently; as the workspace grew (runs 514/516/519)
|
||||
# two+ simultaneous ld processes OOM-killed the runner again even at
|
||||
# line-tables-only. Two jobs keeps at most two links in flight — the
|
||||
# compile-throughput cost is small next to the cache-warm build.
|
||||
# compile-throughput cost is small next to the warm build.
|
||||
#
|
||||
# CARGO_INCREMENTAL=0: incremental artifacts bloat the persistent
|
||||
# target dir for little benefit in CI (rust-cache used to set this
|
||||
# for the same reason).
|
||||
#
|
||||
# CARGO_TARGET_DIR: persistent on the runner host — see the header
|
||||
# comment. The prune step below keeps it from growing unbounded.
|
||||
env:
|
||||
CARGO_PROFILE_DEV_DEBUG: line-tables-only
|
||||
CARGO_BUILD_JOBS: '2'
|
||||
CARGO_INCREMENTAL: '0'
|
||||
CARGO_TARGET_DIR: /home/runner/.cache/ferrous-solitaire/target
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -53,10 +94,21 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.95.0
|
||||
components: clippy, rustfmt
|
||||
components: clippy
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: Swatinem/rust-cache@v2
|
||||
# Toolchain or lockfile bumps strand stale artifacts nothing will
|
||||
# ever reuse; reset the dir when it crosses 40 GiB rather than
|
||||
# curating it (a cold rebuild every few weeks is cheaper than the
|
||||
# bookkeeping).
|
||||
- name: Prune persistent target dir when oversized
|
||||
run: |
|
||||
limit_kb=$((40 * 1024 * 1024))
|
||||
used_kb=$(du -sk "$CARGO_TARGET_DIR" 2>/dev/null | cut -f1 || echo 0)
|
||||
echo "persistent target dir: $((used_kb / 1024)) MiB (limit $((limit_kb / 1024)) MiB)"
|
||||
if [ "${used_kb:-0}" -gt "$limit_kb" ]; then
|
||||
echo "over limit — clearing for a fresh cold build"
|
||||
rm -rf "$CARGO_TARGET_DIR"
|
||||
fi
|
||||
|
||||
# Native link deps for the Bevy crates (engine/app/web) on a bare
|
||||
# ubuntu runner: ALSA + udev for input/audio, X11 + Wayland for winit.
|
||||
@@ -67,9 +119,6 @@ jobs:
|
||||
libasound2-dev libudev-dev pkg-config libx11-dev libxcursor-dev \
|
||||
libxrandr-dev libxi-dev libwayland-dev libxkbcommon-dev
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt --check
|
||||
|
||||
# SQLX_OFFLINE uses the checked-in `.sqlx/` query cache (no live DB),
|
||||
# same as the web-e2e workflow's server prebuild.
|
||||
- name: Clippy (deny warnings)
|
||||
|
||||
@@ -6,6 +6,36 @@ project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.44.0] — 2026-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- **Home is now a real home (menu redesign Phase B).** The mode picker became
|
||||
a hierarchy: a **Continue** card (mode · elapsed · score) appears while a
|
||||
game is in progress and returns to the table; a hero **New Game** button
|
||||
replays your last mode with the current deal options in one tap; deal
|
||||
options (draw 1/3, a new **winnable-only** toggle, difficulty tiers) moved
|
||||
into a disclosure under the hero; the six modes sit in a compact symmetric
|
||||
2×3 grid with descriptions on wide screens; and the stats strip moved to
|
||||
the bottom. Wide viewports (desktop, unfolded foldables) get a two-pane
|
||||
layout — launch surfaces left, Continue/daily/stats right. Cancel is now
|
||||
**Back to table** and only appears while a live game exists. (#175)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Save files are self-contained (schema v6).** Like replays in 0.43.3, a
|
||||
saved game now stores the dealt board via the upstream session serializers
|
||||
instead of re-dealing from the seed on load; v4/v5 saves still load through
|
||||
the legacy path and rewrite as v6 on next save. (#171)
|
||||
|
||||
### Internal
|
||||
|
||||
- **CI is ~9× faster.** Root-caused why the actions cache never restored on
|
||||
the host-executor runner (per-run workdir paths poisoned the cache version
|
||||
hash); the test workflow now uses a persistent on-runner target dir, and a
|
||||
seconds-long `fmt` gate fails formatting mistakes before the build. Warm
|
||||
full-gate runs: 4m29s, down from ~40 min. (#176)
|
||||
|
||||
## [0.43.3] — 2026-07-10
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -569,20 +569,21 @@ fn build_home_context<'a>(
|
||||
// additionally requires at least one move — a fresh untouched deal
|
||||
// is indistinguishable from "New Game" and doesn't earn a card.
|
||||
let game_live = game.is_some_and(|g| !g.0.is_won());
|
||||
let continue_info = game
|
||||
.filter(|g| game_live && g.0.move_count() > 0)
|
||||
.map(|g| {
|
||||
let g = &g.0;
|
||||
let mut caption = format!(
|
||||
"{} \u{2022} {}",
|
||||
game_mode_label(g.mode),
|
||||
format_elapsed(g.elapsed_seconds)
|
||||
);
|
||||
if g.mode != GameMode::Zen {
|
||||
caption.push_str(&format!(" \u{2022} Score {}", format_compact(g.score() as u64)));
|
||||
}
|
||||
ContinueInfo { caption }
|
||||
});
|
||||
let continue_info = game.filter(|g| game_live && g.0.move_count() > 0).map(|g| {
|
||||
let g = &g.0;
|
||||
let mut caption = format!(
|
||||
"{} \u{2022} {}",
|
||||
game_mode_label(g.mode),
|
||||
format_elapsed(g.elapsed_seconds)
|
||||
);
|
||||
if g.mode != GameMode::Zen {
|
||||
caption.push_str(&format!(
|
||||
" \u{2022} Score {}",
|
||||
format_compact(g.score() as u64)
|
||||
));
|
||||
}
|
||||
ContinueInfo { caption }
|
||||
});
|
||||
|
||||
let wide = sources
|
||||
.windows
|
||||
@@ -1023,7 +1024,12 @@ fn handle_home_digit_keys(
|
||||
}
|
||||
|
||||
if let Some(game_mode) = mode.to_game_mode() {
|
||||
persist_last_mode(game_mode, &mut settings, storage_path.as_deref(), &mut changed);
|
||||
persist_last_mode(
|
||||
game_mode,
|
||||
&mut settings,
|
||||
storage_path.as_deref(),
|
||||
&mut changed,
|
||||
);
|
||||
}
|
||||
writers.dispatch(mode);
|
||||
|
||||
@@ -1325,7 +1331,11 @@ fn spawn_new_game_hero(parent: &mut ChildSpawnerCommands, ctx: &HomeContext<'_>)
|
||||
HighContrastBorder::with_default(BORDER_STRONG),
|
||||
))
|
||||
.with_children(|c| {
|
||||
c.spawn((Text::new("New Game"), font_title.clone(), TextColor(BG_BASE)));
|
||||
c.spawn((
|
||||
Text::new("New Game"),
|
||||
font_title.clone(),
|
||||
TextColor(BG_BASE),
|
||||
));
|
||||
c.spawn((Text::new(caption), font_caption.clone(), TextColor(BG_BASE)));
|
||||
});
|
||||
}
|
||||
@@ -1381,7 +1391,11 @@ fn spawn_daily_callout(parent: &mut ChildSpawnerCommands, ctx: &HomeContext<'_>)
|
||||
font_label.clone(),
|
||||
TextColor(TEXT_SECONDARY),
|
||||
));
|
||||
c.spawn((Text::new(date_text), font_value.clone(), TextColor(date_color)));
|
||||
c.spawn((
|
||||
Text::new(date_text),
|
||||
font_value.clone(),
|
||||
TextColor(date_color),
|
||||
));
|
||||
if let Some(goal) = today.goal.as_ref() {
|
||||
c.spawn((
|
||||
Text::new(format!("Goal: {goal}")),
|
||||
|
||||
@@ -181,7 +181,13 @@ pub fn spawn_modal<M: Component, F>(
|
||||
where
|
||||
F: FnOnce(&mut ChildSpawnerCommands),
|
||||
{
|
||||
spawn_modal_sized(commands, plugin_marker, z_panel, MODAL_CARD_MAX_WIDTH, build_card)
|
||||
spawn_modal_sized(
|
||||
commands,
|
||||
plugin_marker,
|
||||
z_panel,
|
||||
MODAL_CARD_MAX_WIDTH,
|
||||
build_card,
|
||||
)
|
||||
}
|
||||
|
||||
/// Default maximum card width in logical pixels — every [`spawn_modal`]
|
||||
|
||||
Reference in New Issue
Block a user