Skip to content

Commit 941b4b1

Browse files
authored
chore: installer CI (#33)
1 parent f1c116a commit 941b4b1

File tree

3 files changed

+192
-30
lines changed

3 files changed

+192
-30
lines changed

.github/workflows/release.yaml

+68-21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ on:
44
push:
55
tags:
66
- '*'
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: 'Version number (e.g. 1.2.3)'
11+
required: true
12+
default: '1.2.3'
713

814
permissions:
915
contents: write
@@ -20,42 +26,83 @@ jobs:
2026
with:
2127
dotnet-version: '8.0.x'
2228

29+
# Necessary for signing Windows binaries.
30+
- name: Setup Java
31+
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
32+
with:
33+
distribution: "zulu"
34+
java-version: "11.0"
35+
2336
- name: Get version from tag
2437
id: version
2538
shell: pwsh
2639
run: |
27-
$tag = $env:GITHUB_REF -replace 'refs/tags/',''
40+
$ErrorActionPreference = "Stop"
41+
if ($env:INPUT_VERSION) {
42+
$tag = $env:INPUT_VERSION
43+
} else {
44+
$tag = $env:GITHUB_REF -replace 'refs/tags/',''
45+
}
2846
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
29-
throw "Tag must be in format v1.2.3"
47+
throw "Version must be in format v1.2.3, got $tag"
3048
}
3149
$version = $tag -replace '^v',''
32-
$assemblyVersion = "$version.0"
33-
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
34-
echo "ASSEMBLY_VERSION=$assemblyVersion" >> $env:GITHUB_OUTPUT
50+
$assemblyVersion = "$($version).0"
51+
Add-Content -Path $env:GITHUB_OUTPUT -Value "VERSION=$version"
52+
Add-Content -Path $env:GITHUB_OUTPUT -Value "ASSEMBLY_VERSION=$assemblyVersion"
53+
Write-Host "Version: $version"
54+
Write-Host "Assembly version: $assemblyVersion"
55+
env:
56+
INPUT_VERSION: ${{ inputs.version }}
3557

36-
- name: Build and publish x64
37-
run: |
38-
dotnet publish src/App/App.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64
39-
dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64
58+
# Setup GCloud for signing Windows binaries.
59+
- name: Authenticate to Google Cloud
60+
id: gcloud_auth
61+
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
62+
with:
63+
workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
64+
service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
65+
token_format: "access_token"
4066

41-
- name: Build and publish arm64
42-
run: |
43-
dotnet publish src/App/App.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64
44-
dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64
67+
- name: Setup GCloud SDK
68+
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
4569

46-
- name: Create ZIP archives
70+
- name: scripts/Release.ps1
71+
id: release
4772
shell: pwsh
4873
run: |
49-
Compress-Archive -Path "publish/x64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip"
50-
Compress-Archive -Path "publish/arm64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip"
74+
$ErrorActionPreference = "Stop"
5175
52-
- name: Create Release
53-
uses: softprops/action-gh-release@v1
76+
$env:EV_CERTIFICATE_PATH = Join-Path $env:TEMP "ev_cert.pem"
77+
$env:JSIGN_PATH = Join-Path $env:TEMP "jsign-6.0.jar"
78+
Invoke-WebRequest -Uri "https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar" -OutFile $env:JSIGN_PATH
79+
80+
& ./scripts/Release.ps1 `
81+
-version ${{ steps.version.outputs.VERSION }} `
82+
-assemblyVersion ${{ steps.version.outputs.ASSEMBLY_VERSION }}
83+
if ($LASTEXITCODE -ne 0) { throw "Failed to publish" }
84+
env:
85+
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
86+
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
87+
EV_KEY: ${{ secrets.EV_KEY }}
88+
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
89+
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
90+
91+
- name: Upload artifact
92+
uses: actions/upload-artifact@v4
93+
with:
94+
name: publish
95+
path: .\publish\
96+
97+
- name: Create release
98+
uses: softprops/action-gh-release@v2
99+
if: startsWith(github.ref, 'refs/tags/')
54100
with:
55-
files: |
56-
./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip
57-
./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip
58101
name: Release ${{ steps.version.outputs.VERSION }}
59102
generate_release_notes: true
103+
# We currently only release the bootstrappers, not the MSIs.
104+
files: |
105+
${{ steps.release.outputs.X64_OUTPUT_PATH }}
106+
${{ steps.release.outputs.ARM64_OUTPUT_PATH }}
60107
env:
61108
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

scripts/Publish.ps1

+76-9
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,68 @@ param (
1515
[string] $outputPath = "", # defaults to "publish\CoderDesktop-$version-$arch.exe"
1616

1717
[Parameter(Mandatory = $false)]
18-
[switch] $keepBuildTemp = $false
18+
[switch] $keepBuildTemp = $false,
19+
20+
[Parameter(Mandatory = $false)]
21+
[switch] $sign = $false
22+
)
23+
24+
$ErrorActionPreference = "Stop"
25+
26+
$ourAssemblies = @(
27+
"Coder Desktop.exe",
28+
"Coder Desktop.dll",
29+
"CoderVpnService.exe",
30+
"CoderVpnService.dll",
31+
32+
"Coder.Desktop.CoderSdk.dll",
33+
"Coder.Desktop.Vpn.dll",
34+
"Coder.Desktop.Vpn.Proto.dll"
1935
)
2036

37+
function Find-Dependencies([string[]] $dependencies) {
38+
foreach ($dependency in $dependencies) {
39+
if (!(Get-Command $dependency -ErrorAction SilentlyContinue)) {
40+
throw "Missing dependency: $dependency"
41+
}
42+
}
43+
}
44+
45+
function Find-EnvironmentVariables([string[]] $variables) {
46+
foreach ($variable in $variables) {
47+
if (!(Get-Item env:$variable -ErrorAction SilentlyContinue)) {
48+
throw "Missing environment variable: $variable"
49+
}
50+
}
51+
}
52+
53+
if ($sign) {
54+
Write-Host "Signing is enabled"
55+
Find-Dependencies java
56+
Find-EnvironmentVariables @("JSIGN_PATH", "EV_KEYSTORE", "EV_KEY", "EV_CERTIFICATE_PATH", "EV_TSA_URL", "GCLOUD_ACCESS_TOKEN")
57+
}
58+
59+
function Add-CoderSignature([string] $path) {
60+
if (!$sign) {
61+
Write-Host "Skipping signing $path"
62+
return
63+
}
64+
65+
Write-Host "Signing $path"
66+
& java.exe -jar $env:JSIGN_PATH `
67+
--storetype GOOGLECLOUD `
68+
--storepass $env:GCLOUD_ACCESS_TOKEN `
69+
--keystore $env:EV_KEYSTORE `
70+
--alias $env:EV_KEY `
71+
--certfile $env:EV_CERTIFICATE_PATH `
72+
--tsmode RFC3161 `
73+
--tsaurl $env:EV_TSA_URL `
74+
$path
75+
if ($LASTEXITCODE -ne 0) { throw "Failed to sign $path" }
76+
}
77+
2178
# CD to the root of the repo
22-
$repoRoot = Join-Path $PSScriptRoot ".."
79+
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
2380
Push-Location $repoRoot
2481

2582
if ($msiOutputPath -eq "") {
@@ -48,11 +105,21 @@ New-Item -ItemType Directory -Path $buildPath -Force
48105

49106
# Build in release mode
50107
$servicePublishDir = Join-Path $buildPath "service"
51-
dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir
108+
& dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir
109+
if ($LASTEXITCODE -ne 0) { throw "Failed to build Vpn.Service" }
52110
# App needs to be built with msbuild
53111
$appPublishDir = Join-Path $buildPath "app"
54112
$msbuildBinary = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe
113+
if ($LASTEXITCODE -ne 0) { throw "Failed to find MSBuild" }
55114
& $msbuildBinary .\App\App.csproj /p:Configuration=Release /p:Platform=$arch /p:OutputPath=$appPublishDir
115+
if ($LASTEXITCODE -ne 0) { throw "Failed to build App" }
116+
117+
# Find any files in the publish directory recursively that match any of our
118+
# assemblies and sign them.
119+
$toSign = Get-ChildItem -Path $buildPath -Recurse | Where-Object { $ourAssemblies -contains $_.Name }
120+
foreach ($file in $toSign) {
121+
Add-CoderSignature $file.FullName
122+
}
56123

57124
# Copy any additional files into the install directory
58125
Copy-Item "scripts\files\License.txt" $buildPath
@@ -63,7 +130,7 @@ $wintunDllPath = Join-Path $vpnFilesPath "wintun.dll"
63130
Copy-Item "scripts\files\wintun-*-$($arch).dll" $wintunDllPath
64131

65132
# Build the MSI installer
66-
dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
133+
& dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
67134
build-msi `
68135
--arch $arch `
69136
--version $version `
@@ -77,11 +144,11 @@ dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
77144
--vpn-dir "vpn" `
78145
--banner-bmp "scripts\files\WixUIBannerBmp.bmp" `
79146
--dialog-bmp "scripts\files\WixUIDialogBmp.bmp"
80-
81-
# TODO: sign the installer
147+
if ($LASTEXITCODE -ne 0) { throw "Failed to build MSI" }
148+
Add-CoderSignature $msiOutputPath
82149

83150
# Build the bootstrapper
84-
dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
151+
& dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
85152
build-bootstrapper `
86153
--arch $arch `
87154
--version $version `
@@ -90,8 +157,8 @@ dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
90157
--icon-file "App\coder.ico" `
91158
--msi-path $msiOutputPath `
92159
--logo-png "scripts\files\logo.png"
93-
94-
# TODO: sign the bootstrapper
160+
if ($LASTEXITCODE -ne 0) { throw "Failed to build bootstrapper" }
161+
Add-CoderSignature $outputPath
95162

96163
if (!$keepBuildTemp) {
97164
Remove-Item -Recurse -Force $buildPath

scripts/Release.ps1

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Usage: Release.ps1 -version <version>
2+
param (
3+
[Parameter(Mandatory = $true)]
4+
[ValidatePattern("^\d+\.\d+\.\d+\.\d+$")]
5+
[string] $version,
6+
7+
[Parameter(Mandatory = $true)]
8+
[ValidatePattern("^\d+\.\d+\.\d+\.\d+$")]
9+
[string] $assemblyVersion
10+
)
11+
12+
$ErrorActionPreference = "Stop"
13+
14+
foreach ($arch in @("x64", "arm64")) {
15+
Write-Host "::group::Publishing $arch"
16+
try {
17+
$archUpper = $arch.ToUpper()
18+
19+
$msiOutputPath = "publish/CoderDesktopCore-$version-$arch.msi"
20+
Add-Content -Path $env:GITHUB_OUTPUT -Value "$($archUpper)_MSI_OUTPUT_PATH=$msiOutputPath"
21+
Write-Host "MSI_OUTPUT_PATH=$msiOutputPath"
22+
23+
$outputPath = "publish/CoderDesktop-$version-$arch.exe"
24+
Add-Content -Path $env:GITHUB_OUTPUT -Value "$($archUpper)_OUTPUT_PATH=$outputPath"
25+
Write-Host "OUTPUT_PATH=$outputPath"
26+
27+
$publishScript = Join-Path $PSScriptRoot "Publish.ps1"
28+
& $publishScript `
29+
-version $assemblyVersion `
30+
-arch $arch `
31+
-msiOutputPath $msiOutputPath `
32+
-outputPath $outputPath `
33+
-sign
34+
if ($LASTEXITCODE -ne 0) { throw "Failed to publish" }
35+
36+
# Verify that the output exe is authenticode signed
37+
$sig = Get-AuthenticodeSignature $outputPath
38+
if ($sig.Status -ne "Valid") {
39+
throw "Output file is not authenticode signed"
40+
}
41+
else {
42+
Write-Host "Output file is authenticode signed"
43+
}
44+
}
45+
finally {
46+
Write-Host "::endgroup::"
47+
}
48+
}

0 commit comments

Comments
 (0)