Compare commits

...

5 Commits

Author SHA1 Message Date
funman300 48ad6b6618 docs(changelog): cut 0.44.0 — Phase B home hierarchy
Android Release / build-apk (push) Successful in 7m48s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:16:27 -07:00
funman300 81ac4a5383 Merge pull request 'ci(test): host-persistent target dir; fast fmt gate; drop dead rust-cache' (#176) from ci/host-persistent-target into master
Test / fmt (push) Successful in 4s
Test / test (push) Successful in 4m27s
2026-07-13 22:14:39 +00:00
funman300 f51ee7b234 Merge pull request 'feat(engine): Phase B home hierarchy — Continue card, hero New Game, deal-options disclosure' (#175) from feat/home-hierarchy into master
Build and Deploy / build-and-push (push) Successful in 9m11s
Web E2E / web-e2e (push) Successful in 8m30s
Test / test (push) Successful in 37m46s
2026-07-13 20:55:37 +00:00
funman300 d593b61af8 ci(test): host-persistent target dir; fast fmt gate; drop dead rust-cache
Test / fmt (pull_request) Successful in 5s
Test / test (pull_request) Successful in 33m27s
The Gitea actions cache on this instance stores caches (1.85 GB saves
confirmed in run 597) but never restores them — every run back through
run 584+ logs 'No cache found' even for exact keys saved an hour
earlier, including master-to-master. Every CI run has therefore been a
full cold build (~37 min), plus ~4 min tarring a cache nobody reads.

- Point CARGO_TARGET_DIR at a persistent path on the rust-host runner
  (host executor — filesystem carries over between runs), with a 40 GiB
  prune guard. Warm runs drop to minutes without touching the broken
  cache API.
- Drop Swatinem/rust-cache (pure overhead until the server is fixed).
- Split cargo fmt --check into a seconds-long fmt job gating the heavy
  test job, so a formatting slip can't burn a 35-minute build again
  (run 600).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:53:34 -07:00
funman300 fc87b13e5b style: cargo fmt
Test / test (pull_request) Successful in 37m46s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:11:54 -07:00
4 changed files with 124 additions and 25 deletions
+56 -7
View File
@@ -2,6 +2,19 @@
# locally, run on every master push and pull request. Until this workflow # 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 # existed, nothing in CI ran the test suite at all — a direct push to
# master was entirely unguarded. # 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 name: Test
on: on:
@@ -28,8 +41,27 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: 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: test:
runs-on: rust-host runs-on: rust-host
needs: fmt
# Full debuginfo made the solitaire_engine test-binary link peak past the # 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. # 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) # test binaries concurrently; as the workspace grew (runs 514/516/519)
# two+ simultaneous ld processes OOM-killed the runner again even at # two+ simultaneous ld processes OOM-killed the runner again even at
# line-tables-only. Two jobs keeps at most two links in flight — the # 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: env:
CARGO_PROFILE_DEV_DEBUG: line-tables-only CARGO_PROFILE_DEV_DEBUG: line-tables-only
CARGO_BUILD_JOBS: '2' CARGO_BUILD_JOBS: '2'
CARGO_INCREMENTAL: '0'
CARGO_TARGET_DIR: /home/runner/.cache/ferrous-solitaire/target
steps: steps:
- name: Checkout - name: Checkout
@@ -53,10 +94,21 @@ jobs:
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.95.0 toolchain: 1.95.0
components: clippy, rustfmt components: clippy
- name: Cache cargo build # Toolchain or lockfile bumps strand stale artifacts nothing will
uses: Swatinem/rust-cache@v2 # 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 # Native link deps for the Bevy crates (engine/app/web) on a bare
# ubuntu runner: ALSA + udev for input/audio, X11 + Wayland for winit. # 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 \ libasound2-dev libudev-dev pkg-config libx11-dev libxcursor-dev \
libxrandr-dev libxi-dev libwayland-dev libxkbcommon-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), # SQLX_OFFLINE uses the checked-in `.sqlx/` query cache (no live DB),
# same as the web-e2e workflow's server prebuild. # same as the web-e2e workflow's server prebuild.
- name: Clippy (deny warnings) - name: Clippy (deny warnings)
+30
View File
@@ -6,6 +6,36 @@ project follows [Semantic Versioning](https://semver.org/).
## [Unreleased] ## [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 ## [0.43.3] — 2026-07-10
### Fixed ### Fixed
+31 -17
View File
@@ -569,20 +569,21 @@ fn build_home_context<'a>(
// additionally requires at least one move — a fresh untouched deal // additionally requires at least one move — a fresh untouched deal
// is indistinguishable from "New Game" and doesn't earn a card. // is indistinguishable from "New Game" and doesn't earn a card.
let game_live = game.is_some_and(|g| !g.0.is_won()); let game_live = game.is_some_and(|g| !g.0.is_won());
let continue_info = game let continue_info = game.filter(|g| game_live && g.0.move_count() > 0).map(|g| {
.filter(|g| game_live && g.0.move_count() > 0) let g = &g.0;
.map(|g| { let mut caption = format!(
let g = &g.0; "{} \u{2022} {}",
let mut caption = format!( game_mode_label(g.mode),
"{} \u{2022} {}", format_elapsed(g.elapsed_seconds)
game_mode_label(g.mode), );
format_elapsed(g.elapsed_seconds) if g.mode != GameMode::Zen {
); caption.push_str(&format!(
if g.mode != GameMode::Zen { " \u{2022} Score {}",
caption.push_str(&format!(" \u{2022} Score {}", format_compact(g.score() as u64))); format_compact(g.score() as u64)
} ));
ContinueInfo { caption } }
}); ContinueInfo { caption }
});
let wide = sources let wide = sources
.windows .windows
@@ -1023,7 +1024,12 @@ fn handle_home_digit_keys(
} }
if let Some(game_mode) = mode.to_game_mode() { 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); writers.dispatch(mode);
@@ -1325,7 +1331,11 @@ fn spawn_new_game_hero(parent: &mut ChildSpawnerCommands, ctx: &HomeContext<'_>)
HighContrastBorder::with_default(BORDER_STRONG), HighContrastBorder::with_default(BORDER_STRONG),
)) ))
.with_children(|c| { .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))); 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(), font_label.clone(),
TextColor(TEXT_SECONDARY), 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() { if let Some(goal) = today.goal.as_ref() {
c.spawn(( c.spawn((
Text::new(format!("Goal: {goal}")), Text::new(format!("Goal: {goal}")),
+7 -1
View File
@@ -181,7 +181,13 @@ pub fn spawn_modal<M: Component, F>(
where where
F: FnOnce(&mut ChildSpawnerCommands), 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`] /// Default maximum card width in logical pixels — every [`spawn_modal`]