Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ body:
attributes:
label: WinMedic version
description: Output of `winmedic.exe --version`
placeholder: "WinMedic 0.3.4"
placeholder: "WinMedic 0.4.1"
validations:
required: true

Expand Down
25 changes: 16 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,20 +253,27 @@ jobs:
# inferred from the commands that were supposed to make it land.
#
# `main` is protected (one approving review, four strict status checks),
# and GITHUB_TOKEN is not allowed through branch protection. There are two
# ways to give this step a path that works, and it fails until one of them
# exists:
# and GITHUB_TOKEN is not allowed through branch protection. Three things
# make this step pass, and it fails until one of them is true:
#
# * run `./scripts/prepare-release.ps1 <version>` and merge what it
# opens *before* releasing. The bump is then already on the branch,
# "Write the version into the tree" finds nothing to write, this step
# confirms what is already true, and no credential anywhere is allowed
# past branch protection. This is the supported path — see
# CONTRIBUTING.md, "Cutting a release"; or
# * set a RELEASE_TOKEN secret — a PAT with `contents: write` owned by
# an account with admin rights, which "Do not allow bypassing" leaves
# unblocked — and the bump lands on the branch directly; or
# unblocked — and the bump lands on the branch directly. Convenient,
# and a long-lived token that can write to a protected branch is
# exactly the credential worth not having; or
# * turn on Settings → Actions → General → Workflow permissions →
# "Allow GitHub Actions to create and approve pull requests", and it
# arrives as a pull request for review instead.
#
# Only the first of those makes this step green. The branch is the sole
# measure: an open pull request is a path to the branch being correct, not
# a substitute for it, and a run that has not updated the branch is red
# Only the first two make this step green. The branch is the sole measure:
# an open pull request is a path to the branch being correct, not a
# substitute for it, and a run that has not updated the branch is red
# whatever the reason. That is deliberate — v0.3.4 went unnoticed for two
# releases precisely because a run that left the branch stale was allowed
# to pass.
Expand Down Expand Up @@ -370,7 +377,7 @@ jobs:
$summary += ""
$summary += "Until that is merged, a build from ``$env:BRANCH`` reports the previous version through CARGO_PKG_VERSION — the header, ``--version``, the HTML report and the update check all read it, so the tool will offer its own release as an available update."
$summary += ""
$summary += "This run stays red once it is merged, as the record that the branch was not updated automatically; re-running it will not help, because ``Resolve version`` refuses a tag that already exists. **To make this step pass by itself in future,** set a ``RELEASE_TOKEN`` secret so the bump lands on the branch directly."
$summary += "This run stays red once it is merged, as the record that the branch was not updated automatically; re-running it will not help, because ``Resolve version`` refuses a tag that already exists. **To avoid this next time,** run ``./scripts/prepare-release.ps1 <version>`` and merge its pull request *before* starting the release, so the bump is already on the branch when this step checks."
} else {
Write-Host "::error::$env:BRANCH was not brought up to $env:TAG and no pull request carries the bump."
$summary += "### The branch was not brought up to ``$env:TAG``"
Expand All @@ -383,7 +390,7 @@ jobs:
$summary += ""
$summary += "**To recover this release:** open a pull request from ``$head`` (it was pushed and still holds the bump), or run ``./scripts/set-version.ps1 $($env:TAG.TrimStart('v'))`` on a branch and merge that."
$summary += ""
$summary += "**To stop it happening again:** set a ``RELEASE_TOKEN`` secret, or turn on Settings -> Actions -> General -> Workflow permissions -> 'Allow GitHub Actions to create and approve pull requests'."
$summary += "**To stop it happening again:** run ``./scripts/prepare-release.ps1 <version>`` and merge its pull request before starting the release, so the bump reaches the branch the same reviewed way every other change does. The alternatives are a ``RELEASE_TOKEN`` secret, or Settings -> Actions -> General -> Workflow permissions -> 'Allow GitHub Actions to create and approve pull requests'."
}

if ($env:GITHUB_STEP_SUMMARY) {
Expand Down
46 changes: 34 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,24 +90,46 @@ string. Use `utils::cmd::ps_single_quoted` — see the module documentation ther

## Cutting a release

Releases are cut from the **Run workflow** button on the
[Release workflow](../../actions/workflows/release.yml) — type the version
(`0.3.3`) and nothing else is needed. The workflow writes that version into
every file that states one, commits it, tags the commit, builds from the tag,
refuses to publish if the binary does not introduce itself as that version, and
then brings `main` up to date.

Do not edit `Cargo.toml` by hand to bump the version. `Cargo.lock`, the README
checksum example and the issue-template placeholder all repeat it, and the one
place the number actually matters — `env!("CARGO_PKG_VERSION")`, which feeds the
header, the help popup, `--version`, the HTML report and the update check — is
the one nobody remembers to check. Use the script the workflow uses:
A release is two steps: land the version bump on `main`, then run the workflow.

```powershell
./scripts/prepare-release.ps1 0.3.3 # opens the "chore(release): v0.3.3" pull request
# merge it, wait for CI on main, then:
gh workflow run release.yml --ref main -f version=0.3.3
```

The bump goes through a pull request rather than being pushed to `main` by the
workflow because `main` is protected and `GITHUB_TOKEN` is not allowed through
its four required checks. The workflow does try — a direct push, then a pull
request as a fallback — and the fallback needs a repository setting that is
deliberately off, so the attempt fails and the run's last step goes red. Giving
CI a token that bypasses branch protection would fix the symptom by removing the
protection; sending the bump down the same reviewed, CI-gated road as every
other change costs one merge and removes nothing. v0.4.0 is the release that
shipped correctly and still went red this way.

Preparing first also makes the workflow's own bump step a no-op: it finds every
version site already correct, tags `HEAD` unchanged, and its "did the bump reach
the branch" check passes. Everything else it does is unchanged — it builds from
the tag it just made and refuses to publish a binary that does not introduce
itself as that version.

Do not edit `Cargo.toml` by hand to bump the version. `Cargo.lock` and the
issue-template placeholder repeat it, and the one place the number actually
matters — `env!("CARGO_PKG_VERSION")`, which feeds the header, the help popup,
`--version`, the HTML report and the update check — is the one nobody remembers
to check. `prepare-release.ps1` calls the script that owns all of them, and it
can be run on its own:

```powershell
./scripts/set-version.ps1 0.3.3 # rewrite every version site
./scripts/set-version.ps1 0.3.3 -Check # report what disagrees, change nothing
```

The README's checksum example is deliberately not on that list: it globs
`winmedic-v*.exe` out of the download directory instead of naming a version, so
it never goes stale.

Pushing a `v*` tag by hand still builds and publishes, but a tag is immutable,
so that path can only run the `-Check` pass: if the tagged tree states a
different version than the tag, the release fails rather than shipping a
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "winmedic"
version = "0.4.0"
version = "0.4.1"
edition = "2024"
# Verified against the locked dependency tree: edition 2024 itself needs 1.85,
# but egui/eframe raises the floor to 1.95. CI enforces this exact version in
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,9 @@ WinMedic is **not code-signed**, so Windows SmartScreen will warn you on first l

```powershell
# Compare the published checksum against the file you downloaded
$expected = (Get-Content .\winmedic-v0.4.0.exe.sha256).Split(' ')[0]
$actual = (Get-FileHash .\winmedic-v0.4.0.exe -Algorithm SHA256).Hash.ToLower()
$exe = Get-Item .\winmedic-v*.exe | Select-Object -First 1
$expected = (Get-Content "$($exe.FullName).sha256").Split(' ')[0]
$actual = (Get-FileHash $exe.FullName -Algorithm SHA256).Hash.ToLower()
if ($expected -eq $actual) { "OK - checksum matches" } else { "MISMATCH - do not run this file" }
```

Expand Down
53 changes: 53 additions & 0 deletions docs/release-notes/v0.4.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# WinMedic v0.4.1

The desktop window that arrived in v0.4.0 gets the interface it was missing, and three findings that survived their own repair stop being raised.

Nothing here changes what WinMedic does to a system beyond the three checks below, which now measure what their repairs can actually reach.

---

## Fixed

### Three findings that no repair could clear

Scan, repair, reboot, scan again — and there they were, unchanged. Each of the three measured something the repair it offered could not touch. Diagnosed against a real machine's `last_scan.json`, audit log and registry rather than from first principles.

**`wu_reboot_pending` — a key the restart leaves behind.** The check tested whether `Component Based Servicing\RebootPending` exists. Windows creates it while servicing and files the outstanding work underneath it; the restart consumes that work and empties the key, but leaves the key itself in place, permanently. On the machine this was found on it had been sitting there empty — 0 subkeys, 0 values — while `Auto Update\RebootRequired` was absent: a restart that no restart could clear. The CBS key now counts only while it still holds entries, and the two signals a boot really does clear are read alongside it, `Auto Update\RebootRequired` and `PendingFileRenameOperations`.

**`sys_clean_winsxs` — a repair that could not move the number.** The finding was raised on DISM's own "cleanup recommended" verdict, which also weighs backups and disabled features — reclaimable only by `/ResetBase`, which WinMedic deliberately never runs. `StartComponentCleanup`, the one repair offered here, removes superseded packages and nothing else. With 4.43 GB of backups and zero reclaimable packages the flag stayed `Yes` however often the repair ran, DISM answered "completed successfully" every time, and the audit log recorded a cleaned store. The finding now needs a reclaimable package to exist, the repair reads the store back afterwards and reports what is left, and both DISM calls pass `/English` — the output arrives in the console code page, so `from_utf8_lossy` had already cost the parser the store-size and cache lines on a German machine.

**`sys_clean_setup_logs` — the scan finding its own log.** "13.2 MB, 2 files" were `CBS.log`, held open by TrustedInstaller since it was created a month earlier, and `dism.log`, written by this module's own `AnalyzeComponentStore` call 34 seconds before the same scan measured it. Both the measurement and the sweep now ask Windows for `DELETE` access first — the same question `remove_file` asks later, and with a fully permissive share mode the probe blocks nobody. What is counted is what can be removed; a rotated `CbsPersist_*.log` still is.

### The issue list could not be ticked

Nothing in the findings list responded to a click. The row was an `egui::Frame` whose response was given a click sense afterwards, and a `Frame` registers its response *after* everything drawn inside it — so the row's click target sat on top of its own checkbox and swallowed every click meant for it. The boxes were drawn, they just could not be reached.

The row is now a `Ui` built with `UiBuilder::sense`, which registers itself before its contents and leaves the checkbox on top. Ticking a box also moves the detail pane to that finding, the way `space` already did.

### The test suite no longer overwrites the last scan

`save_scan_state` wrote to `%APPDATA%\WinMedic\last_scan.json` from any `App`, including the dozens the suite builds — so ticking a checkbox in a test overwrote the scan the developer's own WinMedic had left behind. Persistence now sits behind the same `SystemActions` seam that already guards the browser, the UAC prompt and restore points: off by default, switched on by the desktop front end through `enable_real_system_actions`.

---

## Changed

### A desktop interface instead of a terminal layout in a window

v0.4.0 replaced the TUI with a native window but kept the dense terminal layout inside it. That layout is gone: a fixed sidebar, clearer page headings, larger spacing, muted dark surfaces and teal accents, Segoe UI where it is available with bundled fonts as a fallback, and navigation icons drawn as scalable geometry.

The dashboard groups a circular health indicator, live resources and diagnostic module cards, and before the first scan it shows an explicit unscanned state rather than a health score of 100. Severity links open the corresponding findings and clear stale search and module filters. The same cards, buttons and typography carry across scanning, triage, repairs and settings; confirmation dialogs use a dimmed modal backdrop that blocks clicks on the navigation behind it; scan and repair buttons respect the busy state, and long status messages truncate with the full text on hover.

### Severity marks are drawn, not written

An octagon for critical, a rounded triangle for warning, a disc for info. egui's bundled fonts have no warning sign in them, so a written mark would have reached some machines as an empty box — the same reason the navigation icons are painted. They replace the `[!] CRITICAL` text badges in the list, the detail pane, the filter chips and the dashboard cards, and each carries its name for a screen reader. `Severity::badge` keeps the written form for the report file, which is now its only caller.

---

## Verification

`cargo fmt`, `cargo clippy --all-targets -- -D warnings` and the full suite pass: 361 unit and 266 integration tests, 8 of them new for the three findings above — an empty CBS key, a CBS key still holding work, a recommendation with nothing reclaimable, a cleanup that reclaimed nothing, and a log held open with `FILE_SHARE_NONE` that is neither counted nor reported as a failed deletion. The GUI tests apply the actual theme and cover all five destinations at 960×640, 1400×900 and 1920×1200.

---

**Full changelog**: https://github.com/SecretLUL/WinMedic/compare/v0.4.0...v0.4.1
Loading