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."