From 1b3af49ff708ef27366facda61d33d0ddc03dc75 Mon Sep 17 00:00:00 2001
From: Logan Rupe <11291831+LoganRupe@users.noreply.github.com>
Date: Sat, 10 Oct 2026 19:36:34 +1100
Subject: [PATCH 1/6] feat(ci): build and draft GitHub releases from version
tags
Add a Release workflow: pushing a v* tag builds the self-signed macOS DMG
(identity imported from repo secrets into a temporary keychain) and the
Windows MSI, then attaches both to a draft release with the CHANGELOG
section as notes. Manual runs build artifacts only.
Windows publishing no longer depends on gitignored publish profiles, which
never existed in the repo, so a clean clone built a framework-dependent app.
The csproj now sets SelfContained and WindowsAppSDKSelfContained, and
publish.ps1 fails if coreclr.dll or the Windows App SDK runtime is missing
from the payload. The About dialog now shows major.minor.patch.
---
.github/workflows/release.yml | 117 +++++++++++++++++++++++++
CursorFollowWin/CursorFollowWin.csproj | 9 +-
CursorFollowWin/README.md | 11 +--
CursorFollowWin/SettingsWindow.xaml.cs | 2 +-
CursorFollowWin/scripts/publish.ps1 | 12 ++-
Docs/Distribution/Packaging.md | 7 ++
README.md | 10 ++-
7 files changed, 148 insertions(+), 20 deletions(-)
create mode 100644 .github/workflows/release.yml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..9940857
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,117 @@
+name: Release
+
+# Push a v* tag (matching the app versions) to build the macOS DMG and Windows MSI and
+# attach them to a draft GitHub release. Review the draft, then publish it.
+# Run manually (Actions -> Release -> Run workflow) to build the installers as workflow
+# artifacts without touching releases; pass an existing tag to (re)build its draft release.
+
+on:
+ push:
+ tags: ['v*']
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: Existing tag to build a draft release for (blank = artifacts only)
+ required: false
+
+permissions:
+ contents: read
+
+concurrency:
+ group: release-${{ github.ref }}-${{ inputs.tag }}
+ cancel-in-progress: false
+
+env:
+ TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
+
+jobs:
+ macos:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ env.TAG || github.ref }}
+
+ - name: Import self-signed signing identity
+ env:
+ P12_BASE64: ${{ secrets.MAC_SIGNING_P12 }}
+ P12_PASSWORD: ${{ secrets.MAC_SIGNING_P12_PASSWORD }}
+ run: |
+ if [ -z "$P12_BASE64" ]; then
+ if [ -n "$TAG" ]; then echo "::error::MAC_SIGNING_P12 secret is not set"; exit 1; fi
+ echo "::warning::No signing secret; building ad-hoc signed (artifacts only)"
+ echo "SIGN_IDENTITY=-" >> "$GITHUB_ENV"
+ exit 0
+ fi
+ KC="$RUNNER_TEMP/signing.keychain-db"
+ KC_PASS="$(openssl rand -hex 24)"
+ security create-keychain -p "$KC_PASS" "$KC"
+ security set-keychain-settings -lut 21600 "$KC"
+ security unlock-keychain -p "$KC_PASS" "$KC"
+ echo "$P12_BASE64" | base64 --decode > "$RUNNER_TEMP/identity.p12"
+ security import "$RUNNER_TEMP/identity.p12" -k "$KC" -P "$P12_PASSWORD" -T /usr/bin/codesign
+ rm -f "$RUNNER_TEMP/identity.p12"
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KC" >/dev/null
+ security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
+
+ - name: Build, sign, package, verify
+ run: ./CursorFollowMac/scripts/package.sh
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: macos-dmg
+ path: build/CursorFollowMac-*.dmg
+ if-no-files-found: error
+
+ windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ env.TAG || github.ref }}
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ - name: Build MSI (unsigned)
+ shell: pwsh
+ run: ./CursorFollowWin/scripts/package.ps1 -Rid win-x64
+ - uses: actions/upload-artifact@v4
+ with:
+ name: windows-msi
+ path: CursorFollowWin/installer/dist/*.msi
+ if-no-files-found: error
+
+ release:
+ if: ${{ github.event_name == 'push' || inputs.tag != '' }}
+ needs: [macos, windows]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ env.TAG }}
+ - uses: actions/download-artifact@v4
+ with:
+ path: dist
+ merge-multiple: true
+ - name: Check installer versions match the tag
+ run: |
+ v="${TAG#v}"
+ ls dist
+ ls dist/CursorFollowMac-"$v".dmg dist/CursorFollowWin-"$v"-win-x64.msi
+ - name: Release notes from CHANGELOG.md
+ run: |
+ v="${TAG#v}"
+ awk -v v="$v" '$0 == "## " v {f=1; next} /^## / {f=0} f' CHANGELOG.md > notes.md
+ test -s notes.md || { echo "::error::No '## $v' section in CHANGELOG.md"; exit 1; }
+ - name: Create or update draft release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ if gh release view "$TAG" >/dev/null 2>&1; then
+ gh release upload "$TAG" dist/* --clobber
+ gh release edit "$TAG" --notes-file notes.md
+ else
+ gh release create "$TAG" dist/* --draft --verify-tag --title "CursorFollow ${TAG#v}" --notes-file notes.md
+ fi
diff --git a/CursorFollowWin/CursorFollowWin.csproj b/CursorFollowWin/CursorFollowWin.csproj
index c9bb839..a7a4292 100644
--- a/CursorFollowWin/CursorFollowWin.csproj
+++ b/CursorFollowWin/CursorFollowWin.csproj
@@ -25,7 +25,6 @@
0.5.1
0.5.1.0
0.5.1.0
- 1.0
Logan Rupe
CursorFollow
Logan Rupe
@@ -43,10 +42,6 @@
win-x64
-
- win-$(Platform).pubxml
-
-
@@ -112,6 +107,10 @@
+
+ true
+ true
False
True
- ICE38;ICE43;ICE57;ICE61;ICE91
+
+ ICE03;ICE38;ICE43;ICE57;ICE61;ICE64;ICE91
diff --git a/CursorFollowWin/installer/Package.wxs b/CursorFollowWin/installer/Package.wxs
index 71378a7..4e300c6 100644
--- a/CursorFollowWin/installer/Package.wxs
+++ b/CursorFollowWin/installer/Package.wxs
@@ -103,6 +103,8 @@
Installer has no single-value remove-on-uninstall, and we must not delete
the shared Run key wholesale).
RemoveDataFolder removes %LocalAppData%\CursorFollowWin (settings.json + Logs\).
+ RemoveInstallTree removes what is left of the install folder after RemoveFiles: the
+ per-language subfolders of the self-contained Windows App SDK.
Impersonate=yes so HKCU / %LOCALAPPDATA% resolve to the installing user; Return=ignore so a
missing key/folder never fails the uninstall.
@@ -115,11 +117,17 @@
Directory="INSTALLFOLDER"
ExeCommand="cmd.exe /c rmdir /s /q "%LOCALAPPDATA%\CursorFollowWin""
Execute="deferred" Impersonate="yes" Return="ignore" />
+
+
From 5ecbc112357b429e89f522a5844666ede720cb5e Mon Sep 17 00:00:00 2001
From: Logan Rupe <11291831+LoganRupe@users.noreply.github.com>
Date: Sat, 10 Oct 2026 20:50:24 +1100
Subject: [PATCH 5/6] fix(win/app): close every window before Application.Exit
so Quit exits
With the Settings window open, Application.Exit closed only some of the
three windows and returned. The window it skipped (the output window in
one run, Settings in another) never got a close, so the WinUI message
loop kept running and the process stayed alive. A trace of Closed events
showed Exit never touched the surviving window. ExitApplication now
closes Settings, the panel and the output window itself, then calls Exit.
Not a regression from the self-contained publish: the framework-dependent
Debug build hangs the same way.
Fixes #5
---
CHANGELOG.md | 2 ++
CursorFollowWin/App.xaml.cs | 7 +++++++
2 files changed, 9 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7897f51..7588b91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+- **Windows:** Quit now exits the process. If the Settings window had been opened, the app could hang on Quit with the CursorFollow Output window still up ([#5](https://github.com/LoganRupe/CursorFollow/issues/5)).
+
## 0.5.1
First public release of **CursorFollow**, open source under the MIT License at [github.com/LoganRupe/CursorFollow](https://github.com/LoganRupe/CursorFollow). Runs on **macOS**, **Windows** and **Linux (Hyprland)**.
diff --git a/CursorFollowWin/App.xaml.cs b/CursorFollowWin/App.xaml.cs
index e531556..bb33486 100644
--- a/CursorFollowWin/App.xaml.cs
+++ b/CursorFollowWin/App.xaml.cs
@@ -169,6 +169,13 @@ private void ExitApplication()
_outputWindow?.DisposeCapture();
_singleInstanceMutex?.Dispose();
AppLog.Shutdown();
+
+ // Close every window ourselves rather than relying on Application.Exit to do it. With the
+ // Settings window open (three windows), Exit closed only some of them and returned; the
+ // window it skipped kept the message loop, and so the process, alive (issue #5).
+ _settingsWindow?.Close();
+ _panelWindow?.Close();
+ _outputWindow?.Close();
Current.Exit();
}
From b2b3cfc3cd8611e4b1a951843ba87037b973fb8c Mon Sep 17 00:00:00 2001
From: Logan Rupe <11291831+LoganRupe@users.noreply.github.com>
Date: Sat, 10 Oct 2026 20:54:36 +1100
Subject: [PATCH 6/6] docs(changelog): list the Windows quit fix under 0.5.1
---
CHANGELOG.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7588b91..4667cb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,6 @@
## Unreleased
-- **Windows:** Quit now exits the process. If the Settings window had been opened, the app could hang on Quit with the CursorFollow Output window still up ([#5](https://github.com/LoganRupe/CursorFollow/issues/5)).
-
## 0.5.1
First public release of **CursorFollow**, open source under the MIT License at [github.com/LoganRupe/CursorFollow](https://github.com/LoganRupe/CursorFollow). Runs on **macOS**, **Windows** and **Linux (Hyprland)**.
@@ -32,6 +30,7 @@ CursorFollow captures a target window and renders it into a stable, shareable wi
- Auto-follow (cursor-under-window with foreground fallback), live frame telemetry, and single-instance guard.
- Global hotkeys, Start-with-Windows, and capture-quality presets.
- Distributed as a per-user **WiX MSI** installer (no admin required; clean upgrade and uninstall).
+- Fixed: Quit could hang, leaving the process and output window running, if the Settings window had been opened ([#5](https://github.com/LoganRupe/CursorFollow/issues/5)).
### Linux