teamsiso/.forgejo/workflows/release.yml

215 lines
9.2 KiB
YAML

name: Release
# Triggered by pushing an annotated tag of the form v1.2.3 (or any v-prefixed
# semver). The job runs on a Windows runner because building the WiX MSI
# requires the WiX SDK which is supported on Windows; the engine + console
# can in principle be built on Linux, but for simplicity we do everything in
# one job here so the publish output paths line up for the installer.
on:
push:
tags:
- 'v*.*.*'
jobs:
build-msi:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # tags + full history needed to derive the version
- name: Derive version from tag
id: ver
shell: pwsh
run: |
# GITHUB_REF is refs/tags/vX.Y.Z; strip the prefix.
$tag = "${env:GITHUB_REF}".Replace('refs/tags/', '')
$version = $tag.TrimStart('v')
"tag=$tag" >> $env:GITHUB_OUTPUT
"version=$version" >> $env:GITHUB_OUTPUT
Write-Host "Building version $version (tag $tag)"
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
# Probe for the signing cert up front and expose a step output downstream
# steps can use in their `if:` guards. We can't reference `secrets.*`
# directly from `if:` (Forgejo/GitHub policy), so we set a dummy env
# variable from the secret and check whether it's non-empty here.
- name: Detect signing configuration
id: signcfg
shell: pwsh
env:
PFX_PROBE: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
run: |
if ($env:PFX_PROBE) {
"enabled=true" >> $env:GITHUB_OUTPUT
Write-Host "Code-signing: ENABLED (cert secret detected)."
} else {
"enabled=false" >> $env:GITHUB_OUTPUT
Write-Host "Code-signing: DISABLED (SIGN_CERT_PFX_BASE64 not set). Build will produce unsigned binaries."
}
- name: Restore (Windows solution filter)
run: dotnet restore TeamsISO.Windows.slnf
- name: Build (Release, treat warnings as errors)
run: dotnet build TeamsISO.Windows.slnf --configuration Release --no-restore /p:Version=${{ steps.ver.outputs.version }}
- name: Run unit tests (excluding requires=ndi)
run: >
dotnet test TeamsISO.Windows.slnf
--configuration Release
--no-build
--filter "Category!=ndi&requires!=ndi"
- name: Publish TeamsISO.App (framework-dependent, win-x64)
run: >
dotnet publish src/TeamsISO.App/TeamsISO.App.csproj
--configuration Release
--runtime win-x64
--self-contained false
--output publish/TeamsISO
/p:Version=${{ steps.ver.outputs.version }}
- name: Publish TeamsISO.Console (framework-dependent, win-x64)
run: >
dotnet publish src/TeamsISO.Console/TeamsISO.Console.csproj
--configuration Release
--runtime win-x64
--self-contained false
--output publish/TeamsISO-Console
/p:Version=${{ steps.ver.outputs.version }}
# Code-sign the WPF .exe BEFORE the MSI is built, so the MSI's embedded
# binaries are signed too. Skipped silently when the signing secrets
# aren't configured — that's the default state and keeps unsigned builds
# working unchanged.
#
# To enable signing, set both Forgejo Actions secrets:
# SIGN_CERT_PFX_BASE64 — base64 of your code-signing PFX file
# ( certutil -encode in.pfx out.b64; strip BEGIN/END lines )
# SIGN_CERT_PASSWORD — the PFX password
# Optionally:
# SIGN_TIMESTAMP_URL — RFC 3161 timestamp server (default: digicert)
- name: Sign TeamsISO.exe (optional, skipped if no cert)
if: ${{ steps.signcfg.outputs.enabled == 'true' }}
env:
SIGN_CERT_PFX_BASE64: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
SIGN_CERT_PASSWORD: ${{ secrets.SIGN_CERT_PASSWORD }}
SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
shell: pwsh
run: |
$tsUrl = if ($env:SIGN_TIMESTAMP_URL) { $env:SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
$pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx'
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGN_CERT_PFX_BASE64))
$signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter signtool.exe `
| Where-Object { $_.FullName -match '\\x64\\' } `
| Select-Object -First 1
if (-not $signtool) { throw 'signtool.exe not found on runner' }
& $signtool.FullName sign `
/f $pfxPath `
/p $env:SIGN_CERT_PASSWORD `
/fd SHA256 `
/td SHA256 `
/tr $tsUrl `
'publish/TeamsISO/TeamsISO.exe'
if ($LASTEXITCODE -ne 0) { throw "signtool failed on TeamsISO.exe (exit $LASTEXITCODE)" }
Remove-Item $pfxPath -Force
- name: Build MSI installer
run: >
dotnet build installer/TeamsISO.Installer.wixproj
--configuration Release
/p:Version=${{ steps.ver.outputs.version }}
- name: Locate MSI
id: msi
shell: pwsh
run: |
$msi = Get-ChildItem -Path installer/bin -Recurse -Filter '*.msi' | Select-Object -First 1
if (-not $msi) { throw "No MSI produced under installer/bin." }
"path=$($msi.FullName)" >> $env:GITHUB_OUTPUT
"name=$($msi.Name)" >> $env:GITHUB_OUTPUT
Write-Host "MSI: $($msi.FullName) ($($msi.Length) bytes)"
# Sign the produced MSI itself. Same gate as exe signing — runs only if
# the cert secret is set. Splitting the two stages means the inner exe
# is signed before being embedded, AND the wrapping MSI carries its own
# signature for SmartScreen.
- name: Sign MSI (optional, skipped if no cert)
if: ${{ steps.signcfg.outputs.enabled == 'true' }}
env:
SIGN_CERT_PFX_BASE64: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
SIGN_CERT_PASSWORD: ${{ secrets.SIGN_CERT_PASSWORD }}
SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
MSI_PATH: ${{ steps.msi.outputs.path }}
shell: pwsh
run: |
$tsUrl = if ($env:SIGN_TIMESTAMP_URL) { $env:SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
$pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx'
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGN_CERT_PFX_BASE64))
$signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter signtool.exe `
| Where-Object { $_.FullName -match '\\x64\\' } `
| Select-Object -First 1
& $signtool.FullName sign `
/f $pfxPath `
/p $env:SIGN_CERT_PASSWORD `
/fd SHA256 `
/td SHA256 `
/tr $tsUrl `
$env:MSI_PATH
if ($LASTEXITCODE -ne 0) { throw "signtool failed on MSI (exit $LASTEXITCODE)" }
Remove-Item $pfxPath -Force
- name: Upload MSI as workflow artifact
uses: actions/upload-artifact@v3
with:
name: ${{ steps.msi.outputs.name }}
path: ${{ steps.msi.outputs.path }}
# Forgejo doesn't ship a stable upload-release-asset action, so we use
# the REST API directly. This: (1) finds the release that the tag push
# auto-created, (2) uploads the MSI as an asset on it. Requires that
# the repo's "Create a release on tag push" setting is on, OR that the
# release was created beforehand. If no release exists, we create one.
- name: Attach MSI to release
shell: pwsh
env:
FORGE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FORGE_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ steps.ver.outputs.tag }}
MSI_PATH: ${{ steps.msi.outputs.path }}
MSI_NAME: ${{ steps.msi.outputs.name }}
run: |
$headers = @{ Authorization = "token $env:FORGE_TOKEN" }
# Find the release for this tag.
try {
$release = Invoke-RestMethod -Method Get `
-Uri "$env:FORGE_API/releases/tags/$env:TAG" -Headers $headers
} catch {
Write-Host "No release found for $env:TAG; creating one."
$body = @{
tag_name = $env:TAG
name = "TeamsISO $env:TAG"
body = "Automated build from tag $env:TAG."
draft = $false
prerelease = $env:TAG -match '-(alpha|beta|rc)'
} | ConvertTo-Json
$release = Invoke-RestMethod -Method Post `
-Uri "$env:FORGE_API/releases" -Headers $headers `
-ContentType 'application/json' -Body $body
}
# Upload the MSI as an asset.
$uploadUri = "$env:FORGE_API/releases/$($release.id)/assets?name=$env:MSI_NAME"
curl.exe -fSL `
-H "Authorization: token $env:FORGE_TOKEN" `
-H "Content-Type: application/octet-stream" `
--upload-file "$env:MSI_PATH" `
"$uploadUri"
Write-Host "Asset $env:MSI_NAME attached to release $env:TAG."