Add dep. management security update merge request creation service

What does this MR do and why?

Adds DependencyManagement::SecurityUpdate::CreateMergeRequestService and its supporting OutputParser, which together handle the automated creation of merge requests after a managed dependency security update pipeline completes.

This is part of the Auto-Remediation feature for Dependency Management. When a security vulnerability is detected in a project's dependencies, an orchestrator pipeline bumps the affected packages to patched versions. This service takes the pipeline's output.json artifact, parses the dependency changes and updated file contents, commits them to a new branch, and opens (or updates) a merge request targeting the project's default branch. The merge request is authored by the dependency management service account and is automatically linked to the originating vulnerability.

This MR implements the service only (MR 1 of the implementation plan). A follow-up MR will add the worker that listens for Ci::PipelineFinishedEvent and invokes this service.

Stacked Diffs

References

Screenshots or screen recordings

MR created by GitLab Dependency Management Service Account:

image

MR is linked to the vulnerability:

image

How to set up and validate locally

You can create the script below and execute it locally in the rails console. Alternatively, you can execute it line by line in console as well.

Please let me know if you encounter any issues with it.

create_mr.rb
# ============================================================================
# Local validation for CreateMergeRequestService
#
# Run in rails console:  load 'path/to/this/script.rb'
#
# Prerequisites:
#   - GDK with Ultimate license
#   - A project with a detected vulnerability that has SBOM data
#   - Adjust PROJECT_PATH and VULNERABILITY_ID below
# ============================================================================

PROJECT_PATH = 'your-group/your-project'
VULNERABILITY_ID = 772

# ── Setup ───────────────────────────────────────────────────────────────────

project = Project.find_by_full_path(PROJECT_PATH)
vulnerability = Vulnerability.find(VULNERABILITY_ID)
occurrence = vulnerability.sbom_occurrences.first

dep_name = occurrence.component_name.downcase.gsub(/[^a-z0-9]/, '-').squeeze('-').gsub(/^-|-$/, '')
major = occurrence.version.to_s.match(/^(\d+)/)&.captures&.first || '0'
target_branch = "dependency-management/#{dep_name}-#{major}.x"

# ── Cleanup previous runs ──────────────────────────────────────────────────

if project.repository.branch_exists?(target_branch)
  project.repository.delete_branch(target_branch)
  project.repository.expire_branches_cache
end

existing_mr = project.merge_requests.opened.find_by(source_branch: target_branch)
existing_mr&.close!

Vulnerabilities::MergeRequestLink.where(vulnerability: vulnerability).delete_all

# ── Fake pipeline ───────────────────────────────────────────────────────────

pipeline = Ci::Pipeline.create!(
  project: project, ref: project.default_branch, sha: project.commit.sha,
  source: :push, status: :success, partition_id: Ci::Pipeline.current_partition_value
)

stage = Ci::Stage.create!(
  pipeline: pipeline, project: project, name: 'build', position: 1,
  partition_id: pipeline.partition_id
)

Ci::Build.create!(
  pipeline: pipeline, project: project, name: 'dependency-update',
  status: :success, ref: project.default_branch,
  partition_id: pipeline.partition_id, ci_stage: stage, scheduling_type: :stage
)

Ci::PipelineVariable.create!(
  pipeline: pipeline, key: 'DEPENDENCY_MANAGEMENT_TARGET_BRANCH',
  value: target_branch, partition_id: pipeline.partition_id
)

# ── Fake orchestrator output ────────────────────────────────────────────────

new_version = '2.5.33'
output_json = {
  'dependencies' => [
    { 'name' => occurrence.component_name, 'version' => new_version,
      'previous-version' => occurrence.version.to_s }
  ],
  'updated-files' => [
    { 'path' => occurrence.input_file_path,
      'content' => "# Updated by GitLab Dependency Management\n#{occurrence.component_name}:#{new_version}\n",
      'encoding' => 'text' }
  ]
}.to_json

# ── Execute ─────────────────────────────────────────────────────────────────

service = DependencyManagement::SecurityUpdate::CreateMergeRequestService.new(
  project: project, pipeline: pipeline, vulnerability: vulnerability
)
service.instance_variable_set(:@_test_output, output_json)
def service.fetch_and_parse_output
  DependencyManagement::SecurityUpdate::OutputParser.parse(@_test_output)
end

result = service.execute

# ── Result ──────────────────────────────────────────────────────────────────

if result.success?
  mr = result.payload[:merge_request]

  # Force diff generation (normally async via Sidekiq)
  mr.ensure_merge_request_diff
  mr.merge_request_diff.save!
  MergeRequests::AfterCreateService.new(project: project, current_user: mr.author).execute(mr)
  mr.reload

  url = Gitlab::Routing.url_helpers.project_merge_request_url(project, mr)
  puts "✅ Open in browser: #{url}"
else
  puts "❌ #{result.message}"
end

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Albina Yusupova

Merge request reports

Loading