From c68349ccd9f08634e0ff3cac48b615ae537baace Mon Sep 17 00:00:00 2001 From: Ivan Vydrin Date: Fri, 14 Aug 2026 07:10:50 +0300 Subject: [PATCH] Give the lock-file chore a button, and a failure that names it Dependabot edits Directory.Packages.props but cannot rewrite packages.lock.json: the CentralTransitive entries have to be resolved by NuGet rather than pattern-matched. So every NuGet bump lands with stale lock files and dies at `dotnet restore --locked-mode` with NU1004, before anything is built. That happened to four PRs this week and each one was fixed by hand. Three changes, none of which give a bot write access: - `Refresh lock files`, a workflow_dispatch job that regenerates the lock files on a named branch, proves the result with `--locked-mode` and a Release build, and commits only `**/packages.lock.json`. `contents: write` is scoped to that one job. - CI's restore step is named and, on failure, says which workflow to run and on which branch. NU1004's own text does not mention the fix. - The constraint is written down beside the nuget ecosystem in dependabot.yml, where the next person meets it. Deliberately a button rather than an automatic push on every Dependabot PR: a push made with GITHUB_TOKEN does not re-trigger workflows, so an automatic version would commit the fix and leave the required checks pinned to the superseded commit -- one stall traded for another. Making it automatic needs a PAT or a GitHub App key, and this repository stores no such credential on purpose. --- .github/dependabot.yml | 5 ++ .github/workflows/ci.yml | 11 +++- .github/workflows/refresh-lock-files.yml | 79 ++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/refresh-lock-files.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6914aa9..61af43b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,11 @@ version: 2 updates: + # NOTE: Dependabot updates Directory.Packages.props but cannot rewrite packages.lock.json -- + # the CentralTransitive entries have to be resolved by NuGet, not pattern-matched. Every bump + # here therefore arrives with stale lock files and fails `dotnet restore --locked-mode` (NU1004) + # until they are regenerated. Run the "Refresh lock files" workflow against the bump's branch; + # .github/workflows/refresh-lock-files.yml explains why that is a button and not automatic. - package-ecosystem: nuget directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adaff8f..05b1b5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,16 @@ jobs: - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: global-json-file: global.json - - run: dotnet restore --locked-mode + # Named, so the failure can say what to do about it. NU1004 on a Dependabot branch means + # the lock files were not regenerated, which is a chore rather than a defect -- and the + # error NuGet prints for it does not mention the fix. + - name: Restore + run: | + set -euo pipefail + if ! dotnet restore --locked-mode; then + echo "::error title=Lock files are out of step::dotnet restore --locked-mode failed. If this is a dependency bump, Dependabot cannot rewrite packages.lock.json under central package management. Run the 'Refresh lock files' workflow against ${{ github.head_ref || github.ref_name }}, then re-run these checks." + exit 1 + fi - run: dotnet build --no-restore -c Release # Format is enforced by the build via EnforceCodeStyleInBuild. This catches # whitespace-only drift that the analyzers do not. diff --git a/.github/workflows/refresh-lock-files.yml b/.github/workflows/refresh-lock-files.yml new file mode 100644 index 0000000..a5c7f37 --- /dev/null +++ b/.github/workflows/refresh-lock-files.yml @@ -0,0 +1,79 @@ +name: Refresh lock files + +# Dependabot cannot do this itself. +# +# Under central package management it edits Directory.Packages.props, but it does not rewrite +# packages.lock.json -- and it cannot, because the CentralTransitive entries have to be resolved by +# NuGet, not pattern-matched. So every NuGet bump arrives with lock files that still name the old +# versions, and `dotnet restore --locked-mode` fails before anything builds: +# +# error NU1004: Mistmatch between the requestedVersion of a lock file dependency marked as +# CentralTransitive and the version specified in the central package management file. +# +# Run this against the bump's branch and it fixes itself. +# +# Deliberately `workflow_dispatch` and not an automatic push on every Dependabot PR. A push made +# with GITHUB_TOKEN does not re-trigger workflows, so an automatic version would commit the fix and +# leave the required checks pinned to the superseded commit -- one stall traded for another. Making +# that work needs a PAT or a GitHub App key, and this repository stores no such credential on +# purpose (see SECURITY.md). A human runs this, then re-runs the checks. + +on: + workflow_dispatch: + inputs: + branch: + description: The branch to refresh, e.g. dependabot/nuget/microsoft-extensions-abc123 + required: true + type: string + +permissions: + contents: read + +concurrency: + group: refresh-lock-files-${{ inputs.branch }} + cancel-in-progress: false + +jobs: + refresh: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + # The one job in this repository that writes. Scoped here rather than at the top of the file + # so nothing else in this workflow inherits it. + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ inputs.branch }} + persist-credentials: true + + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + global-json-file: global.json + + # --force-evaluate is the whole point: it re-resolves rather than asserting the lock is right. + - run: dotnet restore --force-evaluate + + # Prove the result before committing it. If locked-mode cannot restore what force-evaluate + # just wrote, something is wrong that a commit would only bury. + - run: dotnet restore --locked-mode + - run: dotnet build --no-restore -c Release + + - name: Commit the refreshed lock files + run: | + set -euo pipefail + # Named paths, never `git add -A`: this job can write, so it says exactly what it writes. + if [ -z "$(git status --porcelain -- '*packages.lock.json')" ]; then + echo "Lock files already match the manifests. Nothing to commit." + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -- '*packages.lock.json' + git commit -m 'Refresh the lock files for this bump + + Regenerated with `dotnet restore --force-evaluate`, then verified with + `--locked-mode` and a Release build. Dependabot cannot write these under + central package management; see .github/workflows/refresh-lock-files.yml.' + git push origin HEAD:'${{ inputs.branch }}' + echo "Pushed. Re-run the pull request's checks -- a GITHUB_TOKEN push does not."