Merge pull request #33 from TheValiantOne/fix/chain-step-validation #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write | |
| jobs: | |
| # Cheap, dotnet-free sanity check, run first so a bad tag fails fast before any | |
| # publish work starts. Also the single source of truth for "version" (a job output) | |
| # every later job consumes - always the value read back from the repo's own | |
| # AssemblyInfo.cs, not the raw tag text, once the two are confirmed equal below. | |
| verify-version: | |
| name: Verify release version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| # WitcherScriptMerger (the WinForms host) has GenerateAssemblyInfo=false and | |
| # hand-maintains its version in Properties/AssemblyInfo.cs instead (see that | |
| # project's CLAUDE.md's "Compatibility constraint: TFM must keep the explicit 7.0 | |
| # OS-version suffix" section) - a `-p:Version=` passed to `dotnet publish` can't | |
| # reach it, unlike WitcherScriptMerger.Headless (see the build job below). | |
| # WitcherScriptMerger.Headless.csproj's own <Version> property is a second, | |
| # independently hand-maintained copy of the same value (see its own comment) with | |
| # nothing else enforcing the two stay in sync. Nothing else enforces either against | |
| # a pushed release tag. Check all three here and fail loudly on any mismatch, | |
| # instead of silently shipping a release with a wrong/inconsistent --version. | |
| - name: Check AssemblyInfo.cs / csproj / tag versions agree | |
| id: check | |
| shell: pwsh | |
| run: | | |
| $tagVersion = $env:GITHUB_REF_NAME -replace '^v', '' | |
| # Select-String matches per-line, so the commented-out SDK boilerplate example | |
| # two lines above the real attribute ("// [assembly: AssemblyVersion(...") | |
| # can never match this ^-anchored pattern - only a line that actually starts | |
| # with "[assembly:" can. | |
| $asmMatch = Select-String -Path 'WitcherScriptMerger/Properties/AssemblyInfo.cs' ` | |
| -Pattern '^\[assembly:\s*AssemblyVersion\("([^"]+)"\)\]' | Select-Object -First 1 | |
| if (-not $asmMatch) { | |
| throw "Could not find an uncommented [assembly: AssemblyVersion(...)] line in WitcherScriptMerger/Properties/AssemblyInfo.cs" | |
| } | |
| $assemblyVersion = $asmMatch.Matches[0].Groups[1].Value | |
| $csprojMatch = Select-String -Path 'WitcherScriptMerger.Headless/WitcherScriptMerger.Headless.csproj' ` | |
| -Pattern '<Version>([^<]+)</Version>' | Select-Object -First 1 | |
| if (-not $csprojMatch) { | |
| throw "Could not find a <Version> element in WitcherScriptMerger.Headless/WitcherScriptMerger.Headless.csproj" | |
| } | |
| $csprojVersion = $csprojMatch.Matches[0].Groups[1].Value | |
| if ($csprojVersion -ne $assemblyVersion) { | |
| throw "WitcherScriptMerger.Headless.csproj's <Version> ('$csprojVersion') does not match WitcherScriptMerger/Properties/AssemblyInfo.cs's AssemblyVersion ('$assemblyVersion') - keep them in sync before tagging a release." | |
| } | |
| if ($assemblyVersion -ne $tagVersion) { | |
| throw "Tag '$env:GITHUB_REF_NAME' (version '$tagVersion') does not match WitcherScriptMerger/Properties/AssemblyInfo.cs's AssemblyVersion ('$assemblyVersion'). Bump AssemblyVersion/AssemblyFileVersion there (and WitcherScriptMerger.Headless.csproj's <Version>) before tagging a release." | |
| } | |
| "version=$assemblyVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| # build.yml (dotnet build + dotnet format whitespace --verify-no-changes) only runs on | |
| # pull_request - a tag pushed directly, without going through a PR, would otherwise | |
| # reach the publish/release steps below with zero build or test verification anywhere | |
| # in this path. Mirrors build.yml's own build step (windows-latest, Release | |
| # configuration) and adds dotnet test, gating the publish matrix job on both passing. | |
| test: | |
| name: Build & test | |
| needs: verify-version | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| cache: true | |
| cache-dependency-path: WitcherScriptMerger/WitcherScriptMerger.csproj | |
| - name: Restore | |
| run: dotnet restore WitcherScriptMerger.sln | |
| - name: Build | |
| run: dotnet build WitcherScriptMerger.sln --no-restore --configuration Release | |
| - name: Test | |
| run: dotnet test WitcherScriptMerger.sln --no-build --configuration Release | |
| # Each matrix entry publishes one host/RID combination via the matching checked-in | |
| # Properties/PublishProfiles/<profile>.pubxml (RuntimeIdentifier, SelfContained, | |
| # PublishSingleFile - see each host's own CLAUDE.md's publish section) and uploads the | |
| # result as a build artifact for the packaging job below. Runs on windows-latest for | |
| # all three (matching build.yml's own convention) even though the linux-x64 leg is a | |
| # cross-compile that doesn't strictly require it - producing that binary doesn't need | |
| # a Linux machine, only running it does (see WitcherScriptMerger.Headless/CLAUDE.md). | |
| build: | |
| name: Publish ${{ matrix.name }} | |
| needs: [verify-version, test] | |
| runs-on: windows-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: WitcherScriptMerger (win-x64) | |
| project: WitcherScriptMerger/WitcherScriptMerger.csproj | |
| profile: win-x64 | |
| publish-dir: WitcherScriptMerger/bin/Release/net10.0-windows7.0/win-x64/publish | |
| artifact-name: WitcherScriptMerger-win-x64 | |
| pass-version: 'false' | |
| - name: WitcherScriptMerger.Headless (win-x64) | |
| project: WitcherScriptMerger.Headless/WitcherScriptMerger.Headless.csproj | |
| profile: win-x64 | |
| publish-dir: WitcherScriptMerger.Headless/bin/Release/net10.0/win-x64/publish | |
| artifact-name: WitcherScriptMerger.Headless-win-x64 | |
| pass-version: 'true' | |
| - name: WitcherScriptMerger.Headless (linux-x64) | |
| project: WitcherScriptMerger.Headless/WitcherScriptMerger.Headless.csproj | |
| profile: linux-x64 | |
| publish-dir: WitcherScriptMerger.Headless/bin/Release/net10.0/linux-x64/publish | |
| artifact-name: WitcherScriptMerger.Headless-linux-x64 | |
| pass-version: 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| cache: true | |
| cache-dependency-path: WitcherScriptMerger/WitcherScriptMerger.csproj | |
| # Not passed --no-restore: a RID-specific self-contained publish needs runtime | |
| # packages a plain solution-wide restore wouldn't fetch, so each publish does its | |
| # own implicit restore for its own RID. | |
| # | |
| # -p:Version (WitcherScriptMerger.Headless legs only - matrix.pass-version) sets | |
| # this build's version from verify-version's checked value. GenerateAssemblyInfo | |
| # is off for the WinForms host (matrix.pass-version: 'false' there), so passing it | |
| # there would be a silent no-op - omitted rather than included-but-ignored. | |
| - name: Publish | |
| shell: pwsh | |
| env: | |
| RELEASE_VERSION: ${{ needs.verify-version.outputs.version }} | |
| run: | | |
| $versionArg = @() | |
| if ("${{ matrix.pass-version }}" -eq 'true') { | |
| $versionArg = @("-p:Version=$env:RELEASE_VERSION") | |
| } | |
| dotnet publish "${{ matrix.project }}" -c Release -p:PublishProfile=${{ matrix.profile }} @versionArg | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact-name }} | |
| path: ${{ matrix.publish-dir }} | |
| if-no-files-found: error | |
| # Runs on ubuntu-latest specifically for the linux-x64 asset: `tar` needs to run on a | |
| # filesystem with real Unix permission bits to set/preserve the executable bit on the | |
| # WitcherScriptMerger.Headless binary before archiving - building the tarball on | |
| # windows-latest (NTFS has no such bit) was tried and confirmed to produce a | |
| # non-executable entry, defeating the entire reason tar (not zip) was chosen for this | |
| # asset. `chmod +x` here is unconditional and explicit rather than relying on the | |
| # download-artifact transfer having preserved any prior mode bit. | |
| package-release: | |
| name: Package & create release | |
| needs: [verify-version, build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download WitcherScriptMerger (win-x64) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: WitcherScriptMerger-win-x64 | |
| path: publish/WitcherScriptMerger-win-x64 | |
| - name: Download WitcherScriptMerger.Headless (win-x64) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: WitcherScriptMerger.Headless-win-x64 | |
| path: publish/WitcherScriptMerger.Headless-win-x64 | |
| - name: Download WitcherScriptMerger.Headless (linux-x64) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: WitcherScriptMerger.Headless-linux-x64 | |
| path: publish/WitcherScriptMerger.Headless-linux-x64 | |
| # Packages each publish output (including the <AssemblyName>.dll.config the SDK | |
| # already copies in next to the exe - see each host's CLAUDE.md's "Publishing" | |
| # section for why that file, not App.config itself, is what | |
| # ConfigurationManager actually reads at runtime) as one archive per host/RID | |
| # combination. zip for the two win-x64 outputs; tar.gz (not zip) for linux-x64 | |
| # specifically - zip doesn't preserve the Unix executable bit, so an unzipped | |
| # Linux binary wouldn't be runnable. | |
| - name: Package release assets | |
| env: | |
| RELEASE_VERSION: ${{ needs.verify-version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| chmod +x "publish/WitcherScriptMerger.Headless-linux-x64/WitcherScriptMerger.Headless" | |
| ( cd publish/WitcherScriptMerger-win-x64 && zip -r "../../dist/WitcherScriptMerger-${RELEASE_VERSION}-win-x64.zip" . ) | |
| ( cd publish/WitcherScriptMerger.Headless-win-x64 && zip -r "../../dist/WitcherScriptMerger.Headless-${RELEASE_VERSION}-win-x64.zip" . ) | |
| ( cd publish/WitcherScriptMerger.Headless-linux-x64 && tar -czf "../../dist/WitcherScriptMerger.Headless-${RELEASE_VERSION}-linux-x64.tar.gz" . ) | |
| # --repo is required here: this job never checks out the repo (it only downloads | |
| # build artifacts from earlier jobs), so gh has no local git context to | |
| # auto-detect the target repository from - confirmed by the first real run of | |
| # this workflow (tag v0.6.2) failing with "failed to run git: fatal: not a git | |
| # repository (or any of the parent directories): .git" on exactly this step. | |
| # $GITHUB_REPOSITORY (a built-in Actions env var, "owner/repo") sidesteps that | |
| # without needing an actions/checkout step just for this. Same class of mistake | |
| # this repo's own CLAUDE.md already warns about for `gh pr create` run without | |
| # --repo/--base - explicit is required, not optional, for any `gh` invocation | |
| # that can't infer its target from a real git working directory. | |
| - name: Create GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_VERSION: ${{ needs.verify-version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| gh release create "$GITHUB_REF_NAME" \ | |
| "dist/WitcherScriptMerger-${RELEASE_VERSION}-win-x64.zip" \ | |
| "dist/WitcherScriptMerger.Headless-${RELEASE_VERSION}-win-x64.zip" \ | |
| "dist/WitcherScriptMerger.Headless-${RELEASE_VERSION}-linux-x64.tar.gz" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --title "$GITHUB_REF_NAME" \ | |
| --generate-notes |