Files
RemoteDesk/.github/workflows/release-windows.yml
T
曾志威 5db6b9ef68
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
Initial commit
2026-08-14 00:35:42 +08:00

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
}