diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b9810fd..cb7df6e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,6 +4,12 @@ on: push: tags: - '*' + workflow_dispatch: + inputs: + version: + description: 'Version number (e.g. 1.2.3)' + required: true + default: '1.2.3' permissions: contents: write @@ -20,42 +26,83 @@ jobs: with: dotnet-version: '8.0.x' + # Necessary for signing Windows binaries. + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + distribution: "zulu" + java-version: "11.0" + - name: Get version from tag id: version shell: pwsh run: | - $tag = $env:GITHUB_REF -replace 'refs/tags/','' + $ErrorActionPreference = "Stop" + if ($env:INPUT_VERSION) { + $tag = $env:INPUT_VERSION + } else { + $tag = $env:GITHUB_REF -replace 'refs/tags/','' + } if ($tag -notmatch '^v\d+\.\d+\.\d+$') { - throw "Tag must be in format v1.2.3" + throw "Version must be in format v1.2.3, got $tag" } $version = $tag -replace '^v','' - $assemblyVersion = "$version.0" - echo "VERSION=$version" >> $env:GITHUB_OUTPUT - echo "ASSEMBLY_VERSION=$assemblyVersion" >> $env:GITHUB_OUTPUT + $assemblyVersion = "$($version).0" + Add-Content -Path $env:GITHUB_OUTPUT -Value "VERSION=$version" + Add-Content -Path $env:GITHUB_OUTPUT -Value "ASSEMBLY_VERSION=$assemblyVersion" + Write-Host "Version: $version" + Write-Host "Assembly version: $assemblyVersion" + env: + INPUT_VERSION: ${{ inputs.version }} - - name: Build and publish x64 - run: | - dotnet publish src/App/App.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64 - dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64 + # Setup GCloud for signing Windows binaries. + - name: Authenticate to Google Cloud + id: gcloud_auth + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 + with: + workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }} + service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }} + token_format: "access_token" - - name: Build and publish arm64 - run: | - dotnet publish src/App/App.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64 - dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64 + - name: Setup GCloud SDK + uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - - name: Create ZIP archives + - name: scripts/Release.ps1 + id: release shell: pwsh run: | - Compress-Archive -Path "publish/x64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip" - Compress-Archive -Path "publish/arm64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip" + $ErrorActionPreference = "Stop" - - name: Create Release - uses: softprops/action-gh-release@v1 + $env:EV_CERTIFICATE_PATH = Join-Path $env:TEMP "ev_cert.pem" + $env:JSIGN_PATH = Join-Path $env:TEMP "jsign-6.0.jar" + Invoke-WebRequest -Uri "https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar" -OutFile $env:JSIGN_PATH + + & ./scripts/Release.ps1 ` + -version ${{ steps.version.outputs.VERSION }} ` + -assemblyVersion ${{ steps.version.outputs.ASSEMBLY_VERSION }} + if ($LASTEXITCODE -ne 0) { throw "Failed to publish" } + env: + EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }} + EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }} + EV_KEY: ${{ secrets.EV_KEY }} + EV_TSA_URL: ${{ secrets.EV_TSA_URL }} + GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: publish + path: .\publish\ + + - name: Create release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') with: - files: | - ./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip - ./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip name: Release ${{ steps.version.outputs.VERSION }} generate_release_notes: true + # We currently only release the bootstrappers, not the MSIs. + files: | + ${{ steps.release.outputs.X64_OUTPUT_PATH }} + ${{ steps.release.outputs.ARM64_OUTPUT_PATH }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1 index 55a2144..2bdc7d6 100644 --- a/scripts/Publish.ps1 +++ b/scripts/Publish.ps1 @@ -15,11 +15,68 @@ param ( [string] $outputPath = "", # defaults to "publish\CoderDesktop-$version-$arch.exe" [Parameter(Mandatory = $false)] - [switch] $keepBuildTemp = $false + [switch] $keepBuildTemp = $false, + + [Parameter(Mandatory = $false)] + [switch] $sign = $false +) + +$ErrorActionPreference = "Stop" + +$ourAssemblies = @( + "Coder Desktop.exe", + "Coder Desktop.dll", + "CoderVpnService.exe", + "CoderVpnService.dll", + + "Coder.Desktop.CoderSdk.dll", + "Coder.Desktop.Vpn.dll", + "Coder.Desktop.Vpn.Proto.dll" ) +function Find-Dependencies([string[]] $dependencies) { + foreach ($dependency in $dependencies) { + if (!(Get-Command $dependency -ErrorAction SilentlyContinue)) { + throw "Missing dependency: $dependency" + } + } +} + +function Find-EnvironmentVariables([string[]] $variables) { + foreach ($variable in $variables) { + if (!(Get-Item env:$variable -ErrorAction SilentlyContinue)) { + throw "Missing environment variable: $variable" + } + } +} + +if ($sign) { + Write-Host "Signing is enabled" + Find-Dependencies java + Find-EnvironmentVariables @("JSIGN_PATH", "EV_KEYSTORE", "EV_KEY", "EV_CERTIFICATE_PATH", "EV_TSA_URL", "GCLOUD_ACCESS_TOKEN") +} + +function Add-CoderSignature([string] $path) { + if (!$sign) { + Write-Host "Skipping signing $path" + return + } + + Write-Host "Signing $path" + & java.exe -jar $env:JSIGN_PATH ` + --storetype GOOGLECLOUD ` + --storepass $env:GCLOUD_ACCESS_TOKEN ` + --keystore $env:EV_KEYSTORE ` + --alias $env:EV_KEY ` + --certfile $env:EV_CERTIFICATE_PATH ` + --tsmode RFC3161 ` + --tsaurl $env:EV_TSA_URL ` + $path + if ($LASTEXITCODE -ne 0) { throw "Failed to sign $path" } +} + # CD to the root of the repo -$repoRoot = Join-Path $PSScriptRoot ".." +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") Push-Location $repoRoot if ($msiOutputPath -eq "") { @@ -48,11 +105,21 @@ New-Item -ItemType Directory -Path $buildPath -Force # Build in release mode $servicePublishDir = Join-Path $buildPath "service" -dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir +& dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir +if ($LASTEXITCODE -ne 0) { throw "Failed to build Vpn.Service" } # App needs to be built with msbuild $appPublishDir = Join-Path $buildPath "app" $msbuildBinary = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe +if ($LASTEXITCODE -ne 0) { throw "Failed to find MSBuild" } & $msbuildBinary .\App\App.csproj /p:Configuration=Release /p:Platform=$arch /p:OutputPath=$appPublishDir +if ($LASTEXITCODE -ne 0) { throw "Failed to build App" } + +# Find any files in the publish directory recursively that match any of our +# assemblies and sign them. +$toSign = Get-ChildItem -Path $buildPath -Recurse | Where-Object { $ourAssemblies -contains $_.Name } +foreach ($file in $toSign) { + Add-CoderSignature $file.FullName +} # Copy any additional files into the install directory Copy-Item "scripts\files\License.txt" $buildPath @@ -63,7 +130,7 @@ $wintunDllPath = Join-Path $vpnFilesPath "wintun.dll" Copy-Item "scripts\files\wintun-*-$($arch).dll" $wintunDllPath # Build the MSI installer -dotnet.exe run --project .\Installer\Installer.csproj -c Release -- ` +& dotnet.exe run --project .\Installer\Installer.csproj -c Release -- ` build-msi ` --arch $arch ` --version $version ` @@ -77,11 +144,11 @@ dotnet.exe run --project .\Installer\Installer.csproj -c Release -- ` --vpn-dir "vpn" ` --banner-bmp "scripts\files\WixUIBannerBmp.bmp" ` --dialog-bmp "scripts\files\WixUIDialogBmp.bmp" - -# TODO: sign the installer +if ($LASTEXITCODE -ne 0) { throw "Failed to build MSI" } +Add-CoderSignature $msiOutputPath # Build the bootstrapper -dotnet.exe run --project .\Installer\Installer.csproj -c Release -- ` +& dotnet.exe run --project .\Installer\Installer.csproj -c Release -- ` build-bootstrapper ` --arch $arch ` --version $version ` @@ -90,8 +157,8 @@ dotnet.exe run --project .\Installer\Installer.csproj -c Release -- ` --icon-file "App\coder.ico" ` --msi-path $msiOutputPath ` --logo-png "scripts\files\logo.png" - -# TODO: sign the bootstrapper +if ($LASTEXITCODE -ne 0) { throw "Failed to build bootstrapper" } +Add-CoderSignature $outputPath if (!$keepBuildTemp) { Remove-Item -Recurse -Force $buildPath diff --git a/scripts/Release.ps1 b/scripts/Release.ps1 new file mode 100644 index 0000000..40bb0fa --- /dev/null +++ b/scripts/Release.ps1 @@ -0,0 +1,48 @@ +# Usage: Release.ps1 -version +param ( + [Parameter(Mandatory = $true)] + [ValidatePattern("^\d+\.\d+\.\d+\.\d+$")] + [string] $version, + + [Parameter(Mandatory = $true)] + [ValidatePattern("^\d+\.\d+\.\d+\.\d+$")] + [string] $assemblyVersion +) + +$ErrorActionPreference = "Stop" + +foreach ($arch in @("x64", "arm64")) { + Write-Host "::group::Publishing $arch" + try { + $archUpper = $arch.ToUpper() + + $msiOutputPath = "publish/CoderDesktopCore-$version-$arch.msi" + Add-Content -Path $env:GITHUB_OUTPUT -Value "$($archUpper)_MSI_OUTPUT_PATH=$msiOutputPath" + Write-Host "MSI_OUTPUT_PATH=$msiOutputPath" + + $outputPath = "publish/CoderDesktop-$version-$arch.exe" + Add-Content -Path $env:GITHUB_OUTPUT -Value "$($archUpper)_OUTPUT_PATH=$outputPath" + Write-Host "OUTPUT_PATH=$outputPath" + + $publishScript = Join-Path $PSScriptRoot "Publish.ps1" + & $publishScript ` + -version $assemblyVersion ` + -arch $arch ` + -msiOutputPath $msiOutputPath ` + -outputPath $outputPath ` + -sign + if ($LASTEXITCODE -ne 0) { throw "Failed to publish" } + + # Verify that the output exe is authenticode signed + $sig = Get-AuthenticodeSignature $outputPath + if ($sig.Status -ne "Valid") { + throw "Output file is not authenticode signed" + } + else { + Write-Host "Output file is authenticode signed" + } + } + finally { + Write-Host "::endgroup::" + } +}