6 Commits
Author SHA1 Message Date
alex 08192d317e fixed tests
Release / release (push) Successful in 8m58s
2026-08-11 16:34:28 +05:00
alex 3bf2018a62 fixed release version in pipeline
Release / release (push) Failing after 2m19s
2026-08-11 15:56:23 +05:00
alex cb39198e0a fixed pipeline
Release / release (push) Failing after 20s
2026-08-11 15:44:26 +05:00
alex a777b71b1a modified README.md
Release / release (push) Canceled after 2m15s
2026-08-11 15:02:04 +05:00
alex 71a8d3e6fb added CI 2026-08-11 15:01:47 +05:00
alex aee93a4ea3 removed "soap" from ico 2026-08-11 15:01:24 +05:00
8 changed files with 416 additions and 40 deletions
+82
View File
@@ -0,0 +1,82 @@
# The check every pull request goes through: the solution builds and the tests pass.
#
# The runner has to be a Windows one with the .NET 10 SDK: the application is
# WPF, so neither the build nor the tests happen anywhere else. The tests raise
# real windows and ask Windows for the foreground one, so the runner has to work
# in an interactive desktop session — as a service in session 0 the end-to-end
# checks have no window to wait for.
name: Pull request
on:
pull_request:
branches:
- master
# A new push into the branch makes the previous run pointless
concurrency:
group: pull-request-${{ github.event.pull_request.number }}
cancel-in-progress: true
defaults:
run:
shell: pwsh
jobs:
build:
runs-on: windows-x64
steps:
- name: Check out the sources
uses: actions/checkout@v4
# The exe icon lives in Git LFS, and without it the checkout leaves a text
# pointer that the build cannot read as an icon.
#
# The objects are fetched here rather than by `lfs: true` on the checkout:
# that way they arrive over a request the LFS endpoint accepts. See the
# comment on the header below
- name: Fetch the LFS objects
run: |
$ErrorActionPreference = 'Stop'
# actions/checkout leaves its own token in the config as an
# http.<server>/.extraheader, and git-lfs sends that header on to the
# LFS endpoint, which turns down the token of a workflow: every object
# comes back 401 and the fetch gives up. The repository is public and
# its LFS objects are readable without a token at all, so the header
# simply goes. A private repository would need credentials of its own
# in lfs.url instead
$keys = git config --local --list --name-only | Where-Object { $_ -like '*.extraheader' }
foreach ($key in $keys) { git config --local --unset-all $key }
git lfs pull
if ($LASTEXITCODE -ne 0) { throw "git lfs pull ended with exit code $LASTEXITCODE." }
# A pointer left in place of a file shows itself much later and in a
# way that is hard to read back: the build breaks on the icon
$pointers = git lfs ls-files --name-only |
Where-Object { (Get-Content $_ -TotalCount 1) -like 'version https://git-lfs*' }
if ($pointers) {
throw "Git LFS left pointers instead of files: $($pointers -join ', ')."
}
- name: Show the toolchain
run: dotnet --info
- name: Restore
run: dotnet restore CursorLang.sln --nologo
- name: Build
run: dotnet build CursorLang.sln --configuration Release --no-restore --nologo
# The tests take the application from bin\Release, which is why the whole
# run is a Release one: in Debug they would find no executable and skip
# themselves — a green run that checked nothing
- name: Test
run: >
dotnet test CursorLang.sln
--configuration Release
--no-build
--nologo
--settings CursorLang.Tests/coverage.runsettings
+166
View File
@@ -0,0 +1,166 @@
# The release: a tag of the form v1.2.3 builds the solution, runs the tests and
# packs the MSIX with the version taken from the tag — three numbers of the tag
# and a zero the Store keeps for itself.
#
# The same requirements to the runner as in pull-request.yml apply: Windows, the
# .NET 10 SDK and an interactive desktop session for the tests. makeappx comes
# with a NuGet package (Packaging\Tools\SdkTools.csproj), so the Windows SDK does
# not have to be installed.
name: Release
on:
push:
tags:
- 'v*'
defaults:
run:
shell: pwsh
jobs:
release:
runs-on: windows-x64
steps:
- name: Check out the sources
uses: actions/checkout@v4
# The exe icon and the MSIX logos live in Git LFS, and without them the
# checkout leaves text pointers in their place — the build fails on the
# icon and the package would carry broken logos.
#
# They are fetched here rather than by `lfs: true` on the checkout: that
# way the objects arrive over a request the LFS endpoint accepts. See the
# comment on the header below
- name: Fetch the LFS objects
run: |
$ErrorActionPreference = 'Stop'
# actions/checkout leaves its own token in the config as an
# http.<server>/.extraheader, and git-lfs sends that header on to the
# LFS endpoint, which turns down the token of a workflow: every object
# comes back 401 and the fetch gives up. The repository is public and
# its LFS objects are readable without a token at all, so the header
# simply goes. A private repository would need credentials of its own
# in lfs.url instead
$keys = git config --local --list --name-only | Where-Object { $_ -like '*.extraheader' }
foreach ($key in $keys) { git config --local --unset-all $key }
git lfs pull
if ($LASTEXITCODE -ne 0) { throw "git lfs pull ended with exit code $LASTEXITCODE." }
# A pointer left in place of a file shows itself much later and in a
# way that is hard to read back: the icon breaks the build, and a logo
# quietly ends up broken inside the package
$pointers = git lfs ls-files --name-only |
Where-Object { (Get-Content $_ -TotalCount 1) -like 'version https://git-lfs*' }
if ($pointers) {
throw "Git LFS left pointers instead of files: $($pointers -join ', ')."
}
# The tag is the only place the version comes from, and it is a plain
# version of three numbers — the same shape the application itself looks
# for in the releases when it checks for an update. A tag of any other
# shape is stopped here rather than halfway through the packaging
- name: Read the version from the tag
id: version
run: |
$ErrorActionPreference = 'Stop'
$tag = '${{ github.ref_name }}'
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
throw "The tag '$tag' does not fit: a release is tagged as v1.2.3 — three numbers. A fourth one does not belong in the tag: the Store keeps the revision for itself, and the package always gets a zero there."
}
# The package takes four numbers with a zero at the end: the Store
# 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
- name: Show the toolchain
run: dotnet --info
- name: Restore
run: dotnet restore CursorLang.sln --nologo
- name: Build
run: dotnet build CursorLang.sln --configuration Release --no-restore --nologo
- name: Test
run: >
dotnet test CursorLang.sln
--configuration Release
--no-build
--nologo
--settings CursorLang.Tests/coverage.runsettings
# The package comes out as Partner Center wants it — the Store puts its own
# signature on it. The identity comes from repository variables and falls
# back to the defaults of the script when a variable is not set.
- name: Pack the MSIX
env:
IDENTITY_NAME: ${{ vars.MSIX_IDENTITY_NAME }}
PUBLISHER: ${{ vars.MSIX_PUBLISHER }}
PUBLISHER_DISPLAY_NAME: ${{ vars.MSIX_PUBLISHER_DISPLAY_NAME }}
run: |
$ErrorActionPreference = 'Stop'
$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
$variables = @{
IdentityName = $env:IDENTITY_NAME
Publisher = $env:PUBLISHER
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 packages
uses: actions/upload-artifact@v4
with:
name: msix-${{ steps.version.outputs.version }}
path: artifacts/packages/
if-no-files-found: error
# Gitea creates a release of its own for a pushed tag, so the release is
# looked up first and only made when it is not there
- 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" }
$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
}
foreach ($file in Get-ChildItem artifacts/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
}
+39
View File
@@ -1,6 +1,8 @@
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace CursorLang.Tests;
@@ -14,6 +16,9 @@ namespace CursorLang.Tests;
///
/// If the application is already running in this session, the checks skip
/// themselves: meddling with someone else's running instance is not their business.
///
/// They skip themselves where there is no desktop to show a window on either — on a
/// build agent living as a Windows service, for one.
/// </remarks>
public sealed class EndToEndTests
{
@@ -59,6 +64,16 @@ public sealed class EndToEndTests
/// <summary>A started application that shuts down together with the check.</summary>
private sealed class Launch : IDisposable
{
private const int UOI_NAME = 2;
private const string InteractiveWindowStation = "WinSta0";
[DllImport("user32.dll")]
private static extern IntPtr GetProcessWindowStation();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern bool GetUserObjectInformation(IntPtr hObj, int nIndex,
StringBuilder pvInfo, int nLength, out int lpnLengthNeeded);
private Launch(Process process) => Process = process;
internal Process Process { get; }
@@ -66,6 +81,11 @@ public sealed class EndToEndTests
/// <summary>Starts the application first — making sure the place is free.</summary>
internal static Launch Start()
{
if (!HasInteractiveDesktop())
{
Assert.Skip("There is no interactive desktop here — the application has nowhere to show its window");
}
if (Process.GetProcessesByName("CursorLang").Length > 0)
{
Assert.Skip("The application is already running — this check keeps out of someone else's run");
@@ -116,6 +136,25 @@ public sealed class EndToEndTests
return IntPtr.Zero;
}
/// <summary>
/// Whether there is a desktop here to show a window on. A service gets a
/// window station of its own — "Service-0x0-3e7$" and the like: a window can
/// be created there, yet nothing shows it. Only "WinSta0" is the interactive one.
/// </summary>
private static bool HasInteractiveDesktop()
{
IntPtr station = GetProcessWindowStation();
if (station == IntPtr.Zero)
{
return false;
}
var name = new StringBuilder(256);
return GetUserObjectInformation(station, UOI_NAME, name, name.Capacity * sizeof(char), out _)
&& name.ToString().Equals(InteractiveWindowStation, StringComparison.OrdinalIgnoreCase);
}
private static string ExecutablePath()
{
string configured = Assembly.GetExecutingAssembly()
@@ -124,9 +124,7 @@ public sealed class LayoutPopupWindowTests
{
var settings = new AppSettings
{
PlacementMode = PopupPlacementMode.FixedPoint,
ScreenPosition = position,
ScreenMargin = 24,
PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = position, ScreenMargin = 24,
};
using var popup = Popup.Create(settings);
@@ -160,9 +158,7 @@ public sealed class LayoutPopupWindowTests
{
var settings = new AppSettings
{
PlacementMode = PopupPlacementMode.AtCursor,
CursorSide = AnchorSide.BottomRight,
CursorOffset = 16,
PlacementMode = PopupPlacementMode.AtCursor, CursorSide = AnchorSide.BottomRight, CursorOffset = 16,
};
using var popup = Popup.Create(settings);
@@ -213,23 +209,48 @@ public sealed class LayoutPopupWindowTests
{
var settings = new AppSettings
{
PlacementMode = PopupPlacementMode.AtCaret,
CaretSide = AnchorSide.BottomRight,
CaretOffset = 8,
PlacementMode = PopupPlacementMode.AtCaret, CaretSide = AnchorSide.BottomRight, CaretOffset = 8,
};
using var popup = Popup.Create(settings);
popup.ViewModel.ShortName = "RU";
PopupWindowNative.Point before = PopupWindowNative.GetCursorPosition();
PopupWindowNative.Point before = default;
for (int attempt = 0; attempt < 10; attempt++)
{
if (CaretNative.TryGetCaretRect() is not null)
{
Assert.Skip("The foreground window reported a caret of its own");
}
before = PopupWindowNative.GetCursorPosition();
popup.Window.ShowPopup();
PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition();
if (before.X == after.X && before.Y == after.Y)
{
break;
}
if (attempt == 9)
{
Assert.Skip("The cursor kept moving the whole time");
}
}
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
double scale = PopupWindowNative.GetScaleAt(before);
// A gentle check: a window holding the input focus may still have a caret
Assert.True(bounds.Right > bounds.Left);
Assert.True(bounds.Bottom > bounds.Top);
Assert.NotEqual(default, before);
PopupWindowNative.Point expected = PopupLayout.NearAnchor(
PopupLayout.AsAnchor(before),
settings.CaretSide,
PopupLayout.ToPixels(settings.CaretOffset, scale),
bounds.Right - bounds.Left,
bounds.Bottom - bounds.Top);
Assert.Equal(expected.X, bounds.Left);
Assert.Equal(expected.Y, bounds.Top);
});
}
@@ -266,8 +287,7 @@ public sealed class LayoutPopupWindowTests
{
var settings = new AppSettings
{
PlacementMode = PopupPlacementMode.FixedPoint,
ScreenPosition = ScreenPosition.TopLeft,
PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = ScreenPosition.TopLeft,
};
using var popup = Popup.Create(settings);
+24
View File
@@ -4,6 +4,21 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CursorLang", "CursorLang\Cu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CursorLang.Tests", "CursorLang.Tests\CursorLang.Tests.csproj", "{558F7646-AC3F-4E5E-853D-9F2A09E89C06}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packaging", "Packaging", "{E927F87B-A585-A586-52B2-1371051E189F}"
ProjectSection(SolutionItems) = preProject
Packaging\AppxManifest.xml = Packaging\AppxManifest.xml
Packaging\build-msix.ps1 = Packaging\build-msix.ps1
Packaging\New-Assets.ps1 = Packaging\New-Assets.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CI", "CI", "{3F1B9C24-7A0E-4C55-9E2D-6B41A8D5E713}"
ProjectSection(SolutionItems) = preProject
.gitea\workflows\pull-request.yml = .gitea\workflows\pull-request.yml
.gitea\workflows\release.yml = .gitea\workflows\release.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdkTools", "Packaging\Tools\SdkTools.csproj", "{788339D3-F0C3-4F1A-9216-501814C7BF78}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -38,8 +53,17 @@ Global
{558F7646-AC3F-4E5E-853D-9F2A09E89C06}.Release|x64.Build.0 = Release|Any CPU
{558F7646-AC3F-4E5E-853D-9F2A09E89C06}.Release|x86.ActiveCfg = Release|Any CPU
{558F7646-AC3F-4E5E-853D-9F2A09E89C06}.Release|x86.Build.0 = Release|Any CPU
{788339D3-F0C3-4F1A-9216-501814C7BF78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{788339D3-F0C3-4F1A-9216-501814C7BF78}.Debug|x64.ActiveCfg = Debug|Any CPU
{788339D3-F0C3-4F1A-9216-501814C7BF78}.Debug|x86.ActiveCfg = Debug|Any CPU
{788339D3-F0C3-4F1A-9216-501814C7BF78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{788339D3-F0C3-4F1A-9216-501814C7BF78}.Release|x64.ActiveCfg = Release|Any CPU
{788339D3-F0C3-4F1A-9216-501814C7BF78}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{788339D3-F0C3-4F1A-9216-501814C7BF78} = {E927F87B-A585-A586-52B2-1371051E189F}
EndGlobalSection
EndGlobal
Binary file not shown.
+34 -11
View File
@@ -135,28 +135,51 @@ dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage
композиционный корень в `App.xaml.cs`, который вместо этого проверяется сквозным
запуском приложения.
## Непрерывная сборка
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
проект собирается, проходит тесты, пакуется в MSIX и выкладывается релизом
с приложенными пакетами. Номер версии берётся только из тега — тег любого
другого вида останавливает прогон в самом начале. Версия пакета получается
`1.2.3.0`: Store принимает четыре числа и последнее оставляет себе, так что тег
на него не влияет.
Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём —
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
остаётся текстовой заглушкой и сборка на ней падает. Работать runner лучше
в интерактивном сеансе рабочего стола: тесты поднимают настоящие окна, и те
проверки, которым нужен свой рабочий стол — сквозные, а также те, что просят
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
сеансе, пропускают себя: показать окно там негде.
Пакет, который несёт релиз, загружается в Partner Center как есть. Identity
берётся из переменных репозитория, а если те не заданы — из значений по
умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER`
и `MSIX_PUBLISHER_DISPLAY_NAME`.
## Сборка пакета MSIX
Ни Visual Studio, ни Windows SDK не требуются — `makeappx` и `signtool`
поставляются из пакета NuGet. Порядок публикации целиком — от регистрации
разработчика до отправки на проверку — описан в
Ни Visual Studio, ни Windows SDK не требуются — `makeappx` поставляется из
пакета NuGet. Порядок публикации целиком — от регистрации разработчика
до отправки на проверку — описан в
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md).
```powershell
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
powershell -File Packaging\New-Assets.ps1
# Сборка и подпись для локальной проверки
powershell -File Packaging\build-msix.ps1 -Architectures x64 -Sign
# Проверка на своей машине: только своя архитектура
powershell -File Packaging\build-msix.ps1 -Architectures x64
# Сборка для Store — identity берётся из Partner Center
# Для Partner Center — identity та, что зарезервирована там
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345Alrakis.CursorLang -Publisher "CN=ABCD1234-..."
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
```
Результат — `artifacts\packages\CursorLang-<version>.msixbundle`, покрывающий x64
и arm64. Загружать в Partner Center его нужно неподписанным — Store подписывает
пакет собственным сертификатом.
Результат — `artifacts\packages\CursorLang-<версия>.msixbundle`, покрывающий x64
и arm64; рядом лежат пакеты отдельных архитектур. Bundle загружается в Partner
Center как есть.
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
MSIX не может установить среду выполнения как зависимость пакета.
@@ -166,5 +189,5 @@ MSIX не может установить среду выполнения как
```powershell
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
Remove-AppxPackage (Get-AppxPackage -Name Alrakis.CursorLang).PackageFullName
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
```
+32 -10
View File
@@ -133,28 +133,50 @@ What stays uncovered is what a test process cannot reach: the code paths that
require an MSIX package identity — `StartupTask` and the package data folder —
and the composition root in `App.xaml.cs`, which is exercised end-to-end instead.
## Continuous integration
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.
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
text pointer and the build fails on it. The runner is better off working in an
interactive desktop session: the tests raise real windows, and the checks that
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`.
## Building the MSIX package
Neither Visual Studio nor the Windows SDK is required — `makeappx` and
`signtool` come from a NuGet package. The full publishing procedure — from
opening a developer account to submitting for certification — is written up in
Neither Visual Studio nor the Windows SDK is required — `makeappx` comes from a
NuGet package. The full publishing procedure — from opening a developer account
to submitting for certification — is written up in
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md) (Russian only).
```powershell
# One-off: draw the logos and the exe icon (already committed, rerun after edits)
powershell -File Packaging\New-Assets.ps1
# Build and sign for local testing
powershell -File Packaging\build-msix.ps1 -Architectures x64 -Sign
# A check on your own machine: your architecture alone
powershell -File Packaging\build-msix.ps1 -Architectures x64
# Build for the Store — identity comes from Partner Center
# For Partner Center — the identity is the one reserved there
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
-IdentityName 12345Alrakis.CursorLang -Publisher "CN=ABCD1234-..."
-IdentityName 12345AleksandrNeychev.CursorLang -Publisher "CN=ABCD1234-..."
```
The result is `artifacts\packages\CursorLang-<version>.msixbundle` covering x64
and arm64. Upload it to Partner Center unsigned — the Store signs it with its own
certificate.
and arm64; next to it lie the packages of single architectures. Upload the bundle
to Partner Center as it is.
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.
@@ -164,5 +186,5 @@ needs Developer Mode and no signature at all:
```powershell
Add-AppxPackage -Register artifacts\layout\x64\AppxManifest.xml
Remove-AppxPackage (Get-AppxPackage -Name Alrakis.CursorLang).PackageFullName
Remove-AppxPackage (Get-AppxPackage -Name CursorLang).PackageFullName
```