refactor(installer): drop self-update, publish stable-named ClaudeDo.Installer.exe
Release workflow now names the installer asset ClaudeDo.Installer.exe (no version) for a permanent download URL; it is still uploaded and checksummed on every release. App + worker keep the git tag version. Removes the self-update preflight from App.OnStartup and deletes the now-dead SelfUpdater / SelfUpdatePromptWindow / SelfUpdateResult plus their tests. App-update detection is unaffected: the manifest records the release tag via DownloadAndExtractStep. Updates the installer CLAUDE.md.
This commit is contained in:
@@ -1,256 +0,0 @@
|
||||
using System.Net.Http;
|
||||
|
||||
namespace ClaudeDo.Releases.Tests;
|
||||
|
||||
public class SelfUpdaterAssetMatchingTests
|
||||
{
|
||||
[Fact]
|
||||
public void FindInstallerAsset_PicksInstallerExeByPattern()
|
||||
{
|
||||
var assets = new[]
|
||||
{
|
||||
new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "https://x/app.zip", 10),
|
||||
new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst.exe", 20),
|
||||
new ReleaseAsset("checksums.txt", "https://x/checks", 1),
|
||||
};
|
||||
|
||||
var result = SelfUpdater.FindInstallerAsset(assets);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("ClaudeDo.Installer-0.3.0.exe", result!.Asset.Name);
|
||||
Assert.Equal("0.3.0", result.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindInstallerAsset_ReturnsNullWhenAbsent()
|
||||
{
|
||||
var assets = new[]
|
||||
{
|
||||
new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "https://x/app.zip", 10),
|
||||
};
|
||||
|
||||
Assert.Null(SelfUpdater.FindInstallerAsset(assets));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindInstallerAsset_IgnoresAppZipThatContainsInstaller()
|
||||
{
|
||||
var assets = new[]
|
||||
{
|
||||
new ReleaseAsset("ClaudeDo.Installer.Portable-0.3.0.zip", "https://x/1", 1),
|
||||
new ReleaseAsset("not-the-installer.exe", "https://x/2", 1),
|
||||
};
|
||||
|
||||
Assert.Null(SelfUpdater.FindInstallerAsset(assets));
|
||||
}
|
||||
}
|
||||
|
||||
public class SelfUpdaterDecisionTests
|
||||
{
|
||||
private sealed class FakeReleaseClient : IReleaseClient
|
||||
{
|
||||
public GiteaRelease? Release { get; set; }
|
||||
public bool Throw { get; set; }
|
||||
|
||||
public Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct)
|
||||
{
|
||||
if (Throw) throw new HttpRequestException("boom");
|
||||
return Task.FromResult(Release);
|
||||
}
|
||||
|
||||
public Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
|
||||
=> throw new NotSupportedException("not used in decision tests");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Decide_NoRelease_NoUpdate()
|
||||
{
|
||||
var client = new FakeReleaseClient { Release = null };
|
||||
var d = await SelfUpdater.DecideUpdateAsync(client, currentVersion: "0.1.0", CancellationToken.None);
|
||||
Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Decide_NetworkError_NoUpdate()
|
||||
{
|
||||
var client = new FakeReleaseClient { Throw = true };
|
||||
var d = await SelfUpdater.DecideUpdateAsync(client, "0.1.0", CancellationToken.None);
|
||||
Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Decide_OlderLatest_NoUpdate()
|
||||
{
|
||||
var client = new FakeReleaseClient
|
||||
{
|
||||
Release = new GiteaRelease("v0.1.0", "rel", new[]
|
||||
{
|
||||
new ReleaseAsset("ClaudeDo.Installer-0.1.0.exe", "u", 1),
|
||||
}),
|
||||
};
|
||||
var d = await SelfUpdater.DecideUpdateAsync(client, "0.2.0", CancellationToken.None);
|
||||
Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Decide_NewerLatestWithAsset_UpdateAvailable()
|
||||
{
|
||||
var client = new FakeReleaseClient
|
||||
{
|
||||
Release = new GiteaRelease("v0.3.0", "rel", new[]
|
||||
{
|
||||
new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x", 20),
|
||||
new ReleaseAsset("checksums.txt", "https://checks", 1),
|
||||
}),
|
||||
};
|
||||
var d = await SelfUpdater.DecideUpdateAsync(client, "0.2.0", CancellationToken.None);
|
||||
Assert.Equal(SelfUpdateDecisionKind.UpdateAvailable, d.Kind);
|
||||
Assert.Equal("0.3.0", d.LatestVersion);
|
||||
Assert.NotNull(d.InstallerAsset);
|
||||
Assert.NotNull(d.ChecksumsAsset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Decide_NewerLatestButNoInstallerAsset_NoUpdate()
|
||||
{
|
||||
var client = new FakeReleaseClient
|
||||
{
|
||||
Release = new GiteaRelease("v0.3.0", "rel", new[]
|
||||
{
|
||||
new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "u", 20),
|
||||
}),
|
||||
};
|
||||
var d = await SelfUpdater.DecideUpdateAsync(client, "0.2.0", CancellationToken.None);
|
||||
Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
public class SelfUpdaterReplaceSelfTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public SelfUpdaterReplaceSelfTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDo.Releases.Tests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose() { try { Directory.Delete(_tempDir, true); } catch { } }
|
||||
|
||||
[Fact]
|
||||
public async Task Replace_DeletesOldAndCopiesCurrent()
|
||||
{
|
||||
var oldPath = Path.Combine(_tempDir, "old.exe");
|
||||
var currentPath = Path.Combine(_tempDir, "current.exe");
|
||||
await File.WriteAllTextAsync(oldPath, "OLD");
|
||||
await File.WriteAllTextAsync(currentPath, "NEW");
|
||||
|
||||
var relaunchedWith = "";
|
||||
var result = await SelfUpdater.HandleReplaceSelfAsync(
|
||||
oldPath: oldPath,
|
||||
currentExePath: currentPath,
|
||||
launchProcess: path => { relaunchedWith = path; return true; },
|
||||
maxWaitMs: 500);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(oldPath, relaunchedWith);
|
||||
Assert.Equal("NEW", await File.ReadAllTextAsync(oldPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replace_TimesOutWhenFileStaysLocked_ReturnsFalse()
|
||||
{
|
||||
var oldPath = Path.Combine(_tempDir, "locked.exe");
|
||||
var currentPath = Path.Combine(_tempDir, "current.exe");
|
||||
await File.WriteAllTextAsync(oldPath, "OLD");
|
||||
await File.WriteAllTextAsync(currentPath, "NEW");
|
||||
|
||||
// Hold an exclusive lock across the wait window.
|
||||
using var lockStream = new FileStream(oldPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
|
||||
|
||||
var result = await SelfUpdater.HandleReplaceSelfAsync(
|
||||
oldPath: oldPath,
|
||||
currentExePath: currentPath,
|
||||
launchProcess: _ => true,
|
||||
maxWaitMs: 200);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
|
||||
public class SelfUpdaterDownloadTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public SelfUpdaterDownloadTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDo.Releases.Tests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose() { try { Directory.Delete(_tempDir, true); } catch { } }
|
||||
|
||||
private sealed class StubReleaseClient : IReleaseClient
|
||||
{
|
||||
public string FileContent { get; set; } = "";
|
||||
public string ChecksumsBody { get; set; } = "";
|
||||
|
||||
public Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct) => Task.FromResult<GiteaRelease?>(null);
|
||||
|
||||
public async Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
|
||||
{
|
||||
if (url.EndsWith("checksums.txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await File.WriteAllTextAsync(destPath, ChecksumsBody, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await File.WriteAllTextAsync(destPath, FileContent, ct);
|
||||
}
|
||||
progress.Report(FileContent.Length);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Download_MatchingChecksum_ReturnsPath()
|
||||
{
|
||||
var content = "FAKE-INSTALLER-BINARY";
|
||||
var hash = Sha256Hex(content);
|
||||
var client = new StubReleaseClient
|
||||
{
|
||||
FileContent = content,
|
||||
ChecksumsBody = $"{hash} ClaudeDo.Installer-0.3.0.exe\n",
|
||||
};
|
||||
var installer = new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst", content.Length);
|
||||
var checksums = new ReleaseAsset("checksums.txt", "https://x/checksums.txt", 100);
|
||||
|
||||
var path = await SelfUpdater.DownloadAndVerifyAsync(
|
||||
client, installer, checksums, _tempDir, new Progress<long>(_ => { }), CancellationToken.None);
|
||||
|
||||
Assert.NotNull(path);
|
||||
Assert.Equal(content, await File.ReadAllTextAsync(path!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Download_ChecksumMismatch_ReturnsNull()
|
||||
{
|
||||
var client = new StubReleaseClient
|
||||
{
|
||||
FileContent = "real",
|
||||
ChecksumsBody = "deadbeef" + new string('0', 56) + " ClaudeDo.Installer-0.3.0.exe\n",
|
||||
};
|
||||
var installer = new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst", 4);
|
||||
var checksums = new ReleaseAsset("checksums.txt", "https://x/checksums.txt", 100);
|
||||
|
||||
var path = await SelfUpdater.DownloadAndVerifyAsync(
|
||||
client, installer, checksums, _tempDir, new Progress<long>(_ => { }), CancellationToken.None);
|
||||
|
||||
Assert.Null(path);
|
||||
}
|
||||
|
||||
private static string Sha256Hex(string s)
|
||||
{
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
return Convert.ToHexString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(s))).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user