Let pushes to protected branches finish their required checks (#4594)

# Description

Makes every push to `master` and `release/*` report both required
checks, so a release branch can be cut from `master`.

Two things stopped `master` commits from getting a green `images
summary`, which the `Release` ruleset requires, including when a branch
is created:

1. **Cancellation.** `CI` (`buildandtest.yml`) and `Images CI`
(`docker-image.yml`) used one concurrency group per ref with
`cancel-in-progress: true`. Each merge to `master` cancels the
still-running checks of the commit before it. A queued run in the same
group is replaced as well ("Canceling since a higher priority waiting
request … exists"). `Images CI` takes about 100 minutes, and `master`
currently gets a merge about every 90 minutes:

   | Commit | `build-and-test summary` | `images summary` |
   | --- | --- | --- |
| `87c35b84b` (#4572) | success | failure: `pumpserver` / `refserver`
cancelled after ~100 min by the #4573 push |
| `20c2d77d5` (#4573) | success | failure: `refserver` / `mcpserver` /
`pumpserver` cancelled by the #4577 push |

The ruleset has no bypass actors, so `git push origin
20c2d77d5:refs/heads/release/2.0` is rejected with `Required status
check "images summary" is failing`.

2. **Push path filter.** `Images CI`'s push trigger had `paths-ignore:
docs/**, tests/**`. A documentation- or test-only push to `master` or
`release/*` never started the workflow, so it never reported `images
summary` and could never be a cut point.

## Changes

- **Concurrency (both workflows):** pushes to protected lines (`master`,
`main`, `master378`, `release/*`) get one concurrency group per commit
(`github.sha`) and never cancel, so every commit runs its checks to
completion. Pull requests and other branches are unchanged: they group
by PR number or ref and cancel superseded runs.
- The per-commit group is needed as well as `cancel-in-progress: false`.
With a shared group GitHub still replaces a *queued* run.
- **`Images CI` push trigger:** the `paths-ignore` is gone. The
`discover` job now decides relevance for pushes the same way it already
did for pull requests.
- It diffs `github.event.before..HEAD` and skips the image builds for
docs/tests-only changes, while `images summary` still reports.
- A new branch (all-zero `before`) or a `before` commit the clone
doesn't have builds everything, and a failed diff still fails the gate.
- `main` and `master378` are not added to the push trigger: `main`
doesn't exist, and `master378` runs its own copy of this workflow.

## Verification

- The `discover` script was extracted and run locally for push events:

  | Push | `relevant_changes` |
  | --- | --- |
  | docs-only | `false` (images skipped, summary reports) |
  | workflow change | `true` |
  | new branch | `true` |
  | unknown `before` | `true` |

- The CI-related `Opc.Ua.Tools.Tests` (`~Ci`, 53 tests, including the
fail-closed diff pin) pass on net10.0.

## Cost

Overlapping merges to `master` now run CI and image builds in parallel
instead of cancelling each other, which uses more runner minutes on busy
days. Image pushes from overlapping `master` runs can finish out of
order, so `latest-master` reflects whichever finishes last. Version tags
are per build and unaffected.

## Related Issues

- Blocks cutting `release/2.0` for `2.0.0-preview.6` (see #4574).

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
romanett
2026-09-29 18:02:18 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 01d6b5d26c
commit 034d9934d1
2 changed files with 46 additions and 15 deletions
+9 -2
View File
@@ -54,9 +54,16 @@ env:
# pull requests from forks.
ENABLE_CODECOV: 'true'
# Pull requests and feature branches cancel a superseded run: only the newest
# commit's result matters there. A push to a protected line (master, main,
# master378, release/*) never cancels, and each commit gets its own group,
# because a queued run in a shared group is replaced as well as a running one
# being cancelled. Every protected commit therefore finishes its required
# checks ('build-and-test summary', 'images summary'), which the Release
# ruleset demands before a release/<major>.<minor> branch can be cut from it.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master378' || startsWith(github.ref, 'refs/heads/release/')) && github.sha) || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || !(github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master378' || startsWith(github.ref, 'refs/heads/release/')) }}
jobs:
discover:
+37 -13
View File
@@ -3,9 +3,10 @@ name: Images CI
on:
push:
branches: [ master, release/*, docker* ]
paths-ignore:
- 'docs/**'
- 'tests/**'
# No paths-ignore here either: every push to master or release/* must
# report `images summary`, which the Release ruleset requires before a
# release branch can be cut from that commit. The discover job skips the
# image builds for documentation- or test-only pushes instead.
pull_request:
branches: [ master, main, master378, develop/*, release/* ]
# NOTE: deliberately no `paths-ignore` filter, and the same branch list as
@@ -21,9 +22,16 @@ on:
# image security update that changes nothing in this repository.
workflow_dispatch:
# Pull requests and feature branches cancel a superseded run: only the newest
# commit's result matters there. A push to a protected line (master, main,
# master378, release/*) never cancels, and each commit gets its own group,
# because a queued run in a shared group is replaced as well as a running one
# being cancelled. Every protected commit therefore finishes its required
# checks ('build-and-test summary', 'images summary'), which the Release
# ruleset demands before a release/<major>.<minor> branch can be cut from it.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master378' || startsWith(github.ref, 'refs/heads/release/')) && github.sha) || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || !(github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master378' || startsWith(github.ref, 'refs/heads/release/')) }}
env:
REGISTRY: ghcr.io
@@ -49,22 +57,38 @@ jobs:
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BEFORE_SHA: ${{ github.event.before }}
run: |
# Reproduces the `paths-ignore` allow-list that used to sit on the
# pull_request trigger: an image is rebuilt for any change that is not
# purely documentation or test code. Filtering the whole workflow out
# would stop `images summary` from ever reporting, which would block
# every documentation-only pull request once it becomes required.
# Reproduces the `paths-ignore` allow-list that used to sit on both
# triggers: an image is rebuilt for any change that is not purely
# documentation or test code. Filtering the whole workflow out would
# stop `images summary` from ever reporting, which would block every
# documentation-only pull request, and every documentation-only push
# as a release-branch cut point, since the check is required.
$relevantChanges = $true
$range = $null
if ($env:EVENT_NAME -eq 'pull_request' -and -not [string]::IsNullOrWhiteSpace($env:BASE_SHA)) {
# Triple-dot: diff from the merge base, not from the current base
# branch head, so a change that landed on the base branch after
# this one was cut is not misread as an image-relevant change.
$changed = @(& git diff --name-only "$($env:BASE_SHA)...HEAD")
$range = "$($env:BASE_SHA)...HEAD"
}
elseif ($env:EVENT_NAME -eq 'push' -and $env:BEFORE_SHA -match '^[0-9a-f]{40}$' -and
$env:BEFORE_SHA -notmatch '^0+$') {
# A new branch has an all-zero 'before', and a force push can name
# a commit this clone does not have: build everything in both cases
# rather than guess what changed.
& git cat-file -e "$($env:BEFORE_SHA)^{commit}" 2>$null
if ($LASTEXITCODE -eq 0) {
$range = "$($env:BEFORE_SHA)..HEAD"
}
}
if ($range) {
$changed = @(& git diff --name-only $range)
if ($LASTEXITCODE -ne 0) {
throw (
"Could not diff pull-request base '$($env:BASE_SHA)' against HEAD " +
"(git exit code $LASTEXITCODE). Refusing to skip required image builds.")
"Could not diff '$range' (git exit code $LASTEXITCODE). " +
'Refusing to skip required image builds.')
}
$relevantChanges = $false
foreach ($file in $changed) {