diff --git a/.github/workflows/Build Module.yml b/.github/workflows/Build Module.yml index 75f5de3..98b5c95 100644 --- a/.github/workflows/Build Module.yml +++ b/.github/workflows/Build Module.yml @@ -18,13 +18,12 @@ on: required: false workflow_dispatch: pull_request: - paths: - - "src/**" - - "build/**" - - "tests/**" - - "tools/**" - - ".github/ci-scripts/**" - - ".github/workflows/Build Module.yml" + # No path filter: this workflow always triggers on PRs to main so its "Build and test module" + # checks always REPORT (a required status check from a path-filtered workflow that never triggers + # stays Pending and blocks the PR forever). The expensive matrix build is gated below by the + # `changes` job — on a PR that doesn't touch build-relevant paths the build job is skipped, and a + # skipped job satisfies the required check, so docs-/CI-only PRs are never blocked. + branches: [main] #push: # branches: # - main @@ -42,9 +41,63 @@ concurrency: jobs: + changes: + name: Detect build-relevant changes + # Only meaningful for a directly-triggered PR. workflow_call (release) passes inputs.ref and must + # always build; workflow_dispatch must always build. Both skip this job (handled in build's `if`). + if: github.event_name == 'pull_request' && inputs.ref == '' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + relevant: ${{ steps.detect.outputs.relevant }} + steps: + - name: Detect whether build-relevant paths changed + id: detect + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + # Detection is intentionally INLINE (not a shared .github/ci-scripts script): keeping the + # merge-gate logic in the workflow file benefits from GitHub's stricter workflow-edit review, + # so a PR can't relocate it to a less-scrutinized script and tamper the skip decision (#228). + # Fail-safe: default to relevant=true so any error enumerating changed files runs the build + # rather than silently skipping it. Use the PAGINATED REST files endpoint -- gh pr view + # --json files caps at 100 files and could miss a later src/ change on a large PR. + $Relevant = $true + try { + $ErrorActionPreference = 'Stop' + # Project previous_filename too, so a rename moving a tracked file OUT of a matched tree + # (reported as the new path in .filename, old path in .previous_filename) is still seen. + $Files = gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[] | .filename, (.previous_filename // empty)' + # Native non-zero exits don't reliably throw in pwsh, so check explicitly -> fail-safe. + if ($LASTEXITCODE -ne 0) { throw "gh api exited $LASTEXITCODE while listing PR files" } + $Patterns = @('^src/', '^build/', '^tests/', '^tools/', '^\.github/ci-scripts/', '^\.github/workflows/Build Module\.yml$') + $Relevant = $false + foreach ($File in $Files) { + foreach ($Pattern in $Patterns) { + if ($File -match $Pattern) { $Relevant = $true; break } + } + if ($Relevant) { break } + } + } catch { + Write-Host "::warning::Changed-file detection failed; defaulting to relevant=true (fail-safe). $_" + $Relevant = $true + } + "relevant=$($Relevant.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + Write-Host "Build-relevant change detected: $Relevant" + build: name: Build and test module (${{ matrix.os }}) + needs: changes + # Always build for workflow_call (release; inputs.ref set) and workflow_dispatch. For a direct PR, + # build only when build-relevant paths changed; otherwise skip (a skipped job still satisfies the + # required "Build and test module" check). always() stops the skip of `changes` in the + # call/dispatch paths from cascading into a skip of build; the `!= 'false'` guard errs toward + # building on any ambiguity, so the release path is never accidentally skipped. + if: ${{ always() && (inputs.ref != '' || github.event_name == 'workflow_dispatch' || needs.changes.outputs.relevant != 'false') }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -178,3 +231,27 @@ jobs: name: module-build path: ./module/DLLPickle if-no-files-found: warn + + build-gate: + name: Build gate + # Aggregate required-check target. GitHub evaluates a matrix job's `if` BEFORE expanding the + # matrix, so skipping the `build` job does NOT create the per-OS "Build and test module (...)" + # check contexts -- requiring those directly would leave non-bundle PRs stuck "Expected - waiting + # for status". This single, always-running job is the stable check to require instead: it passes + # when the matrix build succeeded OR was skipped (no build-relevant changes) and fails when the + # build failed or was cancelled. + needs: build + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Aggregate build result + shell: pwsh + run: | + $Result = '${{ needs.build.result }}' + Write-Host "build job result: $Result" + if ($Result -eq 'success' -or $Result -eq 'skipped') { + Write-Host 'Build gate passed.' + exit 0 + } + Write-Host "::error::Build gate failed (build result: $Result)." + exit 1 diff --git a/.github/workflows/Upstream-Compatibility.yml b/.github/workflows/Upstream-Compatibility.yml index ddc7026..02ebebf 100644 --- a/.github/workflows/Upstream-Compatibility.yml +++ b/.github/workflows/Upstream-Compatibility.yml @@ -11,12 +11,10 @@ on: schedule: - cron: "34 8 * * 1" pull_request: - paths: - - "build/dependency-policy.json" - - "src/DLLPickle.Build/**" - - "tests/**" - - "tools/**" - - ".github/workflows/Upstream-Compatibility.yml" + # No path filter: always trigger on PRs to main so the "Validate upstream compatibility tooling" + # required check always reports. The job is gated below by `pr-changes` — skipped (== passing + # required check) on PRs that don't touch the tooling/policy paths. + branches: [main] permissions: contents: read @@ -26,9 +24,59 @@ concurrency: cancel-in-progress: false jobs: + pr-changes: + name: Detect tooling/policy changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + relevant: ${{ steps.detect.outputs.relevant }} + steps: + - name: Detect whether upstream-compatibility paths changed + id: detect + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + # Detection is intentionally INLINE (not a shared .github/ci-scripts script): keeping the + # merge-gate logic in the workflow file benefits from GitHub's stricter workflow-edit review, + # so a PR can't relocate it to a less-scrutinized script and tamper the skip decision (#228). + # Fail-safe: default relevant=true so any error enumerating changed files runs the + # validation rather than silently skipping a required check. Paginated REST files endpoint + # (gh pr view --json files caps at 100 files and could miss a later build/ or tools/ change). + $Relevant = $true + try { + $ErrorActionPreference = 'Stop' + # Project previous_filename too, so a rename moving a tracked file OUT of a matched tree + # (reported as the new path in .filename, old path in .previous_filename) is still seen. + $Files = gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[] | .filename, (.previous_filename // empty)' + # Native non-zero exits don't reliably throw in pwsh, so check explicitly -> fail-safe. + if ($LASTEXITCODE -ne 0) { throw "gh api exited $LASTEXITCODE while listing PR files" } + $Patterns = @('^build/', '^src/DLLPickle\.Build/', '^tests/', '^tools/', '^\.github/workflows/Upstream-Compatibility\.yml$') + $Relevant = $false + foreach ($File in $Files) { + foreach ($Pattern in $Patterns) { + if ($File -match $Pattern) { $Relevant = $true; break } + } + if ($Relevant) { break } + } + } catch { + Write-Host "::warning::Changed-file detection failed; defaulting to relevant=true (fail-safe). $_" + $Relevant = $true + } + "relevant=$($Relevant.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + Write-Host "Upstream-compatibility-relevant change detected: $Relevant" + pr-smoke-validation: name: Validate upstream compatibility tooling - if: github.event_name == 'pull_request' + needs: pr-changes + # Runs on PRs that touch the tooling/policy paths; skipped (== passing required check) only when + # detection conclusively says false. always() + `!= 'false'` makes it FAIL-SAFE: if pr-changes + # fails or its output is unknown, the validation still runs rather than being skipped (which would + # otherwise satisfy the required check without actually validating a relevant PR). + if: ${{ always() && github.event_name == 'pull_request' && needs.pr-changes.outputs.relevant != 'false' }} runs-on: windows-2025 permissions: contents: read