modified release pipeline (#4)
Release / release (push) Canceled after 47s

await certificate from ssl.com

Reviewed-on: #4
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
2026-08-13 11:17:22 +00:00
committed by alex
parent 5e11b16758
commit 42974ecc8f
34 changed files with 495 additions and 2091 deletions
+81 -67
View File
@@ -78,8 +78,8 @@ WPF took goes back to the system. The price is a cold start of half a second or
the next time the window is asked for.
`CursorLang.Core.dll` is what both hold in common: the models, the settings file,
the layout tracking, the updates. No UI, and it must stay that way — whatever lands
there lands in the background process.
the layout tracking. No UI, and it must stay that way — whatever lands there lands
in the background process.
The connection between the two is `settings.json` and nothing else. The window
writes it — whole, into a temporary file moved into place in one step — and then
@@ -162,57 +162,22 @@ 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 Store updates the app, and the app itself does nothing about it: there is no
updates section in the window, no request to the network and no code for either.
The 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.
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
certificate has turned out to be beyond reach — the certificate authorities that
sell them will not issue one here, and those that would keep the key in a cloud
HSM will not either, while a certificate on a USB token cannot be shipped in.
Without a signature a package installs nowhere but a machine in developer mode,
so there is nothing to hand out from a release and nothing for an updater to
find. The Store signs the package with its own certificate and updates the app
by itself, which leaves the whole question to it.
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 running app keeps working off the old files until it is
restarted.
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
over it anyway.
The app asks about releases every time the settings window appears, and the
answer stays in the section as it is — an unreachable network included. Opening
that window is a deliberate act, rare enough for a request to cost nothing, so
nothing is remembered between openings and there is nothing to turn off. The
button next to the version asks the same question again on demand.
Where the releases are looked for is set in `UpdateOptions` — the repository
belongs to whoever publishes the app, not to the user, so the values live in the
build rather than in `settings.json`:
```csharp
services.AddSingleton(new UpdateOptions
{
ServiceUri = new Uri("https://git.alrakis.kz/"), // the Gitea server itself
Project = "alrakis/cursor-lang",
});
```
The releases come from Gitea, and its API sits on the server itself: the address
is the one the repository is opened at in a browser, and the app asks
`/api/v1/repos/{owner}/{repo}/releases` under it.
A closed repository needs a token. It is read from the `CURSORLANG_UPDATE_TOKEN`
environment variable rather than kept in the source: a secret built into the app
is a secret handed to everyone who got the app.
The version of the running app is in the title of the settings window:
`CursorLang 1.2.3 — Settings`. It is nowhere else in the interface, and it is
there so that a bug report can name it.
## Tests
@@ -257,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 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
@@ -271,9 +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 the release carries goes to Partner Center as it is. 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 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 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
@@ -286,17 +259,22 @@ to submitting for certification — is written up in
# One-off: draw the logos and the exe icon (already committed, rerun after edits)
powershell -File Packaging\New-Assets.ps1
# A check on your own machine: your architecture alone
powershell -File Packaging\build-msix.ps1 -Architectures x64
# A check on your own machine
powershell -File Packaging\build-msix.ps1
# For Partner Center — the identity is the one reserved there
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
and arm64; next to it lie the packages of single architectures. Upload the bundle
to Partner Center as it is.
The result is `artifacts\packages\CursorLang-<version>-x64.msix`. Upload it to
Partner Center as it is.
Only x64 is built. An arm64 build would double the size of every release for the
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.
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.
@@ -304,7 +282,7 @@ MSIX cannot install a runtime as a package dependency.
To see the package working on this machine, build it with `-Install`:
```powershell
pwsh -File Packaging\build-msix.ps1 -Architectures x64 -Install
pwsh -File Packaging\build-msix.ps1 -Install
```
The application then shows up in the Start menu like any installed one. What
@@ -318,5 +296,41 @@ the Store answers to the same name and is left alone.
To remove it by hand:
```powershell
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeychev.CursorLang).PackageFullName
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
```
## Building the installer
The same application also comes as an ordinary MSI, for handing round outside the
Store — to try it out before the Store has passed judgement, or if it never does.
Nothing has to be installed beyond the .NET SDK: WiX comes from a NuGet package,
the same way `makeappx` does.
```powershell
# Build it and run it afterwards, to see what a user sees
pwsh -File Packaging\build-installer.ps1 -Install
# Everything a release needs
pwsh -File Packaging\build-installer.ps1 -Version 1.0.1
```
The result is `artifacts\installers\CursorLang-<version>-x64.msi`. Everything
travels inside the .msi; there is no cabinet to send alongside it.
It installs for the current user alone, into `%LOCALAPPDATA%\Programs\CursorLang`,
and so asks for no administrator rights and no consent dialog. Uninstalling goes
through Apps in Settings like any other program and takes the startup entry with
it — otherwise Windows would go on listing an application that is no longer there.
Nobody signs the installer, so Windows warns about an unknown publisher and the
user has to insist. Buying a certificate would not silence it at once either:
SmartScreen goes by reputation, and a fresh certificate has none until enough
people have installed the application.
Two things about the WiX project are worth knowing before touching it. It pins
WiX 5 rather than the current 7: from version 6 the toolset asks every build to
accept the Open Source Maintenance Fee licence — free below $10,000 of yearly
revenue, but a decision for a person rather than for a build script. And it turns
off three ICE validation rules: MSI still assumes an installation for the whole
machine, and installing into the user's own profile trips rules that describe
exactly what was intended here.