Add tag-driven MSIX release workflow
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Push a tag like v1.2.3 (or v1.2.3.4) to build signed MSIX packages and
|
||||||
|
# publish a GitHub Release. The tag is the source of truth for the version:
|
||||||
|
# it is stamped into Package.appxmanifest at build time, so you don't need to
|
||||||
|
# edit the version by hand before releasing.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build MSIX (${{ matrix.platform }})
|
||||||
|
runs-on: windows-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [x64, arm64]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Derive version from tag
|
||||||
|
id: version
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$tag = "${{ github.ref_name }}"
|
||||||
|
$ver = $tag.TrimStart('v')
|
||||||
|
if ($ver -notmatch '^\d+\.\d+\.\d+(\.\d+)?$') {
|
||||||
|
throw "Tag '$tag' is not a valid version. Expected vMAJOR.MINOR.PATCH (optionally .REVISION)."
|
||||||
|
}
|
||||||
|
if (($ver.Split('.')).Count -eq 3) { $ver = "$ver.0" }
|
||||||
|
"version=$ver" >> $env:GITHUB_OUTPUT
|
||||||
|
Write-Host "Releasing version $ver from tag $tag"
|
||||||
|
|
||||||
|
- name: Stamp version into Package.appxmanifest
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$path = "Wingnal/Package.appxmanifest"
|
||||||
|
[xml]$xml = Get-Content $path
|
||||||
|
$xml.Package.Identity.Version = "${{ steps.version.outputs.version }}"
|
||||||
|
$xml.Save((Resolve-Path $path).Path)
|
||||||
|
Write-Host "Manifest Identity Version set to ${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
- name: Setup MSBuild
|
||||||
|
uses: microsoft/setup-msbuild@v2
|
||||||
|
|
||||||
|
- name: Prepare signing certificate
|
||||||
|
id: cert
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
SIGNING_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE }}
|
||||||
|
SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
$pfx = Join-Path $env:RUNNER_TEMP 'wingnal-signing.pfx'
|
||||||
|
if ($env:SIGNING_CERTIFICATE) {
|
||||||
|
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:SIGNING_CERTIFICATE))
|
||||||
|
$pwd = $env:SIGNING_CERTIFICATE_PASSWORD
|
||||||
|
Write-Host "Signing with certificate from repository secrets."
|
||||||
|
"selfsigned=false" >> $env:GITHUB_OUTPUT
|
||||||
|
} else {
|
||||||
|
Write-Host "::warning::No SIGNING_CERTIFICATE secret set - generating a temporary self-signed certificate. Users must install the attached Wingnal.cer into 'Trusted People' before they can sideload the MSIX. See RELEASING.md to configure a persistent certificate."
|
||||||
|
$pwd = [guid]::NewGuid().ToString('N')
|
||||||
|
$cert = New-SelfSignedCertificate `
|
||||||
|
-Type Custom `
|
||||||
|
-Subject "CN=micro" `
|
||||||
|
-KeyUsage DigitalSignature `
|
||||||
|
-FriendlyName "Wingnal CI (self-signed)" `
|
||||||
|
-CertStoreLocation "Cert:\CurrentUser\My" `
|
||||||
|
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
|
||||||
|
$sec = ConvertTo-SecureString -String $pwd -Force -AsPlainText
|
||||||
|
Export-PfxCertificate -Cert $cert -FilePath $pfx -Password $sec | Out-Null
|
||||||
|
Export-Certificate -Cert $cert -FilePath (Join-Path $env:RUNNER_TEMP 'Wingnal.cer') | Out-Null
|
||||||
|
"selfsigned=true" >> $env:GITHUB_OUTPUT
|
||||||
|
}
|
||||||
|
"pfx=$pfx" >> $env:GITHUB_OUTPUT
|
||||||
|
"password=$pwd" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build & sign MSIX
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
msbuild Wingnal/Wingnal.csproj `
|
||||||
|
/restore `
|
||||||
|
/p:Configuration=Release `
|
||||||
|
/p:Platform=${{ matrix.platform }} `
|
||||||
|
/p:UapAppxPackageBuildMode=SideloadOnly `
|
||||||
|
/p:AppxBundle=Never `
|
||||||
|
/p:GenerateAppxPackageOnBuild=true `
|
||||||
|
/p:AppxPackageSigningEnabled=true `
|
||||||
|
/p:PackageCertificateKeyFile="${{ steps.cert.outputs.pfx }}" `
|
||||||
|
/p:PackageCertificatePassword="${{ steps.cert.outputs.password }}" `
|
||||||
|
/p:AppxPackageDir="${{ github.workspace }}\artifacts\msix\\"
|
||||||
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
id: collect
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$out = Join-Path $env:GITHUB_WORKSPACE 'artifacts\release'
|
||||||
|
New-Item -ItemType Directory -Force -Path $out | Out-Null
|
||||||
|
$msix = Get-ChildItem -Path (Join-Path $env:GITHUB_WORKSPACE 'artifacts\msix') -Recurse -Include *.msix, *.msixbundle |
|
||||||
|
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||||
|
if (-not $msix) { throw "No MSIX package was produced." }
|
||||||
|
$ext = $msix.Extension
|
||||||
|
$dest = Join-Path $out "Wingnal-${{ steps.version.outputs.version }}-${{ matrix.platform }}$ext"
|
||||||
|
Copy-Item $msix.FullName $dest -Force
|
||||||
|
Write-Host "Packaged $dest"
|
||||||
|
if ('${{ steps.cert.outputs.selfsigned }}' -eq 'true' -and '${{ matrix.platform }}' -eq 'x64') {
|
||||||
|
Copy-Item (Join-Path $env:RUNNER_TEMP 'Wingnal.cer') (Join-Path $out 'Wingnal.cer') -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: msix-${{ matrix.platform }}
|
||||||
|
path: artifacts/release/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Publish GitHub Release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Publish release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: dist/*
|
||||||
|
generate_release_notes: true
|
||||||
|
fail_on_unmatched_files: true
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Releasing Wingnal
|
||||||
|
|
||||||
|
Releases are automated by [`.github/workflows/release.yml`](.github/workflows/release.yml).
|
||||||
|
Pushing a version tag builds signed MSIX packages (x64 + ARM64) and publishes a
|
||||||
|
GitHub Release with auto-generated notes.
|
||||||
|
|
||||||
|
## Cutting a release
|
||||||
|
|
||||||
|
The **git tag is the source of truth for the version.** You do not need to edit
|
||||||
|
`Package.appxmanifest` first — the workflow stamps the tag's version into the
|
||||||
|
manifest at build time.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git tag v1.0.2 # must be vMAJOR.MINOR.PATCH (optionally .REVISION)
|
||||||
|
git push origin v1.0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
The workflow then:
|
||||||
|
|
||||||
|
1. Derives the version from the tag (`v1.0.2` → `1.0.2.0`).
|
||||||
|
2. Stamps it into `Wingnal/Package.appxmanifest`.
|
||||||
|
3. Builds and signs an MSIX for x64 and ARM64.
|
||||||
|
4. Creates a GitHub Release named after the tag, with the MSIX files attached
|
||||||
|
and release notes generated from the commits/PRs since the last tag.
|
||||||
|
|
||||||
|
To adjust the notes, edit the release on GitHub after it's created.
|
||||||
|
|
||||||
|
## Code signing
|
||||||
|
|
||||||
|
MSIX packages must be signed to install. The workflow looks for two repository
|
||||||
|
secrets:
|
||||||
|
|
||||||
|
| Secret | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `SIGNING_CERTIFICATE` | Base64-encoded `.pfx` code-signing certificate. |
|
||||||
|
| `SIGNING_CERTIFICATE_PASSWORD` | Password for the `.pfx`. |
|
||||||
|
|
||||||
|
Add them under **Settings → Secrets and variables → Actions**.
|
||||||
|
|
||||||
|
To base64-encode a `.pfx` (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
[Convert]::ToBase64String([IO.File]::ReadAllBytes("Wingnal.pfx")) | Set-Clipboard
|
||||||
|
```
|
||||||
|
|
||||||
|
The certificate's subject **must** match the manifest `Publisher` (`CN=micro`),
|
||||||
|
otherwise signing fails.
|
||||||
|
|
||||||
|
### No certificate configured (fallback)
|
||||||
|
|
||||||
|
If the secrets are absent, the workflow generates a throwaway self-signed
|
||||||
|
certificate each run and attaches `Wingnal.cer` to the release. To install a
|
||||||
|
package signed this way, a user must first import that `.cer` into
|
||||||
|
**Local Machine → Trusted People**, then double-click the `.msix`. Because a new
|
||||||
|
certificate is generated per release, this is fine for testing but not
|
||||||
|
recommended for distribution — configure a persistent certificate via the
|
||||||
|
secrets above for real releases.
|
||||||
Reference in New Issue
Block a user