diff --git a/.gitea/workflows/pull-request.yml b/.gitea/workflows/pull-request.yml new file mode 100644 index 0000000..81d8cf9 --- /dev/null +++ b/.gitea/workflows/pull-request.yml @@ -0,0 +1,54 @@ +# 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: + # lfs: true is not a nicety: the exe icon lives in Git LFS, and without it + # the checkout leaves a text pointer that the build cannot read as an icon + - name: Check out the sources + uses: actions/checkout@v4 + with: + lfs: true + + - 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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..2f90208 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,133 @@ +# The release: a tag of the form v1.2.3.0 builds the solution, runs the tests +# and packs the MSIX with the version taken from the tag. +# +# 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: + # lfs: true is not a nicety: the exe icon and the MSIX logos live in Git + # LFS, and without it the checkout leaves text pointers in their place — + # the build fails on the icon and the package would carry broken logos + - name: Check out the sources + uses: actions/checkout@v4 + with: + lfs: true + + # The tag is the only place the version comes from: the Store takes four + # numbers ending in zero, so anything else 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+\.0$') { + throw "The tag '$tag' does not fit: a release is tagged as v1.2.3.0 — four numbers ending in zero." + } + + "version=$($tag.Substring(1))" | 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 + } diff --git a/CursorLang.sln b/CursorLang.sln index 96a3be2..752572b 100644 --- a/CursorLang.sln +++ b/CursorLang.sln @@ -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