Skip to content

deps: explain why Dependabot PRs land red, and add the one command that fixes them - #140

Merged
tylerkron merged 2 commits into
mainfrom
claude/quirky-feistel-f401d9
Aug 31, 2026
Merged

tylerkron merged 2 commits into
mainfrom
claude/quirky-feistel-f401d9

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

What

Dependabot PRs in this repo arrive with a broken build, and there was nothing telling anyone why or what to do about it. This adds that explanation, plus a script that does the fix in one command.

It also settles a question #139 deliberately left open — whether the Android head should stay in Dependabot's directories or be dropped like the iOS head. It stays. The reasoning is in the README.

No configuration changes. The only edit to dependabot.yml is a comment.

Why

The app is one shared library (Daqifi.Avalonia) plus four thin heads that reference it — Desktop, Android, iOS, and the capture harness. Each of those five projects has a packages.lock.json, and a lock file lists every package the project ends up using, including everything inherited from the library. So Sentry is written into all five files, not just the library's.

Dependabot only updates the lock file in the folder it edited. Bump one package in the shared library and it updates one lock file, leaving the other four describing a world that no longer exists. CI checks lock files strictly, so those four heads fail with:

error NU1004: The project references daqifi.avalonia whose dependencies has changed.

This isn't a prediction — it's happening now. #96 and #130 are both red on exactly this. It affects 23 of the 28 packages we manage, which is essentially every dependency that isn't an Avalonia platform package.

Why not just drop the Android head from directories? That was the alternative, and testing killed it:

  • It fixes nothing. The Android head fails because of what it inherits from the shared library, not because of its own two packages. The iOS head proves this — it's already excluded and fails in exactly the same way.
  • The version-consistency check that was supposed to cover it never runs on these PRs. It waits for all three heads to build first, so when they fail it's skipped entirely.
  • We'd lose the only thing watching Avalonia.Android for updates.

How

Verified, not assumed. I reproduced the whole thing locally on the pinned SDK: bumped Sentry in the shared library, updated only its lock file the way Dependabot does, then built each head. All four failed. Ran the new script. All four passed.

refresh_lock_files.sh regenerates all six lock files. The important part is what it won't do — if the SDK is wrong or a required workload is missing, it stops and tells you, without writing anything. A half-finished refresh is worse than none: it looks done and leaves the skipped head broken. Two details that cost real debugging time in the past are baked in — it walks projects individually because the solution file is missing the capture harness, and it never uses -r, which quietly rewrites lock files to a single platform.

test_refresh_lock_files.py feeds it a fake dotnet and checks it refuses in four different situations without touching any files. It's picked up automatically by the existing self-test job. I confirmed it actually fails when the refusal is removed, so it isn't a test that only ever passes.

Docs go where the related history already lives — the section in .github/dependency-updates/README.md that deferred this decision now answers it, and dependabot.yml warns before someone tries to merge one of these PRs.

What this doesn't do

It doesn't automate the fix. A workflow could run this script on Dependabot PRs and push the result, so they'd arrive green — but that needs write access to PR branches and a macOS runner for the iOS head, which bills at 10× Linux. That's a call worth making on purpose rather than slipping into this PR. Related to #132.

Checks

  • All five script self-tests pass, including the new one.
  • dependabot.yml still parses to the same configuration.
  • Script verified end to end against a simulated Dependabot PR.

🤖 Generated with Claude Code

…R needs, and script it

#139 left a decision open: the Android head is in Dependabot's `directories`
but Dependabot cannot refresh its packages.lock.json, so the choice was
between documenting a manual refresh and dropping the head the way the iOS
head is dropped. This takes the first option, because investigating it turned
up a bigger problem that decides the question.

Four of the five app lock files reach their packages through a
ProjectReference to Daqifi.Avalonia and record its full transitive closure.
Dependabot rewrites the lock file only in the directory whose manifest it
edited. So a one-line bump in the shared library invalidates five lock files
and refreshes one, and locked mode fails the other four with NU1004. That is
23 of the 28 packages managed here, and it is the current state of the repo:
#96 and #130 are both red on it now, on the Android and iOS heads alike.

That refutes the alternative. Dropping /Daqifi.Avalonia.Android would fix
nothing — its NU1004 comes from the ProjectReference closure, not from its own
two pins, and the iOS head proves it by already being out of `directories` and
failing identically. The guard offered as cover cannot help either:
avalonia-graph `needs: [desktop, android, ios]`, so it is SKIPPED on exactly
the PRs in question. Dropping the head would only cost the one automated watch
on Avalonia.Android.

Reproduced on the pinned SDK 10.0.302 by doing what Dependabot does — bump
Sentry 6.8.0 -> 6.9.0 in the shared library, refresh only its own lock file,
then restore each head in locked mode. All four fail NU1004; the new script
makes all four pass.

- refresh_lock_files.sh regenerates all six lock files. It refuses to do a
  partial job: wrong SDK or a missing android/ios workload exits 2 without
  writing, because a half-refresh looks finished and leaves the skipped head
  failing NU1004 anyway. $DOTNET is authoritative rather than one candidate
  among several. It iterates projects rather than the slnx, which does not
  contain AvaloniaCapture, and never passes -r, which would prune the lock
  file to one RID.
- test_refresh_lock_files.py drives it with a stub dotnet across four refusal
  cases and asserts nothing is written. Picked up by the existing `scripts`
  job, which globs test_*.py. Verified to fail when the refusal is removed.
- The README section that deferred this decision now records it, and the
  dependabot.yml comment says which PRs are affected before someone tries to
  merge one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Document and automate Dependabot lock-file refreshes

🐞 Bug fix 📝 Documentation 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Documents why shared-library Dependabot updates invalidate dependent projects’ NuGet lock files.
• Adds one guarded command to regenerate all six committed lock files safely.
• Tests wrong-SDK and missing-workload refusals without invoking real restores.
Diagram

graph TD
  A["Dependabot PR"] --> B["Shared package bump"] --> C["Stale head locks"] --> D["Refresh script"] --> E{"Preflight valid?"}
  E -- "No" --> F["Exit without writes"]
  E -- "Yes" --> G["Restore six projects"] --> H["Current lock files"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Automate refreshes in GitHub Actions
  • ➕ Makes Dependabot PRs arrive with synchronized lock files
  • ➕ Removes the manual branch update step
  • ➖ Requires write access to Dependabot PR branches
  • ➖ Needs a billed macOS runner for the iOS workload
  • ➖ Introduces workflow security and cost decisions beyond this PR
2. Restore the solution once
  • ➕ Shorter implementation with a single restore command
  • ➖ Silently omits AvaloniaCapture because it is outside the solution
  • ➖ Makes complete lock-file coverage dependent on solution membership
3. Remove the Android Dependabot directory
  • ➕ Avoids Android workload failures in Dependabot’s own lock updater
  • ➖ Does not fix transitive lock drift from shared-library updates
  • ➖ Removes automated monitoring for Android-specific packages
  • ➖ Cannot rely on the skipped version-consistency job during failed head builds

Recommendation: Keep the guarded, per-project manual refresh as the safe immediate remedy: it covers all six committed lock files and prevents partial updates before restore begins. Pursue an automated macOS workflow separately if branch-write permissions and runner cost are accepted; retaining Android coverage is preferable because removing it does not address the shared transitive-lock failure.

Files changed (4) +420 / -7

Bug fix (1) +132 / -0
refresh_lock_files.shAdd guarded repository-wide lock-file refresh command +132/-0

Add guarded repository-wide lock-file refresh command

• Introduces a single command that verifies the pinned SDK plus Android and iOS workloads before writing anything. It force-restores all six projects carrying committed lock files without a runtime identifier and reports the resulting lock-file diff.

.github/scripts/refresh_lock_files.sh

Tests (1) +148 / -0
test_refresh_lock_files.pyTest lock refresh preflight refusals +148/-0

Test lock refresh preflight refusals

• Uses stub dotnet executables to verify wrong SDK and missing workload scenarios exit with code 2 and actionable errors. Each case also confirms that no committed lock file changes when preflight fails.

.github/scripts/test_refresh_lock_files.py

Documentation (2) +140 / -7
dependabot.ymlExplain required lock refreshes beside Dependabot configuration +22/-0

Explain required lock refreshes beside Dependabot configuration

• Adds an operational warning that shared-library dependency bumps leave dependent lock files stale. It points maintainers to the refresh command, distinguishes unaffected leaf-only updates, and preserves the Android directory without changing Dependabot behavior.

.github/dependabot.yml

README.mdDocument transitive lock drift and the chosen remedy +118/-7

Document transitive lock drift and the chosen remedy

• Explains how project references propagate shared dependencies into every app lock file, why locked restores fail with NU1004, and which dependency updates require regeneration. It documents script prerequisites, the decision to retain Android monitoring, rejected alternatives, and future workflow automation.

.github/dependency-updates/README.md

@qodo-code-review

qodo-code-review Bot commented Aug 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. CI blocks lock refresh ✗ Dismissed 🐞 Bug ≡ Correctness
Description
The restore command does not disable the repo's conditional RestoreLockedMode, so with
CI=true—including the documented future GitHub Actions use—stale locks fail with NU1004 instead of
being regenerated. --force-evaluate reevaluates the graph but does not override the independently
enabled locked-mode property.
Code

.github/scripts/refresh_lock_files.sh[120]

+  "${DOTNET_BIN}" restore "${project}" --force-evaluate
Relevance

●● Moderate

The failure is technically specific, but prior restore-mode feedback rejected redundant
command-level mode enforcement; intent for CI execution remains uncertain.

PR-#77

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The script invokes restore only with --force-evaluate, while the repository sets
RestoreLockedMode=true whenever CI or ContinuousIntegrationBuild is true. The build workflow
confirms GitHub Actions supplies this condition, and the new README explicitly proposes running this
script in a future GitHub Actions workflow.

.github/scripts/refresh_lock_files.sh[118-121]
Directory.Build.props[61-65]
.github/workflows/build.yml[6-9]
.github/dependency-updates/README.md[304-310]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The refresh command remains in locked mode whenever `CI=true` or `ContinuousIntegrationBuild=true`, so it cannot regenerate stale lock files in CI environments.

## Issue Context
`Directory.Build.props` enables `RestoreLockedMode` from those environment-backed properties. The refresh operation must explicitly override locked mode while retaining `--force-evaluate`.

## Fix Focus Areas
- .github/scripts/refresh_lock_files.sh[118-121]
- .github/scripts/test_refresh_lock_files.py[39-64]
- Directory.Build.props[61-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Failed refresh leaves partial locks ✓ Resolved 🐞 Bug ☼ Reliability
Description
Each project is restored directly and sequentially, so if a later restore fails, earlier restores
may already have rewritten their lock files before set -e exits. This violates the script's stated
partial-refresh safety goal and can leave a mixed lock-file set that is accidentally committed.
Code

.github/scripts/refresh_lock_files.sh[R118-121]

+for project in "${PROJECTS[@]}"; do
+  echo "==> ${project}"
+  "${DOTNET_BIN}" restore "${project}" --force-evaluate
+done
Relevance

●●● Strong

Recent reviews accept safeguards preventing partial or misleading multi-project state; this directly
contradicts the script’s stated no-partial-refresh guarantee.

PR-#137

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The repository documents that ordinary unlocked restore rewrites drifted lock files. The new loop
runs six independent restores against the working tree with no staging, rollback, or temporary
worktree, while set -e only stops after a failing command and cannot undo files written by prior
iterations.

.github/scripts/refresh_lock_files.sh[36-41]
.github/scripts/refresh_lock_files.sh[118-121]
Directory.Build.props[17-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failure after one or more successful restores leaves those earlier lock files modified, producing the partial refresh the script is intended to prevent.

## Issue Context
Preserve the exact pre-run state, including any Dependabot-generated lock-file changes, and restore that state on failure; alternatively perform regeneration in an isolated worktree and copy results only after every restore succeeds.

## Fix Focus Areas
- .github/scripts/refresh_lock_files.sh[41-43]
- .github/scripts/refresh_lock_files.sh[118-121]
- .github/scripts/test_refresh_lock_files.py[95-144]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread .github/scripts/refresh_lock_files.sh Outdated
Comment thread .github/scripts/refresh_lock_files.sh
Qodo review on #140, finding 2. The preflight refuses to start without the
pinned SDK and both workloads, but once restores begin they run one project at
a time — so a failure on the fourth left the first three already rewritten.
That is the same partial refresh the preflight exists to prevent, arriving by a
different door, and it looks like an ordinary refresh diff on the way out.

The script now snapshots every lock file before the first restore and rolls all
of them back on failure, exiting 1. The snapshot is of the WORKING TREE, not
HEAD: on a Dependabot branch the shared library's lock file is already
legitimately modified, and rolling back to HEAD would discard Dependabot's own
work along with ours.

Exit codes are now 0 refreshed / 1 a restore failed, rolled back / 2 could not
run, which lines up with the contract the other scripts here follow.

The self-test grows a rollback group: a stub dotnet that mutates each lock file
it restores and fails on the third. It carries a CONTROL case whose stub never
fails, because a rollback test whose stub never wrote anything would pass
without proving anything. Verified to fail when the rollback call is removed —
three lock files left modified.

Also records what the review's other finding got wrong. --force-evaluate IS
what overrides RestoreLockedMode, so CI=true does not block the refresh.
NuGet's own NU1004 text says so, and it is verified: same state, obj cleared,
CI=true, RestoreLockedMode evaluating true — a plain restore fails NU1004 and
the same restore with --force-evaluate regenerates the lock file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron
tylerkron merged commit 97540c6 into main Aug 31, 2026
6 checks passed
@tylerkron
tylerkron deleted the claude/quirky-feistel-f401d9 branch August 31, 2026 18:01
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.

1 participant