From b9092e57dd6a8ba43f84baaf6a80eb2730be479c Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:01:51 -0500 Subject: [PATCH 1/7] build: move runtime to .NET 10 LTS and bump packages to 10.0.11 .NET 9 enters EOL on 2026-11-10. Because FieldKit publishes SelfContained + PublishSingleFile, the runtime is baked into the exe, so every download after that date would ship an unpatched runtime with no servicing path. .NET 10 is LTS through 2028-11-14. - TargetFramework net9.0-windows -> net10.0-windows - System.Management and System.ServiceProcess.ServiceController 9.0.0 -> 10.0.11 (supersedes stale Dependabot PRs #9 and #10, which proposed 10.0.9) - Version 1.0.0 -> 1.1.0, app.manifest assemblyIdentity to match - Pin the SDK in global.json so the build is reproducible rather than floating to whatever SDK is newest on the machine - Update README build instructions and ROADMAP UI note Co-Authored-By: Claude Opus 5 --- FieldKit/FieldKit.csproj | 8 ++++---- FieldKit/app.manifest | 2 +- README.md | 6 +++--- ROADMAP.md | 2 +- global.json | 6 ++++++ 5 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 global.json diff --git a/FieldKit/FieldKit.csproj b/FieldKit/FieldKit.csproj index d342d5e..bf9fc57 100644 --- a/FieldKit/FieldKit.csproj +++ b/FieldKit/FieldKit.csproj @@ -2,7 +2,7 @@ WinExe - net9.0-windows + net10.0-windows enable true enable @@ -10,7 +10,7 @@ fieldkit.ico FieldKit FieldKit - 1.0.0 + 1.1.0 Keith S. Crawford FieldKit Portable Windows maintenance in a single exe. No installer, no bloat, no telemetry. @@ -23,8 +23,8 @@ - - + + diff --git a/FieldKit/app.manifest b/FieldKit/app.manifest index 1e04c29..d311e66 100644 --- a/FieldKit/app.manifest +++ b/FieldKit/app.manifest @@ -1,6 +1,6 @@ - + diff --git a/README.md b/README.md index 092833d..a58b049 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ FieldKit reports these cases clearly in the status column and log instead of fai ## Building From Source -Requires the [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0). +Requires the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). The exact SDK version is pinned in `global.json`. ```bash dotnet build FieldKit.sln @@ -119,10 +119,10 @@ dotnet build FieldKit.sln To publish a portable single-file exe: ```bash -dotnet publish FieldKit.sln -c Release +dotnet publish FieldKit/FieldKit.csproj -c Release -o publish ``` -Output: `FieldKit/bin/Release/net9.0-windows/win-x64/publish/FieldKit.exe` +Output: `publish/FieldKit.exe` ## Privacy diff --git a/ROADMAP.md b/ROADMAP.md index 93e978c..09b25c6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -99,7 +99,7 @@ That means the user experience should answer: ## UI Direction -v1.0 migrated from WinForms to WPF with .NET 9 Fluent dark theme. +v1.0 migrated from WinForms to WPF with a Fluent dark theme. v1.1 moved the runtime to .NET 10 (LTS). Current state: diff --git a/global.json b/global.json new file mode 100644 index 0000000..01cb658 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.400", + "rollForward": "latestFeature" + } +} From c1de7f998bb97f2a20b10692eab6c41847025221 Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:02:20 -0500 Subject: [PATCH 2/7] fix: remove duplicate Logger and handle unhandled UI exceptions MainWindow's parameterless constructor created a Logger, and the real constructor chained through it via `: this()` before overwriting the field. The orphaned Logger opened a StreamWriter that nothing ever disposed. The symptom was on disk: five zero-byte FieldKit-*.log files in the build output directory, one per app launch. App already held the %TEMP% log path with FileShare.Read, so the orphan's open failed, the Logger constructor's fallback swallowed it, and it landed in AppContext.BaseDirectory instead. App.xaml uses Startup= with no StartupUri, so MainWindow is only ever constructed through the two-arg overload. The parameterless constructor is deleted rather than patched. Also adds a DispatcherUnhandledException handler. Tasks already catch their own failures, so anything reaching it is a UI-layer bug; the app now logs it and keeps the log path visible instead of vanishing. Co-Authored-By: Claude Opus 5 --- FieldKit/App.xaml.cs | 22 ++++++++++++++++++++++ FieldKit/MainWindow.xaml.cs | 8 +------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/FieldKit/App.xaml.cs b/FieldKit/App.xaml.cs index 6857d30..546ba1f 100644 --- a/FieldKit/App.xaml.cs +++ b/FieldKit/App.xaml.cs @@ -1,4 +1,5 @@ using System.Windows; +using System.Windows.Threading; using FieldKit.Services; using FieldKit.Tasks; @@ -10,6 +11,8 @@ public partial class App : Application private void App_Startup(object sender, StartupEventArgs e) { + DispatcherUnhandledException += OnDispatcherUnhandledException; + try { if (!DisclaimerWindow.HasAccepted()) @@ -53,6 +56,25 @@ private void App_Startup(object sender, StartupEventArgs e) } } + /// + /// Last-chance handler for exceptions raised on the UI thread after startup. + /// Individual maintenance tasks already catch their own failures, so anything + /// reaching here is a UI-layer bug. The app stays alive so the user keeps + /// access to the log path rather than losing it to a silent crash. + /// + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + _logger?.Log($"Unhandled UI exception: {e.Exception}", "ERROR"); + + var logNote = _logger is null ? "" : $"\n\nLog: {_logger.LogFilePath}"; + MessageBox.Show( + $"FieldKit hit an unexpected error:\n\n{e.Exception.Message}{logNote}", + "FieldKit — Unexpected Error", + MessageBoxButton.OK, MessageBoxImage.Error); + + e.Handled = true; + } + protected override void OnExit(ExitEventArgs e) { _logger?.Dispose(); diff --git a/FieldKit/MainWindow.xaml.cs b/FieldKit/MainWindow.xaml.cs index 311fb19..792da8a 100644 --- a/FieldKit/MainWindow.xaml.cs +++ b/FieldKit/MainWindow.xaml.cs @@ -15,15 +15,9 @@ public partial class MainWindow : Window private readonly ObservableCollection _taskViewModels = new(); private CancellationTokenSource? _runCts; - public MainWindow() + public MainWindow(IReadOnlyList tasks, Logger logger) { InitializeComponent(); - _tasks = Array.Empty(); - _logger = new Logger(); - } - - public MainWindow(IReadOnlyList tasks, Logger logger) : this() - { _tasks = tasks; _logger = logger; From faf0f4c79159462a6c0c86816b4ab57b33c3f896 Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:02:21 -0500 Subject: [PATCH 3/7] ci: verify the published artifact and bump pinned versions `dotnet build` never exercised single-file packing, self-contained runtime bundling, or compression, so CI was green on a configuration that is not the one released. Adds a publish step plus a size assertion that fails if the self-contained runtime goes missing. - dotnet-version 9.0.x -> 10.0.x - actions/checkout v5 -> v7, actions/setup-dotnet v4 -> v6 - gitignore publish/ so the new output directory stays untracked Co-Authored-By: Claude Opus 5 --- .github/workflows/build.yml | 22 +++++++++++++++++++--- .gitignore | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3b7074..142b59a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,8 +10,24 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-dotnet@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 with: - dotnet-version: "9.0.x" + dotnet-version: "10.0.x" + - run: dotnet build FieldKit.sln --configuration Release + + # Verify the configuration that actually ships. `dotnet build` does not + # exercise single-file packing, self-contained runtime bundling, or + # compression — all of which have their own failure modes. + - name: Verify release publish + run: dotnet publish FieldKit/FieldKit.csproj --configuration Release --output publish + + - name: Confirm single-file artifact + shell: pwsh + run: | + $exe = "publish/FieldKit.exe" + if (-not (Test-Path $exe)) { throw "Expected single-file artifact at $exe" } + $sizeMb = [math]::Round((Get-Item $exe).Length / 1MB, 1) + Write-Host "FieldKit.exe = $sizeMb MB" + if ($sizeMb -lt 20) { throw "Artifact is $sizeMb MB - self-contained runtime looks missing" } diff --git a/.gitignore b/.gitignore index 2789d71..94b2bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ obj/ +publish/ *.user *.suo .vs/ From b3a91b6cb05d18c37ad5be87769a386a80586fc3 Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:21:59 -0500 Subject: [PATCH 4/7] chore: add CodeQL, changelog, and issue/PR templates Closes three Material gaps from the pre-publish repo audit run for the v1.1.0 release. - CodeQL static analysis on push, PR, and a weekly schedule. Uses manual build mode rather than buildless so the analyzer gets full type resolution across the partial classes WPF generates from XAML. Pinned to codeql-action v4 (the repo's "latest" release is a bundle tag, not the action major). - CHANGELOG.md covering v1.0.0 and v1.1.0, in Keep a Changelog format. - Issue forms for bugs and features, a PR template, and a security contact link that routes reports to the private advisory flow instead of a public issue. - README: .NET badge 9.0 -> 10.0, and a changelog link from Download. Co-Authored-By: Claude Opus 5 --- .github/ISSUE_TEMPLATE/bug_report.yml | 53 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 33 ++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 20 ++++++++ .github/workflows/codeql.yml | 42 +++++++++++++++ CHANGELOG.md | 60 ++++++++++++++++++++++ README.md | 4 +- 7 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/codeql.yml create mode 100644 CHANGELOG.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..d1b8800 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,53 @@ +name: Bug report +description: Something in FieldKit did not work as expected. +labels: [bug] +body: + - type: markdown + attributes: + value: | + Every run writes a timestamped log. Attaching it makes a report far + easier to act on — use **Open Log** in the toolbar to find it. + + - type: textarea + id: what-happened + attributes: + label: What happened + description: What you did, what you expected, and what you got instead. + validations: + required: true + + - type: input + id: operation + attributes: + label: Which operation + description: The name shown in the operations list, if the problem was in one specific task. + placeholder: Clear Temporary Files + validations: + required: false + + - type: input + id: version + attributes: + label: FieldKit version + description: Right-click FieldKit.exe, then Properties, then Details. + placeholder: 1.1.0 + validations: + required: true + + - type: input + id: windows + attributes: + label: Windows version + description: Run `winver`, or Settings, then System, then About. + placeholder: Windows 11 24H2 (26100.2314) + validations: + required: true + + - type: textarea + id: log + attributes: + label: Log output + description: Paste the relevant part of the log, or attach the whole file. + render: text + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9e977cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/tsudo/fieldkit/security/policy + about: Report privately. Do not open a public issue for a security problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..53a7c19 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,33 @@ +name: Feature request +description: Suggest an operation or change for FieldKit. +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + [ROADMAP.md](https://github.com/tsudo/fieldkit/blob/main/ROADMAP.md) + covers what is already planned and what is deliberately out of scope. + Worth a look first. + + - type: textarea + id: problem + attributes: + label: What problem does this solve + description: Describe the situation you run into, not the implementation you have in mind. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: What you would like it to do + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: How you handle it today + description: The manual steps, script, or other tool you currently use. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..82119c4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## What this changes + + + +## Why + + + +## How it was verified + + + +- [ ] `dotnet build FieldKit.sln --configuration Release` succeeds +- [ ] Ran the affected operation on Windows 10 or 11 +- [ ] No new warnings + +## Notes for the reviewer + + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3df0552 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Weekly, so advisories that land after a quiet period still get caught. + - cron: "23 5 * * 1" + +jobs: + analyze: + name: Analyze C# + runs-on: windows-latest + permissions: + security-events: write + contents: read + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: "10.0.x" + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: csharp + build-mode: manual + + # Manual build mode rather than buildless: FieldKit already has a known-good + # build, and compiling gives CodeQL full type resolution across the WPF + # partial classes that XAML generates. + - name: Build + run: dotnet build FieldKit.sln --configuration Release + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:csharp" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1320f35 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable changes to FieldKit are recorded here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and +this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2026-08-15 + +Maintenance release. No new operations — this moves the runtime off a version +heading for end of support and fixes two defects found during review. + +### Changed + +- Runtime moved from .NET 9 to .NET 10 (LTS). .NET 9 reaches end of support on + 2026-11-10; because FieldKit publishes as a self-contained single file, the + runtime is embedded in the exe, so shipping on .NET 9 past that date would + hand every downloader an unpatched runtime. .NET 10 is supported through + 2028-11-14. +- `System.Management` and `System.ServiceProcess.ServiceController` updated from + 9.0.0 to 10.0.11. +- The SDK version is pinned in `global.json` so builds are reproducible instead + of following whatever SDK happens to be newest on the build machine. + +### Fixed + +- Removed a duplicate `Logger` instance. `MainWindow`'s parameterless + constructor created its own logger, and the real constructor chained through + it before replacing the field, leaving an undisposed `StreamWriter` behind. + The visible symptom was an empty `FieldKit-*.log` file appearing next to the + exe on every launch. +- Unhandled exceptions on the UI thread are now caught, written to the log, and + reported with the log path, instead of closing the app without explanation. + +### Added + +- CodeQL static analysis, running on every push and pull request to `main` plus + a weekly schedule. +- Continuous integration now verifies the published single-file artifact, not + just `dotnet build`. Single-file packing, self-contained runtime bundling, and + compression have their own failure modes that a plain build never exercises. +- Issue and pull request templates. +- This changelog. + +## [1.0.0] - 2026-04-06 + +First release. + +### Added + +- 12 maintenance operations across cleanup, updates, and system health. +- Presets for focused runs: All, Cleanup, Updates, Health, Defaults. +- WPF interface with a Fluent dark theme. +- System Information panel with export. +- Timestamped log file for every run. +- Security hardening: absolute paths for system binaries, and symlink and + junction protection on temp cleanup. + +[1.1.0]: https://github.com/tsudo/fieldkit/releases/tag/v1.1.0 +[1.0.0]: https://github.com/tsudo/fieldkit/releases/tag/v1.0.0 diff --git a/README.md b/README.md index a58b049..a916ab2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![License](https://img.shields.io/github/license/tsudo/fieldkit) ![Release](https://img.shields.io/github/v/release/tsudo/fieldkit) ![Downloads](https://img.shields.io/github/downloads/tsudo/fieldkit/total) -![.NET](https://img.shields.io/badge/.NET-9.0-512BD4) +![.NET](https://img.shields.io/badge/.NET-10.0-512BD4) Portable Windows maintenance in a single exe. No installer, no bloat, no telemetry. @@ -11,7 +11,7 @@ FieldKit runs cleanup, updates, and system health checks from one window. Pick a ## Download -Grab the latest `FieldKit.exe` from the [Releases](https://github.com/tsudo/fieldkit/releases) page. +Grab the latest `FieldKit.exe` from the [Releases](https://github.com/tsudo/fieldkit/releases) page. See the [changelog](CHANGELOG.md) for what changed. - Windows 10 or 11, 64-bit - No installer — run it from anywhere From 0cb5977649ebb1b8beec5c083a290ac8e5e37aae Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:25:47 -0500 Subject: [PATCH 5/7] ci: grant CodeQL the actions:read permission The analyze step reads workflow run metadata; without actions:read it fails on some repo configurations. Co-Authored-By: Claude Opus 5 --- .github/workflows/codeql.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3df0552..e03f985 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,6 +15,7 @@ jobs: runs-on: windows-latest permissions: security-events: write + actions: read contents: read steps: From a322673d0fc2d39b151543e25ef49aeeac381939 Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:46:46 -0500 Subject: [PATCH 6/7] fix: report aborted runs honestly instead of "Run complete" Review of the previous commit found that the new DispatcherUnhandledException handler turned a crash into a false success report. RunSelectedTasksAsync's finally block unconditionally set the status bar to "Run complete" and the progress bar to 100%, and ShowCompletionSummary sat outside that finally. So an exception escaping the loop meant: remaining operations never ran, the in-flight row stayed "Running", no summary appeared -- and the UI asserted the run had finished. Before the handler existed the process died, which is wrong but unmistakable. The run loop now owns its own failure: logs the abort, marks unfinished operations "Aborted", shows a distinct dialog, and reports "Run aborted" at 0%. Verified with a structural harness covering clean run, escaped exception, and contained task failure -- 7/7 assertions. Two related fixes: - The handler was subscribed before the startup sequence, contradicting its own doc comment and killing the deliberate Shutdown(1) path for failures inside the disclaimer dialog. It now subscribes after the main window shows. - Closing the window mid-run called _runCts.Dispose() while the run loop still read the token, so every remaining operation failed with ObjectDisposedException -- reading in the log as many broken operations rather than one lifecycle bug. Now cancels and lets the loop dispose. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 15 +++++++++++++-- FieldKit/App.xaml.cs | 20 +++++++++++++------- FieldKit/MainWindow.xaml.cs | 32 +++++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1320f35..98e91e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,11 @@ heading for end of support and fixes two defects found during review. 2028-11-14. - `System.Management` and `System.ServiceProcess.ServiceController` updated from 9.0.0 to 10.0.11. -- The SDK version is pinned in `global.json` so builds are reproducible instead - of following whatever SDK happens to be newest on the build machine. +- `global.json` sets .NET 10 as the minimum SDK, so a build on an older SDK + fails immediately with a clear message. It is a floor rather than a pin by + design: the SDK in use determines which runtime version is embedded in the + self-contained exe, and a self-contained app only receives runtime fixes when + it is rebuilt, so tracking forward is the safer default. ### Fixed @@ -31,6 +34,14 @@ heading for end of support and fixes two defects found during review. exe on every launch. - Unhandled exceptions on the UI thread are now caught, written to the log, and reported with the log path, instead of closing the app without explanation. +- A run that stops early now says so. Previously the status bar read "Run + complete" at 100% whether or not the run finished; an interrupted run reports + "Run aborted", marks the operations that did not finish, and shows what went + wrong instead of a completion summary. +- Closing the window mid-run no longer disposes the cancellation token while the + run loop is still using it. Every remaining operation used to fail with + `ObjectDisposedException`, which read in the log as many broken operations + rather than one lifecycle bug. ### Added diff --git a/FieldKit/App.xaml.cs b/FieldKit/App.xaml.cs index 546ba1f..8e630b3 100644 --- a/FieldKit/App.xaml.cs +++ b/FieldKit/App.xaml.cs @@ -11,8 +11,6 @@ public partial class App : Application private void App_Startup(object sender, StartupEventArgs e) { - DispatcherUnhandledException += OnDispatcherUnhandledException; - try { if (!DisclaimerWindow.HasAccepted()) @@ -47,6 +45,12 @@ private void App_Startup(object sender, StartupEventArgs e) var mainWindow = new MainWindow(tasks, _logger); mainWindow.Show(); + + // Registered only once startup has succeeded. Anything that fails + // before this point belongs to the catch below, which shuts down + // deliberately — including failures inside the disclaimer dialog, + // whose whole job is obtaining consent before destructive work. + DispatcherUnhandledException += OnDispatcherUnhandledException; } catch (Exception ex) { @@ -58,15 +62,17 @@ private void App_Startup(object sender, StartupEventArgs e) /// /// Last-chance handler for exceptions raised on the UI thread after startup. - /// Individual maintenance tasks already catch their own failures, so anything - /// reaching here is a UI-layer bug. The app stays alive so the user keeps - /// access to the log path rather than losing it to a silent crash. + /// Maintenance tasks catch their own failures and the run loop reports its + /// own aborts, so anything reaching here is a UI-layer bug outside a run. + /// The app stays alive so the user keeps access to the log path rather than + /// losing it to a silent crash. /// private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { - _logger?.Log($"Unhandled UI exception: {e.Exception}", "ERROR"); + var logger = _logger; + logger?.Log($"Unhandled UI exception: {e.Exception}", "ERROR"); - var logNote = _logger is null ? "" : $"\n\nLog: {_logger.LogFilePath}"; + var logNote = logger is null ? "" : $"\n\nLog: {logger.LogFilePath}"; MessageBox.Show( $"FieldKit hit an unexpected error:\n\n{e.Exception.Message}{logNote}", "FieldKit — Unexpected Error", diff --git a/FieldKit/MainWindow.xaml.cs b/FieldKit/MainWindow.xaml.cs index 792da8a..9518849 100644 --- a/FieldKit/MainWindow.xaml.cs +++ b/FieldKit/MainWindow.xaml.cs @@ -31,7 +31,10 @@ public MainWindow(IReadOnlyList tasks, Logger logger) Closed += (_, _) => { _logger.LogWritten -= OnLogWritten; - _runCts?.Dispose(); + // Cancel, don't dispose — the run loop is still reading this token + // and owns disposal in its finally. Disposing here made every + // remaining operation fail with ObjectDisposedException. + _runCts?.Cancel(); }; UpdateSummary(); @@ -170,6 +173,7 @@ private async Task RunSelectedTasksAsync() SetRunningState(true); _logger.Log($"Run started. Preview: {(dryRun ? "ON" : "OFF")}. Log: {_logger.LogFilePath}", "INFO"); + var completed = false; try { for (int i = 0; i < selected.Count; i++) @@ -209,18 +213,36 @@ private async Task RunSelectedTasksAsync() RefreshDetails(); } + + completed = true; + } + catch (Exception ex) + { + // The run owns its own failure. Letting this reach the global + // dispatcher handler would leave the status bar reading + // "Run complete" on a run that stopped halfway through. + _logger.Log($"Run aborted: {ex}", "ERROR"); + + foreach (var vm in selected.Where(vm => vm.Status == "Running")) + vm.Status = "Aborted"; + + MessageBox.Show(this, + $"The run stopped early and did not finish.\n\n{ex.Message}\n\nLog: {_logger.LogFilePath}", + "FieldKit — Run Aborted", MessageBoxButton.OK, MessageBoxImage.Error); } finally { - ProgressBar.Value = 100; - StatusText.Text = "Run complete"; + ProgressBar.Value = completed ? 100 : 0; + StatusText.Text = completed ? "Run complete" : "Run aborted"; SetRunningState(false); _runCts.Dispose(); _runCts = null; + RefreshDetails(); } - ShowCompletionSummary(dryRun, success, warning, error, skipped, - startFree, TryGetSystemDriveFreeSpace(), rebootRecommended); + if (completed) + ShowCompletionSummary(dryRun, success, warning, error, skipped, + startFree, TryGetSystemDriveFreeSpace(), rebootRecommended); } private void ShowCompletionSummary(bool dryRun, int success, int warning, int error, int skipped, From ad7e03574cfceb45994613d90b9202b2cae16b6f Mon Sep 17 00:00:00 2001 From: "Keith S. Crawford" <2959153+tsudo@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:46:46 -0500 Subject: [PATCH 7/7] ci: assert single-file-ness, retain the artifact, correct the SDK claim The publish check asserted only that FieldKit.exe existed and was >= 20 MB. Measured: publishing with IncludeNativeLibrariesForSelfExtract=false yields a 58.8 MB exe that passes that floor while scattering five native DLLs beside it (D3DCompiler_47_cor3, PenImc_cor3, PresentationNative_cor3, vcruntime140_cor3, wpfgfx_cor3). Uploading just the exe would then ship a requireAdministrator binary that resolves those from its launch directory. The check now asserts the publish directory contains nothing but the exe and its pdb, with a size band rather than a one-sided floor. - Retain the published exe as a build artifact. It was previously destroyed with the runner, so a failed assertion left nothing to inspect and the exe was never smoke-tested off the author's machine. - global.json claimed to pin the SDK; rollForward latestFeature is a floor, not a pin, so both the README and CHANGELOG asserted a guarantee the config disabled. Kept as a floor -- the SDK in use determines which runtime is embedded in the self-contained exe, and a self-contained app only gets runtime fixes when rebuilt -- and lowered it to 10.0.100 so contributors on the GA SDK are not blocked. Docs now describe it as a minimum. - CodeQL also analyzes `actions`. GitHub already identifies this repo as having analyzable workflows, so the three workflow files added in this branch were the one thing it added scanning for that went unscanned. - Dependabot now covers the github-actions ecosystem; without it the action pins get no update PRs and drift silently. - Explicit permissions block on build.yml. Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 8 ++++++++ .github/workflows/build.yml | 27 ++++++++++++++++++++++++++- .github/workflows/codeql.yml | 30 +++++++++++++++++++++++------- README.md | 2 +- global.json | 3 ++- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fbbad3c..9da2e3c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,11 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 5 + + # The workflows pin actions by major tag; without this ecosystem those pins + # get no update PRs and drift silently. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 142b59a..634bd10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: build: runs-on: windows-latest @@ -13,6 +16,9 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: + # Latest 10.0 SDK. global.json sets the floor; the SDK in use + # determines which runtime version gets embedded in the + # self-contained exe, so tracking forward here is deliberate. dotnet-version: "10.0.x" - run: dotnet build FieldKit.sln --configuration Release @@ -28,6 +34,25 @@ jobs: run: | $exe = "publish/FieldKit.exe" if (-not (Test-Path $exe)) { throw "Expected single-file artifact at $exe" } + + # A size floor alone does not prove single-file: dropping + # IncludeNativeLibrariesForSelfExtract still yields a ~59 MB exe but + # scatters native DLLs beside it, which an elevated binary would then + # resolve from its launch directory. Assert nothing else is emitted. + $stray = Get-ChildItem publish -File | + Where-Object { $_.Name -ne 'FieldKit.exe' -and $_.Extension -ne '.pdb' } + if ($stray) { throw "Not single-file - loose files present: $($stray.Name -join ', ')" } + $sizeMb = [math]::Round((Get-Item $exe).Length / 1MB, 1) Write-Host "FieldKit.exe = $sizeMb MB" - if ($sizeMb -lt 20) { throw "Artifact is $sizeMb MB - self-contained runtime looks missing" } + if ($sizeMb -lt 40) { throw "Artifact is $sizeMb MB - self-contained runtime looks missing" } + if ($sizeMb -gt 100) { throw "Artifact is $sizeMb MB - compression looks disabled" } + + # Retain it: without this the artifact that just passed verification is + # destroyed with the runner, so a failed assertion leaves nothing to + # inspect and the exe is never smoke-tested off the author's machine. + - uses: actions/upload-artifact@v7 + with: + name: FieldKit-exe + path: publish/FieldKit.exe + if-no-files-found: error diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e03f985..6eae4c2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -11,33 +11,49 @@ on: jobs: analyze: - name: Analyze C# + name: Analyze ${{ matrix.language }} runs-on: windows-latest permissions: security-events: write actions: read contents: read + strategy: + fail-fast: false + matrix: + include: + # Manual build mode rather than buildless: FieldKit already has a + # known-good build, and compiling gives CodeQL full type resolution + # across the partial classes WPF generates from XAML. + - language: csharp + build-mode: manual + # The workflows in this repo are themselves analyzable. + - language: actions + build-mode: none + steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 + if: matrix.language == 'csharp' with: + # Latest 10.0 SDK, matching build.yml. Resolving from global.json + # instead would freeze the embedded self-contained runtime at + # whatever that SDK bundles — and a self-contained app only gets + # runtime CVE fixes by being rebuilt. dotnet-version: "10.0.x" - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: - languages: csharp - build-mode: manual + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} - # Manual build mode rather than buildless: FieldKit already has a known-good - # build, and compiling gives CodeQL full type resolution across the WPF - # partial classes that XAML generates. - name: Build + if: matrix.language == 'csharp' run: dotnet build FieldKit.sln --configuration Release - name: Perform CodeQL analysis uses: github/codeql-action/analyze@v4 with: - category: "/language:csharp" + category: "/language:${{ matrix.language }}" diff --git a/README.md b/README.md index a916ab2..eedabe4 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ FieldKit reports these cases clearly in the status column and log instead of fai ## Building From Source -Requires the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). The exact SDK version is pinned in `global.json`. +Requires the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). `global.json` sets `10.0.100` as the minimum; the build uses the newest .NET 10 SDK you have installed. ```bash dotnet build FieldKit.sln diff --git a/global.json b/global.json index 01cb658..28820ed 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { + "//": "Minimum SDK, not a pin. rollForward latestFeature uses the newest .NET 10 SDK installed, which keeps the embedded self-contained runtime current on rebuild.", "sdk": { - "version": "10.0.400", + "version": "10.0.100", "rollForward": "latestFeature" } }