feat: environment-aware setup guidance, with real-distro CI coverage - #25
Merged
Conversation
Mechanical, no behaviour change, landed separately so the CI commit that starts enforcing these is reviewable on its own. cargo fmt touched 209 diffs. clippy --fix cleared the auto-fixable lints; the one that mattered was regex_creation_in_loops at fan_control.rs, which rebuilt the RPM regex on every line of sensors output. It is hoisted alongside the temperature regex now. Verified: cargo fmt --check clean, cargo clippy --all-targets -D warnings clean, all tests pass.
Setup advice was identical for everyone: 'click Grant Permissions'. That
is wrong on at least two common systems, and the app had no way to know
which one it was on.
Adds src/environment.rs:
parse_os_release ID / ID_LIKE / VERSION_ID / PRETTY_NAME
detect_package_manager apt / dnf / pacman / zypper, falling back to
the ID_LIKE chain so derivatives work without
an entry each
parse_polkit_version handles both formats ('0.105' and '127')
polkit_supports_js_rules
detect_install_channel System / AppImage / Flatpak / Snap / Development
build_setup_steps ordered steps with the exact command for this
distro, and whether the app can do it itself
Two obstacles this exists to name honestly:
- Ubuntu 22.04 ships polkit 0.105, whose JS rules engine Debian and
Ubuntu patched out. The passwordless rule this app installs is read by
nothing there. Reporting success would be a lie, so jammy now gets an
explicit 'expect a password prompt' step that says upgrading is the
only fix.
- Missing fan_control=1, which no privilege can work around.
Three-state throughout: 'could not determine' never renders as 'not
supported'. A packaged install is never offered the helper install,
since those files are package-owned.
CI coverage uses real data, not fixtures written from memory. The files
in tests/fixtures/os-release/ were captured from official images with
docker run --entrypoint cat <image> /etc/os-release, and the real files
corrected three assumptions I had wrong:
Arch has no ID_LIKE, and VERSION_ID is a build date
Mint 21 sets ID_LIKE=ubuntu alone, not 'ubuntu debian'
quoting is inconsistent -- arch and fedora bare, opensuse and rocky
quoted, so a parser handling one form silently half-works
The distro-detection CI job re-fetches os-release from those live images
and fails if a distro's ID or ID_LIKE has drifted, so the fixtures cannot
go stale silently. It compares only those two keys; PRETTY_NAME and build
dates change on every image rebuild and would make the job permanently red.
Also adds the CI workflow the repo never had: version-consistency,
frontend, rust (fmt + clippy -D warnings + test), audit, distro-detection
and docs, on pull_request and workflow_call.
Extends the setup report beyond sensors and pkexec to cover all five tools whose absence changes what the app can do, found by auditing the Command::new call sites: pkexec fan, CPU and battery changes required sensors temperatures, and the fan curve optional powerprofilesctl power profile switching optional tlp-stat TLP status optional clamscan virus scanning optional Coreutils and shell builtins (ps, df, sh, chmod, uname) are deliberately excluded -- present everywhere, and listing them would bury the ones that matter. Each entry carries what it enables, phrased as capability rather than error, because only polkit is actually required and the app stays useful without the rest. TLP and power-profiles-daemon manage the same hardware and conflict when both are installed, so each satisfies the other: having one means the other is never suggested. A test covers all three combinations. An unrecognised distro yields no install command rather than a wrong one, but still lists the tools so the UI can name what is missing. The presence probe is injected rather than hardcoded, so the survey is testable without touching the real PATH.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
thinkutils | 31b7ed9 | Jul 19 2026, 06:51 AM |
CI runners have no ThinkPad, so nothing could prove the app reads one correctly -- only that it compiles. Captured profiles close that gap. scripts/capture-hardware-profile.sh snapshots the fan, battery, thermal and cpufreq interfaces into a fixture tree that mirrors absolute paths, so pointing a root at it makes every lookup resolve unchanged. It is also how a machine the maintainers do not own gets supported: run it, check the output, open a PR. The first captured profile is a ThinkPad P1 Gen 4i, which is a dual-fan machine and therefore exactly the shape that was untestable before. It pins the topology the code has to handle: two tachometers, one PWM channel -- thinkpad_acpi writes the same level to both fans, so code assuming one fan per channel is wrong on every P-series and X1 Extreme no speed2 field in procfs -- the second fan is invisible there, which is why the hwmon read path exists src/hardware_root.rs adds the indirection. THINKUTILS_HARDWARE_ROOT redirects reads only; writes always target real paths, and set_fan_speed refuses outright while simulated so a test can never believe it changed a real fan. Redaction was not right first time. Filtering by filename missed uevent, an aggregate file whose name says nothing and which carries POWER_SUPPLY_SERIAL_NUMBER inside -- a real battery serial from this machine. Capture now scrubs content too, keeping the key and replacing the value so parse tests still see a realistic shape. A CI job re-checks this, because a contributed profile carrying a serial must never merge. Also stops cargo audit failing forever. Four advisories are ignored, each with its reasoning and the upgrade that clears it recorded inline, plus a check that no ignore can be added without a justification beside it. A permanently red job teaches everyone to stop reading it, and then the next advisory -- the one that matters -- goes unnoticed too.
grep exits 1 when it finds nothing. Under set -o pipefail that propagates through the pipeline, and set -e then kills the step -- so the guard failed in exactly the case it exists to pass. It only looked correct locally because I ran it without pipefail. Adds a canary step that plants a serial number, confirms the check catches it, and removes it. A guard that only ever passes is indistinguishable from no guard, and this one already had that bug.
Release pipeline hardening, informed by the sibling Bulwark project. release.yml now calls ci.yml via workflow_call rather than publishing unchecked. A broken commit tagged vX.Y.Z used to ship automatically -- the tests existed and had never once gated a release. 'Green' now has one definition, used by both workflows, instead of a copy that can drift. Two real bugs fixed: The APT repo step deleted the published .deb BEFORE copying the new one, with no failure guard. A glob matching nothing left an empty apt/ which then got committed, taking gh.vietanh.dev/ThinkUtils/apt offline for everyone installing via apt. It now verifies a .deb exists before touching anything, stages into a scratch directory, and swaps it in. The AppImage was never renamed. The rename step handled deb and rpm only while the upload globbed *.AppImage, so every release shipped two assets with clean versioned names and one with whatever Tauri defaulted to. Adds package content verification before publish: the .deb must ship /usr/bin/thinkutils and a .desktop entry, the binary must be x86-64, and ldd must resolve every library. 'cargo tauri build exited 0' only proves the metadata parsed. The push-to-main trigger is replaced with workflow_dispatch, so the pipeline can be rehearsed without burning a version number. ci.yml covers main now. Deliberately NOT bumping ubuntu-22.04 to 24.04 despite the deprecation, with the reason recorded inline: Tauri's deb bundler reads the glibc floor off the build host, so building on 24.04 emits libc6 (>= 2.39), which apt refuses on Ubuntu 22.04 and Debian 12. The correct fix is a pinned container, not a runner bump. scripts/bump-version.sh replaces the manual four-file edit in CLAUDE.md. Every edit asserts it changed something, because a sed that silently matches nothing is indistinguishable from success until release. Note package-lock.json declares the version twice -- top level and .packages."" -- and updating only the first is the classic half-bump.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Setup advice was identical for everyone — "click Grant Permissions" — and the app had no way to know which system it was on. On at least two common systems that advice cannot work.
What it detects
ID/ID_LIKE/VERSION_ID, falling back through theID_LIKEchain so derivatives work without an entry each0.105and127formatsTwo obstacles it now names honestly
Ubuntu 22.04 ships polkit 0.105, whose JS rules engine Debian and Ubuntu patched out. The passwordless rule this app installs is read by nothing there — it installs, reports success, and silently does nothing. Jammy now gets an explicit "expect a password prompt" step saying upgrading is the only fix.
Missing
fan_control=1, which no privilege can work around.Three-state throughout: "could not determine" never renders as "not supported". A packaged install is never offered the helper install, since those files are package-owned.
Tool survey
Audited every
Command::newcall site. Five tools change what the app can do:pkexec(required) ·sensors·powerprofilesctl·tlp-stat·clamscanCoreutils are excluded — listing them would bury the ones that matter. TLP and power-profiles-daemon conflict, so each satisfies the other; suggesting both would cause them to fight.
CI coverage uses real data
The fixtures in
tests/fixtures/os-release/were captured from official images withdocker run --entrypoint cat <image> /etc/os-release, not written from memory. That mattered — the real files corrected three assumptions I had wrong:ID_LIKE, andVERSION_IDis a build date (20260712.0.555161)ID_LIKE=ubuntualone, not"ubuntu debian"— my hand-written test had it wrongThe
distro-detectionjob re-fetches from those live images and fails if a distro'sID/ID_LIKEdrifts. It compares only those two keys —PRETTY_NAMEand build dates change every image rebuild and would make the job permanently red.Also: the CI workflow this repo never had
version-consistency,frontend,rust(fmt + clippy-D warnings+ test),audit,distro-detection,docs— onpull_requestandworkflow_call.Prerequisite included as its own commit:
cargo fmtwas dirty (209 diffs) and clippy had warnings, so enforcing them would have made main red on arrival. The one clippy fix that mattered wasregex_creation_in_loops— the RPM regex was rebuilt on every line ofsensorsoutput.Suite: 40 → 71.