removed update

This commit is contained in:
2026-08-13 16:24:23 +05:00
parent 1aa9d9b44e
commit f75ab0f077
4 changed files with 64 additions and 415 deletions
+14 -204
View File
@@ -2,17 +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 # The package goes to the Store and nowhere else, so it leaves the run as an
# things of it. The Store gets a package with the identity reserved in Partner # artifact: someone picks it up and uploads it to Partner Center, which puts its
# Center and no signature — Partner Center signs it there. The release gets a # own signature on it. Nothing is signed here and nothing is attached to the
# package signed here, with SSL.com's certificate the private key of which never # release — a publicly trusted code signing certificate is not to be had, and an
# leaves their HSM; Windows installs nothing else. The two only differ inside, # unsigned package would look like something to install and install nowhere.
# so the Store one carries a suffix in its name and never leaves the artifacts. # Gitea makes the release for the tag itself, and it carries the tag alone.
# #
# 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 and # .NET 10 SDK and an interactive desktop session for the tests. makeappx comes
# signtool come with a NuGet package (Packaging\Tools\SdkTools.csproj), so the # with a NuGet package (Packaging\Tools\SdkTools.csproj), so the Windows SDK does
# Windows SDK does not have to be installed. # not have to be installed.
name: Release name: Release
on: on:
@@ -84,25 +84,6 @@ 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
@@ -123,12 +104,7 @@ 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 }}
@@ -136,11 +112,7 @@ jobs:
run: | run: |
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$arguments = @{ $arguments = @{ Version = '${{ steps.version.outputs.version }}' }
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
@@ -156,172 +128,10 @@ jobs:
./Packaging/build-msix.ps1 @arguments ./Packaging/build-msix.ps1 @arguments
- name: Keep the Store packages # The artifact is where the package waits to be uploaded to Partner Center
uses: actions/upload-artifact@v4 - name: Keep the package
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/release/packages/ path: artifacts/packages/
if-no-files-found: error if-no-files-found: error
# The .wixpdb next to each installer is left out on purpose: it is of use
# only when something has to be traced back to the WiX source
- name: Keep the installer
uses: actions/upload-artifact@v4
with:
name: installer-${{ steps.version.outputs.plain }}
path: artifacts/installers/*.msi
if-no-files-found: error
# Only the installers go into the release. The MSIX stays in the artifacts
# of the run: unsigned, it installs nowhere, and its one destination is
# Partner Center
- name: Publish the release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
$ErrorActionPreference = 'Stop'
# The GITHUB_ names are what Gitea itself hands to the workflow — its
# actions repeat those of GitHub, and the addresses in them point at
# this Gitea instance. GITHUB_API_URL used not to reach the steps at
# all, so the address is put together from the server one when empty
$root = if ($env:GITHUB_API_URL) { $env:GITHUB_API_URL } else { "$env:GITHUB_SERVER_URL/api/v1" }
$api = "$root/repos/$env:GITHUB_REPOSITORY/releases"
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
# Gitea makes a release of its own for a pushed tag, so the release is
# looked up first and only made when it is not there
$release = $null
try { $release = Invoke-RestMethod "$api/tags/$env:TAG" -Headers $headers } catch { }
if (-not $release) {
$body = @{ tag_name = $env:TAG; name = $env:TAG; draft = $false; prerelease = $false } | ConvertTo-Json
$release = Invoke-RestMethod $api -Method Post -Headers $headers -ContentType 'application/json' -Body $body
}
# 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 }
foreach ($asset in $existing) {
Invoke-RestMethod "$api/$($release.id)/assets/$($asset.id)" -Method Delete -Headers $headers | Out-Null
}
Write-Host "Uploading $($file.Name)"
Invoke-RestMethod "$api/$($release.id)/assets?name=$($file.Name)" -Method Post -Headers $headers -Form @{ attachment = $file } | Out-Null
}
+11 -98
View File
@@ -3,15 +3,14 @@
Builds the CursorLang MSIX package. Builds the CursorLang MSIX package.
.DESCRIPTION .DESCRIPTION
Neither Visual Studio nor the Windows SDK is needed: makeappx and signtool Neither Visual Studio nor the Windows SDK is needed: makeappx arrives as a
arrive as a NuGet package (Tools\SdkTools.csproj) and the application is NuGet package (Tools\SdkTools.csproj) and the application is built by the
built by the plain .NET SDK. plain .NET SDK.
The package comes out of here in one of two shapes. Left unsigned it goes to The package is handed to Partner Center as it comes out of here — the Store
Partner Center as it is — the Store puts its own signature on it and does the puts its own signature on it and does the rest. Nothing is signed here: a
rest. Signed with -CertificateThumbprint it is a package anyone can install publicly trusted code signing certificate is not to be had, and the Store
from a release, because Windows only takes a package whose signature it asks for none.
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.
@@ -21,17 +20,6 @@
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
@@ -43,14 +31,8 @@
.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 `
-Publisher "CN=Aleksandr Neichev, O=Aleksandr Neichev, C=KZ" ` -IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
-CertificateThumbprint A1B2C3D4E5F60718293A4B5C6D7E8F9012345678
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
@@ -64,15 +46,6 @@ param(
[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
@@ -108,26 +81,6 @@ 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) {
# Checked before the build rather than after it: the build takes minutes, and # Checked before the build rather than after it: the build takes minutes, and
# this does not get any truer while it runs # this does not get any truer while it runs
@@ -205,34 +158,11 @@ 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
@@ -257,7 +187,6 @@ 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 = @()
$layout = Join-Path $layoutRoot 'x64' $layout = Join-Path $layoutRoot 'x64'
@@ -289,7 +218,7 @@ $manifest = (Get-Content $manifestTemplate -Raw).
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$suffix.msix" $package = Join-Path $packagesPath "CursorLang-$Version-$architecture.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
@@ -308,23 +237,12 @@ 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$suffix.msixbundle" $result = Join-Path $packagesPath "CursorLang-$Version.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...' -ForegroundColor Cyan Write-Host 'Installing...' -ForegroundColor Cyan
@@ -338,12 +256,7 @@ Write-Host 'Done.' -ForegroundColor Green
Write-Host " $result" Write-Host " $result"
Write-Host '' 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) { if ($Install) {
Write-Host '' Write-Host ''
+20 -55
View File
@@ -167,27 +167,8 @@ 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 установит только тогда, когда Дело не во вкусе, а в цене подписи. MSIX Windows установит только тогда, когда
доверяет подписи на нём, а публично доверенный сертификат для подписи кода доверяет подписи на нём, а публично доверенный сертификат для подписи кода
@@ -246,11 +227,11 @@ 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`:
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег Store принимает четыре числа и последнее оставляет себе, так что тег на него не
на него не влияет. влияет.
Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём — Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём —
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка .NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
@@ -260,29 +241,18 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
сеансе, пропускают себя: показать окно там негде. сеансе, пропускают себя: показать окно там негде.
Пакет собирается дважды: Store и релиз хотят от него разного. Пакет идёт в Store и больше никуда: он не подписан, подпись на него ставит сам
Partner Center. Поэтому прогон оставляет его в артефактах под именем
`msix-1.2.3.0`, откуда его забирают и загружают руками; к релизу не прикладывается
ничего — релиз по тегу Gitea заводит сама, и в нём один только тег. Неподписанный
пакет, висящий в релизе, выглядел бы как то, что можно установить, и не
устанавливался бы нигде — см. раздел об обновлениях.
Тот, что для Store, несёт identity, зарезервированную в Partner Center, и не Identity берётся из переменных репозитория, а если те не заданы — из значений по
подписан — подпись на него ставит сам Partner Center. Дальше артефактов прогона умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER`
он не уходит: его забирают и загружают руками. Что это он, видно по имени — и `MSIX_PUBLISHER_DISPLAY_NAME`. Вместе identity и publisher задают family name
`CursorLang-1.2.3.0-store.msixbundle`. Identity берётся из переменных пакета, поэтому от версии к версии оба должны оставаться прежними — иначе Store
репозитория, а если те не заданы — из значений по умолчанию в скрипте: примет следующую за другое приложение.
`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
@@ -312,13 +282,8 @@ Partner Center как есть.
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже. на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
`-CertificateThumbprint` подписывает всё, что собрано, сертификатом с этим Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
отпечатком из личного хранилища текущего пользователя; `-Publisher` тогда должен на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
называть его subject — скрипт говорит об этом до сборки, а не после. Обычно
подписывает пайплайн, но то же работает и руками, как только eSigner CKA — или
сертификат любого другого рода — положит сертификат в хранилище.
`-PackageSuffix` дописывается в конец имён файлов: сборки для Store и для релиза
различаются тем, что внутри, и больше ничем.
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
MSIX не может установить среду выполнения как зависимость пакета. MSIX не может установить среду выполнения как зависимость пакета.
+19 -58
View File
@@ -162,27 +162,8 @@ after a switch of the mode, and that is a question asked once.
## Updates ## Updates
The check runs when the settings window is opened, not when the machine is The Store updates the app, and the app itself does nothing about it: there is no
switched on: the background half no longer goes to the network at all, and there updates section in the window, no request to the network and no code for either.
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 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 MSIX only when it trusts the signature on it, and a publicly trusted code signing
@@ -241,11 +222,10 @@ 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 signed packages tested and packed into an MSIX, which is left in the artifacts of the run. The
attached. The version is taken from the tag alone — a tag shaped any other way version is taken from the tag alone — a tag shaped any other way stops the run
stops the run right at the start. The package version ends up as `1.2.3.0`: the right at the start. The package version ends up as `1.2.3.0`: the Store takes
Store takes four numbers and keeps the last one for itself, so the tag has no four numbers and keeps the last one for itself, so the tag has no say in it.
say in it.
Both pipelines ask for a Windows runner labelled `windows-x64` with the .NET 10 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 SDK and git-lfs on it. The checkout pulls LFS files: without them the icon is a
@@ -255,31 +235,18 @@ 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 is built twice over, because the Store and a release want different The package goes to the Store and nowhere else: it is unsigned, and Partner
things of it. 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 Store one carries the identity reserved in Partner Center and no signature — The identity comes from repository variables and falls back to the defaults of
Partner Center signs it there. It never leaves the artifacts of the run: someone the script when unset: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` and
picks it up and uploads it by hand. Its name says which one it is — `MSIX_PUBLISHER_DISPLAY_NAME`. Together the identity and the publisher decide
`CursorLang-1.2.3.0-store.msixbundle`. The identity comes from repository the family name of the package, so both have to stay as they are from version to
variables and falls back to the defaults of the script when unset: version, or the Store takes the next one for a different app.
`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
@@ -309,14 +276,8 @@ 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 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. on this machine the layout is registered instead — see `-Install` below.
`-CertificateThumbprint` signs everything the build produces, with the Nothing here is signed: the Store signs the package itself, and for installing it
certificate of that thumbprint out of the personal store of the current user; on this machine the layout is registered instead — see `-Install` below.
`-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.