added msi to release pipeline

This commit is contained in:
2026-08-13 16:24:59 +05:00
parent f75ab0f077
commit 2c46f41db8
6 changed files with 273 additions and 34 deletions
+59
View File
@@ -84,6 +84,10 @@ 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 installer answers to nobody about a fourth number and takes the
# tag as it is
"plain=$($tag.Substring(1))" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
- name: Show the toolchain - name: Show the toolchain
run: dotnet --info run: dotnet --info
@@ -128,6 +132,12 @@ jobs:
./Packaging/build-msix.ps1 @arguments ./Packaging/build-msix.ps1 @arguments
# The other half of the release: the same application as an ordinary
# installer, for handing round outside the Store. Nobody signs it, so
# SmartScreen warns about it — see Packaging\installer.iss
- name: Build the installer
run: ./Packaging/build-installer.ps1 -Version ${{ steps.version.outputs.plain }}
# The artifact is where the package waits to be uploaded to Partner Center # The artifact is where the package waits to be uploaded to Partner Center
- name: Keep the package - name: Keep the package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -135,3 +145,52 @@ jobs:
name: msix-${{ steps.version.outputs.version }} name: msix-${{ steps.version.outputs.version }}
path: artifacts/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
}
foreach ($file in Get-ChildItem artifacts/installers -File -Filter *.msi) {
# 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
}
+48
View File
@@ -0,0 +1,48 @@
<!--
The installer project. It is not part of CursorLang.sln and is not meant to be
built on its own: build-installer.ps1 publishes the application first and then
builds this, passing in the folder it published to.
WiX 5 rather than the current 7: from version 6 onwards the toolset asks every
build to accept the Open Source Maintenance Fee licence, which is a decision for
a person to make and not for a build script. Version 5 carries no such
requirement. Moving up later means changing the version here and adding
-p:AcceptEula=true to the build.
-->
<Project Sdk="WixToolset.Sdk/5.0.2">
<PropertyGroup>
<OutputName>CursorLang</OutputName>
<OutputType>Package</OutputType>
<!--
MSI validation still assumes an installation for the whole machine.
Installing into the user's own profile trips three of its rules: a
component whose key path is a file (ICE38), a folder it wants listed
for removal (ICE64), and a warning that the files will not follow other
users of the machine (ICE91). All three describe exactly what was
intended here, so they are turned off rather than worked around.
-->
<SuppressIces>ICE38;ICE64;ICE91</SuppressIces>
<!--
What the script passes in, gathered into the constants the source file
reads. They are gathered here rather than handed over as one property
from the command line: a semicolon in a property value is where MSBuild
stops reading it, and the separator between constants is a semicolon.
-->
<DefineConstants>
Version=$(CursorLangVersion);
PayloadDir=$(PayloadDir);
IconFile=$(IconFile);
LicenseFile=$(LicenseFile)
</DefineConstants>
</PropertyGroup>
<ItemGroup>
<!-- CloseApplication and the launch at the end of the wizard come from here -->
<PackageReference Include="WixToolset.Util.wixext" Version="5.0.2" />
<PackageReference Include="WixToolset.UI.wixext" Version="5.0.2" />
</ItemGroup>
</Project>
+163
View File
@@ -0,0 +1,163 @@
<#
.SYNOPSIS
Builds the CursorLang installer for handing the application round outside
the Store.
.DESCRIPTION
Nothing has to be installed beyond the .NET SDK: WiX arrives as a NuGet
package, the same way makeappx does for the MSIX build.
Nobody signs the result, so Windows warns about an unknown publisher and the
user has to insist. Short of a certificate from a trusted authority there is
no way round that, and it is the reason this installer is meant for people who
already know where the file came from.
The application is published with its own copy of .NET: Windows carries no
runtime of its own, and a self-contained build is tied to x64.
.PARAMETER Install
Runs the installer once it is built, to see the thing through the way a user
would.
.EXAMPLE
# Build and run it, to see what a user sees
pwsh -File Packaging\build-installer.ps1 -Install
.EXAMPLE
# What a release needs
pwsh -File Packaging\build-installer.ps1 -Version 1.0.1
#>
[CmdletBinding()]
param(
# Three numbers: unlike the Store, an installer keeps no place for a fourth
[string] $Version = '1.0.0',
[switch] $Install,
[string] $OutputPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$repository = Split-Path -Parent $root
$agentProject = Join-Path $repository 'CursorLang.Agent\CursorLang.Agent.csproj'
$settingsProject = Join-Path $repository 'CursorLang.Settings\CursorLang.Settings.csproj'
$installerProject = Join-Path $root 'Installer\CursorLang.wixproj'
$icon = Join-Path $repository 'CursorLang.Core\Resources\CursorLang.ico'
$license = Join-Path $repository 'LICENSE'
if (-not $OutputPath) { $OutputPath = Join-Path $repository 'artifacts' }
$stagingRoot = Join-Path $OutputPath 'installer-staging'
$installersPath = Join-Path $OutputPath 'installers'
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
throw "The version '$Version' does not fit: an installer is versioned by three numbers, for example 1.0.0."
}
if (-not (Test-Path $icon)) {
throw "The icon is missing from '$icon'. Git LFS may have left a pointer in its place: run git lfs pull."
}
function Invoke-Tool {
<#
.SYNOPSIS
Runs a program and fails the build if it returned an error.
.DESCRIPTION
A wrapper of our own is needed because PowerShell does not treat the
failure of an external program as an error and quietly moves on.
#>
param(
[string] $Path,
[string[]] $Arguments
)
& $Path @Arguments
if ($LASTEXITCODE -ne 0) {
throw "'$([System.IO.Path]::GetFileName($Path))' exited with code $LASTEXITCODE."
}
}
function Write-LicenseRtf {
<#
.SYNOPSIS
Turns the plain-text LICENSE into the RTF the wizard needs.
.DESCRIPTION
The licence page of a Windows Installer wizard reads RTF and nothing
else, and keeping a second copy of the licence in the repository would
mean keeping the two in step by hand. The conversion is as plain as it
looks: the text carries no formatting to preserve.
#>
param([string] $Path)
$text = (Get-Content $license -Raw) -replace '\\', '\\\\' -replace '{', '\{' -replace '}', '\}'
$body = ($text -split "`r?`n") -join '\par' + "`r`n"
Set-Content -Path $Path -Value "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}}\fs18 $body}" -Encoding ASCII
}
Remove-Item $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $installersPath -Force | Out-Null
$licenseRtf = Join-Path $stagingRoot 'License.rtf'
New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null
Write-LicenseRtf -Path $licenseRtf
Write-Host 'Building...' -ForegroundColor Cyan
$staging = Join-Path $stagingRoot 'x64'
# Both halves go into one folder: each of them looks for the other beside
# itself — the tray menu opens the settings window, and the settings window
# registers the agent for startup
foreach ($half in @($agentProject, $settingsProject)) {
Invoke-Tool -Path 'dotnet' -Arguments @(
'publish', $half,
'--configuration', 'Release',
'--runtime', 'win-x64',
'--self-contained', 'true',
"-p:Version=$Version",
'--output', $staging,
'--nologo'
)
}
# Debug symbols only make the download bigger; they are of no use to anyone
# who installs the application
Get-ChildItem $staging -Recurse -Filter '*.pdb' | Remove-Item -Force
$installer = Join-Path $installersPath "CursorLang-$Version-x64.msi"
Invoke-Tool -Path 'dotnet' -Arguments @(
'build', $installerProject,
'--configuration', 'Release',
# The architecture of the package is the architecture of what goes in it:
# a self-contained build fits nothing else
'-p:InstallerPlatform=x64',
# Handed over one by one rather than as a ready list of constants: a
# semicolon in a property value is where MSBuild stops reading it. The
# project gathers them into DefineConstants itself
"-p:CursorLangVersion=$Version",
"-p:PayloadDir=$staging",
"-p:IconFile=$icon",
"-p:LicenseFile=$licenseRtf",
"-p:OutputPath=$installersPath\",
"-p:OutputName=CursorLang-$Version-x64",
'--nologo'
)
if ($Install) {
Write-Host "Running $([System.IO.Path]::GetFileName($installer))..." -ForegroundColor Cyan
# Started the way a user would, with the wizard showing: the point of -Install
# is to see what the person on the other end sees
Start-Process 'msiexec.exe' -ArgumentList '/i', "`"$installer`"" -Wait
}
Write-Host ''
Write-Host 'Done.' -ForegroundColor Green
Write-Host " $installer"
Write-Host ''
Write-Host ' Nobody signed it, so Windows warns about an unknown publisher.'
+3 -28
View File
@@ -43,9 +43,6 @@ param(
[string] $Publisher = 'CN=Aleksandr Neichev', [string] $Publisher = 'CN=Aleksandr Neichev',
[string] $PublisherDisplayName = 'Aleksandr Neichev', [string] $PublisherDisplayName = 'Aleksandr Neichev',
[ValidateSet('x64', 'arm64')]
[string[]] $Architectures = @('x64', 'arm64'),
[switch] $Install, [switch] $Install,
[string] $OutputPath [string] $OutputPath
@@ -187,7 +184,7 @@ foreach ($installed in @(Get-AppxPackage -Name $IdentityName)) {
Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $layoutRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null New-Item -ItemType Directory -Path $packagesPath -Force | Out-Null
$built = @() Write-Host 'Building...' -ForegroundColor Cyan
$layout = Join-Path $layoutRoot 'x64' $layout = Join-Path $layoutRoot 'x64'
@@ -218,30 +215,8 @@ $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.msix" $result = Join-Path $packagesPath "CursorLang-$Version-x64.msix"
Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $package) Invoke-Tool -Path $makeappx -Arguments @('pack', '/o', '/d', $layout, '/p', $result)
$built += $package
}
$result = $built[0]
if ($built.Count -gt 1) {
Write-Host 'Bringing the architectures into one package...' -ForegroundColor Cyan
# makeappx bundle takes everything from a folder, so the separate packages
# are gathered into one of their own first — otherwise the results of
# earlier builds would end up in the bundle
$bundleInput = Join-Path $OutputPath 'bundle'
Remove-Item $bundleInput -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $bundleInput -Force | Out-Null
$built | ForEach-Object { Copy-Item $_ -Destination $bundleInput }
$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 ($Install) { if ($Install) {
Write-Host 'Installing...' -ForegroundColor Cyan Write-Host 'Installing...' -ForegroundColor Cyan
-3
View File
@@ -282,9 +282,6 @@ Partner Center как есть.
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже. на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
MSIX не может установить среду выполнения как зависимость пакета. MSIX не может установить среду выполнения как зависимость пакета.
-3
View File
@@ -276,9 +276,6 @@ 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.
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 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.