diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index ffa85f4..e69413a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -17,7 +17,13 @@ on: jobs: build: - runs-on: windows-latest + # Pinned, not windows-latest. windows-latest now provisions + # windows-2025-vs2026, and WinDevices.dll links the dynamic CRT + # (MSVCP140 / VCRUNTIME140), so a binary built with a newer toolset + # requires a redistributable at least as new on the target machine. + # Every other component in this program builds on VS 2022, so the + # native library is built on VS 2022 here too. + runs-on: windows-2022 strategy: matrix: config: [Debug, Release] diff --git a/.github/workflows/increment-version.yaml b/.github/workflows/increment-version.yaml index b87a6f3..9314850 100644 --- a/.github/workflows/increment-version.yaml +++ b/.github/workflows/increment-version.yaml @@ -210,7 +210,10 @@ jobs: build-and-release: needs: increment-version - runs-on: windows-latest + # Pinned to VS 2022: this job runs install.cmd and ships the resulting + # binaries as release assets, so it must use the same toolset as the + # rest of the program rather than whatever windows-latest points at. + runs-on: windows-2022 permissions: contents: write actions: read @@ -225,10 +228,14 @@ jobs: fetch-depth: 0 fetch-tags: true - - name: Setup CMake - uses: jwlawson/actions-setup-cmake@v1.14 - with: - cmake-version: '3.29' + # Use the CMake preinstalled on the runner image rather than pinning a + # version. windows-latest moved to windows-2025-vs2026, and a CMake + # older than the image's Visual Studio cannot name its generator: it + # silently falls back to NMake Makefiles, and install.cmd then fails + # with "CMAKE_CXX_COMPILER not set". Same fix as build-test.yaml. + - name: Report CMake version + shell: cmd + run: cmake --version - name: Build and Install Debug shell: cmd diff --git a/.github/workflows/publish-nuget.yaml b/.github/workflows/publish-nuget.yaml index bf29ca0..fec9821 100644 --- a/.github/workflows/publish-nuget.yaml +++ b/.github/workflows/publish-nuget.yaml @@ -9,6 +9,21 @@ on: push: tags: - 'v*' + + # Version Increment pushes its tag using GITHUB_TOKEN, and events created + # with that token deliberately do not start other workflows, so the tag + # push above never fires for an automated release. workflow_run is not a + # token-created event: it fires when the other workflow finishes, so the + # release path reaches here without a stored PAT. + # + # Kept as a trigger on this file rather than a reusable workflow called + # from Version Increment, because the Trusted Publisher policy is bound to + # this workflow's file name. + workflow_run: + workflows: ["Version Increment"] + types: + - completed + workflow_dispatch: inputs: version: @@ -23,7 +38,16 @@ on: jobs: pack: - runs-on: windows-latest + # Pinned to VS 2022 deliberately. The native library shipped inside the + # package links the dynamic CRT, so its toolset sets the minimum + # Visual C++ Redistributable every consumer's machine must carry. + # windows-latest is now windows-2025-vs2026, which would silently raise + # that floor for every downstream deployment. + runs-on: windows-2022 + + # workflow_run fires whatever the outcome of the run that triggered it, + # so a failed Version Increment must not reach the publisher. + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' permissions: contents: read @@ -36,11 +60,39 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 with: + # Deliberately no `ref:`. A workflow_run job is privileged — it + # holds id-token: write and can mint a nuget.org publishing token — + # and this job then builds what it checks out. Checking out + # workflow_run.head_sha would run code from a commit this workflow + # did not choose, in that privileged context: the pattern CodeQL + # flags as actions/untrusted-checkout, and a real escalation path + # to publishing a package. + # + # The default checkout gives the default branch, which is trusted. + # Version Increment tags a commit on that branch, so the tag is + # present. If the branch has moved past the tag in between, the + # exact-tag guard below refuses to publish rather than releasing a + # mislabelled package, which is the correct failure. + # # Version is derived from the Git tag, so the full history and all # tags must be present. fetch-depth: 0 fetch-tags: true + # One place decides whether this run publishes, so the three steps + # below cannot drift apart. A tag push and a completed Version + # Increment both publish; a manual dispatch publishes only when asked. + - name: Decide whether to publish + id: gate + shell: pwsh + run: | + $publish = + '${{ github.event_name }}' -eq 'workflow_run' -or + '${{ github.ref }}'.StartsWith('refs/tags/v') -or + '${{ inputs.publish }}' -eq 'true' + Write-Host "event=${{ github.event_name }} ref=${{ github.ref }} publish=$publish" + "publish=$($publish.ToString().ToLower())" | Out-File $env:GITHUB_OUTPUT -Append + # The version is derived with `git describe --tags --abbrev=0`, which # returns the nearest *ancestor* tag. On a branch that has moved past # its last tag that silently produces a package claiming a version its @@ -50,7 +102,7 @@ jobs: # This is not hypothetical: 0.1.1 was published from a main that was # six commits ahead of the v0.1.1 tag. - name: Require HEAD to be exactly at a tag - if: (startsWith(github.ref, 'refs/tags/v') || inputs.publish) && inputs.version == '' + if: steps.gate.outputs.publish == 'true' && inputs.version == '' shell: pwsh run: | $tag = git describe --exact-match --tags HEAD 2>$null @@ -136,14 +188,14 @@ explicit version input to publish deliberately. # against owner TorinKS, repository WinDeviceslib and workflow file # publish-nuget.yaml. Renaming this file invalidates that policy. - name: NuGet login (Trusted Publishing) - if: startsWith(github.ref, 'refs/tags/v') || inputs.publish + if: steps.gate.outputs.publish == 'true' id: nuget_login uses: NuGet/login@v1 with: user: ${{ vars.NUGET_USER }} - name: Push to nuget.org - if: startsWith(github.ref, 'refs/tags/v') || inputs.publish + if: steps.gate.outputs.publish == 'true' shell: pwsh env: TEMP_NUGET_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}