CI intelligence: restore the classification the dorny v4 bump broke, cancel superseded PR runs, un-trap the nightly, bound the jobs - #1715
Conversation
Concurrency groups for build.yml and sql-validation.yml, keyed per PR number for pull_request events only. Push and release runs get a unique per-run group (run_id), so dev/main integration builds, release builds, and every push commit's required-check result are never queued behind, replaced by, or cancelled through this mechanism. Rationale: the shared Windows runner pool is what serializes everyone's CI (#1697 sat queued behind two dev builds), so a stale PR run cancelled at re-push time is reclaimed capacity for whatever sits queued behind it. claude-review.yml already had exactly this shape; build.yml and sql-validation.yml now match it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rrors it Ratifies #1712's two flagged judgment calls (CITATION.cff and the Screenshots/ entry stay on the documentation allowlist - citation metadata and screenshot images cannot affect any build.yml job) but replaces the bare directory globs docs/** and Screenshots/** with extension-explicit patterns for markdown, SVG, and image files. Both directories hold exactly those types today, so nothing classified as documentation before this commit stops being documentation - but a .sql or script dropped into either directory tomorrow now defaults to being CODE, which restores the allowlist's own stated principle (an unfamiliar file type should default to the safe direction) that a directory-wide grant quietly undermined. check-version-bump.yml gets the same allowlist as exclusions: its gate still said *.md-only, so a LICENSE or .gitignore correction in a dev to main PR would demand a version bump while build.yml calls the same diff documentation. The two filters now state the same policy and say so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hat executes Scheduled workflows always execute the DEFAULT branch's copy of the workflow file, while nightly artifacts deliberately build from dev's tree. That skew is exactly how the 2026-07-26 scheduled nightly failed (run 30194606068): main's stale copy still read Dashboard/Dashboard.csproj, a path #1612 moved to deprecated/ on dev, so 'Set nightly version' died on a file missing from the tree it had just checked out. Same trap as before (#1550/#1551) - syncing the file again would fix tonight and re-break at the next pre-release layout drift. Structural fix instead: on schedule the workflow does nothing but re-dispatch itself onto the dev ref (GITHUB_TOKEN can create workflow_dispatch runs; the Actions recursion guard exempts workflow_dispatch, and the dispatched run cannot loop back because it arrives as workflow_dispatch, not schedule). A workflow_dispatch run executes the DISPATCHED ref's copy of the file - dev's, current by definition - so after a one-time sync of this file to main, main's copy has exactly one tree-independent job that must keep working, and nightly logic changes take effect the night they merge to dev. Semantics preserved: manual dispatches (from_schedule defaults false) always build, exactly as before; the scheduled path keeps the 24h new-commit check via from_schedule=true; the artifact-publishing build job stays pinned to ref dev so a branch dispatch still cannot ship branch binaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every job that never carries the release/signing path gets a timeout-minutes ceiling sized at roughly 3x its worst observed or cold-cache path: build.yml darling-pg 30 (max observed 3m40s warm, cold pg-runtime fetch adds a ~340MB download), nightly check 10 / build 90 / darling-pg 60, sql-validation 30 per matrix leg, claude-review 30. The failure these bound is hung-not-slow - pg_ctl -w waiting forever on a cluster that will never come up, a SQL Server container that never reaches healthy - which otherwise holds a shared-pool runner for the 6h default while every other PR queues behind it. Deliberately NOT bounded: build.yml's build job. On release it waits on SignPath's manual approval gate, which can legitimately take hours; a timeout there could kill a release mid-signing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-07-26 08:02 action-pin bump moved dorny/paths-filter v3 to v4.
v4 evaluates every filter pattern as an INDEPENDENT predicate under the
default predicate-quantifier 'some' (a filter is true when any changed
file matches at least one rule), so a bare '!**/*.md' line stopped
being a subtraction and became its own rule: 'any file that is not
markdown'. Every area filter ending in that line went true for ANY
non-markdown change anywhere in the repo, and the code: filter ('**'
plus exclusions) could never be false again.
Measured consequences, straight from run logs:
- darling-pg ran the full TimescaleDB suite on every PR since the bump,
including md-only ones. Run 30218459544 (PR #1709, two .md files):
'Filter darling = true, Matching files: CHANGELOG.md' - CHANGELOG.md
matched '!Darling/**/*.md' by virtue of not being a Darling markdown.
- A single root .gitignore edit built and tested every product: run
30219202642 (throwaway PR #1714) lit lite, dashboard, AND darling,
each reporting 'Matching files: .gitignore' via its '!**/*.md' rule.
- The #1712 docs fast path shipped unable to engage: every changed file
matches '**', so code: was always true and setup/restore always ran.
Its measured 1m43s md-only runs were real but came from the area
filters (markdown matches no include), not from the fast path.
The fix keeps v4 (v3 is on the deprecated-runtime track) and removes
every bare negation instead:
- Area filters state the markdown carve-out INSIDE each include as an
extglob - 'Darling/**/!(*.md)' - where quantifier semantics cannot
detach it. The positive-only docs allowlist needed no change.
- The uninvertible code: filter becomes an all: counter. The classify
step decides docs-only by all_count == docs_count, and additionally
refuses to engage while any area filter is lit, because an area build
step with restore skipped would run dotnet build --no-restore against
nothing. Counts and areas derive from the same allowlist today; the
veto exists for the day someone edits one and not the other.
- check-version-bump.yml had the identical '**'-plus-exclusions shape,
so its md-only skip had also silently stopped working. Same counter
fix, same allowlist, and its gate now says why.
Validation: probe commit on throwaway PR #1714 (this build.yml plus a
Darling .txt probe and a Darling .md probe) makes the filter log a
per-file truth table; results recorded in the CI-intelligence PR body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Stage 3 complete - the docs fast path measured end to end. #1714 was rebased to a .gitignore-only diff against dev carrying this PR: both required checks reported in 13 seconds each (run 30220291489). build job: |
|
Correction to the merge-queue recommendation above: it is not available on this repository. Merge queues exist only on organization-owned repos (public on any plan, private on Enterprise Cloud) - personal-account repositories are excluded entirely. Attempting the recommended ruleset here returns |
Lands the prerequisite erikdarlingdata#1715's merge-queue recommendation named: without a merge_group trigger the required 'build' and 'Darling PostgreSQL tests' checks never report inside a queue, and every queued PR stalls - enabling the ruleset before this wiring would brick dev merges. The trigger is inert until the queue ruleset is actually enabled. 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: 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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI build-time and change-classification intelligence pass. The audit was supposed to find incremental wins; it found a same-day regression that had silently reduced ALL change classification to "markdown-only vs build-everything", fixed it, and validated the fix with per-file probes. Absorbed starting material: #1712 (merged while this was in flight) is the baseline; its two flagged allowlist judgment calls are ratified below.
The headline find: the dorny v4 pin bump broke every path filter (fixed here)
The 2026-07-26 08:02 action-pin bump moved
dorny/paths-filterv3 -> v4. v4 evaluates each filter pattern as an INDEPENDENT predicate under its defaultpredicate-quantifier: some- a filter is true when any changed file matches at least one rule - so a bare!**/*.mdexclusion line stopped being a subtraction and became its own rule meaning "any file that is not markdown". Receipts, straight from run logs:Filter darling = true, Matching files: CHANGELOG.md- CHANGELOG.md matched!Darling/**/*.mdby virtue of not being a Darling markdown file..gitignoreedit built and tested all four products. Throwaway PR [THROWAWAY - do not merge] Validate the #1712 docs fast path #1714, run 30219202642:lite,dashboard, anddarlingall true, each reportingMatching files: .gitignorevia its!**/*.mdrule. Full six-project restore, four builds, all four test suites, Lite + Darling publishes.code:filter contains'**', which every changed file matches, socodecould never befalse. The 1m43s md-only runs its body cites were real, but they came from the AREA filters (markdown matches no include) - a mechanism that predates CI: stop charging documentation changes for a .NET restore #1712 - not from the fast path.The bump could not have caught itself: its own PR touched build.yml, so
root=trueforced a full build that looked exactly like a correct run, and everything after it over-built, which also looks exactly like a correct run.The fix keeps v4 (v3 is on the deprecated-runtime track) and removes every bare negation instead:
Darling/**/!(*.md)), where quantifier semantics cannot detach it.code:filter is replaced by anall:counter; the classify step decides docs-only byall_count == docs_count, and additionally refuses to engage while any area filter is lit (an area build with restore skipped would rundotnet build --no-restoreagainst nothing - the two classifications derive from the same allowlist today, the veto is for the day someone edits one and not the other).check-version-bump.ymlhad the identical'**'-plus-exclusions shape, so its md-only skip was also silently dead; same counter fix.Baseline, measured before changing anything
Per-job and per-step timings for the 30 most recent completed build.yml runs (all of them post-bump, so every row below is the BROKEN classification - "full build" rows include PRs that should have been single-area):
Full 30-run table
Caveat: in rows at or before run 30216390743 (#1701's merge), the
t-litecolumn reads "skip" because the Lite test step had different names before #1701 collapsed the fast/analysis split; the suites did run. Collection artifact, not a skip.What changed, commit by commit
build.yml,sql-validation.yml): a PR re-push cancels that PR's superseded in-flight runs. Push and release runs get a UNIQUE per-run group (run_id), so dev/main integration builds and release builds are never queued behind, replaced by, or cancelled through this mechanism - every push commit keeps its own check result.claude-review.ymlalready had this shape.build.yml,check-version-bump.yml): CITATION.cff and Screenshots/ stay documentation (ratifying CI: stop charging documentation changes for a .NET restore #1712's flagged judgment calls - citation metadata and screenshot images cannot affect any build.yml job), but the directory-wide grants become extension-explicit (docs/**/*.{md,svg,png,jpg,jpeg,gif}), so a.sqlor script dropped into docs/ tomorrow defaults to CODE - the allowlist's own stated principle. Note: the brief's spec listedTHIRD_PARTY_NOTICES.mdexplicitly; it is covered by**/*.md. check-version-bump.yml now mirrors the same allowlist, so a LICENSE-only dev->main correction is documentation to both gates.nightly.yml): see below.buildjob stays unbounded ON PURPOSE: on release it waits on SignPath's manual approval gate, which can legitimately take hours.Throwaway validation (PR #1714 - stays open for Stage 3, then closes unmerged)
Stage 1 - proved the regression against dev's shipped workflow. A
.gitignore-only diff was supposed to engage the #1712 fast path (~1m45s); instead it paid the full build and the full PG suite (run 30219202642, receipts above).Stage 2 - proved the fix with a per-file truth table. The fixed build.yml plus three probes was pushed to the same PR: diff =
.gitignore(docs),.github/workflows/build.yml(root),Darling/throwaway-probe.txt(darling),Darling/throwaway-probe.md(docs, NOT darling). Filter log from run 30219765613:Run 30219765613 concluded success (the fixed workflow also passed its own root-triggered full build end to end — concurrency block, classify step, extglob filters, notices, timeouts all live on the runner). The classify step's decision line:
docs_count=2is exactly.gitignore+throwaway-probe.md. Per-file matched list (darling-pg job'sdarlingfilter, same pattern family as the build job's):Darling/throwaway-probe.mdis absent (the markdown carve-out holds inside the include) and.gitignoreis absent (the predicate leak is gone). Compare Stage 1, where.gitignoreALONE madelite,dashboard, anddarlingall true. Every filter now reads exactly as designed, per file, on the real runner.Stage 3 - end-to-end fast-path engagement needs a docs-only diff against a dev that already carries this PR's build.yml, so it runs after this merges: #1714's branch gets rebased to the
.gitignore-only diff for one final run, expected ~45s-1m (setup + restore + versioning all skipped, notice naming the file), then closed.Projected effect (attribution, not hand-waving)
Nightly: diagnosis and the ONE manual step this PR cannot do
Scheduled run 30194606068 (2026-07-26 06:00 UTC) failed in
Set nightly version: scheduled workflows always execute the DEFAULT branch's copy of the workflow file, and main's stale copy still readsDashboard/Dashboard.csproj- a path #1612 moved todeprecated/on dev - against the dev tree it checks out. Same trap as #1550/#1551. Syncing the file again would fix tonight and re-break at the next pre-release layout drift, so this PR restructures instead: on schedule, nightly.yml does nothing but re-dispatch itself onto the dev ref (workflow_dispatchruns execute the DISPATCHED ref's copy of the file - dev's, current by definition; GITHUB_TOKEN can create workflow_dispatch runs, the recursion guard exempts them, and the dispatched run cannot loop because it arrives as workflow_dispatch, not schedule). Manual dispatches still always build (from_scheduledefaults false), the scheduled path keeps the 24h new-commit check, and the artifact job stays pinned toref: devso a branch dispatch can never ship branch binaries.Until main's copy is synced once, the 06:00 UTC schedule keeps failing. After this merges to dev, one of:
(requires your admin bypass if branch protection is enforced for admins;
check-pr-branch.ymlblocks a non-dev hotfix PR to main by design), or simply let the next dev->main release promotion carry it - at the cost of a red scheduled nightly each morning until then. After that one sync, this trap class is dead: main's copy has exactly one job that must keep working, and it references no tree paths at all.Measured and deliberately NOT done
Recommendation (repo setting - your click, deliberately not attempted): merge queue on dev
The CHANGELOG-conflict re-push train is structural: serialized auto-merges each conflict on CHANGELOG.md, re-push, and re-run full CI. A dev merge queue removes the train tax (the queue rebases and revalidates serially). Settings -> Rules -> Rulesets -> New branch ruleset targeting
dev-> enable Require merge queue; suggested: merge method Merge commit, build concurrency 5, min/max PRs per group 1/5, wait 1 min, "Only merge non-failing entries". Prerequisite first: build.yml (and darling-pg's filter step) must handle themerge_grouptrigger - a queued PR's required checks otherwise never report and every entry stalls. That trigger wiring (plus a dorny base for merge_group events) is a follow-up PR if you want the queue; enabling the setting before it would brick dev merges.Corrections to the brief (reported, not silently deviated from)
rootis four literal build files, markdown never triggered full builds, and the real gap was non-markdown documentation. Confirmed independently here.🤖 Generated with Claude Code