Add package sign to pipeline #5
+154
-23
@@ -2,17 +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 goes to the Store and nowhere else, so it leaves the run as an
|
||||
# artifact: someone picks it up and uploads it to Partner Center, which puts its
|
||||
# own signature on it. Nothing is signed here and nothing is attached to the
|
||||
# release — a publicly trusted code signing certificate is not to be had, and an
|
||||
# unsigned package would look like something to install and install nowhere.
|
||||
# Gitea makes the release for the tag itself, and it carries the tag alone.
|
||||
# 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:
|
||||
@@ -84,9 +84,24 @@ 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 installer answers to nobody about a fourth number and takes the
|
||||
# tag as it is
|
||||
"plain=$($tag.Substring(1))" | 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
|
||||
@@ -108,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 }}
|
||||
@@ -116,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
|
||||
@@ -132,18 +156,123 @@ jobs:
|
||||
|
||||
./Packaging/build-msix.ps1 @arguments
|
||||
|
||||
# The other half of the release: the same application as an ordinary
|
||||
# installer, for handing round outside the Store. Nobody signs it, so
|
||||
# SmartScreen warns about it — see Packaging\installer.iss
|
||||
- name: Build the installer
|
||||
run: ./Packaging/build-installer.ps1 -Version ${{ steps.version.outputs.plain }}
|
||||
- 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
|
||||
|
||||
# The artifact is where the package waits to be uploaded to Partner Center
|
||||
- name: Keep the package
|
||||
# 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
|
||||
|
||||
# The .wixpdb next to each installer is left out on purpose: it is of use
|
||||
@@ -183,7 +312,9 @@ jobs:
|
||||
$release = Invoke-RestMethod $api -Method Post -Headers $headers -ContentType 'application/json' -Body $body
|
||||
}
|
||||
|
||||
foreach ($file in Get-ChildItem artifacts/installers -File -Filter *.msi) {
|
||||
# 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 }
|
||||
|
||||
+124
-12
@@ -3,14 +3,15 @@
|
||||
Builds the CursorLang MSIX package.
|
||||
|
||||
.DESCRIPTION
|
||||
Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a
|
||||
NuGet package (Tools\SdkTools.csproj) and the application is built by the
|
||||
plain .NET SDK.
|
||||
Neither Visual Studio nor the Windows SDK is needed: makeappx and signtool
|
||||
arrive as a NuGet package (Tools\SdkTools.csproj) and the application is
|
||||
built by the plain .NET SDK.
|
||||
|
||||
The package is handed to Partner Center as it comes out of here — the Store
|
||||
puts its own signature on it and does the rest. Nothing is signed here: a
|
||||
publicly trusted code signing certificate is not to be had, and the Store
|
||||
asks for none.
|
||||
The package comes out of here in one of two shapes. Left unsigned it goes to
|
||||
Partner Center as it is — the Store puts its own signature on it and does the
|
||||
rest. Signed with -CertificateThumbprint it is a package anyone can install
|
||||
from a release, because Windows only takes a package whose signature it
|
||||
trusts.
|
||||
|
||||
The application is published with its own copy of .NET: Windows does not
|
||||
carry one, and MSIX cannot install the runtime as a package dependency.
|
||||
@@ -20,6 +21,17 @@
|
||||
section. It is handed out there together with the reserved application name;
|
||||
the default is only good enough for a check on your own machine.
|
||||
|
||||
.PARAMETER PackageSuffix
|
||||
Goes at the end of the file names. The two builds of a release differ in the
|
||||
identity inside them and in nothing a file listing shows, so the one meant
|
||||
for the Store is told apart by a suffix of its own.
|
||||
|
||||
.PARAMETER CertificateThumbprint
|
||||
Signs the packages with the certificate of this thumbprint from the personal
|
||||
store of the current user. The private key is none of this script's business:
|
||||
signtool asks the store for it, and behind a cloud certificate — eSigner CKA
|
||||
among them — the store answers over the network.
|
||||
|
||||
.PARAMETER Install
|
||||
Puts the package onto this machine to see it working. Developer mode has to
|
||||
be on; a signature is not needed, because what gets registered is the layout
|
||||
@@ -31,8 +43,14 @@
|
||||
|
||||
.EXAMPLE
|
||||
# A build for the Store — the identity comes from Partner Center
|
||||
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 -PackageSuffix store `
|
||||
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
|
||||
|
||||
.EXAMPLE
|
||||
# A build for a release — the publisher is the subject of the certificate
|
||||
pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
||||
-Publisher "CN=Aleksandr Neichev, O=Aleksandr Neichev, C=KZ" `
|
||||
-CertificateThumbprint A1B2C3D4E5F60718293A4B5C6D7E8F9012345678
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -43,6 +61,18 @@ param(
|
||||
[string] $Publisher = 'CN=Aleksandr Neichev',
|
||||
[string] $PublisherDisplayName = 'Aleksandr Neichev',
|
||||
|
||||
[ValidateSet('x64', 'arm64')]
|
||||
[string[]] $Architectures = @('x64', 'arm64'),
|
||||
|
||||
[string] $PackageSuffix,
|
||||
|
||||
[string] $CertificateThumbprint,
|
||||
|
||||
# SSL.com's timestamp server, to go with the certificate the pipeline signs
|
||||
# with. A signature without a timestamp is only good while the certificate
|
||||
# is: the day it expires the package stops installing everywhere at once
|
||||
[string] $TimestampUrl = 'http://ts.ssl.com',
|
||||
|
||||
[switch] $Install,
|
||||
|
||||
[string] $OutputPath
|
||||
@@ -78,6 +108,26 @@ if (-not (Test-Path $assets)) {
|
||||
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first."
|
||||
}
|
||||
|
||||
if ($CertificateThumbprint) {
|
||||
# Both things below are checked before the build for the same reason the
|
||||
# ones under -Install are: the build takes minutes and neither answer
|
||||
# changes while it runs
|
||||
|
||||
$certificate = Get-ChildItem "Cert:\CurrentUser\My\$CertificateThumbprint" -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $certificate) {
|
||||
throw "No certificate with the thumbprint '$CertificateThumbprint' in the personal store of this user. A cloud certificate has to be loaded into the store first — eSigner CKA is what does that."
|
||||
}
|
||||
|
||||
# The Publisher of a package is not a name of the publisher's choosing: it
|
||||
# is the subject of the certificate the package is signed with, letter for
|
||||
# letter. signtool turns down a package that claims any other, and it does
|
||||
# so at the very end — after everything has already been built
|
||||
if ($certificate.Subject -ne $Publisher) {
|
||||
throw "The publisher '$Publisher' is not the subject of the certificate, which reads '$($certificate.Subject)'. A package carries the name of whoever signs it."
|
||||
}
|
||||
}
|
||||
|
||||
if ($Install) {
|
||||
# Checked before the build rather than after it: the build takes minutes, and
|
||||
# this does not get any truer while it runs
|
||||
@@ -155,11 +205,34 @@ function Get-SdkTool {
|
||||
return $tool.FullName
|
||||
}
|
||||
|
||||
function Invoke-Signing {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Signs a package with the certificate the build was given.
|
||||
#>
|
||||
param(
|
||||
[string] $Path,
|
||||
[string] $SignTool
|
||||
)
|
||||
|
||||
Write-Host "Signing $([System.IO.Path]::GetFileName($Path))..." -ForegroundColor Cyan
|
||||
|
||||
Invoke-Tool -Path $SignTool -Arguments @(
|
||||
'sign',
|
||||
'/fd', 'sha256',
|
||||
'/tr', $TimestampUrl,
|
||||
'/td', 'sha256',
|
||||
'/sha1', $CertificateThumbprint,
|
||||
$Path
|
||||
)
|
||||
}
|
||||
|
||||
Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan
|
||||
Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
|
||||
|
||||
$sdkTools = Get-SdkToolsPath
|
||||
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools
|
||||
$signtool = if ($CertificateThumbprint) { Get-SdkTool -Name 'signtool.exe' -PackagePath $sdkTools } else { $null }
|
||||
|
||||
# A package registered out of the layout runs straight from that folder, and
|
||||
# the folder is about to be wiped. Left in place, the registration would point
|
||||
@@ -184,7 +257,8 @@ foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) {
|
||||
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
|
||||
|
||||
Write-Host 'Building...' -ForegroundColor Cyan
|
||||
$suffix = if ($PackageSuffix) { "-$PackageSuffix" } else { '' }
|
||||
$built = @()
|
||||
|
||||
$layout = Join-Path $layoutRoot 'x64'
|
||||
|
||||
@@ -215,8 +289,41 @@ $manifest = (Get-Content $manifestTemplate -Raw).
|
||||
|
||||
Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8
|
||||
|
||||
$result = Join-Path $packagesPath "CursorLang-$Version-x64.msix"
|
||||
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $result)
|
||||
$package = Join-Path $packagesPath "CursorLang-$Version-$architecture$suffix.msix"
|
||||
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package)
|
||||
|
||||
$built += $package
|
||||
}
|
||||
|
||||
$result = $built[0]
|
||||
|
||||
if ($built.Count -gt 1) {
|
||||
Write-Host 'Bringing the architectures into one package...' -ForegroundColor Cyan
|
||||
|
||||
# makeappx bundle takes everything from a folder, so the separate packages
|
||||
# are gathered into one of their own first — otherwise the results of
|
||||
# earlier builds would end up in the bundle
|
||||
$bundleInput = Join-Path $OutputPath 'bundle'
|
||||
Remove-Item $bundleInput -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null
|
||||
$built | ForEach-Object { Copy-Item $_ -Destination $bundleInput }
|
||||
|
||||
$result = Join-Path $packagesPath "CursorLang-$Version$suffix.msixbundle"
|
||||
Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version)
|
||||
|
||||
Remove-Item $bundleInput -Recurse -Force
|
||||
}
|
||||
|
||||
if ($CertificateThumbprint) {
|
||||
# Signing comes after the bundle rather than before it: makeappx copies the
|
||||
# packages into the bundle as they are, and a signature on what lies inside
|
||||
# says nothing about the bundle around it. Windows asks the outer file, so
|
||||
# every file that leaves here is signed on its own — each one of them is a
|
||||
# package someone may install
|
||||
foreach ($package in (@($built) + @($result) | Select-Object -Unique)) {
|
||||
Invoke-Signing -Path $package -SignTool $signtool
|
||||
}
|
||||
}
|
||||
|
||||
if ($Install) {
|
||||
Write-Host 'Installing...' -ForegroundColor Cyan
|
||||
@@ -231,7 +338,12 @@ Write-Host 'Done.' -ForegroundColor Green
|
||||
Write-Host " $result"
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' This file is uploaded to Partner Center as it is.'
|
||||
|
||||
if ($CertificateThumbprint) {
|
||||
Write-Host ' This file is signed: Windows installs it on any machine.'
|
||||
} else {
|
||||
Write-Host ' This file is uploaded to Partner Center as it is.'
|
||||
}
|
||||
|
||||
if ($Install) {
|
||||
Write-Host ''
|
||||
|
||||
+56
-18
@@ -167,8 +167,27 @@ MSIX всегда выполняются в контексте вошедшег
|
||||
|
||||
## Обновления
|
||||
|
||||
Приложение обновляет Store, а само приложение об этом не заботится: раздела
|
||||
обновлений в окне нет, запросов в сеть нет и кода для них тоже нет.
|
||||
Проверка выполняется при открытии окна настроек, а не при включении машины:
|
||||
фоновая половина в сеть больше не ходит вовсе, да и показать ответ ей нечем.
|
||||
Настройка в окне так и написана.
|
||||
|
||||
Приложение ищет новые версии среди выпусков собственного репозитория. Выпуск
|
||||
годится, если его тег — это просто версия (`v1.2.3` или `1.2.3`) и к нему
|
||||
приложен пакет MSIX. Тег, в котором есть что-то ещё, — в том числе `v1.2.3-beta`
|
||||
— пропускается: предварительную версию берут намеренно, приложение её не
|
||||
предлагает.
|
||||
|
||||
Из приложенных файлов предпочитается `.msixbundle` — он несёт обе архитектуры.
|
||||
Если его нет, берётся пакет, в имени которого стоит архитектура этой машины:
|
||||
`CursorLang-1.2.3.0-x64.msix`. Такие имена даёт `build-msix.ps1`, так что выпуск
|
||||
делается прикладыванием того, что он собрал.
|
||||
|
||||
Пакет скачивается во временную папку и передаётся установщику приложений
|
||||
Windows: тот показывает издателя, спрашивает подтверждение и заменяет
|
||||
установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску
|
||||
пакет должен быть подписан — неподписанный установится только на машине в режиме
|
||||
разработчика; пайплайн подписывает то, что прикладывает. Работающее приложение
|
||||
до перезапуска продолжает жить на старых файлах.
|
||||
|
||||
Дело не во вкусе, а в цене подписи. MSIX Windows установит только тогда, когда
|
||||
доверяет подписи на нём, а публично доверенный сертификат для подписи кода
|
||||
@@ -227,11 +246,11 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
|
||||
|
||||
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
|
||||
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
|
||||
проект собирается, проходит тесты и пакуется в MSIX, который остаётся
|
||||
в артефактах прогона. Номер версии берётся только из тега — тег любого другого
|
||||
вида останавливает прогон в самом начале. Версия пакета получается `1.2.3.0`:
|
||||
Store принимает четыре числа и последнее оставляет себе, так что тег на него не
|
||||
влияет.
|
||||
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
|
||||
с приложенными подписанными пакетами. Номер версии берётся только из тега — тег
|
||||
любого другого вида останавливает прогон в самом начале. Версия пакета получается
|
||||
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег
|
||||
на него не влияет.
|
||||
|
||||
Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём —
|
||||
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
|
||||
@@ -241,18 +260,29 @@ Store принимает четыре числа и последнее оста
|
||||
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
|
||||
сеансе, пропускают себя: показать окно там негде.
|
||||
|
||||
Пакет идёт в Store и больше никуда: он не подписан, подпись на него ставит сам
|
||||
Partner Center. Поэтому прогон оставляет его в артефактах под именем
|
||||
`msix-1.2.3.0`, откуда его забирают и загружают руками; к релизу не прикладывается
|
||||
ничего — релиз по тегу Gitea заводит сама, и в нём один только тег. Неподписанный
|
||||
пакет, висящий в релизе, выглядел бы как то, что можно установить, и не
|
||||
устанавливался бы нигде — см. раздел об обновлениях.
|
||||
Пакет собирается дважды: Store и релиз хотят от него разного.
|
||||
|
||||
Identity берётся из переменных репозитория, а если те не заданы — из значений по
|
||||
умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER`
|
||||
и `MSIX_PUBLISHER_DISPLAY_NAME`. Вместе identity и publisher задают family name
|
||||
пакета, поэтому от версии к версии оба должны оставаться прежними — иначе Store
|
||||
примет следующую за другое приложение.
|
||||
Тот, что для Store, несёт identity, зарезервированную в Partner Center, и не
|
||||
подписан — подпись на него ставит сам Partner Center. Дальше артефактов прогона
|
||||
он не уходит: его забирают и загружают руками. Что это он, видно по имени —
|
||||
`CursorLang-1.2.3.0-store.msixbundle`. Identity берётся из переменных
|
||||
репозитория, а если те не заданы — из значений по умолчанию в скрипте:
|
||||
`MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` и `MSIX_PUBLISHER_DISPLAY_NAME`.
|
||||
|
||||
Тот, что приложен к релизу, пайплайн подписывает сертификатом от SSL.com:
|
||||
закрытый ключ остаётся в их HSM и на runner не попадает — на нём ставится
|
||||
eSigner CKA как поставщик хранилища ключей, и `signtool` спрашивает ключ у него
|
||||
так же, как спросил бы у токена. Учётная запись — три секрета репозитория:
|
||||
`ESIGNER_USERNAME`, `ESIGNER_PASSWORD` и `ESIGNER_TOTP_SECRET`; последний — тот
|
||||
секрет, который eSigner выдаёт для автоматической подписи, а не код с телефона.
|
||||
Проверяются они до сборки, а не на шаге подписи: релиз без подписанного пакета —
|
||||
не релиз.
|
||||
|
||||
`Publisher` у него не `MSIX_PUBLISHER`, а subject этого сертификата, прочитанный
|
||||
прямо в прогоне: пакет, называющий кого-то другого, Windows считает подделкой.
|
||||
Вместе с identity этот subject задаёт family name пакета, поэтому от релиза к
|
||||
релизу оба должны оставаться прежними — иначе обновление встанет рядом со старой
|
||||
версией, а не заменит её.
|
||||
|
||||
## Сборка пакета MSIX
|
||||
|
||||
@@ -282,6 +312,14 @@ Partner Center как есть.
|
||||
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
||||
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
||||
|
||||
`-CertificateThumbprint` подписывает всё, что собрано, сертификатом с этим
|
||||
отпечатком из личного хранилища текущего пользователя; `-Publisher` тогда должен
|
||||
называть его subject — скрипт говорит об этом до сборки, а не после. Обычно
|
||||
подписывает пайплайн, но то же работает и руками, как только eSigner CKA — или
|
||||
сертификат любого другого рода — положит сертификат в хранилище.
|
||||
`-PackageSuffix` дописывается в конец имён файлов: сборки для Store и для релиза
|
||||
различаются тем, что внутри, и больше ничем.
|
||||
|
||||
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
||||
MSIX не может установить среду выполнения как зависимость пакета.
|
||||
|
||||
|
||||
@@ -162,8 +162,27 @@ after a switch of the mode, and that is a question asked once.
|
||||
|
||||
## Updates
|
||||
|
||||
The Store updates the app, and the app itself does nothing about it: there is no
|
||||
updates section in the window, no request to the network and no code for either.
|
||||
The check runs when the settings window is opened, not when the machine is
|
||||
switched on: the background half no longer goes to the network at all, and there
|
||||
would be nothing in it to show the answer. The setting in the window says as much.
|
||||
|
||||
The app looks for new versions among the releases of its own repository. A
|
||||
release counts when its tag is a plain version — `v1.2.3` or `1.2.3` — and an
|
||||
MSIX package is attached to it. A tag with anything else in it, `v1.2.3-beta`
|
||||
among them, is passed over: a pre-release version is asked for on purpose, not
|
||||
offered by the app.
|
||||
|
||||
Out of the attached files the `.msixbundle` is preferred — it carries both
|
||||
architectures. Failing that, the package whose name holds the architecture of
|
||||
this machine is taken: `CursorLang-1.2.3.0-x64.msix`. Those are the names
|
||||
`build-msix.ps1` produces, so a release is made by attaching what it built.
|
||||
|
||||
The package is downloaded to the temp folder and handed to the Windows app
|
||||
installer: it shows the publisher, asks for a confirmation and replaces the
|
||||
installed version. Windows checks the signature, so the package attached to a
|
||||
release has to be signed — an unsigned one installs nowhere but a machine in
|
||||
developer mode; the pipeline signs what it attaches. The running app keeps
|
||||
working off the old files until it is restarted.
|
||||
|
||||
That is not a matter of taste but of what a signature costs. Windows installs an
|
||||
MSIX only when it trusts the signature on it, and a publicly trusted code signing
|
||||
@@ -222,10 +241,11 @@ running agent and fails if any part of the WPF renderer is in it.
|
||||
|
||||
The pipelines live in `.gitea/workflows` and run on Gitea Actions. A pull
|
||||
request into `master` is built and tested; a tag of the form `v1.2.3` is built,
|
||||
tested and packed into an MSIX, which is left in the artifacts of the run. The
|
||||
version is taken from the tag alone — a tag shaped any other way stops the run
|
||||
right at the start. The package version ends up as `1.2.3.0`: the Store takes
|
||||
four numbers and keeps the last one for itself, so the tag has no say in it.
|
||||
tested, packed into an MSIX and published as a release with the signed packages
|
||||
attached. The version is taken from the tag alone — a tag shaped any other way
|
||||
stops the run right at the start. The package version ends up as `1.2.3.0`: the
|
||||
Store takes four numbers and keeps the last one for itself, so the tag has no
|
||||
say in it.
|
||||
|
||||
Both pipelines ask for a Windows runner labelled `windows-x64` with the .NET 10
|
||||
SDK and git-lfs on it. The checkout pulls LFS files: without them the icon is a
|
||||
@@ -235,18 +255,31 @@ need a desktop of their own — the end-to-end ones, and those that ask for the
|
||||
foreground window or the caret — skip themselves on a runner that lives as a
|
||||
service in session 0, where there is no desktop to show a window on.
|
||||
|
||||
The package goes to the Store and nowhere else: it is unsigned, and Partner
|
||||
Center puts its own signature on it. So the run leaves it in the artifacts under
|
||||
the name `msix-1.2.3.0`, where whoever uploads it picks it up by hand; nothing
|
||||
is attached to the release, which Gitea makes for the tag by itself and which
|
||||
carries the tag alone. An unsigned package hanging off a release would look like
|
||||
something to install and install nowhere — see the section on updates.
|
||||
The package is built twice over, because the Store and a release want different
|
||||
things of it.
|
||||
|
||||
The identity comes from repository variables and falls back to the defaults of
|
||||
the script when unset: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and
|
||||
`MSIX_PUBLISHER_DISPLAY_NAME`. Together the identity and the publisher decide
|
||||
the family name of the package, so both have to stay as they are from version to
|
||||
version, or the Store takes the next one for a different app.
|
||||
The Store one carries the identity reserved in Partner Center and no signature —
|
||||
Partner Center signs it there. It never leaves the artifacts of the run: someone
|
||||
picks it up and uploads it by hand. Its name says which one it is —
|
||||
`CursorLang-1.2.3.0-store.msixbundle`. The identity comes from repository
|
||||
variables and falls back to the defaults of the script when unset:
|
||||
`MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and `MSIX_PUBLISHER_DISPLAY_NAME`.
|
||||
|
||||
The one attached to the release is signed by the pipeline with a certificate
|
||||
from SSL.com, whose private key stays in their HSM and never comes down to the
|
||||
runner: eSigner CKA is installed on it as a key storage provider, and `signtool`
|
||||
asks that for the key the way it would ask a token. The account is three
|
||||
repository secrets — `ESIGNER_USERNAME`, `ESIGNER_PASSWORD` and
|
||||
`ESIGNER_TOTP_SECRET`; the last one is the secret eSigner hands out for
|
||||
automated signing, not a code read off a phone. They are checked before the
|
||||
build rather than at the signing step: a release without a signed package is
|
||||
not a release.
|
||||
|
||||
Its `Publisher` is not `MSIX_PUBLISHER` but the subject of that certificate,
|
||||
read off it in the run — Windows takes a package naming anyone else for a
|
||||
forgery. Together with the identity that subject decides the family name of the
|
||||
package, so both have to stay as they are from release to release, or an update
|
||||
installs beside the old version instead of replacing it.
|
||||
|
||||
## Building the MSIX package
|
||||
|
||||
@@ -276,6 +309,15 @@ sake of machines that run the x64 one under emulation anyway.
|
||||
Nothing here is signed: the Store signs the package itself, and for installing it
|
||||
on this machine the layout is registered instead — see `-Install` below.
|
||||
|
||||
`-CertificateThumbprint` signs everything the build produces, with the
|
||||
certificate of that thumbprint out of the personal store of the current user;
|
||||
`-Publisher` then has to name its subject, and the script says so before it
|
||||
starts building rather than after. The pipeline is what normally signs, but the
|
||||
same works by hand once eSigner CKA — or a certificate of any other kind — has
|
||||
put a certificate into the store. `-PackageSuffix` goes at the end of the file
|
||||
names: the builds for the Store and for a release differ in what is inside them
|
||||
and in nothing else.
|
||||
|
||||
The app ships with its own copy of .NET: Windows does not include .NET 10, and
|
||||
MSIX cannot install a runtime as a package dependency.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user