ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / package-preview (push) Canceled after 0s
ci / package-installer (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
204 lines
9.4 KiB
YAML
204 lines
9.4 KiB
YAML
name: release-windows
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*.*.*'
|
|
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
attestations: write
|
|
|
|
concurrency:
|
|
group: windows-production-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: windows-latest
|
|
environment: windows-production
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
cache-dependency-path: client/web/package-lock.json
|
|
- name: Install web dependencies
|
|
working-directory: client/web
|
|
run: npm ci
|
|
- name: Validate release tag and repository version
|
|
id: version
|
|
shell: pwsh
|
|
run: |
|
|
$metadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json
|
|
if ($LASTEXITCODE -ne 0) { throw 'Unable to read Cargo workspace metadata' }
|
|
$package = $metadata.packages | Where-Object name -eq 'remotedesk-control-service'
|
|
if (-not $package) { throw 'remotedesk-control-service package metadata is missing' }
|
|
$version = [string]$package.version
|
|
if ($version -notmatch '^\d+\.\d+\.\d+$') { throw "Invalid release version: $version" }
|
|
if ($env:GITHUB_REF_NAME -ne "v$version") {
|
|
throw "Tag $env:GITHUB_REF_NAME does not match workspace version $version"
|
|
}
|
|
"version=$version" >> $env:GITHUB_OUTPUT
|
|
- name: Import protected signing material
|
|
id: signing
|
|
shell: pwsh
|
|
env:
|
|
WINDOWS_SIGNING_PFX_BASE64: ${{ secrets.WINDOWS_SIGNING_PFX_BASE64 }}
|
|
WINDOWS_SIGNING_PFX_PASSWORD: ${{ secrets.WINDOWS_SIGNING_PFX_PASSWORD }}
|
|
UPDATE_ED25519_PRIVATE_KEY_PEM: ${{ secrets.UPDATE_ED25519_PRIVATE_KEY_PEM }}
|
|
UPDATE_ED25519_PUBLIC_KEY_BASE64: ${{ secrets.UPDATE_ED25519_PUBLIC_KEY_BASE64 }}
|
|
run: |
|
|
foreach ($name in @(
|
|
'WINDOWS_SIGNING_PFX_BASE64',
|
|
'WINDOWS_SIGNING_PFX_PASSWORD',
|
|
'UPDATE_ED25519_PRIVATE_KEY_PEM',
|
|
'UPDATE_ED25519_PUBLIC_KEY_BASE64'
|
|
)) {
|
|
if ([string]::IsNullOrWhiteSpace((Get-Item "Env:$name").Value)) {
|
|
throw "Required production secret $name is not configured"
|
|
}
|
|
}
|
|
$pfxPath = Join-Path $env:RUNNER_TEMP "remotedesk-signing-$PID.pfx"
|
|
$keyPath = Join-Path $env:RUNNER_TEMP "remotedesk-update-$PID.pem"
|
|
try {
|
|
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:WINDOWS_SIGNING_PFX_BASE64))
|
|
$password = ConvertTo-SecureString $env:WINDOWS_SIGNING_PFX_PASSWORD -AsPlainText -Force
|
|
$imported = @(Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation Cert:\CurrentUser\My -Password $password)
|
|
$certificate = $imported | Where-Object HasPrivateKey | Select-Object -First 1
|
|
if (-not $certificate) { throw 'PFX did not import a certificate with a private key' }
|
|
[IO.File]::WriteAllText($keyPath, $env:UPDATE_ED25519_PRIVATE_KEY_PEM, [Text.UTF8Encoding]::new($false))
|
|
"thumbprint=$($certificate.Thumbprint)" >> $env:GITHUB_OUTPUT
|
|
"key_path=$keyPath" >> $env:GITHUB_OUTPUT
|
|
} finally {
|
|
if (Test-Path -LiteralPath $pfxPath) { Remove-Item -LiteralPath $pfxPath -Force }
|
|
}
|
|
- name: Build, sign and verify Windows installer
|
|
shell: pwsh
|
|
run: |
|
|
./packaging/windows/package-installer.ps1 `
|
|
-SigningCertificateThumbprint '${{ steps.signing.outputs.thumbprint }}' `
|
|
-TimestampUrl 'https://timestamp.digicert.com' `
|
|
-ReleaseChannel stable
|
|
./packaging/windows-host/package-host.ps1 `
|
|
-SigningCertificateThumbprint '${{ steps.signing.outputs.thumbprint }}' `
|
|
-TimestampUrl 'https://timestamp.digicert.com'
|
|
- name: Create signed update manifest
|
|
id: manifest
|
|
shell: pwsh
|
|
env:
|
|
EXPECTED_UPDATE_PUBLIC_KEY: ${{ secrets.UPDATE_ED25519_PUBLIC_KEY_BASE64 }}
|
|
run: |
|
|
$version = '${{ steps.version.outputs.version }}'
|
|
$installer = Get-ChildItem -LiteralPath artifacts -Filter "RemoteDesk-M0-$version-*-windows-x64.msi" -File
|
|
if (@($installer).Count -ne 1) { throw 'Expected exactly one versioned x64 MSI' }
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $installer.FullName
|
|
if ([string]$signature.Status -ne 'Valid' -or
|
|
$signature.SignerCertificate.Thumbprint -ne '${{ steps.signing.outputs.thumbprint }}') {
|
|
throw 'Final MSI Authenticode verification failed before manifest generation'
|
|
}
|
|
$hostInstaller = Get-ChildItem -LiteralPath artifacts -Filter "RemoteDesk-Host-$version-*-windows-x64.msi" -File
|
|
if (@($hostInstaller).Count -ne 1) { throw 'Expected exactly one versioned x64 Host MSI' }
|
|
$hostSignature = Get-AuthenticodeSignature -LiteralPath $hostInstaller.FullName
|
|
if ([string]$hostSignature.Status -ne 'Valid' -or
|
|
$hostSignature.SignerCertificate.Thumbprint -ne '${{ steps.signing.outputs.thumbprint }}') {
|
|
throw 'Final Host MSI Authenticode verification failed before publication'
|
|
}
|
|
$url = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/$env:GITHUB_REF_NAME/$($installer.Name)"
|
|
node ./packaging/windows/create-update-manifest.mjs `
|
|
--installer $installer.FullName `
|
|
--installer-url $url `
|
|
--private-key '${{ steps.signing.outputs.key_path }}' `
|
|
--version $version `
|
|
--channel stable `
|
|
--target windows-x64 `
|
|
--output ./artifacts/stable.json `
|
|
--public-key-output ./artifacts/update-public-key.txt
|
|
if ($LASTEXITCODE -ne 0) { throw 'Update manifest generation failed' }
|
|
$actualKey = (Get-Content -LiteralPath ./artifacts/update-public-key.txt -Raw).Trim()
|
|
if ($actualKey -cne $env:EXPECTED_UPDATE_PUBLIC_KEY.Trim()) {
|
|
throw 'Generated update public key does not match the protected expected public key'
|
|
}
|
|
"installer=$($installer.FullName)" >> $env:GITHUB_OUTPUT
|
|
"host_installer=$($hostInstaller.FullName)" >> $env:GITHUB_OUTPUT
|
|
- name: Generate SPDX SBOM
|
|
uses: anchore/sbom-action@v0
|
|
with:
|
|
path: .
|
|
format: spdx-json
|
|
output-file: artifacts/remotedesk-${{ steps.version.outputs.version }}.spdx.json
|
|
upload-artifact: false
|
|
- name: Attest final Windows installers
|
|
uses: actions/attest-build-provenance@v2
|
|
with:
|
|
subject-path: |
|
|
${{ steps.manifest.outputs.installer }}
|
|
${{ steps.manifest.outputs.host_installer }}
|
|
- name: Upload immutable workflow artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: remotedesk-windows-${{ steps.version.outputs.version }}-signed
|
|
path: |
|
|
${{ steps.manifest.outputs.installer }}
|
|
${{ steps.manifest.outputs.host_installer }}
|
|
artifacts/INSTALLER-SHA256SUMS.txt
|
|
artifacts/HOST-INSTALLER-SHA256SUMS.txt
|
|
artifacts/stable.json
|
|
artifacts/update-public-key.txt
|
|
artifacts/remotedesk-${{ steps.version.outputs.version }}.spdx.json
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
- name: Publish GitHub release assets
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
gh release create $env:GITHUB_REF_NAME `
|
|
'${{ steps.manifest.outputs.installer }}' `
|
|
'${{ steps.manifest.outputs.host_installer }}' `
|
|
./artifacts/INSTALLER-SHA256SUMS.txt `
|
|
./artifacts/HOST-INSTALLER-SHA256SUMS.txt `
|
|
./artifacts/stable.json `
|
|
./artifacts/update-public-key.txt `
|
|
./artifacts/remotedesk-${{ steps.version.outputs.version }}.spdx.json `
|
|
--verify-tag `
|
|
--generate-notes `
|
|
--title "RemoteDesk ${{ steps.version.outputs.version }}"
|
|
- name: Publish stable update channel
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
$channelTag = 'stable-channel'
|
|
gh release view $channelTag *> $null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
gh release create $channelTag `
|
|
--target $env:GITHUB_SHA `
|
|
--title 'RemoteDesk stable update channel' `
|
|
--notes 'Machine-readable signed update channel. Installers remain immutable on versioned releases.' `
|
|
--prerelease
|
|
if ($LASTEXITCODE -ne 0) { throw 'Unable to create stable update channel' }
|
|
}
|
|
gh release upload $channelTag `
|
|
./artifacts/stable.json `
|
|
./artifacts/update-public-key.txt `
|
|
--clobber
|
|
if ($LASTEXITCODE -ne 0) { throw 'Unable to update stable channel assets' }
|
|
- name: Remove signing material
|
|
if: always()
|
|
shell: pwsh
|
|
run: |
|
|
$thumbprint = '${{ steps.signing.outputs.thumbprint }}'
|
|
if ($thumbprint -match '^[0-9A-Fa-f]{40}$') {
|
|
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$thumbprint" -Force -ErrorAction SilentlyContinue
|
|
}
|
|
$keyPath = '${{ steps.signing.outputs.key_path }}'
|
|
if ($keyPath -and (Test-Path -LiteralPath $keyPath)) {
|
|
Remove-Item -LiteralPath $keyPath -Force
|
|
}
|