modified release pipeline
Pull request / build (pull_request) Successful in 54s

This commit is contained in:
2026-08-13 02:21:50 +05:00
parent 5e11b16758
commit 460ce52017
7 changed files with 345 additions and 42 deletions
+157 -8
View File
@@ -2,10 +2,17 @@
# packs the MSIX with the version taken from the tag — three numbers of the tag
# and a zero the Store keeps for itself.
#
# The package is built twice, because the two places it goes to want different
# things of it. The Store gets a package with the identity reserved in Partner
# Center and no signature — Partner Center signs it there. The release gets a
# package signed here, with SSL.com's certificate the private key of which never
# leaves their HSM; Windows installs nothing else. The two only differ inside,
# so the Store one carries a suffix in its name and never leaves the artifacts.
#
# The same requirements to the runner as in pull-request.yml apply: Windows, the
# .NET 10 SDK and an interactive desktop session for the tests. makeappx comes
# with a NuGet package (Packaging\Tools\SdkTools.csproj), so the Windows SDK does
# not have to be installed.
# .NET 10 SDK and an interactive desktop session for the tests. makeappx and
# signtool come with a NuGet package (Packaging\Tools\SdkTools.csproj), so the
# Windows SDK does not have to be installed.
name: Release
on:
@@ -77,6 +84,25 @@ jobs:
# reserves the last one, so it carries nothing the tag could tell
"version=$($tag.Substring(1)).0" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
# The signing account is asked about before anything is built rather than
# at the step that needs it: a release without a signed package is not a
# release, and finding that out after the build and the tests costs the
# whole run
- name: Check the signing credentials
env:
ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }}
ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }}
ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }}
run: |
$ErrorActionPreference = 'Stop'
$missing = @('ESIGNER_USERNAME', 'ESIGNER_PASSWORD', 'ESIGNER_TOTP_SECRET') |
Where-Object { -not (Get-Item "Env:$_" -ErrorAction SilentlyContinue).Value }
if ($missing) {
throw "The repository secrets $($missing -join ', ') are not set. They are the SSL.com account the package is signed with; the TOTP secret is the one eSigner hands out for automated signing, not a six-digit code."
}
- name: Show the toolchain
run: dotnet --info
@@ -97,7 +123,12 @@ jobs:
# The package comes out as Partner Center wants it — the Store puts its own
# signature on it. The identity comes from repository variables and falls
# back to the defaults of the script when a variable is not set.
- name: Pack the MSIX
#
# This one is picked up by hand and uploaded to Partner Center, so it goes
# no further than the artifacts of the run: attached to the release it
# would sit there as a package nobody can install, next to one that
# installs — telling the two apart is what the suffix in the name is for
- name: Pack the MSIX for the Store
env:
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
PUBLISHER: ${{ vars.MSIX_PUBLISHER }}
@@ -105,7 +136,11 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'
$arguments = @{ Version = '${{ steps.version.outputs.version }}' }
$arguments = @{
Version = '${{ steps.version.outputs.version }}'
PackageSuffix = 'store'
OutputPath = 'artifacts/store'
}
# An empty variable is left out rather than passed on: the script has
# defaults of its own, and an empty string would wipe them
@@ -121,11 +156,123 @@ jobs:
./Packaging/build-msix.ps1 @arguments
- name: Keep the packages
- name: Keep the Store packages
uses: actions/upload-artifact@v4
with:
name: msix-store-${{ steps.version.outputs.version }}
path: artifacts/store/packages/
if-no-files-found: error
# eSigner CKA is a key storage provider: it puts the certificate into the
# store of this user and answers signtool's requests for the private key
# over SSL.com's API, so the key itself never comes down to the runner.
# From signtool's side it looks like a certificate on a token, minus the
# token and minus the person who would plug it in.
#
# The TOTP secret is what stands in for that person: eSigner hands it out
# once, for automated signing, and the tool makes the codes out of it
# itself
- name: Load the signing certificate
id: certificate
env:
CKA_URL: https://github.com/SSLcom/eSignerCKA/releases/download/v1.0.6/SSL.COM-eSigner-CKA_1.0.6.zip
ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }}
ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }}
ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }}
run: |
$ErrorActionPreference = 'Stop'
$temporary = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP }
$archive = Join-Path $temporary 'eSignerCKA.zip'
$unpacked = Join-Path $temporary 'eSignerCKA'
# Everything of the adapter's own — the installation and the master
# key it keeps the account in — lives outside the workspace: the
# workspace is what gets packed and uploaded
$suite = Join-Path $env:USERPROFILE '.signingsuite'
$installation = Join-Path $suite 'eSignerCKA'
Remove-Item $unpacked -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $suite -Force | Out-Null
Invoke-WebRequest -Uri $env:CKA_URL -OutFile $archive
Expand-Archive -Path $archive -DestinationPath $unpacked -Force
$installer = Get-ChildItem $unpacked -Recurse -Filter '*.exe' | Select-Object -First 1
if (-not $installer) { throw "No installer inside the eSigner CKA archive at '$env:CKA_URL'." }
# /CURRENTUSER, so that the certificate lands in the store of the user
# the build runs as — the same one signtool then looks in
& $installer.FullName /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES "/DIR=$installation" | Out-Null
$tool = Join-Path $installation 'eSignerCKATool.exe'
if (-not (Test-Path $tool)) { throw "eSigner CKA did not install: '$tool' is not there." }
& $tool config -mode product -user $env:ESIGNER_USERNAME -pass $env:ESIGNER_PASSWORD -totp $env:ESIGNER_TOTP_SECRET -key (Join-Path $suite 'master.key') -r
if ($LASTEXITCODE -ne 0) { throw "eSigner CKA turned down the account: the tool exited with code $LASTEXITCODE." }
# A run of the same runner could have left a certificate loaded from
# another account; unload says nothing when there is nothing to
# unload, and its exit code is of no interest for that reason
& $tool unload | Out-Null
& $tool load
if ($LASTEXITCODE -ne 0) { throw "eSigner CKA could not load the certificate: the tool exited with code $LASTEXITCODE." }
# An account may hold more than one certificate — a renewal leaves the
# old one behind — and the one that lives longest is the one to sign
# with: a signature made by a certificate about to expire is timestamped
# and stays good, but the next release would have to be made anyway
$certificate = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if (-not $certificate) {
throw 'eSigner CKA loaded nothing into the certificate store. Does the account hold a code signing certificate — a document signature is a different thing and cannot sign a package.'
}
Write-Host "Signing as $($certificate.Subject), good until $($certificate.NotAfter.ToString('yyyy-MM-dd'))."
"thumbprint=$($certificate.Thumbprint)" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
"subject=$($certificate.Subject)" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
# The package of the release carries the identity the app is known by and
# the publisher of the certificate — Windows works out the family name of
# a package from the two together, so an update replaces the installed
# version only while both stay as they were. The names have no suffix:
# that is what the app looks for when it checks for an update
- name: Pack and sign the MSIX for the release
env:
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
PUBLISHER: ${{ steps.certificate.outputs.subject }}
PUBLISHER_DISPLAY_NAME: ${{ vars.MSIX_PUBLISHER_DISPLAY_NAME }}
THUMBPRINT: ${{ steps.certificate.outputs.thumbprint }}
run: |
$ErrorActionPreference = 'Stop'
$arguments = @{
Version = '${{ steps.version.outputs.version }}'
OutputPath = 'artifacts/release'
Publisher = $env:PUBLISHER
CertificateThumbprint = $env:THUMBPRINT
}
$variables = @{
IdentityName = $env:IDENTITY_NAME
PublisherDisplayName = $env:PUBLISHER_DISPLAY_NAME
}
foreach ($name in $variables.Keys) {
if ($variables[$name]) { $arguments[$name] = $variables[$name] }
}
./Packaging/build-msix.ps1 @arguments
- name: Keep the signed packages
uses: actions/upload-artifact@v4
with:
name: msix-${{ steps.version.outputs.version }}
path: artifacts/packages/
path: artifacts/release/packages/
if-no-files-found: error
# Gitea creates a release of its own for a pushed tag, so the release is
@@ -153,7 +300,9 @@ jobs:
$release = Invoke-RestMethod $api -Method Post -Headers $headers -ContentType 'application/json' -Body $body
}
foreach ($file in Get-ChildItem artifacts/packages -File) {
# Only the signed packages: the Store one stays in the artifacts of
# the run, where whoever uploads it to Partner Center picks it up
foreach ($file in Get-ChildItem artifacts/release/packages -File) {
# A tag can be pushed again after it was deleted; the old file of
# the same name is dropped, otherwise the upload is refused
$existing = $release.assets | Where-Object { $_.name -eq $file.Name }