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 # packs the MSIX with the version taken from the tag — three numbers of the tag
# and a zero the Store keeps for itself. # 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 # 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 # .NET 10 SDK and an interactive desktop session for the tests. makeappx and
# with a NuGet package (Packaging\Tools\SdkTools.csproj), so the Windows SDK does # signtool come with a NuGet package (Packaging\Tools\SdkTools.csproj), so the
# not have to be installed. # Windows SDK does not have to be installed.
name: Release name: Release
on: on:
@@ -77,6 +84,25 @@ jobs:
# reserves the last one, so it carries nothing the tag could tell # 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 "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 - name: Show the toolchain
run: dotnet --info run: dotnet --info
@@ -97,7 +123,12 @@ jobs:
# The package comes out as Partner Center wants it — the Store puts its own # 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 # signature on it. The identity comes from repository variables and falls
# back to the defaults of the script when a variable is not set. # 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: env:
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }} IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
PUBLISHER: ${{ vars.MSIX_PUBLISHER }} PUBLISHER: ${{ vars.MSIX_PUBLISHER }}
@@ -105,7 +136,11 @@ jobs:
run: | run: |
$ErrorActionPreference = 'Stop' $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 # An empty variable is left out rather than passed on: the script has
# defaults of its own, and an empty string would wipe them # defaults of its own, and an empty string would wipe them
@@ -121,11 +156,123 @@ jobs:
./Packaging/build-msix.ps1 @arguments ./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 uses: actions/upload-artifact@v4
with: with:
name: msix-${{ steps.version.outputs.version }} name: msix-${{ steps.version.outputs.version }}
path: artifacts/packages/ path: artifacts/release/packages/
if-no-files-found: error if-no-files-found: error
# Gitea creates a release of its own for a pushed tag, so the release is # 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 $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 # A tag can be pushed again after it was deleted; the old file of
# the same name is dropped, otherwise the upload is refused # the same name is dropped, otherwise the upload is refused
$existing = $release.assets | Where-Object { $_.name -eq $file.Name } $existing = $release.assets | Where-Object { $_.name -eq $file.Name }
+1 -1
View File
@@ -22,7 +22,7 @@
<AssemblyVersion>1.0.0.0</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion> <FileVersion>1.0.0.0</FileVersion>
<Product>CursorLang</Product> <Product>CursorLang</Product>
<Company>Aleksandr Neychev</Company> <Company>Aleksandr Neichev</Company>
<Description>Shows the keyboard layout at the cursor</Description> <Description>Shows the keyboard layout at the cursor</Description>
<Copyright>Copyright (c) 2026</Copyright> <Copyright>Copyright (c) 2026</Copyright>
</PropertyGroup> </PropertyGroup>
+1 -1
View File
@@ -16,7 +16,7 @@
<AssemblyVersion>1.0.0.0</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion> <FileVersion>1.0.0.0</FileVersion>
<Product>CursorLang</Product> <Product>CursorLang</Product>
<Company>Aleksandr Neychev</Company> <Company>Aleksandr Neichev</Company>
<Description>Shared part of CursorLang: models, settings, layout tracking, updates</Description> <Description>Shared part of CursorLang: models, settings, layout tracking, updates</Description>
<Copyright>Copyright (c) 2026</Copyright> <Copyright>Copyright (c) 2026</Copyright>
</PropertyGroup> </PropertyGroup>
@@ -24,7 +24,7 @@
<AssemblyVersion>1.0.0.0</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion> <FileVersion>1.0.0.0</FileVersion>
<Product>CursorLang</Product> <Product>CursorLang</Product>
<Company>Aleksandr Neychev</Company> <Company>Aleksandr Neichev</Company>
<Description>Settings window of CursorLang</Description> <Description>Settings window of CursorLang</Description>
<Copyright>Copyright (c) 2026</Copyright> <Copyright>Copyright (c) 2026</Copyright>
</PropertyGroup> </PropertyGroup>
+108 -12
View File
@@ -1,14 +1,17 @@
<# <#
.SYNOPSIS .SYNOPSIS
Builds the CursorLang MSIX package for the Microsoft Store. Builds the CursorLang MSIX package.
.DESCRIPTION .DESCRIPTION
Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a Neither Visual Studio nor the Windows SDK is needed: makeappx and signtool
NuGet package (Tools\SdkTools.csproj) and the application is built by the arrive as a NuGet package (Tools\SdkTools.csproj) and the application is
plain .NET SDK. built by the plain .NET SDK.
The package is handed to Partner Center as it comes out of here — the Store The package comes out of here in one of two shapes. Left unsigned it goes to
is where it gets everything else done to it. 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 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. carry one, and MSIX cannot install the runtime as a package dependency.
@@ -18,6 +21,17 @@
section. It is handed out there together with the reserved application name; 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. 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 .PARAMETER Install
Puts the package onto this machine to see it working. Developer mode has to 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 be on; a signature is not needed, because what gets registered is the layout
@@ -33,8 +47,14 @@
.EXAMPLE .EXAMPLE
# A build for the Store — the identity comes from Partner Center # A build for the Store — the identity comes from Partner Center
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 ` pwsh -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..." -Publisher "CN=Aleksandr Neichev, O=Aleksandr Neichev, C=KZ" `
-CertificateThumbprint A1B2C3D4E5F60718293A4B5C6D7E8F9012345678
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
@@ -42,12 +62,21 @@ param(
[string] $Version = '1.0.0.0', [string] $Version = '1.0.0.0',
[string] $IdentityName = 'CursorLang', [string] $IdentityName = 'CursorLang',
[string] $Publisher = 'CN=Aleksandr Neychev', [string] $Publisher = 'CN=Aleksandr Neichev',
[string] $PublisherDisplayName = 'Aleksandr Neychev', [string] $PublisherDisplayName = 'Aleksandr Neichev',
[ValidateSet('x64', 'arm64')] [ValidateSet('x64', 'arm64')]
[string[]] $Architectures = @('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, [switch] $Install,
[string] $OutputPath [string] $OutputPath
@@ -64,7 +93,14 @@ $assets = Join-Path $root 'Assets'
$manifestTemplate = Join-Path $root 'AppxManifest.xml' $manifestTemplate = Join-Path $root 'AppxManifest.xml'
$toolsProject = Join-Path $root 'Tools\SdkTools.csproj' $toolsProject = Join-Path $root 'Tools\SdkTools.csproj'
if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' } if (-not $OutputPath) {
$OutputPath = Join-Path $repository 'artifacts'
} elseif (-not [System.IO.Path]::IsPathRooted($OutputPath)) {
# Windows names the folder a package was registered from in full, and the
# layout is looked for by that name below. A relative path would never be
# found there, and the registration of a build gone by would be left behind
$OutputPath = Join-Path (Get-Location).Path $OutputPath
}
$layoutRoot = Join-Path $OutputPath 'layout' $layoutRoot = Join-Path $OutputPath 'layout'
$packagesPath = Join-Path $OutputPath 'packages' $packagesPath = Join-Path $OutputPath 'packages'
@@ -76,6 +112,26 @@ if (-not (Test-Path $assets)) {
throw "No logos found in '$assets'. Run Packaging\New-Assets.ps1 first." 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) { if ($Install) {
# Both things below are checked before the build rather than after it: the # Both things below are checked before the build rather than after it: the
# build takes minutes, and neither of them gets any truer while it runs # build takes minutes, and neither of them gets any truer while it runs
@@ -160,11 +216,34 @@ function Get-SdkTool {
return $tool.FullName 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 Write-Host 'Fetching the Windows SDK programs...' -ForegroundColor Cyan
Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo') Invoke-Tool -Path 'dotnet' -Arguments @('restore', $toolsProject, '--nologo')
$sdkTools = Get-SdkToolsPath $sdkTools = Get-SdkToolsPath
$makeappx = Get-SdkTool -Name 'makeappx.exe' -PackagePath $sdkTools $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 # 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 # the folder is about to be wiped. Left in place, the registration would point
@@ -189,6 +268,7 @@ foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) {
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
$suffix = if ($PackageSuffix) { "-$PackageSuffix" } else { '' }
$built = @() $built = @()
foreach ($architecture in $Architectures) { foreach ($architecture in $Architectures) {
@@ -223,7 +303,7 @@ foreach ($architecture in $Architectures) {
Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8 Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8
$package = Join-Path $packagesPath "CursorLang-$Version-$architecture.msix" $package = Join-Path $packagesPath "CursorLang-$Version-$architecture$suffix.msix"
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package) Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package)
$built += $package $built += $package
@@ -242,12 +322,23 @@ if ($built.Count -gt 1) {
New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null
$built | ForEach-Object { Copy-Item $_ -Destination $bundleInput } $built | ForEach-Object { Copy-Item $_ -Destination $bundleInput }
$result = Join-Path $packagesPath "CursorLang-$Version.msixbundle" $result = Join-Path $packagesPath "CursorLang-$Version$suffix.msixbundle"
Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version) Invoke-Tool -Path $makeappx -Arguments @('bundle', '/o', '/d', $bundleInput, '/p', $result, '/bv', $Version)
Remove-Item $bundleInput -Recurse -Force 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) { if ($Install) {
Write-Host "Installing the $machineArchitecture build..." -ForegroundColor Cyan Write-Host "Installing the $machineArchitecture build..." -ForegroundColor Cyan
@@ -261,7 +352,12 @@ Write-Host 'Done.' -ForegroundColor Green
Write-Host " $result" Write-Host " $result"
Write-Host '' Write-Host ''
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.' Write-Host ' This file is uploaded to Partner Center as it is.'
}
if ($Install) { if ($Install) {
Write-Host '' Write-Host ''
+37 -10
View File
@@ -186,8 +186,8 @@ MSIX всегда выполняются в контексте вошедшег
Windows: тот показывает издателя, спрашивает подтверждение и заменяет Windows: тот показывает издателя, спрашивает подтверждение и заменяет
установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску
пакет должен быть подписан — неподписанный установится только на машине в режиме пакет должен быть подписан — неподписанный установится только на машине в режиме
разработчика. Работающее приложение до перезапуска продолжает жить на старых разработчика; пайплайн подписывает то, что прикладывает. Работающее приложение
файлах. до перезапуска продолжает жить на старых файлах.
У приложения, установленного из Store, раздела обновлений нет вовсе: его У приложения, установленного из Store, раздела обновлений нет вовсе: его
обновляет Store, а пакет со стороны Windows поверх него всё равно не примет. обновляет Store, а пакет со стороны Windows поверх него всё равно не примет.
@@ -263,8 +263,8 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3` на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
с приложенными пакетами. Номер версии берётся только из тега — тег любого с приложенными подписанными пакетами. Номер версии берётся только из тега — тег
другого вида останавливает прогон в самом начале. Версия пакета получается любого другого вида останавливает прогон в самом начале. Версия пакета получается
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег `1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег
на него не влияет. на него не влияет.
@@ -276,10 +276,29 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
сеансе, пропускают себя: показать окно там негде. сеансе, пропускают себя: показать окно там негде.
Пакет, который несёт релиз, загружается в Partner Center как есть. Identity Пакет собирается дважды: Store и релиз хотят от него разного.
берётся из переменных репозитория, а если те не заданы — из значений по
умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` Тот, что для Store, несёт identity, зарезервированную в Partner Center, и не
и `MSIX_PUBLISHER_DISPLAY_NAME`. подписан — подпись на него ставит сам 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 ## Сборка пакета MSIX
@@ -297,13 +316,21 @@ powershell -File Packaging\build-msix.ps1 -Architectures x64
# Для Partner Center — identity та, что зарезервирована там # Для Partner Center — identity та, что зарезервирована там
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 ` powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..." -IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
``` ```
Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64 Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64
и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
Center как есть. Center как есть.
`-CertificateThumbprint` подписывает всё, что собрано, сертификатом с этим
отпечатком из личного хранилища текущего пользователя; `-Publisher` тогда должен
называть его subject — скрипт говорит об этом до сборки, а не после. Обычно
подписывает пайплайн, но то же работает и руками, как только eSigner CKA — или
сертификат любого другого рода — положит сертификат в хранилище.
`-PackageSuffix` дописывается в конец имён файлов: сборки для Store и для релиза
различаются тем, что внутри, и больше ничем.
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
MSIX не может установить среду выполнения как зависимость пакета. MSIX не может установить среду выполнения как зависимость пакета.
@@ -324,5 +351,5 @@ pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
Удалить вручную: Удалить вручную:
```powershell ```powershell
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
``` ```
+39 -8
View File
@@ -181,8 +181,8 @@ 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 installer: it shows the publisher, asks for a confirmation and replaces the
installed version. Windows checks the signature, so the package attached to a 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 release has to be signed — an unsigned one installs nowhere but a machine in
developer mode. The running app keeps working off the old files until it is developer mode; the pipeline signs what it attaches. The running app keeps
restarted. working off the old files until it is restarted.
An app installed from the Store has no updates section at all: the Store An app installed from the Store has no updates section at all: the Store
updates it, and a package from the side is something Windows would not accept updates it, and a package from the side is something Windows would not accept
@@ -257,7 +257,7 @@ 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 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, request into `master` is built and tested; a tag of the form `v1.2.3` is built,
tested, packed into an MSIX and published as a release with the packages 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 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 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 Store takes four numbers and keeps the last one for itself, so the tag has no
@@ -271,9 +271,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 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. service in session 0, where there is no desktop to show a window on.
The package the release carries goes to Partner Center as it is. The identity The package is built twice over, because the Store and a release want different
comes from repository variables and falls back to the defaults of the script when things of it.
unset: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and `MSIX_PUBLISHER_DISPLAY_NAME`.
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 ## Building the MSIX package
@@ -291,13 +313,22 @@ powershell -File Packaging\build-msix.ps1 -Architectures x64
# For Partner Center — the identity is the one reserved there # For Partner Center — the identity is the one reserved there
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 ` powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..." -IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
``` ```
The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64 The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64
and arm64; next to it lie the packages of single architectures. Upload the bundle and arm64; next to it lie the packages of single architectures. Upload the bundle
to Partner Center as it is. to Partner Center as it is.
`-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 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. MSIX cannot install a runtime as a package dependency.
@@ -318,5 +349,5 @@ the Store answers to the same name and is left alone.
To remove it by hand: To remove it by hand:
```powershell ```powershell
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
``` ```