From f07aad1c6aaf3495971a5e0995d1dc6bac9d833e Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 8 May 2026 00:48:57 -0400 Subject: [PATCH] ci(forgejo): release workflow on tag push -> MSI artifact + release asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .forgejo/workflows/release.yml triggers on annotated tag pushes matching v*.*.*. The workflow runs on a Windows runner (required for WiX MSI), restores and builds the Windows solution filter, runs unit tests (skipping the requires=ndi tier — CI runners don't have NDI), publishes TeamsISO.App + TeamsISO.Console for win-x64 framework-dependent, builds the WiX MSI scaffold, and uploads the MSI both as a workflow artifact (downloadable from the run page) and as an asset on the auto-created Release for the tag. Tag version is parsed from refs/tags/vX.Y.Z and threaded into /p:Version on every dotnet build invocation so the publish output, the assembly metadata, and the MSI ProductVersion all agree. Release-asset upload uses the Forgejo REST API directly via curl + Invoke-RestMethod rather than depending on a third-party action; if the auto-create-release-on-tag-push setting is off, the workflow creates the release itself. Pre-release flag is set when the tag contains -alpha/-beta/-rc. docs/RELEASING.md walks through cutting a release and flags the code-signing TODO (SignOutput property is wired but no cert; SmartScreen will warn on first launch until that lands). --- .forgejo/workflows/release.yml | 132 +++++++++++++++++++++++++++++++++ docs/RELEASING.md | 50 +++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 .forgejo/workflows/release.yml create mode 100644 docs/RELEASING.md diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..6e3f019 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,132 @@ +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 + + - 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 }} + + - 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)" + + - 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." diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..dc314da --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,50 @@ +# Releasing TeamsISO + +The release workflow at `.forgejo/workflows/release.yml` runs on **annotated tag pushes +matching `v*.*.*`**. It builds, tests, publishes, packages an MSI, and uploads the +MSI as a release asset. + +## Prerequisites + +- A **Windows runner** registered to this Forgejo instance. WiX MSI builds require + Windows; the existing CI runs on Linux for unit tests, but releases need a + separate Windows runner. Register one with `forgejo-runner register` against a + Windows host that has the .NET 8 SDK + WiX SDK access (the WiX SDK pulls itself + via NuGet at build time, so no separate install). +- The repository's **Create release on tag push** setting on (default), or skip it — + the workflow will create the release if one doesn't exist. + +## Cutting a release + +```sh +# Bump the version in Directory.Build.props if you haven't already. +git tag -a v1.0.0 -m "TeamsISO 1.0.0" +git push origin v1.0.0 +``` + +The workflow will: + +1. Restore + build `TeamsISO.Windows.slnf` in Release with the tag's version. +2. Run unit tests (the `requires=ndi` integration tier is skipped — it needs a + real NDI runtime which a CI runner won't have). +3. Publish `TeamsISO.App` and `TeamsISO.Console` for `win-x64`, + framework-dependent (.NET 8 Desktop runtime is the user's responsibility). +4. Build `installer/TeamsISO.Installer.wixproj`, producing + `TeamsISO-Setup-.msi`. +5. Upload the MSI as a workflow artifact (downloadable from the run page). +6. Attach the MSI to the GitHub-style Release for the tag, creating the release + first if it doesn't exist. Pre-release flag is set automatically when the + tag contains `-alpha`, `-beta`, or `-rc`. + +## Code signing (TODO) + +The `wixproj` has a `SignOutput` property hook but no actual cert wiring. For a +v1.0 release, sign the MSI with an EV cert before publishing: + +1. Add a `SIGNING_CERT_BASE64` and `SIGNING_CERT_PASSWORD` to repo Secrets. +2. Decode the cert into the runner's cert store at the start of the workflow. +3. Set `/p:SignOutput=true` on the `dotnet build` of the wixproj and configure + `signtool` invocation (the installer project will need a custom target). + +Until that lands, downstream users will see the standard Windows SmartScreen +warning on first launch — annoying but not blocking for early adopters.