Skip to content

feat: environment-aware setup guidance, with real-distro CI coverage - #25

Merged
vietanhdev merged 6 commits into
mainfrom
feat/environment-aware-setup
Jul 19, 2026
Merged

feat: environment-aware setup guidance, with real-distro CI coverage#25
vietanhdev merged 6 commits into
mainfrom
feat/environment-aware-setup

Conversation

@vietanhdev

Copy link
Copy Markdown
Owner

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

Distro ID / ID_LIKE / VERSION_ID, falling back through the ID_LIKE chain so derivatives work without an entry each
Package manager apt / dnf / pacman / zypper → exact install commands
polkit version Handles both 0.105 and 127 formats
Install channel System / AppImage / Flatpak / Snap / Development
Tools All five the app shells out to

Two 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::new call site. Five tools change what the app can do:

pkexec (required) · sensors · powerprofilesctl · tlp-stat · clamscan

Coreutils 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 with docker run --entrypoint cat <image> /etc/os-release, not written from memory. That mattered — the real files corrected three assumptions I had wrong:

  • Arch has no ID_LIKE, and VERSION_ID is a build date (20260712.0.555161)
  • Mint 21 sets ID_LIKE=ubuntu alone, not "ubuntu debian" — my hand-written test had it wrong
  • Quoting is inconsistent — arch/fedora bare, opensuse/rocky quoted, so a parser handling one form silently half-works

The distro-detection job re-fetches from those live images and fails if a distro's ID/ID_LIKE drifts. It compares only those two keysPRETTY_NAME and 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 — on pull_request and workflow_call.

Prerequisite included as its own commit: cargo fmt was dirty (209 diffs) and clippy had warnings, so enforcing them would have made main red on arrival. The one clippy fix that mattered was regex_creation_in_loops — the RPM regex was rebuilt on every line of sensors output.

Suite: 40 → 71.

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 19, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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.
@vietanhdev
vietanhdev merged commit 2527c0b into main Jul 19, 2026
7 of 8 checks passed
@vietanhdev
vietanhdev deleted the feat/environment-aware-setup branch July 19, 2026 06:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant