Skip to content

Quieten + harden the dependency pipeline: semiannual grouped updates, cooldown, SCA gate (#224)#226

Merged
FrankRay78 merged 6 commits into
mainfrom
feature/dependency-supply-chain-hardening
Jul 14, 2026
Merged

Quieten + harden the dependency pipeline: semiannual grouped updates, cooldown, SCA gate (#224)#226
FrankRay78 merged 6 commits into
mainfrom
feature/dependency-supply-chain-hardening

Conversation

@FrankRay78

@FrankRay78 FrankRay78 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Closes #224.

Reshapes NetPace's dependency-update pipeline to be both quiet (routine bumps stop being noise) and hardened (a bad version is caught), while keeping the human merge gate — Dependabot still opens maintainer-assigned PRs and nothing auto-merges.

Two independent channels

Channel Behaviour Where
Routine version updates twice a year (semiannually), all bumps collapsed into one grouped PR, and never a version younger than 14 days .github/dependabot.yml
Security updates prompt, separate per-CVE PRs — ignore the routine schedule and the cooldown repo Settings → Code security (manual, see below)

Backstop: a hard-failing SCA gate in the PR build (.github/workflows/dotnet.yml) runs dotnet list package --vulnerable --include-transitive and fails the job on any advisory (the command exits 0 on findings, so the step inspects its output and exits non-zero itself).

Net effect: routine maintenance drops from ~monthly per-package chore PRs to one batched PR every six months, while anything with a real advisory still gets patched promptly and the CI gate stops a vulnerable dependency merging unnoticed.

Changes

  1. .github/dependabot.yml — routine cadence monthly → semiannually; add a groups: block bundling all bumps into one PR; keep the 14-day cooldown (stops the scheduled run grabbing a same-day publish).
  2. .github/workflows/dotnet.yml — the SCA gate described above.

The CIR (docs/change-intent-records/2026-07-13-dependency-supply-chain-hardening.md) records the full reasoning, including the cadence decision.

Scope change during implementation: lock files dropped

The original issue scoped a third change — committed packages.lock.json files with --locked-mode restore on CI. It was implemented, then dropped after it failed CI on a legitimate tree (NU1403): IsAotCompatible=true pulls in the SDK-implicit Microsoft.NET.ILLink.Tasks, whose content hash is specific to the .NET SDK build, and global.json's rollForward: latestFeature lets CI float to a newer SDK band (10.0.301) than the lock was generated on (10.0.109). Making lock files viable would require pinning the SDK band across dev and CI — an ongoing toolchain-coupling cost judged disproportionate here. The CIR records the attempt and rationale (decision 5).

Verification (local)

  • dotnet build src and dotnet test src pass with zero warnings; 616 tests, 0 skipped.
  • dependabot.yml validated as well-formed; interval: semiannually, one all-dependencies group, cooldown.default-days: 14.
  • SCA marker string (has the following vulnerable packages) confirmed against a known-vulnerable package (System.Net.Http 4.3.0GHSA-7jgj-8wvc-jh57); clean tree reports none.

Required post-merge maintainer step (cannot be automated in-repo)

Enable Dependabot security alerts and security updates in repo Settings → Code security. This is the linchpin of the new posture — it's what lets routine updates go infrequent safely, because CVE patches then arrive on their own prompt channel independent of the semiannual schedule.

@claude please review.

🤖 Generated with Claude Code

FrankRay78 and others added 3 commits July 13, 2026 21:27
Records the decision to keep the human-gated Dependabot flow and harden
around it — lock files + CI-only locked-mode restore, a 14-day cooldown,
and a hard-failing vulnerable-dependency CI gate — rather than switching
to manual dependency bumps. Implementation tracked in #224.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#224)

Three supply-chain assurances around the existing human merge gate:

1. Lock files + CI-only locked-mode restore. RestorePackagesWithLockFile
   commits a packages.lock.json per src/ project; RestoreLockedMode gated to
   CI=true fails restore (NU1004) if the resolved graph drifts from the lock,
   covering implicit publish/pack restores too. Local restores stay unlocked.
   Freezes the floating Roslynator [4.15.0, ) range as a side effect.
2. 14-day Dependabot cooldown so a same-day compromised publish ages out
   before a bump PR is opened.
3. Hard-failing SCA step in the PR build: dotnet list package --vulnerable
   --include-transitive, which exits 0 on findings, so the step greps its
   output and exits non-zero on any advisory.

docs/RELEASING.md documents the release-runner locked-mode restore contract.
The human review gate is unchanged: Dependabot still opens maintainer-assigned
PRs and nothing auto-merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Locked-mode restore proved infeasible without pinning the SDK: IsAotCompatible
pulls in the SDK-implicit Microsoft.NET.ILLink.Tasks, whose content hash is
specific to the .NET SDK build. With global.json on rollForward: latestFeature,
CI floats to a newer SDK feature band (10.0.301) than the lock was generated on
(10.0.109), so locked-mode restore fails NU1403 on a legitimate tree. Pinning
the SDK band across dev and CI to make lock files viable is an ongoing
toolchain-coupling cost judged disproportionate for this project.

Reverts the packages.lock.json files, the RestorePackagesWithLockFile /
RestoreLockedMode properties, and the RELEASING.md locked-mode section. The
14-day Dependabot cooldown and the hard-failing SCA gate remain. CIR updated to
record the attempt and why it was dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FrankRay78 FrankRay78 changed the title Harden dependency pipeline: lock files, Dependabot cooldown, SCA gate (#224) Harden dependency pipeline: Dependabot cooldown + SCA gate (#224) Jul 13, 2026
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Review this pull request. Analyse the code changes and provide feedback covering:

  • Bugs or correctness issues
  • Security concerns (including any flagged by static analysis)
  • Adherence to the project conventions in CLAUDE.md
  • Test coverage — do the tests adequately cover the new behaviour?
  • Any spec/test-plan mismatches

@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FrankRay78's task in 1m 54s —— View job


Review: Harden dependency pipeline (#226)

  • Gather context (diff, CIR, CLAUDE.md conventions)
  • Review .github/dependabot.yml change
  • Review .github/workflows/dotnet.yml SCA gate
  • Review CIR doc
  • Check test coverage implications
  • Check spec/test-plan alignment
  • Post final review

A small, well-scoped PR — 40 lines across two config files plus a CIR. Overall this is solid, low-risk hardening. No bugs found; a couple of minor robustness points worth considering.

Bugs / correctness

Nothing incorrect. Verified:

  • .github/dependabot.yml:12-13cooldown: default-days: 14 is nested correctly under the nuget update entry, matching Dependabot's cooldown schema.
  • .github/workflows/dotnet.yml:30-37dotnet list src package --vulnerable --include-transitive works against src/ because it contains a single NetPace.sln (same pattern as the existing dotnet build src step), so the directory-as-target argument is valid.
  • The report=$(...) assignment under set -euo pipefail correctly propagates a non-zero exit from dotnet list itself (bash treats command-substitution failure inside an assignment as fatal under -e), so a transient failure to reach the NuGet vulnerability feed fails the job rather than silently passing.
  • This PR's own CI run exercised the new SCA step against a clean tree and passed (run 29285557130), and the PR description confirms the marker string was validated against a known-vulnerable package (System.Net.Http 4.3.0). That's real evidence the gate fires correctly in both directions.

Security

This is the point of the PR, and it's a genuine improvement:

  • 14-day cooldown reduces the same-day-compromised-publish window before Dependabot even opens a PR.
  • The SCA gate turns dotnet list package --vulnerable from an ignorable warning into a hard CI failure — closes the "known-CVE dependency merged unnoticed" gap.

One fragility worth flagging (not a blocker): the gate keys off matching the literal string "has the following vulnerable packages" in dotnet list's human-readable output. This is standard practice for this command (there's no --format json support for --vulnerable output in current SDKs), but it does mean a future SDK update that changes this wording would silently turn the gate into a no-op rather than failing loudly. Worth a one-line note in the CIR's "Known residual" section, or a periodic sanity check (e.g. re-run the known-vulnerable-package verification the author did locally whenever the SDK version bumps) — not something to block on now.

CLAUDE.md / constitution adherence

  • CIR is well-justified per the decision table in docs/conventions/change-intent-records.md (trade-off with a plausible rejected alternative — lock files) and follows the template exactly (Intent/Behaviour/Constraints/Decisions/Date).
  • No specs/ references introduced (checked — none present).
  • .github/workflows/dotnet.yml changes are documented; this doesn't touch release-binaries.yml so docs/RELEASING.md is correctly untouched.
  • Honest write-up of the dropped lock-file scope (decision 4) with a concrete root cause (NU1403 from AOT's SDK-implicit Microsoft.NET.ILLink.Tasks hash + rollForward: latestFeature) — this is exactly the kind of non-obvious reasoning a CIR should capture, and it correctly avoids scope creep by not trying to force lock files to work via SDK pinning.

Test coverage

No unit tests added, and none are needed — these are CI/YAML config changes with no C# surface, consistent with "Don't test... third-party library behaviour" and the general principle that TDD applies to production code, not pipeline config. The author's local verification (real CI run against a clean tree + a manual check against a known-vulnerable package) is the appropriate substitute here and is documented in the PR body.

Spec/test-plan mismatches

None found. No spec.md/test-plan.md exists for this change (it's a CIR-driven infra change, not a spec-kit feature), and the CIR's Behaviour section's given/when/then statements match what's actually implemented in both files.

Summary

No blocking issues. The one non-blocking suggestion: note the marker-string's brittleness against future dotnet list --vulnerable output-format changes somewhere (CIR residual section or a code comment) so a silent regression is more discoverable.

Routine version updates were monthly and one-PR-per-package — maintenance noise
disproportionate to this project. Now they run twice a year (semiannually) and
collapse into a single grouped PR. Prompt patching of known advisories is carried
by Dependabot security updates (repo-settings, advisory-driven, unaffected by this
schedule or the cooldown) plus the in-CI SCA gate, so reducing routine cadence
does not weaken the vulnerability path. Cooldown stays: it stops the scheduled run
from grabbing a same-day publish. CIR updated to record the cadence decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FrankRay78 FrankRay78 changed the title Harden dependency pipeline: Dependabot cooldown + SCA gate (#224) Quieten + harden the dependency pipeline: semiannual grouped updates, cooldown, SCA gate (#224) Jul 14, 2026
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Please re-review, identify only showstoppers or important issues (P1/P2).

@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FrankRay78's task in 3m 28s —— View job


Re-review: P1/P2 only (#226)

  • Re-read .github/dependabot.yml and .github/workflows/dotnet.yml diffs
  • Check schedule.interval: "semiannually" against Dependabot's documented schema
  • Re-check the SCA gate's failure-detection robustness
  • Post findings

Per request, this pass only reports showstopper/important issues — the rest of the first review (CIR quality, cooldown nesting, grouping syntax, gate script mechanics) still stands and isn't repeated here.

P1 — interval: "semiannually" may not be a value Dependabot's schema accepts

.github/dependabot.yml:9

Dependabot's documented schedule.interval enum has historically been daily / weekly / monthly only. I can't reach GitHub's docs or run dotnet/curl from this sandbox to confirm definitively, but I don't have a value for semiannually (or quarterly) ever being added to that schema — and the CIR's claim that it runs "first of January and July" (docs/change-intent-records/2026-07-13-dependency-supply-chain-hardening.md) reads like an assumption about behavior for a value that may not be officially supported, rather than documented fact.

If it's not a recognized enum value, this is a showstopper for the PR's actual goal: GitHub either rejects the whole dependabot.yml (visible as a config error in the repo's Dependabot/Insights view, not in this repo's GitHub Actions checks, so CI here would stay green while the config silently fails) or the schedule doesn't behave as intended. Either way the routine-update channel could end up never firing at all — strictly worse than the "too noisy" status quo this PR sets out to fix, and the failure would be invisible in dotnet.yml's CI run.

Please verify before merge: confirm current Dependabot docs actually list semiannually as valid, or check the repo's Dependabot config status (Insights → Dependency graph → Dependabot, or trigger "Check for updates" manually) after this lands. If unsupported, the safe fallback is monthly + the existing groups: block (still collapses everything into one PR/month) or a cron-style schedule if the installed Dependabot version supports one.

Fix this →

P2 — SCA gate keys off literal English prose, with no fallback if it stops matching

.github/workflows/dotnet.yml:32-37

grep -q 'has the following vulnerable packages' against dotnet list package --vulnerable's human-readable output is the mechanism the whole "hardened" half of this PR relies on. This was flagged as non-blocking in the first review, but re-flagging as P2 because a silent match failure (SDK wording change, or a runner locale that isn't en-US) doesn't fail loudly — it makes the gate report success while vulnerabilities exist, which is worse than not having the gate (false sense of security). Newer .NET SDKs (8+) support dotnet list package --format json; if that also covers --vulnerable --include-transitive on the SDK version pinned here (10.0.x), switching to JSON and checking for a non-empty vulnerabilities array would remove the locale/wording coupling entirely. Worth confirming either way before relying on this as the actual security backstop.

Fix this →

Everything else

No other P1/P2 issues found. The cooldown nesting, groups syntax, gate script's set -euo pipefail propagation, and CIR reasoning all check out as covered in the previous review pass.
· branch feature/dependency-supply-chain-hardening

FrankRay78 and others added 2 commits July 14, 2026 16:20
dotnet list package --format json is stable since the .NET 9 SDK (confirmed on
the 10.0.x SDK this workflow pins). Switch the vulnerable-dependency check to
parse the JSON — failing on any package with a non-empty vulnerabilities array —
rather than grepping the sentence "has the following vulnerable packages", which
is English prose that could be localised or reworded between SDK versions. On a
finding the step now prints the offending package, version, severity, and
advisory URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Please re-review, identify only showstoppers or important issues (P1/P2).

@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FrankRay78's task in 1m 53s —— View job


Re-review #2: P1/P2 only (#226)

  • Check what changed since the last review pass (commits since db233e1)
  • Verify P2 fix: SCA gate JSON parsing (.github/workflows/dotnet.yml)
  • Verify P1 concern: interval: "semiannually" validity against Dependabot's schema
  • Check CI status on this PR
  • Post findings

Per request, this pass only reports showstopper/important issues.

P2 from last pass — RESOLVED

.github/workflows/dotnet.yml:30-44 (commit 3e8740a) now parses dotnet list src package --vulnerable --include-transitive --format json with jq instead of grepping for the English sentence "has the following vulnerable packages". Checked:

  • The jq filter walks .projects[].frameworks[].(topLevelPackages + transitivePackages)[], filters on non-empty .vulnerabilities, and builds a path/id/resolvedVersion/vulnerabilities summary — field names (topLevelPackages, transitivePackages, vulnerabilities[].severity, vulnerabilities[].advisoryurl) match the documented dotnet list package --format json schema.
  • set -euo pipefail still applies, so a dotnet list failure (e.g. feed unreachable) fails the job rather than silently passing an empty $report to jq.
  • This PR's own CI run (job 87130636392) exercised this exact step and passed — real evidence the new parsing path works against a clean tree.

This removes the locale/wording-fragility concern entirely. No longer a P2.

P1 from last pass — still open, unresolved

.github/dependabot.yml:9interval: "semiannually" is unchanged since the last review round; no commit since db233e1 touches this line.

I still cannot reach GitHub's docs from this sandbox (no network egress) to confirm definitively, so treat this as a strong flag rather than a proven failure. To the best of my knowledge, GitHub's documented schedule.interval enum for dependabot.yml is daily / weekly / monthly only — semiannually (and quarterly) have been long-standing, unimplemented community feature requests, not accepted values. Because Dependabot config validation happens out-of-band (repo Insights → Dependency graph → Dependabot), not as a PR check, this repo's green CI here (Build and Test, CodeQL) gives no signal either way — an invalid dependabot.yml fails silently from GitHub Actions' point of view.

If unsupported, the risk is exactly what the first pass described: the routine-update channel could silently never fire, which is strictly worse than the "too noisy" status quo this PR sets out to fix, since Dependabot security updates (the actual CVE-response channel) are unaffected either way, but routine bug/feature-fix updates would stop entirely without anyone noticing.

Recommend before merge: confirm semiannually against current Dependabot docs, or after merge check the repo's Dependabot config status (Insights → Dependency graph → Dependabot, or trigger "Check for updates" manually) to verify the schedule was accepted. If unsupported, fall back to monthly + the existing groups: block (still collapses everything into one PR/month) — or check whether a cron-style schedule.cronjob field is available on the currently installed Dependabot version.

Fix this →

Everything else

No new P1/P2 issues found. Cooldown nesting, groups syntax, and CIR reasoning still check out as covered in the first review pass.

Summary

One item remains before merge: verify interval: "semiannually" is a value Dependabot's schema actually accepts — this is the only open P1. The P2 (SCA gate prose-matching fragility) is fixed in commit 3e8740a and confirmed passing in this PR's own CI run.
· branch feature/dependency-supply-chain-hardening

@FrankRay78
FrankRay78 merged commit 008d2ff into main Jul 14, 2026
3 checks passed
@FrankRay78
FrankRay78 deleted the feature/dependency-supply-chain-hardening branch July 14, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden dependency pipeline against supply-chain risk (lock files, cooldown, SCA gate)

1 participant