Skip the auto-remediation commit when the output is unchanged

Why

The gap

  • After the workload, the service always commits the output.
  • It never checks whether the open MR already holds that output.
  • An identical commit still adds a diff version and an MR pipeline, and resets approvals.
  • This repeats every cooldown for as long as the MR stays open.
  • This is the bug in #632419.

Why this approach

  • The new content is only known after the workload, so the check runs just before the commit.
  • With dependency_management_workload_ref_refresh on, !261586 (merged) leaves the branch untouched until then.
  • If the MR already holds the output, skipping the commit leaves the branch exactly as it was.
  • Comparing against the merge request's own base means a moved default branch is not a change.
  • Comparing git blob IDs computed locally avoids loading blob data.

What

  • Skips the commit when all of these hold:
    • an open MR exists, and its change touches only output files
    • each output file's blob ID at the tip matches the output
    • the tip's commit subject is the one this run would write
    • the MR is confirmed mergeable on the branch tip
  • To confirm it, the recorded head of the MR must be the branch tip, and a synchronous mergeability check must come back mergeable. Every push resets the merge status, so an async check could be reset before the next refresh read it.
  • Still commits in any of these cases:
    • a conflict, a merge status it cannot confirm, or an error from the check
    • a fast-forward-only project whose branch does not contain pipeline.sha
    • a different subject, such as after a move to Conventional Commits
  • On a skip, it logs skip_reason: :unchanged and updates the title and description, which pushes nothing. It then links the vulnerability, records the remediation and starts the cooldown. If the update fails, it returns the error and does none of these.
  • With dependency_management_workload_ref_refresh on, a skip leaves the branch head as it was. The conflict refresh limit from !261586 (merged) would then hold that head for the cooldown period. A skip only happens once the MR is confirmed mergeable, so it releases the hold with RefreshCooldown.release_head, and a conflict found later is refreshed straight away.
  • No flag of its own. With dependency_management_workload_ref_refresh off, the reset leaves the merge request's change empty, which never counts as unchanged.
  • .gitattributes do not affect the comparison. A spec pins this.
  • The "head unchanged" specs run the second refresh a minute later, because identical commits made in the same second share a SHA.

References

Manual QA

These runs predate the synchronous mergeability check and the subject match. Specs cover those.

  1. In cargo-vuln-demo with the flag on, take an open remediation MR mr whose last commit came from a refresh. Expire the cooldown and run the scheduler:

    Gitlab::Redis::SharedState.with { |r| r.del("dependency_management:remediation_refresh:#{mr.id}") }
    DependencyManagement::SecurityUpdate::SchedulerService.execute(project: project)
  2. Expect nothing pushed, and skip_reason: unchanged in log/application_json.log.

  3. Repeat after moving the default branch, or rebasing the MR: nothing pushed. After a commit that changes Cargo.lock: one commit. After a conflicting change on the default branch: one commit, then nothing.

  4. With the flag off, the refresh commits as today.

GDK result on 5033dfa01. Starting values are taken after each case's setup.

Case Flag Branch tip Diff versions MR pipelines Approval Log
Identical output Off 4876355e to f162bd5f 66 to 68 32 to 33 Reset
Identical output On f162bd5f, unchanged 68, unchanged 33, unchanged Kept skip_reason: unchanged
Default branch moved, unrelated change On f162bd5f, unchanged 68, unchanged 33, unchanged Kept skip_reason: unchanged
Rebase by a person, then refresh On 3fdfbe23, unchanged 69, unchanged 34, unchanged Kept skip_reason: unchanged
Output differs from the branch On 28b4f46d to d6e68adb 70 to 71 35 to 36 One commit
Conflict, first refresh On 4876355e to 85772fb5 74 to 75 36 to 37 One commit, merge request mergeable
Conflict, second refresh On 85772fb5, unchanged 75, unchanged 37, unchanged skip_reason: unchanged

Combined with the conflict refresh limit from !261586 (merged), GDK on 651f8dd5, flag on, a yaml-rust MR, with the conflict refresh's workload cancelled at once to stand in for a failure:

Step Workloads started Branch head Conflict claim TTL
Cooldown refresh, output unchanged 1 unchanged, commit skipped none, the hold is released
Default branch changed so the MR conflicts, one scheduler run 1, straight away unchanged 28797 s, the conflict refresh's claim
Combined stack evidence

Cooldown refresh with unchanged output: the commit is skipped and the hold is released.

30-rerun-cooldown-skip

The MR made to conflict: one workload straight away.

31-rerun-conflict-after-skip

Evidence

Flag off, before the refresh: 4876355e:

01-s1-before-commits

Flag off, after: a new identical commit f162bd5f:

02-s1-after-commits

Flag off, after: a new version:

03-s1-after-versions

Flag off, after: a new MR pipeline:

04-s1-after-pipelines

Flag off, after: the approval was reset:

05-s1-after-approvals

Flag on, before the refresh: f162bd5f:

06-s2-before-commits

Flag on, after: still f162bd5f:

07-s2-after-commits

Flag on, after: no new version:

08-s2-after-versions

Flag on, after: no new MR pipeline:

09-s2-after-pipelines

Flag on, after: the approval is kept:

10-s2-after-approvals

Default branch moved: still f162bd5f:

11-s3-after-commits

After a rebase by a person: the rebased commit 3fdfbe23 is kept:

12-s4-after-commits

Output differs from the branch: one new commit d6e68adb:

13-s5-after-commits

Conflict, after both refreshes: one commit 85772fb5:

14-s6-after-commits

Edited by Philip Cunningham

Merge request reports

Loading
Loading