# 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 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 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./.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 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 # 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/packages/ if-no-files-found: error