From 6f0dd9ef20801790fdca22aedc014fa7b9a921a6 Mon Sep 17 00:00:00 2001 From: Lee Messenger Date: Sat, 18 Apr 2026 13:48:30 +0100 Subject: [PATCH 1/2] Document Windows installer signing plan + refresh CLAUDE.md for Avalonia port Adds a "Code signing" section recording the chosen approach (SignPath.io OSS), the three-part status (Part 1 in-repo done, Parts 2-3 waiting on SignPath application), and the deliberate exclusion of macOS from the signing scope. Also refreshes stale WPF-era references throughout: entry points, solution layout, gotchas, and adds the macOS build section. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d6a3583..d4eaffe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,10 +4,10 @@ Orientation for Claude / new agents working in this repo. Keep this file short a ## Project overview -ImageResize is a minimal, cross-platform image-resize middleware for ASP.NET Core, backed by SkiaSharp, plus a Windows desktop companion app that exposes the same engine through an Explorer context-menu. +ImageResize is a minimal, cross-platform image-resize middleware for ASP.NET Core, backed by SkiaSharp, plus a cross-platform desktop companion app (Windows + macOS, built on Avalonia UI) that exposes the same engine through an Explorer context-menu (Windows) / Finder Services (macOS). -- Current version: **3.1.0** (published as the `ImageResize` NuGet package). Version lives in [Directory.Build.props](Directory.Build.props) — do NOT re-add `` in any csproj. -- Target runtime: **.NET 10** (`net10.0`, plus `net10.0-windows` for the WPF app) +- Current version: **3.2.0** (published as the `ImageResize` NuGet package). Version lives in [Directory.Build.props](Directory.Build.props) — do NOT re-add `` in any csproj. +- Target runtime: **.NET 10** (`net10.0` across all projects — ContextMenu is Avalonia UI and cross-platform) - License: MIT ## Solution layout @@ -15,7 +15,7 @@ ImageResize is a minimal, cross-platform image-resize middleware for ASP.NET Cor The solution is [ImageResize.sln](ImageResize.sln) with four projects: - [ImageResize.Core/](ImageResize.Core/) — the NuGet library. ASP.NET Core middleware + SkiaSharp codec + disk cache. `net10.0`, framework-refs `Microsoft.AspNetCore.App`. `GeneratePackageOnBuild=true`, so a Release build emits a `.nupkg`. -- [ImageResize.ContextMenu/](ImageResize.ContextMenu/) — WPF desktop app (`net10.0-windows`, `WinExe`, `UseWPF=true`). Registers a Windows 11 Explorer context-menu entry; single-instance via mutex, forwards additional invocations over a named pipe. +- [ImageResize.ContextMenu/](ImageResize.ContextMenu/) — Avalonia UI desktop app (`net10.0`, cross-platform). Surfaces via a Windows 11 Explorer context-menu entry and a macOS Finder Quick Action. Single-instance + arg-forwarding is platform-split behind `ISingleInstance`: named pipe + mutex on Windows, Unix domain socket + exclusive lockfile on macOS/Linux. - [ImageResize.Example/](ImageResize.Example/) — Razor Pages demo that consumes the middleware; useful as a smoke test. - [ImageResize.Tests/](ImageResize.Tests/) — NUnit 4 + Shouldly + Moq unit tests for resize math, cache, path matching, and bounds validation. @@ -23,6 +23,7 @@ The solution is [ImageResize.sln](ImageResize.sln) with four projects: - .NET 10 SDK; `Nullable=enable`, `ImplicitUsings=enable`, `LangVersion=latest` - SkiaSharp **3.119.2** (+ Linux / macOS / Win32 native assets) +- Avalonia UI **11.2.5** (ContextMenu); `MessageBox.Avalonia` **3.3.1.1**; `Tmds.DBus.Protocol` pinned to **0.92.0** to clear GHSA-xrw6-gwf8-vvr9 - Microsoft.Extensions.{DependencyInjection,Logging,Options} **10.0.6** - NUnit **4.5.1**, NUnit3TestAdapter **6.2.0**, Shouldly **4.3.0**, Moq **4.20.72**, Coverlet **10.0.0** @@ -48,6 +49,15 @@ Windows installer for the ContextMenu app (PowerShell; requires Inno Setup on PA Output: `publish/installer/ImageResize-ContextMenu-Setup-*.exe`. +macOS universal `.app` + `.dmg` (must run on macOS — needs `lipo` from Xcode CLT; `brew install create-dmg` optional): + +```bash +./build-macos.sh # Release, default +./build-macos.sh Debug +``` + +Output: `publish/installer/ImageResize-ContextMenu-Setup--universal.dmg`. CI at `macos-latest` runs this on every PR and uploads the DMG as an artefact. + ## Key architectural patterns - DI + middleware registration: `AddImageResize()` / `UseImageResize()` in [ImageResizeServiceCollectionExtensions.cs](ImageResize.Core/Extensions/ImageResizeServiceCollectionExtensions.cs). Registers `IValidateOptions` with validate-on-start. @@ -55,13 +65,14 @@ Output: `publish/installer/ImageResize-ContextMenu-Setup-*.exe`. - Thundering-herd protection via `AsyncKeyedLocker` in [ImageResizerService.cs](ImageResize.Core/Services/ImageResizerService.cs). - Atomic cache writes (temp file → flush → rename) + XxHash128-keyed folder sharding in [FileSystemImageCache.cs](ImageResize.Core/Cache/FileSystemImageCache.cs). Hashing helpers centralised in [HashingUtilities.cs](ImageResize.Core/Utilities/HashingUtilities.cs). - Options-pattern config in [ImageResizeOptions.cs](ImageResize.Core/Configuration/ImageResizeOptions.cs) with nested `Bounds` / `Cache` / `ResponseCache` sub-options and a `MaxSourceBytes` decompression-bomb cap; validated by [ImageResizeOptionsValidator.cs](ImageResize.Core/Configuration/ImageResizeOptionsValidator.cs). -- ContextMenu single-instance + IPC (mutex + named pipe) in [App.xaml.cs](ImageResize.ContextMenu/App.xaml.cs). Version-string resolution in [VersionInfo.cs](ImageResize.ContextMenu/VersionInfo.cs) (reads `AssemblyInformationalVersion` → flows from `Directory.Build.props`). +- ContextMenu single-instance + IPC: [`ISingleInstance`](ImageResize.ContextMenu/Services/ISingleInstance.cs) with [`WindowsSingleInstance`](ImageResize.ContextMenu/Services/WindowsSingleInstance.cs) (mutex + named pipe) and [`UnixSingleInstance`](ImageResize.ContextMenu/Services/UnixSingleInstance.cs) (exclusive lockfile + Unix domain socket); wired in [App.axaml.cs](ImageResize.ContextMenu/App.axaml.cs). Shared per-OS paths / logging in [AppPaths.cs](ImageResize.ContextMenu/Services/AppPaths.cs) + [AppLog.cs](ImageResize.ContextMenu/Services/AppLog.cs). Version-string resolution in [VersionInfo.cs](ImageResize.ContextMenu/VersionInfo.cs) (reads `AssemblyInformationalVersion` → flows from `Directory.Build.props`). ## Entry points - Middleware pipeline: [ImageResizeMiddleware.cs](ImageResize.Core/Middleware/ImageResizeMiddleware.cs) - Web demo: [ImageResize.Example/Program.cs](ImageResize.Example/Program.cs) -- Desktop app: [ImageResize.ContextMenu/App.xaml.cs](ImageResize.ContextMenu/App.xaml.cs) → [MainWindow.xaml.cs](ImageResize.ContextMenu/MainWindow.xaml.cs) +- Desktop app: [ImageResize.ContextMenu/Program.cs](ImageResize.ContextMenu/Program.cs) → [App.axaml.cs](ImageResize.ContextMenu/App.axaml.cs) → [MainWindow.axaml.cs](ImageResize.ContextMenu/MainWindow.axaml.cs) +- macOS packaging sources: [ImageResize.ContextMenu/macos/](ImageResize.ContextMenu/macos/) (`Info.plist` templated with `{VERSION}`; `ResizeImages.workflow` is a hand-rolled Automator Quick Action) ## Configuration @@ -84,7 +95,21 @@ Output: `publish/installer/ImageResize-ContextMenu-Setup-*.exe`. - `AllowUpscale` defaults to `false`; resizes never enlarge past the source. - The middleware must be registered **before** `UseStaticFiles()` and before routing — otherwise static files win and the middleware never sees the request. - `GeneratePackageOnBuild` is on for `ImageResize.Core`, so every Release build drops a `.nupkg` into `nupkgs/`. Don't commit them; the `.gitignore` already excludes them. -- ContextMenu is `net10.0-windows` — it will not build on Linux/macOS. Core + Example + Tests do build cross-platform. +- ContextMenu is Avalonia and builds on Windows + macOS + Linux. Windows-only code paths (Explorer COM, Win32 P/Invoke to Progman/WorkerW, named-pipe IPC) are guarded both at compile time with `[SupportedOSPlatform("windows")]` and at runtime inside `OperatingSystem.IsWindows()` blocks — do not hoist them out. +- **Do not re-introduce WPF.** The port to Avalonia was deliberate. No ``, no `net10.0-windows` TFM on ContextMenu, no `System.Windows.*` references, no `.xaml` files (Avalonia's source generator only picks up `.axaml`). +- **Do not sign macOS builds with a paid Apple cert.** By design: no Apple Developer Program ($99/yr) — the `.dmg` ships unsigned, users right-click → Open once to bypass Gatekeeper. README documents this. + +## Code signing (Windows installer) + +Windows users see a SmartScreen *"Unknown publisher"* warning on first run because the Inno Setup installer is not Authenticode-signed. Chosen fix: **SignPath.io free OSS program** (no cost, no hardware tokens, signs in CI). + +Status: + +- ✅ **Part 1 (in-repo, done):** workaround documented in [ImageResize.ContextMenu/README.md](ImageResize.ContextMenu/README.md); [Installer.iss](ImageResize.ContextMenu/Installer.iss) publisher string set, icon enabled. +- ⏳ **Part 2 (offline, waiting on Lee):** register at and request the free OSS plan for `YodasMyDad/ImageResize`. Once approved they issue an org ID, project slug, signing-policy slug, and API token. +- ⏳ **Part 3 (wire into CI, after Part 2):** add `SIGNPATH_API_TOKEN` secret, update [.github/workflows/release.yml](.github/workflows/release.yml) to submit the `.exe` via `signpath-io/github-action-submit-signing-request@v1` and publish the *signed* artefact in the Release. The existing "Build + sign installers" step currently doesn't actually sign. + +macOS `.dmg` is intentionally unsigned — no Apple Developer Program. Do not add `codesign` / notarisation steps. ## Where to read more From c4cc8632a4973f45717990e3616cdb9be8ebd785 Mon Sep 17 00:00:00 2001 From: Lee Messenger Date: Sat, 18 Apr 2026 13:50:42 +0100 Subject: [PATCH 2/2] Attach macOS .dmg to GitHub Releases Adds a build-dmg job to release.yml (macos-latest, mirrors the Windows installer job's shape) that runs build-macos.sh against the bumped Directory.Build.props, produces the universal .dmg, and hands it off to the publish job for inclusion in the GitHub Release alongside the .exe installers. v3.4.0 released without the DMG because the macos job was only wired into build.yml (PR CI), not release.yml. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4926581..9b3f1d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,10 +129,41 @@ jobs: name: installers path: publish/installer/*.exe + build-dmg: + name: Build ContextMenu DMG (macos) + runs-on: macos-latest + needs: build-library + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore bumped Directory.Build.props + uses: actions/download-artifact@v4 + with: + name: bumped-props + path: . + + - name: Install create-dmg + run: brew install create-dmg + + - name: Build universal .app + .dmg + run: ./build-macos.sh Release + + - name: Upload DMG + uses: actions/upload-artifact@v4 + with: + name: dmg + path: publish/installer/*.dmg + publish: name: Publish to nuget.org + GitHub Release runs-on: ubuntu-latest - needs: [build-library, build-installer] + needs: [build-library, build-installer, build-dmg] if: ${{ inputs.dry_run != true }} steps: - uses: actions/checkout@v4 @@ -150,6 +181,11 @@ jobs: name: installers path: release/ + - uses: actions/download-artifact@v4 + with: + name: dmg + path: release/ + - uses: actions/download-artifact@v4 with: name: bumped-props @@ -194,6 +230,7 @@ jobs: release/ImageResize.*.nupkg release/ImageResize.*.snupkg release/ImageResize-ContextMenu-Setup-*.exe + release/ImageResize-ContextMenu-Setup-*.dmg generate_release_notes: true draft: false prerelease: ${{ contains(needs.build-library.outputs.version, '-') }}