name: Manual Beta Release on: workflow_dispatch: inputs: release_title: description: Optional GitHub release title override required: false type: string permissions: contents: write packages: read jobs: release-beta: name: Build and publish beta release runs-on: windows-latest env: PROJECT_PATH: Wino.Mail.WinUI/Wino.Mail.WinUI.csproj MANIFEST_PATH: Wino.Mail.WinUI/Package.appxmanifest CHANGELOG_PATH: CHANGELOG.md NUGET_CONFIG_PATH: ${{ github.workspace }}\nuget.config PACKAGE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\package RELEASE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\release CERTIFICATE_PFX_PATH: ${{ github.workspace }}\artifacts\signing\beta-signing-cert.pfx CERTIFICATE_CER_PATH: ${{ github.workspace }}\artifacts\release\Wino-Mail-Beta.cer steps: - name: Checkout selected branch uses: actions/checkout@v4 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Fetch tags from origin shell: pwsh run: git fetch origin --force --tags - name: Validate release secrets shell: pwsh env: BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }} run: | if ([string]::IsNullOrWhiteSpace($env:BETA_SIGNING_CERT_PFX_BASE64)) { throw "Missing required secret: BETA_SIGNING_CERT_PFX_BASE64" } - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Compute beta version and release metadata id: metadata shell: pwsh env: RELEASE_TITLE_INPUT: ${{ github.event.inputs.release_title }} run: | $manifestPath = Join-Path $env:GITHUB_WORKSPACE $env:MANIFEST_PATH if (-not (Test-Path $manifestPath)) { throw "Package manifest not found: $manifestPath" } $changelogPath = Join-Path $env:GITHUB_WORKSPACE $env:CHANGELOG_PATH if (-not (Test-Path $changelogPath)) { throw "Release notes file not found: $changelogPath" } [xml]$manifest = Get-Content -LiteralPath $manifestPath $identityNode = $manifest.Package.Identity if (-not $identityNode) { throw "Could not locate the Package/Identity node in $manifestPath" } $currentVersionText = [string]$identityNode.Version if ($currentVersionText -notmatch '^(?\d+)\.(?\d+)\.(?\d+)\.(?\d+)$') { throw "Manifest version '$currentVersionText' is not a four-part numeric version." } $baseVersion = '{0}.{1}.{2}' -f $Matches.major, $Matches.minor, $Matches.patch $packageVersion = $currentVersionText $matchingTags = git tag --list "v$baseVersion.*" $releaseNumbers = @() foreach ($tag in $matchingTags) { if ($tag -match "^v$([regex]::Escape($baseVersion))\.(\d+)$") { $releaseNumbers += [int]$Matches[1] } } $nextReleaseNumber = if ($releaseNumbers.Count -gt 0) { ($releaseNumbers | Measure-Object -Maximum).Maximum + 1 } else { 1 } $releaseTag = "v$baseVersion.$nextReleaseNumber" $releaseTitleInput = $env:RELEASE_TITLE_INPUT $releaseTitle = if ([string]::IsNullOrWhiteSpace($releaseTitleInput)) { $releaseTag } else { $releaseTitleInput.Trim() } $headSha = (git rev-parse HEAD).Trim() if ([string]::IsNullOrWhiteSpace($headSha)) { throw "Failed to resolve the checked out commit SHA." } $notesInput = Get-Content -LiteralPath $changelogPath -Raw if ([string]::IsNullOrWhiteSpace($notesInput)) { throw "Release notes file is empty: $changelogPath" } $notesInput = $notesInput.Trim() New-Item -ItemType Directory -Path $env:RELEASE_OUTPUT_DIR -Force | Out-Null $releaseNotesPath = Join-Path $env:RELEASE_OUTPUT_DIR 'beta-release-notes.md' $notesInput | Set-Content -LiteralPath $releaseNotesPath -Encoding utf8 "package_version=$packageVersion" >> $env:GITHUB_OUTPUT "base_version=$baseVersion" >> $env:GITHUB_OUTPUT "release_tag=$releaseTag" >> $env:GITHUB_OUTPUT "release_title=$releaseTitle" >> $env:GITHUB_OUTPUT "release_notes_path=$releaseNotesPath" >> $env:GITHUB_OUTPUT "head_sha=$headSha" >> $env:GITHUB_OUTPUT - name: Materialize signing certificate shell: pwsh env: BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }} run: | $signingDir = Split-Path -Parent $env:CERTIFICATE_PFX_PATH New-Item -ItemType Directory -Path $signingDir -Force | Out-Null [IO.File]::WriteAllBytes($env:CERTIFICATE_PFX_PATH, [Convert]::FromBase64String($env:BETA_SIGNING_CERT_PFX_BASE64)) $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($env:CERTIFICATE_PFX_PATH, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) New-Item -ItemType Directory -Path (Split-Path -Parent $env:CERTIFICATE_CER_PATH) -Force | Out-Null [IO.File]::WriteAllBytes($env:CERTIFICATE_CER_PATH, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) - name: Restore WinUI project dependencies shell: pwsh run: | if (-not (Test-Path $env:NUGET_CONFIG_PATH)) { throw "NuGet config file not found: $env:NUGET_CONFIG_PATH" } dotnet restore $env:PROJECT_PATH ` --configfile $env:NUGET_CONFIG_PATH ` -p:Platform=x64 ` /p:RestoreConfigFile="$env:NUGET_CONFIG_PATH" - name: Build MSIX bundle shell: pwsh run: | New-Item -ItemType Directory -Path $env:PACKAGE_OUTPUT_DIR -Force | Out-Null dotnet build $env:PROJECT_PATH ` --configuration Release ` --no-restore ` --configfile $env:NUGET_CONFIG_PATH ` /p:Platform=x64 ` /p:RestoreConfigFile="$env:NUGET_CONFIG_PATH" ` /p:GenerateAppxPackageOnBuild=true ` /p:UapAppxPackageBuildMode=SideloadOnly ` /p:AppxBundle=Always ` /p:AppxBundlePlatforms="x86|x64|arm64" ` /p:AppxPackageDir="$env:PACKAGE_OUTPUT_DIR\\" ` /p:AppxPackageVersion=${{ steps.metadata.outputs.package_version }} ` /p:PackageCertificateKeyFile="$env:CERTIFICATE_PFX_PATH" ` /p:PackageCertificatePassword= ` /p:PackageCertificateThumbprint= ` /p:AppxPackageSigningEnabled=true - name: Collect packaged artifacts id: package shell: pwsh run: | $bundle = Get-ChildItem -Path $env:PACKAGE_OUTPUT_DIR -Recurse -Filter *.msixbundle | Select-Object -First 1 if (-not $bundle) { throw "No .msixbundle file was generated under $env:PACKAGE_OUTPUT_DIR" } $packageZipPath = Join-Path $env:RELEASE_OUTPUT_DIR 'Wino-Mail-Beta-PackageOutput.zip' if (Test-Path $packageZipPath) { Remove-Item -LiteralPath $packageZipPath -Force } Compress-Archive -Path (Join-Path $env:PACKAGE_OUTPUT_DIR '*') -DestinationPath $packageZipPath -Force "bundle_path=$($bundle.FullName)" >> $env:GITHUB_OUTPUT "bundle_name=$($bundle.Name)" >> $env:GITHUB_OUTPUT "package_zip_path=$packageZipPath" >> $env:GITHUB_OUTPUT - name: Upload workflow artifacts uses: actions/upload-artifact@v4 with: name: beta-release-assets-${{ steps.metadata.outputs.release_tag }} path: | ${{ steps.package.outputs.bundle_path }} ${{ env.CERTIFICATE_CER_PATH }} ${{ steps.metadata.outputs.release_notes_path }} ${{ steps.package.outputs.package_zip_path }} if-no-files-found: error - name: Create GitHub prerelease shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "${{ steps.metadata.outputs.release_tag }}" ` "${{ steps.package.outputs.bundle_path }}" ` "${{ env.CERTIFICATE_CER_PATH }}" ` "${{ steps.metadata.outputs.release_notes_path }}" ` "${{ steps.package.outputs.package_zip_path }}" ` --repo "${{ github.repository }}" ` --target "${{ steps.metadata.outputs.head_sha }}" ` --title "${{ steps.metadata.outputs.release_title }}" ` --notes-file "${{ steps.metadata.outputs.release_notes_path }}" ` --prerelease