diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cced3d..90f44f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,10 @@ name: Release # Builds the three published artifacts on a real Windows runner and attaches -# them to a GitHub Release. Triggered by pushing a version tag, e.g. v1.1.3. +# them to a GitHub Release. Triggered by pushing a version tag (v1.1.3), or run +# manually with the tag as an input — the manual path creates the tag itself, so +# a release can be cut without local push access to refs/tags, and a failed +# release can be retried without inventing a new version number. # # The build itself is build-release.ps1 — the same script used locally — so a # release built here and one built by hand cannot drift apart. @@ -9,6 +12,12 @@ name: Release on: push: tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Version tag to publish, e.g. v1.1.3" + required: true + type: string permissions: contents: write @@ -21,6 +30,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Passed through the environment rather than interpolated into the script + # body, so the input cannot inject PowerShell. + - name: Resolve release tag + shell: pwsh + env: + INPUT_TAG: ${{ inputs.tag }} + run: | + $tag = if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { $env:INPUT_TAG } else { $env:GITHUB_REF_NAME } + $tag = $tag.Trim() + if ($tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$') { + throw "Refusing to publish an unexpected tag format: '$tag'" + } + Add-Content -Path $env:GITHUB_ENV -Value "RELEASE_TAG=$tag" + Write-Host "Publishing $tag" + - name: Set up .NET 10 uses: actions/setup-dotnet@v4 with: @@ -37,7 +61,7 @@ jobs: - name: Verify tag matches project version shell: pwsh run: | - $tag = $env:GITHUB_REF_NAME + $tag = $env:RELEASE_TAG $scriptVersion = (Select-String -Path build-release.ps1 -Pattern '^\$Version = "(.+)"').Matches[0].Groups[1].Value $csprojVersion = ([xml](Get-Content WinBatLens.csproj)).Project.PropertyGroup.Version | Where-Object { $_ } $issVersion = (Select-String -Path installer/WinBatLens.iss -Pattern '#define MyAppVersion "(.+)"').Matches[0].Groups[1].Value @@ -48,6 +72,17 @@ jobs: if ($csprojVersion -ne $scriptVersion) { throw "WinBatLens.csproj version $csprojVersion does not match $scriptVersion" } if ($issVersion -ne $scriptVersion) { throw "installer/WinBatLens.iss version $issVersion does not match $scriptVersion" } + # Publishing the same version twice would either fail late or quietly + # replace what people have already downloaded. + - name: Verify the release does not already exist + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release view $env:RELEASE_TAG 2>$null + if ($LASTEXITCODE -eq 0) { throw "Release $env:RELEASE_TAG already exists" } + Write-Host "$env:RELEASE_TAG is free" + - name: Build release packages shell: pwsh run: ./build-release.ps1 @@ -55,7 +90,7 @@ jobs: - name: Verify all three artifacts exist shell: pwsh run: | - $version = $env:GITHUB_REF_NAME.TrimStart('v') + $version = $env:RELEASE_TAG.TrimStart('v') $expected = @( "dist/WinBatLens_v${version}_Setup_x64.exe", "dist/WinBatLens_v${version}_Portable_x64.exe", @@ -70,7 +105,7 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: winbatlens-${{ github.ref_name }} + name: winbatlens-${{ env.RELEASE_TAG }} path: dist/* if-no-files-found: error @@ -87,7 +122,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - $tag = $env:GITHUB_REF_NAME + $tag = $env:RELEASE_TAG $notes = ".github/release-notes/$tag.md" $isPrerelease = $tag -match '^v[0-9.]+-' @@ -95,7 +130,14 @@ jobs: $isPrerelease = $true } - $ghArgs = @("release", "create", $tag, "--title", "WinBat Lens $tag", "--verify-tag") + $ghArgs = @("release", "create", $tag, "--title", "WinBat Lens $tag") + + # On a tag push the tag is already there and must be the one we built. + # On a manual run it does not exist yet, so it is created here against + # the exact commit that produced these artifacts. + if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { $ghArgs += @("--target", $env:GITHUB_SHA) } + else { $ghArgs += "--verify-tag" } + if ($isPrerelease) { $ghArgs += "--prerelease" } if (Test-Path $notes) { $ghArgs += @("--notes-file", $notes) } else { $ghArgs += "--generate-notes" }