Repository navigation
Conversation
Dependabot pull requests had no merge actor, so updates that passed every required check stayed open. A scheduled and manually dispatchable workflow runs the default-branch policy script, classifies each open Dependabot pull request from GitHub API metadata, and squash-merges one only when it is a verified single Dependabot commit confined to one manifest set, every update is a compatible patch or minor version, and every required status check succeeded for the exact head commit. Major updates, 0.x minor updates, Docker updates, and anything unclassified stay open for manual review. The workflow has no pull request trigger and never checks out pull request content. The review gate reports the same classification and succeeds for updates that need manual review, so a reviewed major update is no longer blocked by a required check that cannot pass. Merges made with the Actions token start no push workflow. The reconcile workflow dispatches CI and the security audit for the main head when no run exists for it, and release validation accepts a dispatched CI run on main for the exact release commit. Dependabot groups the CodeQL actions, which fail when updated separately, groups the remaining action updates by update type, and updates transitive Cargo dependencies so an advisory against a lockfile-only crate is resolved by a routine update. The cooldown and the required status check names are unchanged.
A Cargo update whose required checks fail, or whose branch conflicts with main, had no path to a merge. The reconcile workflow now plans one repair per run and carries it through three separated jobs. A generate job runs OpenCode with the model credential; the model reads the failure logs and edits Cargo manifests and Rust source, but cannot run commands, so no dependency code runs beside the credential. An apply job without the credential runs the requested `cargo update` operations and verifies that the lockfile resolves. A publish job commits the validated files to the pull request branch through the Git database API, which GitHub signs, and advances the branch only as a fast-forward once the signature is confirmed. A conflict is resolved by merging main into the branch, never by a rebase or a forced update. The same repair runs locally with `scripts/dependency-repair.sh candidate`. A branch update made with the Actions token leaves pull request workflows waiting for approval, so a repaired head gets its required checks by dispatch. The review gate and Dependency Review accept that dispatch, and it is refused unless the branch workflows equal the main workflows. The classifier accepts verified repair commits by github-actions[bot] on top of the Dependabot commit. A repair confined to the lockfile keeps the update routine; one that changes a manifest or source needs an approving review. Cargo patch and minor updates, including 0.x versions, and GitHub Actions updates pinned to a full commit SHA are routine again. The review gate fails for a major or unclassified update until a repository owner, member, or collaborator approves the exact head commit, and reruns when a review is submitted. Before a merge, the reconcile workflow now compares the effective branch rules with the declared required checks and fails closed on a rule it does not verify, requires every review thread to be resolved, and confirms that the pull request metadata and main are unchanged. It merges one update per run, holds merges until the required checks completed for the current main commit, and confirms that each dispatched run exists for the intended commit. Workflow updates are not repaired: the Actions token cannot write workflow files. The Dependabot configuration comment no longer states that updating transitive dependencies resolves an advisory.
…el run Merging and recovery: - Merge a Dependabot update only when its head contains the current main commit, so its required checks ran on the tree that the squash merge produces. Refresh one branch per run that lacks the main commit through the branch merge API, with the marker that lets Dependabot keep rebasing it. - Dispatch validation for a refreshed workflow update whose only difference from the main workflows is its own pinned actions. - Rerun a workflow run that was cancelled or failed once. A failed check counts as a defect of the update once a rerun reproduced it. - Keep a repaired patch or minor update routine when the repair stays inside its manifest set and Rust source. Major Cargo updates still need review. - Record a repair by Dependabot commit, main commit, and failing checks, for published repairs and for repairs that cannot proceed in any stage, in place of a fixed repair count. - Verify author, committer, signature, and parents of a repair commit before the branch advances. Repair engine: - Remove the model credential from the environment at entry and export it only to the model process. Run Cargo with a reduced environment. - Record the tree before the model runs and compare files afterwards without running Git on the edited checkout. Any change besides the content of a repairable tracked file fails the stage. - Fix the model and require the pinned OpenCode release. - Refuse a newly locked version published on crates.io within the last hour. - Resolve stage state through the Git directory of the checkout, so a linked worktree works. Drop the file count, request count, and file size limits. - Exit with status 3 when a repair cannot proceed, apart from operational failures. Tests sign their fixture commits with a key generated for the run.
The refresh of a stale Dependabot branch merged main directly into the pull request branch, so the branch advanced before the merge commit was checked. The merge is now made on a scratch branch that the run creates at the pull request head. The pull request branch advances only after the commit is a verified github-actions[bot] merge of main into that head and the pull request is unchanged, as a fast-forward. The scratch branch is removed in every outcome; one that cannot be removed is reported by name. A refresh that did not advance a branch no longer uses the one refresh of a run, so the next stale branch is tried. A model run that reaches its time limit is an operational failure rather than a repair that cannot proceed, so an unreachable provider leaves no record and the plan is tried again. The model process keeps TMPDIR, and the internal credential variable is never exported. A failed check counts as reproduced only when the same job failed or timed out in the attempt before, and a failed workflow run is rerun until every failing job of its latest attempt meets that condition. A first failure after a cancelled attempt, or a job that first fails on a rerun, is rerun instead of repaired or left as a finding.
| - name: Fetch the pull request head as data | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ needs.plan.outputs.head_sha }} | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| path: work | ||
|
|
||
| - name: Download the plan |
There was a problem hiding this comment.
2 blocking findings open
2 findings posted inline
1 suppressed
Restore lockfile conflicts using package records
scripts/dependency-repair.sh:543
Suppressed: cites a line the named construct does not sit on.
Severity error, confidence 0.99.
The prepare stage records only the conflicted lockfile path, but this loop reads each record as a package name and version. For a recorded Cargo.lock, version is empty, so the validation at the next lines skips every restoration and the candidate retains the main branch's lockfile instead of the planned dependency updates. Record package/version pairs when preparing the conflict, or iterate the plan's updates directly and verify that every planned update is restored.
| is_ancestor "$parent" "$BRANCH" || merges_ok=false | ||
| done < <(jq -r '.[1:][] | select((.parents | length) == 2) | .parents[1]' "$dir/commits.json") | ||
| fi | ||
|
|
||
| fingerprint=$(jq -cS -n \ | ||
| --slurpfile pull "$dir/pull.json" \ | ||
| --slurpfile commits "$dir/commits.json" \ | ||
| --slurpfile files "$dir/files.json" '{ | ||
| pull: ($pull[0] | {state, draft, user: .user.login, base: .base.ref, | ||
| base_repo: .base.repo.full_name, head: .head.sha, head_ref: .head.ref, | ||
| head_repo: .head.repo.full_name, commits, changed_files}), | ||
| commits: $commits[0], | ||
| files: $files[0] | ||
| }' | sha256sum | cut -d ' ' -f 1) || return 2 | ||
|
|
||
| jq -n \ | ||
| --argjson number "$number" \ | ||
| --arg repo "$REPO" \ | ||
| --arg bot "$BOT_LOGIN" \ | ||
| --arg repair_bot "$REPAIR_LOGIN" \ | ||
| --arg branch "$BRANCH" \ | ||
| --arg fingerprint "$fingerprint" \ | ||
| --argjson merges_ok "$merges_ok" \ | ||
| --slurpfile pull "$dir/pull.json" \ | ||
| --slurpfile commits "$dir/commits.json" \ | ||
| --slurpfile files "$dir/files.json" \ | ||
| --slurpfile reviews "$dir/reviews.json" ' | ||
| def metadata: | ||
| split("\n") as $lines | ||
| | [range(0; $lines | length) | ||
| | select($lines[.] == "---" and $lines[. + 1] == "updated-dependencies:")] as $starts | ||
| | if ($starts | length) != 1 then null else | ||
| ($starts[0] + 2) as $from | ||
| | ([range($from; $lines | length) | select($lines[.] == "...")] | first) as $to | ||
| | if $to == null then null else | ||
| reduce $lines[$from:$to][] as $line ({ok: true, items: []}; | ||
| if .ok | not then . | ||
| elif $line | test("^- dependency-name: \\S") then | ||
| .items += [{"dependency-name": ($line | sub("^- dependency-name: "; ""))}] | ||
| elif ((.items | length) > 0) and ($line | test("^ [a-z-]+: \\S")) then | ||
| ($line | capture("^ (?<key>[a-z-]+): (?<value>.*)$")) as $pair | ||
| | .items[-1][$pair.key] = $pair.value | ||
| else .ok = false | ||
| end) | ||
| | if .ok and (.items | length) > 0 then .items else null end | ||
| end | ||
| end; | ||
|
|
||
| def level: | ||
| (."update-type" // "") as $type | ||
| | (."dependency-version" // "") as $version | ||
| | if $version | test("^[0-9]+(\\.[0-9]+){0,2}(\\+[0-9A-Za-z.-]+)?$") | not then "unclassified" | ||
| elif $type == "version-update:semver-patch" or $type == "version-update:semver-minor" | ||
| then "compatible" | ||
| elif $type == "version-update:semver-major" then "major" | ||
| else "unclassified" | ||
| end; | ||
|
|
||
| def file_class: | ||
| if test("^Cargo\\.(toml|lock)$") then "cargo-root" | ||
| elif test("^fuzz/Cargo\\.(toml|lock)$") then "cargo-fuzz" | ||
| elif test("^\\.github/workflows/[^/]+\\.ya?ml$") then "github-actions" | ||
| else "other" | ||
| end; | ||
|
|
||
| def is_source($class): | ||
| if $class == "cargo-root" | ||
| then test("^(src|tests|benches)/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+\\.rs$") | ||
| elif $class == "cargo-fuzz" | ||
| then test("^fuzz/fuzz_targets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+\\.rs$") | ||
| else false | ||
| end; | ||
|
|
||
| def branch_prefix: | ||
| if . == "github-actions" then "dependabot/github_actions/" else "dependabot/cargo/" end; | ||
|
|
||
| def changed_lines: | ||
| if . == null then null else [split("\n")[] | select(test("^[+-]"))] end; | ||
|
|
||
| # Every added line is an action reference pinned to a full commit SHA. | ||
| def pinned_actions: | ||
| all(.[]; .patch != null | ||
| and (.patch | changed_lines | map(select(startswith("+"))) | length > 0) | ||
| and all(.patch | changed_lines[] | select(startswith("+")); | ||
| test("^\\+\\s*(- )?uses: [^\\s@]+@[0-9a-f]{40}(\\s+#.*)?$"))); | ||
|
|
||
| def trust: | ||
| $pull[0] as $p | ||
| | $commits[0] as $c | ||
| | if $p.number != $number or $p.state != "open" or $p.draft != false then | ||
| "the pull request is not open and ready" | ||
| elif $p.user.login != $bot or $p.user.type != "Bot" then | ||
| "the pull request is not a Dependabot update" | ||
| elif $p.base.ref != $branch or $p.base.repo.full_name != $repo then | ||
| "the pull request does not target the protected branch" | ||
| elif $p.head.repo.full_name != $repo or (($p.head.ref // "") | startswith("dependabot/") | not) then | ||
| "the head branch is not a Dependabot branch in this repository" | ||
| elif ($c | length) == 0 or $p.commits != ($c | length) or $c[-1].sha != $p.head.sha then | ||
| "the commit list is empty, incomplete, or does not end at the head" | ||
| elif $c[0].author != $bot or $c[0].committer != "web-flow" or $c[0].verified != true then | ||
| "the first commit is not a verified Dependabot commit" | ||
| elif any(range(1; $c | length); . as $i | ||
| | $c[$i].author != $repair_bot | ||
| or $c[$i].verified != true | ||
| or (($c[$i].committer == "web-flow" or $c[$i].committer == $repair_bot) | not) | ||
| or (($c[$i].parents | length) < 1 or ($c[$i].parents | length) > 2) | ||
| or $c[$i].parents[0] != $c[$i - 1].sha) then |
There was a problem hiding this comment.
Require every repair commit to be contained in main
error · confidence 0.94 · kind: risk
The provenance check calls is_ancestor only for later commits with exactly two parents, while trust accepts one-parent repair commits as long as their author, signature, and parent chain look valid. Such a commit can contain arbitrary repair changes, remain eligible, and be auto-merged even though it is not present in main; this defeats the stated boundary that every repair merged must already be in main. Verify every commit after the Dependabot commit is an ancestor of main, or otherwise reject non-main repair commits before eligibility.
|
|
||
| fn reserve_bounded_output(total: &AtomicUsize, amount: usize) -> Result<(), BoundedOutputError> { | ||
| let reserved = total.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { | ||
| let reserved = total.try_update(Ordering::AcqRel, Ordering::Acquire, |current| { |
There was a problem hiding this comment.
Use the available atomic update API
error · confidence 0.94 · kind: risk
AtomicUsize provides fetch_update, but this change calls try_update at both output-reservation sites. Unless an extension trait is added elsewhere, the production crate no longer compiles; the same replacement also breaks the integration test. Restore fetch_update or add and import a verified try_update implementation, then compile the affected targets.
Dependabot pull requests in this repository pass a required classification check, but nothing merges them and nothing repairs one whose checks fail or whose branch conflicts with
main, so routine updates stay open. This change adds a scheduledDependabot reconcileworkflow that merges eligible updates and repairs failing Cargo updates, as documented under Dependency updates. A Cargo patch or minor update, or a GitHub Actions update pinned to a full commit SHA, merges without a maintainer review once every required check succeeded for its exact head commit, that head contains the currentmaincommit, and every review thread is resolved. A major Cargo update or an unclassified update still needs an approval of its exact head commit. Repair is limited to Cargo updates and to their manifests, lockfiles, and Rust source, and a repaired update gets its required checks again before it merges under the same conditions. The workflow runs only themaincopies of its scripts and never checks out pull request content with a repository token. A repair is split across three jobs: a language model, run through a pinned OpenCode release, edits files without the ability to run commands; Cargo applies the lockfile updates in a job that holds neither the model credential nor a repository token; a third job publishes the result as a signed commit.The change also updates
rustlsfrom 0.23.42 to 0.23.45 for RUSTSEC-2026-0285, in which TLS 1.3 handshake messages are incorrectly accepted across encryption level boundaries. That lockfile change is the output of a local run of the repair script with its fixed model and OpenCode release, andcargo auditreports no advisory after it. It also clears the warnings that the Rust 1.99 toolchain reports, which fail the Clippy step that denies warnings. The policy script has 449 fixture checks that run inCI, and the 1631 tests of the Cargo suite pass locally. The reconcile workflow has not run on GitHub Actions: the token rights it needs, the signatures of the commits it creates, and reruns of Dependabot-triggered runs are covered only by fixtures.