diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index ccbc0cf..23834ff 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -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 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 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 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 -# signtool come 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 comes +# with a NuGet package (Packaging\Tools\SdkTools.csproj), so the Windows SDK does +# not have to be installed. name: Release on: @@ -84,25 +84,6 @@ jobs: # reserves the last one, so it carries nothing the tag could tell "version=$($tag.Substring(1)).0" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 - # The signing account is asked about before anything is built rather than - # at the step that needs it: a release without a signed package is not a - # release, and finding that out after the build and the tests costs the - # whole run - - name: Check the signing credentials - env: - ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }} - ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }} - ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }} - run: | - $ErrorActionPreference = 'Stop' - - $missing = @('ESIGNER_USERNAME', 'ESIGNER_PASSWORD', 'ESIGNER_TOTP_SECRET') | - Where-Object { -not (Get-Item "Env:$_" -ErrorAction SilentlyContinue).Value } - - if ($missing) { - throw "The repository secrets $($missing -join ', ') are not set. They are the SSL.com account the package is signed with; the TOTP secret is the one eSigner hands out for automated signing, not a six-digit code." - } - - name: Show the toolchain run: dotnet --info @@ -123,12 +104,7 @@ 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. - # - # 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 + - name: Pack the MSIX env: IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }} PUBLISHER: ${{ vars.MSIX_PUBLISHER }} @@ -136,11 +112,7 @@ jobs: run: | $ErrorActionPreference = 'Stop' - $arguments = @{ - Version = '${{ steps.version.outputs.version }}' - PackageSuffix = 'store' - OutputPath = 'artifacts/store' - } + $arguments = @{ Version = '${{ steps.version.outputs.version }}' } # An empty variable is left out rather than passed on: the script has # defaults of its own, and an empty string would wipe them @@ -156,172 +128,10 @@ jobs: ./Packaging/build-msix.ps1 @arguments - - 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 + # The artifact is where the package waits to be uploaded to Partner Center + - name: Keep the package uses: actions/upload-artifact@v4 with: name: msix-${{ steps.version.outputs.version }} - path: artifacts/release/packages/ + path: artifacts/packages/ 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 - } diff --git a/Packaging/build-msix.ps1 b/Packaging/build-msix.ps1 index e297160..b2144a3 100644 --- a/Packaging/build-msix.ps1 +++ b/Packaging/build-msix.ps1 @@ -3,15 +3,14 @@ Builds the CursorLang MSIX package. .DESCRIPTION - 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. + 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. - 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 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 application is published with its own copy of .NET: Windows does not 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; 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 @@ -43,14 +31,8 @@ .EXAMPLE # 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 ` - -Publisher "CN=Aleksandr Neichev, O=Aleksandr Neichev, C=KZ" ` - -CertificateThumbprint A1B2C3D4E5F60718293A4B5C6D7E8F9012345678 + -IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..." #> [CmdletBinding()] param( @@ -64,15 +46,6 @@ param( [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 @@ -108,26 +81,6 @@ 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 @@ -205,34 +158,11 @@ 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 @@ -257,7 +187,6 @@ foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) { Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null -$suffix = if ($PackageSuffix) { "-$PackageSuffix" } else { '' } $built = @() $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 - $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) $built += $package @@ -308,23 +237,12 @@ if ($built.Count -gt 1) { New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null $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) 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 @@ -338,12 +256,7 @@ Write-Host 'Done.' -ForegroundColor Green Write-Host " $result" 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) { Write-Host '' diff --git a/README.RU.md b/README.RU.md index 6e6200e..7682f9e 100644 --- a/README.RU.md +++ b/README.RU.md @@ -167,27 +167,8 @@ MSIX всегда выполняются в контексте вошедшег ## Обновления -Проверка выполняется при открытии окна настроек, а не при включении машины: -фоновая половина в сеть больше не ходит вовсе, да и показать ответ ей нечем. -Настройка в окне так и написана. - -Приложение ищет новые версии среди выпусков собственного репозитория. Выпуск -годится, если его тег — это просто версия (`v1.2.3` или `1.2.3`) и к нему -приложен пакет MSIX. Тег, в котором есть что-то ещё, — в том числе `v1.2.3-beta` -— пропускается: предварительную версию берут намеренно, приложение её не -предлагает. - -Из приложенных файлов предпочитается `.msixbundle` — он несёт обе архитектуры. -Если его нет, берётся пакет, в имени которого стоит архитектура этой машины: -`CursorLang-1.2.3.0-x64.msix`. Такие имена даёт `build-msix.ps1`, так что выпуск -делается прикладыванием того, что он собрал. - -Пакет скачивается во временную папку и передаётся установщику приложений -Windows: тот показывает издателя, спрашивает подтверждение и заменяет -установленную версию. Подпись проверяет Windows, поэтому приложенный к выпуску -пакет должен быть подписан — неподписанный установится только на машине в режиме -разработчика; пайплайн подписывает то, что прикладывает. Работающее приложение -до перезапуска продолжает жить на старых файлах. +Приложение обновляет Store, а само приложение об этом не заботится: раздела +обновлений в окне нет, запросов в сеть нет и кода для них тоже нет. Дело не во вкусе, а в цене подписи. MSIX Windows установит только тогда, когда доверяет подписи на нём, а публично доверенный сертификат для подписи кода @@ -246,11 +227,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: без них иконка @@ -260,29 +241,18 @@ dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом сеансе, пропускают себя: показать окно там негде. -Пакет собирается дважды: Store и релиз хотят от него разного. +Пакет идёт в Store и больше никуда: он не подписан, подпись на него ставит сам +Partner Center. Поэтому прогон оставляет его в артефактах под именем +`msix-1.2.3.0`, откуда его забирают и загружают руками; к релизу не прикладывается +ничего — релиз по тегу Gitea заводит сама, и в нём один только тег. Неподписанный +пакет, висящий в релизе, выглядел бы как то, что можно установить, и не +устанавливался бы нигде — см. раздел об обновлениях. -Тот, что для 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 пакета, поэтому от релиза к -релизу оба должны оставаться прежними — иначе обновление встанет рядом со старой -версией, а не заменит её. +Identity берётся из переменных репозитория, а если те не заданы — из значений по +умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER` +и `MSIX_PUBLISHER_DISPLAY_NAME`. Вместе identity и publisher задают family name +пакета, поэтому от версии к версии оба должны оставаться прежними — иначе Store +примет следующую за другое приложение. ## Сборка пакета MSIX @@ -312,13 +282,8 @@ Partner Center как есть. Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки на эту машину вместо пакета регистрируется layout — см. `-Install` ниже. -`-CertificateThumbprint` подписывает всё, что собрано, сертификатом с этим -отпечатком из личного хранилища текущего пользователя; `-Publisher` тогда должен -называть его subject — скрипт говорит об этом до сборки, а не после. Обычно -подписывает пайплайн, но то же работает и руками, как только eSigner CKA — или -сертификат любого другого рода — положит сертификат в хранилище. -`-PackageSuffix` дописывается в конец имён файлов: сборки для Store и для релиза -различаются тем, что внутри, и больше ничем. +Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки +на эту машину вместо пакета регистрируется layout — см. `-Install` ниже. Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а MSIX не может установить среду выполнения как зависимость пакета. diff --git a/README.md b/README.md index 998366d..0a03655 100644 --- a/README.md +++ b/README.md @@ -162,27 +162,8 @@ after a switch of the mode, and that is a question asked once. ## Updates -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. +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. 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 @@ -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 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 -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. +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. 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 @@ -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 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 -things of it. +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 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. +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. ## 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 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. +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. 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.