Skip to content

Commit 26706bb

Browse files
committed
chore: appcast generation
1 parent 5072e24 commit 26706bb

File tree

9 files changed

+301
-19
lines changed

9 files changed

+301
-19
lines changed

.github/workflows/release.yaml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ jobs:
6868
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
6969
token_format: "access_token"
7070

71+
- name: Install gcloud
72+
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # 2.1.4
73+
7174
- name: Install wix
7275
shell: pwsh
7376
run: |
@@ -120,6 +123,43 @@ jobs:
120123
env:
121124
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122125

126+
- name: Update appcast
127+
if: startsWith(github.ref, 'refs/tags/')
128+
shell: pwsh
129+
$ErrorActionPreference = "Stop"
130+
131+
$keyPath = Join-Path $env:TEMP "appcast-key.pem"
132+
$key = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:APPCAST_SIGNATURE_KEY_BASE64))
133+
Set-Content -Path $keyPath -Value $key
134+
135+
$oldAppCastPath = Join-Path $env:TEMP "appcast.old.xml"
136+
& gsutil cp $env:APPCAST_GCS_URI $oldAppCastPath
137+
if ($LASTEXITCODE -ne 0) { throw "Failed to download appcast" }
138+
139+
$newAppCastPath = Join-Path $env:TEMP "appcast.new.xml"
140+
$newAppCastSignaturePath = $newAppCastPath + ".signature"
141+
& ./scripts/Update-AppCast.ps1 `
142+
-tag "${{ github.ref_name }}" `
143+
-version "${{ steps.version.outputs.VERSION }}" `
144+
-channel stable `
145+
-x64Path "${{ steps.release.outputs.X64_OUTPUT_PATH }}" `
146+
-arm64Path "${{ steps.release.outputs.ARM64_OUTPUT_PATH }}" `
147+
-keyPath $keyPath `
148+
-inputAppCastPath $oldAppCastPath `
149+
-outputAppCastPath $newAppCastPath `
150+
-outputAppCastSignaturePath $newAppCastSignaturePath
151+
if ($LASTEXITCODE -ne 0) { throw "Failed to generate new appcast" }
152+
153+
& gsutil cp $newAppCastPath $env:APPCAST_GCS_URI
154+
if ($LASTEXITCODE -ne 0) { throw "Failed to upload new appcast" }
155+
& gsutil cp $newAppCastSignaturePath $env:APPCAST_SIGNATURE_GCS_URI
156+
if ($LASTEXITCODE -ne 0) { throw "Failed to upload new appcast signature" }
157+
env:
158+
APPCAST_GCS_URI: gs://releases.coder.com/coder-desktop/windows/appcast.xml
159+
APPCAST_SIGNATURE_GCS_URI: gs://releases.coder.com/coder-desktop/windows/appcast.xml.signature
160+
APPCAST_SIGNATURE_KEY_BASE64: ${{ secrets.APPCAST_SIGNATURE_KEY_BASE64 }}
161+
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
162+
123163
winget:
124164
runs-on: depot-windows-latest
125165
needs: release
@@ -177,7 +217,6 @@ jobs:
177217
# to GitHub and then making a PR in a different repo.
178218
WINGET_GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
179219

180-
181220
- name: Comment on PR
182221
run: |
183222
# wait 30 seconds

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,12 @@ publish
411411
*.wixmdb
412412
*.wixprj
413413
*.wixproj
414+
415+
appcast.xml
416+
appcast.xml.signature
417+
*.key
418+
*.key.*
419+
*.pem
420+
*.pem.*
421+
*.pub
422+
*.pub.*

App/Assets/changelog.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
77
Changes:
88
- Removed @media queries in favor of requiring `[data-theme]` attributes
9+
on the body themselves
10+
- Overrides `--bgColor-default` to transparent
911
*/
1012

1113
.markdown-body {
@@ -19,8 +21,10 @@
1921
--base-text-weight-semibold: 600;
2022
--fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
2123
--fgColor-accent: Highlight;
24+
25+
--bgColor-default: transparent !important;
2226
}
23-
.markdown-body[data-theme="dark"] {
27+
body[data-theme="dark"] .markdown-body {
2428
/* dark */
2529
color-scheme: dark;
2630
--focus-outlineColor: #1f6feb;
@@ -74,7 +78,7 @@
7478
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
7579
--color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d;
7680
}
77-
.markdown-body[data-theme="light"] {
81+
body[data-theme=""] .markdown-body {
7882
/* light */
7983
color-scheme: light;
8084
--focus-outlineColor: #0969da;

App/Services/UpdateController.cs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel.DataAnnotations;
4+
using System.Runtime.InteropServices;
45
using System.Threading;
56
using System.Threading.Tasks;
67
using Coder.Desktop.App.ViewModels;
@@ -43,7 +44,7 @@ public class UpdaterConfig
4344
public bool EnableUpdater { get; set; } = true;
4445
//[Required] public string UpdateAppCastUrl { get; set; } = "https://releases.coder.com/coder-desktop/windows/appcast.xml";
4546
[Required] public string UpdateAppCastUrl { get; set; } = "http://localhost:8000/appcast.xml";
46-
[Required] public string UpdatePublicKeyBase64 { get; set; } = "Uxc0ir6j3GMhkL5D1O/W3lsD4BNk5puwM9hohNfm32k=";
47+
[Required] public string UpdatePublicKeyBase64 { get; set; } = "NNWN4c+3PmMuAf2G1ERLlu0EwhzHfSiUugOt120hrH8=";
4748
public UpdateChannel? ForcedUpdateChannel { get; set; } = null;
4849
}
4950

@@ -83,8 +84,7 @@ public SparkleUpdateController(ILogger<SparkleUpdateController> logger, IOptions
8384
// Swift's Sparkle does not support verifying app cast signatures yet,
8485
// but we use this functionality on Windows for added security against
8586
// malicious release notes.
86-
// TODO: REENABLE STRICT CHECKING
87-
var checker = new Ed25519Checker(SecurityMode.Unsafe,
87+
var checker = new Ed25519Checker(SecurityMode.Strict,
8888
publicKey: _config.UpdatePublicKeyBase64,
8989
readFileBeingVerifiedInChunks: true);
9090

@@ -93,7 +93,7 @@ public SparkleUpdateController(ILogger<SparkleUpdateController> logger, IOptions
9393
// TODO: custom Configuration for persistence, could just specify
9494
// our own save path with JSONConfiguration TBH
9595
LogWriter = new CoderSparkleLogger(logger),
96-
AppCastHelper = new CoderSparkleAppCastHelper(logger, _config.ForcedUpdateChannel),
96+
AppCastHelper = new CoderSparkleAppCastHelper(_config.ForcedUpdateChannel),
9797
UIFactory = uiFactory,
9898
UseNotificationToast = uiFactory.CanShowToastMessages(),
9999
RelaunchAfterUpdate = true,
@@ -103,7 +103,7 @@ public SparkleUpdateController(ILogger<SparkleUpdateController> logger, IOptions
103103

104104
// TODO: user preference for automatic checking. Remember to
105105
// StopLoop/StartLoop if it changes.
106-
#if !DEBUG || true
106+
#if !DEBUG
107107
_ = _sparkle.StartLoop(true, UpdateCheckInterval);
108108
#endif
109109
}
@@ -157,22 +157,20 @@ public void PrintMessage(string message, params object[]? arguments)
157157
}
158158
}
159159

160-
public class CoderSparkleAppCastHelper : AppCastHelper
160+
public class CoderSparkleAppCastHelper(UpdateChannel? forcedChannel) : AppCastHelper
161161
{
162-
private readonly UpdateChannel? _forcedChannel;
163-
164-
public CoderSparkleAppCastHelper(ILogger<SparkleUpdateController> logger, UpdateChannel? forcedChannel) : base()
165-
{
166-
_forcedChannel = forcedChannel;
167-
}
162+
// This might return some other OS if the user compiled the app for some
163+
// different arch, but the end result is the same: no updates will be found
164+
// for that arch.
165+
private static string CurrentOperatingSystem => $"win-{RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant()}";
168166

169167
public override List<AppCastItem> FilterUpdates(List<AppCastItem> items)
170168
{
171169
items = base.FilterUpdates(items);
172170

173-
// TODO: factor in user choice too once we have a settings page
174-
var channel = _forcedChannel ?? UpdateChannel.Stable;
175-
return items.FindAll(i => i.Channel != null && i.Channel == channel.ChannelName());
171+
// TODO: factor in user channel choice too once we have a settings page
172+
var channel = forcedChannel ?? UpdateChannel.Stable;
173+
return items.FindAll(i => i.Channel == channel.ChannelName() && i.OperatingSystem == CurrentOperatingSystem);
176174
}
177175
}
178176

App/ViewModels/UpdaterUpdateAvailableViewModel.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ public async Task Changelog_Loaded(object sender, RoutedEventArgs e)
189189
settings.IsStatusBarEnabled = false;
190190

191191
// Hijack navigation to prevent links opening in the web view.
192-
// TODO: block new windows as well
193192
webView.CoreWebView2.NavigationStarting += (_, e) =>
194193
{
195194
// webView.NavigateToString uses data URIs, so allow those to work.
@@ -203,6 +202,14 @@ public async Task Changelog_Loaded(object sender, RoutedEventArgs e)
203202
if (Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri) && uri is { Scheme: "http" or "https" })
204203
Process.Start(new ProcessStartInfo(e.Uri) { UseShellExecute = true });
205204
};
205+
webView.CoreWebView2.NewWindowRequested += (_, e) =>
206+
{
207+
// Prevent new windows from being launched (e.g. target="_blank").
208+
e.Handled = true;
209+
// Launch HTTP or HTTPS URLs in the default browser.
210+
if (Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri) && uri is { Scheme: "http" or "https" })
211+
Process.Start(new ProcessStartInfo(e.Uri) { UseShellExecute = true });
212+
};
206213

207214
var html = await ChangelogHtml(CurrentItem);
208215
webView.NavigateToString(html);

scripts/Create-AppCastSigningKey.ps1

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# This is mostly just here for reference.
2+
#
3+
# Usage: Create-AppCastSigningKey.ps1 -outputKeyPath <path>
4+
param (
5+
[Parameter(Mandatory = $true)]
6+
[string] $outputKeyPath
7+
)
8+
9+
$ErrorActionPreference = "Stop"
10+
11+
& openssl.exe genpkey -algorithm ed25519 -out $outputKeyPath
12+
if ($LASTEXITCODE -ne 0) { throw "Failed to generate ED25519 private key" }
13+
14+
# Export the public key in DER format
15+
$pubKeyDerPath = "$outputKeyPath.pub.der"
16+
& openssl.exe pkey -in $outputKeyPath -pubout -outform DER -out $pubKeyDerPath
17+
if ($LASTEXITCODE -ne 0) { throw "Failed to export ED25519 public key" }
18+
19+
# Remove the DER header to get the actual key bytes
20+
$pubBytes = [System.IO.File]::ReadAllBytes($pubKeyDerPath)[-32..-1]
21+
Remove-Item $pubKeyDerPath
22+
23+
# Base64 encode and print
24+
Write-Output "NetSparkle formatted public key:"
25+
Write-Output ([Convert]::ToBase64String($pubBytes))
26+
Write-Output ""
27+
Write-Output "Private key written to $outputKeyPath"

scripts/Get-Mutagen.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ param (
55
[string] $arch
66
)
77

8+
$ErrorActionPreference = "Stop"
9+
810
function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
911
Write-Host "Downloading '$url' to '$outputPath'"
1012
# We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.

scripts/Get-WindowsAppSdk.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ param (
55
[string] $arch
66
)
77

8+
$ErrorActionPreference = "Stop"
9+
810
function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
911
Write-Host "Downloading '$url' to '$outputPath'"
1012
# We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.

0 commit comments

Comments
 (0)