Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,26 @@ permissions:
id-token: write
actions: read

# A re-push to a PR cancels that PR's superseded in-flight run: finishing a build of code
# that is no longer the PR's head helps nobody, and the shared Windows runner pool is what
# serializes everyone's CI (#1697 sat queued behind two dev builds), so a cancelled stale
# run is reclaimed capacity for whatever is queued. Push, release, and merge-queue runs
# deliberately get a UNIQUE group per run (run_id): integration builds, release builds, and
# queue validations are never queued behind, replaced by, or cancelled through this
# mechanism — every push commit keeps its own reported check result, which release cuts and
# merge gating read.
# A re-push to a PR cancels that PR's superseded in-flight run, and a push to dev/main
# cancels that BRANCH's superseded in-flight run — newest SHA wins. Finishing a build of
# code that is no longer the head helps nobody, and the shared Windows runner pool is what
# serializes everyone's CI (#1697 sat queued behind two dev builds; on 2026-07-26 a
# ~20-merge train left 13 of the day's 30 dev-push runs finishing SHAs a newer merge had
# already replaced — ~60 reclaimable runner-minutes in one evening). Cancelling a
# superseded PUSH run is safe because a push run produces nothing any other run consumes:
# every upload-artifact step in this workflow is gated to the release event (the SignPath
# signing path) or to failure() (darling-pg diagnostics), nothing in the repo downloads
# cross-run artifacts (no download-artifact, gh run download, or workflow_run consumer
# exists), nightly.yml builds its own tree from its own checkout, and a release compiles
# fresh on the release event. The accepted trade: push builds are diff-scoped, so a
# cancelled run's areas are not re-verified until the next change touches them — the
# nightly and the all-areas dev->main release PR are the backstops. Release and
# merge-queue runs deliberately keep a UNIQUE group per run (run_id) and are NEVER
# cancelled: a release build waits on SignPath's manual approval gate, and a queue
# validation is the last check before its result lands on dev.
concurrency:
group: ${{ github.event_name == 'pull_request' && format('build-pr-{0}', github.event.pull_request.number) || format('build-run-{0}', github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ${{ github.event_name == 'pull_request' && format('build-pr-{0}', github.event.pull_request.number) || github.event_name == 'push' && format('build-push-{0}', github.ref) || format('build-run-{0}', github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}

jobs:
build:
Expand Down
15 changes: 10 additions & 5 deletions .github/workflows/sql-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ on:
branches: [dev]
paths: ['install/**', '.github/sql/**', '.github/workflows/sql-validation.yml']

# A re-push to a PR cancels that PR's superseded in-flight matrix — four SQL Server
# containers per run on the shared runner pool, validating code that is no longer the
# PR's head. Push runs get a unique per-run group (run_id) and are never cancelled.
# A re-push to a PR cancels that PR's superseded in-flight matrix, and a dev push cancels
# dev's superseded in-flight matrix — four SQL Server containers per run on the shared
# runner pool, validating code that is no longer the head. Newest-wins costs strictly
# nothing here (same policy and 2026-07-26 baseline as build.yml): this workflow uploads
# no artifacts at all, and it validates the FULL install/ tree at the SHA it checks out
# rather than a diff, so the superseding run's coverage is a superset of whatever the
# cancelled run was midway through. Any event this workflow ever grows beyond push and
# pull_request falls to a unique per-run group and is never cancelled.
concurrency:
group: ${{ github.event_name == 'pull_request' && format('sql-validation-pr-{0}', github.event.pull_request.number) || format('sql-validation-run-{0}', github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ${{ github.event_name == 'pull_request' && format('sql-validation-pr-{0}', github.event.pull_request.number) || github.event_name == 'push' && format('sql-validation-push-{0}', github.ref) || format('sql-validation-run-{0}', github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}

jobs:
validate-sql:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **CI: a dev/main push now cancels that branch's superseded in-flight build — newest SHA wins** ([#1729]) - [#1715] scoped cancel-in-progress to pull requests and deliberately left every push run uncancellable; 2026-07-26's ~20-merge evening measured what that costs at train speed: of the day's 30 dev-push `build.yml` runs, **13 were superseded mid-flight** (a newer merge landed before the run finished) and **~60 runner-minutes went to SHAs that were already stale** — capacity the shared Windows runner pool bills against every queued PR (#1697's original complaint). Push events in `build.yml` and `sql-validation.yml` now share a per-branch concurrency group with cancel-in-progress, so only the newest head keeps building. "Safe" was verified from the workflow graph, not asserted: a push run produces nothing any other run consumes — every `upload-artifact` in `build.yml` is gated to the release event (the SignPath path) or `failure()` (darling-pg diagnostics), `sql-validation.yml` uploads nothing at all, no `download-artifact` / `gh run download` / `workflow_run` consumer exists anywhere in the repo, nightly builds its own tree from its own checkout, and a release compiles fresh on the `release` event. Release and merge-queue runs keep their unique per-run groups and remain uncancellable: a release waits on SignPath's manual approval gate, and a queue validation is the last check before its result lands on dev. The accepted trade, stated rather than hidden: push builds are diff-scoped, so a cancelled run's areas are not re-verified until the next change touches them — the nightly and the all-areas dev→main release PR are the backstops. The merge queue that would eliminate the train itself stays unavailable on this personal-account repo ([#1716]: organization-owned repositories only, `422 Invalid rule 'merge_queue'`, re-verified against GitHub's GA announcement); this is the slice of that win that IS available here.
- **CI: build.yml handles the merge_group event** ([#1716]) - lands what [#1715] named as the merge-queue prerequisite: without a `merge_group` trigger, the required `build` and `Darling PostgreSQL tests` checks would never report inside a queue and every queued PR would stall. Queue runs take the same always-restore path as dev/main pushes - a queue run is the last validation before its result lands on dev - and the per-run concurrency group, so they are never cancelled or replaced. Path classification works unchanged in a queue: dorny/paths-filter v4.0.1+ resolves merge_group diffs from the payload's base_sha/head_sha whenever the `base` input is empty, which is exactly what the filter steps pass for non-push events. **Post-merge correction to this entry's original "safe one-click" claim: the click does not exist here.** Merge queues are available only on ORGANIZATION-owned repositories (public on any plan, private on Enterprise Cloud - per the GA announcement, and confirmed empirically: creating the ruleset on this personal-account repo returns `422 Invalid rule 'merge_queue'`). The `.gitattributes merge=union` alternative for the CHANGELOG conflict trains is also out: GitHub's server-side PR merging ignores merge attributes (community discussion #9288), so it would only automate local resolution, not the DIRTY state or the re-push. The wiring stays - inert, zero cost, live the day the repo ever moves to an organization - and until then the train-tax relief is [#1715]'s classification fix: single-area re-pushes re-run ~3m40s instead of ~6m30s, docs-only re-pushes 13s.
- **CI: the change classification the v4 pin bump silently broke is restored - and this time it is probe-validated** ([#1715]) - a timing baseline over the 30 most recent build.yml runs plus a throwaway probe PR (#1714) turned the planned build-time audit into a regression find. The 2026-07-26 08:02 pin bump moved dorny/paths-filter v3 -> v4, and v4 evaluates every filter pattern as an INDEPENDENT predicate under its default predicate-quantifier 'some' (a filter is true when any changed file matches at least one rule), so a bare `!**/*.md` exclusion stopped being a subtraction and became its own rule: "any file that is not markdown". Every area filter carrying that line went true for ANY non-markdown change anywhere in the repo - a single root .gitignore edit built and tested all four products (probe run 30219202642), darling-pg ran the full TimescaleDB suite on every PR since the bump including md-only ones (run 30218459544 matched CHANGELOG.md against the darling filter), and the [#1712] docs fast path shipped unable to engage, because every file matches '**' so its code: gate was never false - its measured md-only 1m43s runs were real, but they were the area filters at work (markdown matches no include), not the fast path. The regression was invisible by construction: the bump's own PR touched build.yml, so root=true forced a full build that looks identical to a correct run, and so does every over-built run after it. Fixed keeping v4 (v3 is on the deprecated-runtime track): area filters carry the markdown carve-out INSIDE each include as an extglob (`Darling/**/!(*.md)`) where quantifier semantics cannot detach it; the uninvertible code: filter is replaced by an `all:` counter with docs-only decided by all_count == docs_count; the classify step additionally refuses to engage while any area filter is lit, so the two classifications can never disagree into a `dotnet build --no-restore` with no restore behind it; and check-version-bump.yml, which had the identical '**'-plus-exclusions shape and an equally dead md-only skip, takes the same counter fix. Validated with a per-file truth table on the probe PR (run 30219765613: a Darling .txt probe, a Darling .md probe, .gitignore and the workflow file in one diff - each filter's matched-file list recorded in the PR body). **Also in this pass:** a PR re-push now cancels that PR's superseded in-flight build.yml/sql-validation.yml runs, while push and release runs keep unique per-run concurrency groups - never queued behind or cancelled by anything, every dev/main commit keeps its own check result; the [#1712] allowlist's flagged judgment calls are ratified (CITATION.cff, Screenshots/) but its directory-wide grants become extension-explicit (`docs/**/*.{md,svg,png,jpg,jpeg,gif}`) so a .sql dropped into docs/ tomorrow defaults to code; the scheduled nightly re-dispatches itself onto the dev ref instead of doing real work from main's copy of the workflow file - scheduled workflows execute the DEFAULT branch's copy against dev's checked-out tree, which is exactly how the 2026-07-26 06:00 nightly failed (run 30194606068: main's stale copy read Dashboard/Dashboard.csproj, moved to deprecated/ by #1612 - the #1550 trap again) - so after a ONE-TIME sync of nightly.yml to main (command in the PR body; the schedule stays red each morning until it happens) nightly logic changes take effect the night they merge to dev, with manual dispatches still always building and the artifact job still pinned to dev; and every job that never carries the release/signing path gets a timeout-minutes ceiling at ~3x its worst cold path (darling-pg 30, nightly build 90 / pg 60 / check 10, sql-validation 30 per leg, claude-review 30) so hung-not-slow failures stop holding a shared-pool runner for the 6h default - build.yml's build job stays unbounded on purpose, because the release path waits on SignPath's manual approval gate. **Measured and deliberately not done** (numbers in the PR body): per-area restore splitting (warm restore is 19-33s; four condition-mirrored restore steps buy seconds at the price of the drift risk [#1701] just retired) and cross-job test splitting (Run Lite tests ~2m25s dominates the full build, but a second Windows job costs ~2m45s of checkout/setup/restore/build before its first test - a net loss on a shared serialized pool). Merge queue remains a recommendation with exact settings in the PR body: it is a repo setting, and build.yml needs a merge_group trigger first or queued PRs stall on never-reporting required checks.
- **CI: a documentation change stops paying for a .NET restore** ([#1712]) - non-executable changes now skip .NET setup, restore and versioning in the required `build` job, while the job still RUNS so the check reports and cannot block a merge. The gate is an explicit **allowlist** of non-executable content (`**/*.md`, `LICENSE`, `CITATION.cff`, `.gitignore`, `.gitattributes`, `docs/**`, `Screenshots/**`) rather than a subtraction, so an unfamiliar new file type defaults to being treated as code - the safe direction to be wrong in. `*.sql`, `*.yml`, `*.csproj`/`*.props`/`packages.lock.json` are deliberately excluded from the allowlist because some job compiles or consumes each of them, and `*.cs` is excluded however comment-only a change looks, since the compiler is what proves it still builds. **Two guards against under-building**: the fast path never engages on a `release` event or on a push to `dev`/`main` (those restore unconditionally - it only forces the restore back on, per-product build/test steps stay path-gated exactly as before), and both jobs now emit a `::notice::` naming WHY they took the path they took plus the file list classified as documentation, because a job that reports success having quietly run nothing is indistinguishable from one that tested everything. Measured against the real gap: markdown-only changes were ALREADY skipping every heavy step (PR #1707, pure `.md`, ran `build` in 1m43s), so what this actually fixes is non-markdown documentation - a `LICENSE`, `.gitignore` or screenshot edit previously matched the catch-all and paid a six-project locked-mode restore for nothing. The remaining ~1m45s floor is `actions/checkout` on a Windows runner, not work this gate can remove.
Expand Down Expand Up @@ -1713,3 +1714,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1694]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1694
[#1698]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1698
[#1701]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1701
[#1729]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1729
Loading