From 9db18c594e2ac1cef6ca7bdca08580dbc82dc491 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 10 Oct 2026 18:40:42 -0500 Subject: [PATCH] feat(release): pack the dev channel as its own app so it installs beside stable Prereleases pack as Velopack app id gclo-dev (stable stays gclo): separate install folder, Start menu entry, update feed and data root. The entry point reads the installed app id from Velopack and sets GcloPaths.Edition, which puts the dev edition's data under %LOCALAPPDATA%\KofTwentyTwo\gclo-dev and '(dev)' in the window title; local and CLI runs are unaffected. release.yml derives the app id from the tag and the verify-signatures job downloads the matching asset names. RELEASING.md documents the split and that pre-split dev installs cannot update into it. Closes #114 Co-Authored-By: Claude Fable 5.1 Signed-off-by: James Maes --- .github/workflows/release.yml | 12 +++++++---- docs/RELEASING.md | 15 ++++++++++++++ gclo.Engine.Tests/GcloPathsTests.cs | 15 ++++++++++++++ gclo.ViewModels/GcloPaths.cs | 31 ++++++++++++++++++++++++++--- gclo/MainWindow.xaml.cs | 5 ++++- gclo/Program.cs | 9 +++++++++ 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0fe18bb..b9dfbf5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -153,7 +153,10 @@ jobs: with: solution: gclo.slnx app-project: gclo/gclo.csproj - app-id: gclo + # Prereleases pack as their own Velopack app (gclo-dev): separate install + # folder, Start menu entry, update feed and data root, so a dev build can be + # installed beside the stable one (#114). Asset names follow the app id. + app-id: ${{ contains(github.ref_name, '-') && 'gclo-dev' || 'gclo' }} main-exe: gclo.exe icon: gclo/Assets/Brand/gclo.ico platform: x64 @@ -193,6 +196,7 @@ jobs: TAG: ${{ github.ref_name }} K22_VERSION: ${{ needs.release.outputs.version }} K22_CHANNEL: ${{ needs.release.outputs.prerelease == 'true' && 'dev' || 'stable' }} + K22_APP_ID: ${{ needs.release.outputs.prerelease == 'true' && 'gclo-dev' || 'gclo' }} EXPECTED_SUBJECT: ${{ vars.TRUSTED_SIGNING_SUBJECT }} steps: - name: Download the published assets @@ -202,9 +206,9 @@ jobs: GH_REPO: ${{ github.repository }} run: | New-Item -ItemType Directory -Force assets | Out-Null - gh release download $env:TAG --dir assets --pattern "gclo-$env:K22_CHANNEL-Setup.exe" --pattern "gclo-$env:K22_CHANNEL-Portable.zip" --pattern "gclo-cli-win-x64-$env:K22_VERSION.zip" + gh release download $env:TAG --dir assets --pattern "$env:K22_APP_ID-$env:K22_CHANNEL-Setup.exe" --pattern "$env:K22_APP_ID-$env:K22_CHANNEL-Portable.zip" --pattern "gclo-cli-win-x64-$env:K22_VERSION.zip" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Expand-Archive assets/gclo-$env:K22_CHANNEL-Portable.zip -DestinationPath assets/portable + Expand-Archive assets/$env:K22_APP_ID-$env:K22_CHANNEL-Portable.zip -DestinationPath assets/portable Expand-Archive assets/gclo-cli-win-x64-$env:K22_VERSION.zip -DestinationPath assets/cli - name: signtool verify /pa /all on every shipped executable @@ -217,7 +221,7 @@ jobs: } $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1 - $files = @(Get-Item assets/gclo-$env:K22_CHANNEL-Setup.exe) + @(Get-ChildItem assets/portable, assets/cli -Recurse -Include *.exe, *.dll | + $files = @(Get-Item assets/$env:K22_APP_ID-$env:K22_CHANNEL-Setup.exe) + @(Get-ChildItem assets/portable, assets/cli -Recurse -Include *.exe, *.dll | Where-Object { $_.Name -like 'gclo*' -or $_.Name -in 'Update.exe' }) $failed = 0 foreach ($f in $files) diff --git a/docs/RELEASING.md b/docs/RELEASING.md index aa730c8..d0ba1e4 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -23,6 +23,21 @@ Any prerelease identifier works (`-alpha`, `-beta.2`, `-rc.1`, ...) — the rule is simply: **a `-` in the version means prerelease, which means the `dev` channel**. +### The dev edition is a separate app + +A prerelease is packed as its own Velopack app, id **`gclo-dev`**, while stable +releases use **`gclo`**. The two install side by side: separate install folder +(`%LOCALAPPDATA%\gclo-dev` vs `%LOCALAPPDATA%\gclo`), Start menu entry, update +feed and data root (`%LOCALAPPDATA%\KofTwentyTwo\gclo-dev` vs `…\gclo`), and +the dev edition's window title reads "gclo (dev)". A machine can therefore keep +the stable app for real work and a dev build for trying the next release; each +only ever updates within its own channel. The dev edition's assets follow its +app id: `gclo-dev-dev-Setup.exe`, `gclo-dev-dev-Portable.zip`, +`gclo-dev-1.2.3-beta.1-dev-full.nupkg`, `releases.dev.json`. The CLI zip is the +same for both (`gclo-cli-win-x64-.zip`). Dev installs made before this +split (app id `gclo` on the dev channel, up to 1.0.2-beta.3) cannot update into +`gclo-dev`: uninstall them, or install the stable app over them. + ### How self-update channels map The desktop app updates itself with [Velopack](https://velopack.io). Each diff --git a/gclo.Engine.Tests/GcloPathsTests.cs b/gclo.Engine.Tests/GcloPathsTests.cs index 3c84e13..b87cd3f 100644 --- a/gclo.Engine.Tests/GcloPathsTests.cs +++ b/gclo.Engine.Tests/GcloPathsTests.cs @@ -30,6 +30,21 @@ public sealed class GcloPathsTests + /// The dev edition keeps its own data root beside the stable one (#114). + [Fact] + public void DataFolderNameFor_DevEdition_UsesItsOwnFolder() + { + // Edition itself is process-wide state set once by the app's entry point; the + // tests exercise the pure mapping so they cannot race the vault's scoping. + Assert.Equal("gclo", GcloPaths.DataFolderNameFor("")); + Assert.Equal("gclo-dev", GcloPaths.DataFolderNameFor("dev")); + Assert.Throws(() => GcloPaths.DataFolderNameFor(null!)); + Assert.Equal("", GcloPaths.Edition); + Assert.Equal("gclo", GcloPaths.DataFolderName); + } + + + /// The default root is never the install root nor anything under it. [Fact] public void DefaultDataRoot_IsNotTheVelopackInstallRoot() diff --git a/gclo.ViewModels/GcloPaths.cs b/gclo.ViewModels/GcloPaths.cs index 62467d0..77404a8 100644 --- a/gclo.ViewModels/GcloPaths.cs +++ b/gclo.ViewModels/GcloPaths.cs @@ -34,11 +34,36 @@ public static class GcloPaths /// - /// %LOCALAPPDATA%\KofTwentyTwo\gclo: the root used when GCLO_DATA_DIR is not - /// set. Outside the Velopack install root on purpose (see the type remarks). + /// The installed edition: "" for the stable app (Velopack app id gclo) and + /// "dev" for the dev-channel app (gclo-dev), which installs beside the + /// stable one and keeps its own data root so both can run on one machine (#114). + /// Set once by the app's entry point before anything reads ; + /// local and CLI runs leave it empty. + /// + public static string Edition { get; set; } = ""; + + + + /// The folder name under %LOCALAPPDATA%\KofTwentyTwo: "gclo", or "gclo-dev" for the dev edition. + public static string DataFolderName => DataFolderNameFor(Edition); + + + + /// "gclo" for the stable edition (empty ), "gclo-<edition>" otherwise. + public static string DataFolderNameFor(string edition) + { + ArgumentNullException.ThrowIfNull(edition); + return edition.Length == 0 ? "gclo" : $"gclo-{edition}"; + } + + + + /// + /// %LOCALAPPDATA%\KofTwentyTwo\gclo (or gclo-dev): the root used when GCLO_DATA_DIR + /// is not set. Outside the Velopack install root on purpose (see the type remarks). /// public static string DefaultDataRoot => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "KofTwentyTwo", "gclo"); + "KofTwentyTwo", DataFolderName); } diff --git a/gclo/MainWindow.xaml.cs b/gclo/MainWindow.xaml.cs index f03711e..0e8551b 100644 --- a/gclo/MainWindow.xaml.cs +++ b/gclo/MainWindow.xaml.cs @@ -121,7 +121,10 @@ public sealed partial class MainWindow : Window InitializeComponent(); ApplyBackdrop(); - Title = "gclo — Git Clone Large Organizations"; + // The dev edition installs beside the stable app, so the title says which one this is. + Title = GcloPaths.Edition.Length == 0 + ? "gclo — Git Clone Large Organizations" + : $"gclo ({GcloPaths.Edition}) — Git Clone Large Organizations"; // Quick Sync is declared in XAML; its Guid tag and the account items are // runtime data, so they are filled in here. diff --git a/gclo/Program.cs b/gclo/Program.cs index 6026c45..c864d3a 100644 --- a/gclo/Program.cs +++ b/gclo/Program.cs @@ -4,6 +4,7 @@ */ using System; +using gclo.ViewModels; using Velopack; @@ -25,6 +26,14 @@ public static class Program // Must be first: handles Velopack install/update/uninstall hooks and may exit. VelopackApp.Build().Run(); + // The dev channel is packed as its own app (id gclo-dev) so it installs beside + // the stable one; it keeps its own data root and says so in the title (#114). + // Local and CLI runs have no Velopack app id and stay on the stable paths. + if(string.Equals(Velopack.Locators.VelopackLocator.Current?.AppId, "gclo-dev", StringComparison.Ordinal)) + { + GcloPaths.Edition = "dev"; + } + // The remainder mirrors the XAML-generated Main (obj\...\App.g.i.cs) verbatim. global::WinRT.ComWrappersSupport.InitializeComWrappers(); global::Microsoft.UI.Xaml.Application.Start((p) => -- 2.54.0