feat(release): pack the dev channel as its own app so it installs beside stable #115

Merged
KofTwentyTwo merged 1 commits from feat/dev-edition into main 2026-10-10 23:47:27 +00:00
6 changed files with 79 additions and 8 deletions

No files matched your search

+8 -4
View File
@@ -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)
+15
View File
@@ -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-<version>.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
+15
View File
@@ -30,6 +30,21 @@ public sealed class GcloPathsTests
/// <summary>The dev edition keeps its own data root beside the stable one (#114).</summary>
[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<ArgumentNullException>(() => GcloPaths.DataFolderNameFor(null!));
Assert.Equal("", GcloPaths.Edition);
Assert.Equal("gclo", GcloPaths.DataFolderName);
}
/// <summary>The default root is never the install root nor anything under it.</summary>
[Fact]
public void DefaultDataRoot_IsNotTheVelopackInstallRoot()
+28 -3
View File
@@ -34,11 +34,36 @@ public static class GcloPaths
/// <summary>
/// %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 <c>gclo</c>) and
/// "dev" for the dev-channel app (<c>gclo-dev</c>), 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 <see cref="DataRoot"/>;
/// local and CLI runs leave it empty.
/// </summary>
public static string Edition { get; set; } = "";
/// <summary>The folder name under %LOCALAPPDATA%\KofTwentyTwo: "gclo", or "gclo-dev" for the dev edition.</summary>
public static string DataFolderName => DataFolderNameFor(Edition);
/// <summary>"gclo" for the stable edition (empty <paramref name="edition"/>), "gclo-&lt;edition&gt;" otherwise.</summary>
public static string DataFolderNameFor(string edition)
{
ArgumentNullException.ThrowIfNull(edition);
return edition.Length == 0 ? "gclo" : $"gclo-{edition}";
}
/// <summary>
/// %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).
/// </summary>
public static string DefaultDataRoot =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"KofTwentyTwo", "gclo");
"KofTwentyTwo", DataFolderName);
}
+4 -1
View File
@@ -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.
+9
View File
@@ -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) =>