Add minimum coverage threshold (4/6): Blockage reason

What does this MR do and why?

This is MR 4 of 6 in a stacked series that adds a minimum coverage threshold to the Coverage-Check approval rule.

MR Branch Base Content
1 6284-coverage-threshold-1-db-model-ff master DB migration & model validation (!226363 (merged))
2 6284-coverage-threshold-2-service-rest 6284-coverage-threshold-1-db-model-ff Service logic, REST API & feature flag (!226370 (merged))
3 6284-coverage-threshold-3-graphql 6284-coverage-threshold-2-service-rest GraphQL mutations & fields (!226372 (merged))
4 (this MR) 6284-coverage-threshold-4-blockage-reason 6284-coverage-threshold-3-graphql blockage_reason feature & GraphQL exposure
5 6284-coverage-threshold-5-frontend 6284-coverage-threshold-4-blockage-reason Frontend: Settings UI + MR widget feedback
6 6284-coverage-threshold-6-docs master Documentation (standalone, no dependency)

Splitting was discussed and agreed upon with @kherman1 and @hfyngvason in !224343 (closed).

This MR includes:

  • ApprovalWrappedRule#blockage_reason method that returns a human-readable explanation of why a coverage rule is blocking a merge request. Two cases are supported:
    • Threshold case: "Coverage falls below the minimum threshold of 80.0% (current: 70.0%)" - when pipeline coverage is below the configured coverage_minimum_threshold
    • Decrease case: "Coverage decreased from 90.0% to 85.0%" - when pipeline coverage decreased compared to the base pipeline (existing behavior, now surfaced)
  • Private helper coverage_blockage_reason that checks threshold first, then relative decrease
  • blockageReason field on ApprovalRuleType GraphQL type, allowing the frontend to display why a rule is blocking
  • Updated generated GraphQL reference documentation and introspection schemas
  • 8 new RSpec examples covering all edge cases (not a report approver, approved, no coverage, below threshold, above threshold but decreased, above threshold and not decreased, no threshold set, no base pipeline)

Files to review in this MR

Note: This is a stacked MR. The diff shows cumulative changes against master. Please focus your review on the files listed below, which are the ones introduced in this MR.

  • ee/app/models/approval_wrapped_rule.rb - blockage_reason + coverage_blockage_reason methods
  • ee/app/graphql/types/approval_rule_type.rb - blockageReason field
  • ee/spec/models/approval_wrapped_rule_spec.rb - 8 test cases for blockage_reason
  • ee/spec/graphql/types/approval_rule_type_spec.rb - Field list update
  • doc/api/graphql/reference/_index.md - Generated GraphQL docs
  • public/-/graphql/introspection_result.json - Generated introspection schema
  • public/-/graphql/introspection_result_no_deprecated.json - Generated introspection schema

Related to #6284 Extracted from !224343 (closed)

How to set up and validate locally

Prerequisites

1. Set up test data in Rails console

Open a Rails console (bin/rails console) and run:

# Find a project and an open merge request
project = Project.first
mr = project.merge_requests.opened.last

# Store pipeline partition info
sha = project.repository.commit('HEAD').id
partition_id = Ci::Partition.current.id

# Create a base pipeline with 90% coverage
base_pipeline = Ci::Pipeline.new(
  project: project, ref: mr.target_branch, sha: sha,
  source: :push, status: :success, partition_id: partition_id
)
base_pipeline.save!(validate: false)
Ci::Build.new(
  project: project, pipeline: base_pipeline, name: 'test',
  status: :success, coverage: 90.0, partition_id: partition_id,
  scheduling_type: :stage
).save!(validate: false)

# Create a head pipeline with 70% coverage
head_pipeline = Ci::Pipeline.new(
  project: project, ref: mr.source_branch, sha: sha,
  source: :merge_request_event, status: :success,
  merge_request: mr, partition_id: partition_id
)
head_pipeline.save!(validate: false)
Ci::Build.new(
  project: project, pipeline: head_pipeline, name: 'test',
  status: :success, coverage: 70.0, partition_id: partition_id,
  scheduling_type: :stage
).save!(validate: false)
mr.update_column(:head_pipeline_id, head_pipeline.id)

2. Test blockage_reason with threshold

# Create an MR-level approval rule with 80% threshold
approver = User.where.not(id: mr.author_id).first
mr_rule = ApprovalMergeRequestRule.create!(
  merge_request: mr, name: 'Coverage-Check',
  rule_type: :report_approver, report_type: :code_coverage,
  approvals_required: 1, coverage_minimum_threshold: 80.0
)
mr_rule.users << approver

# Check blockage_reason - should show threshold message
wrapped = ApprovalWrappedRule.new(mr.reload, mr_rule)
wrapped.blockage_reason
# => "Coverage falls below the minimum threshold of 80.0% (current: 70.0%)"

3. Test blockage_reason with decrease (no threshold)

# Remove threshold, keep coverage at 70% (below base 90%)
mr_rule.update!(coverage_minimum_threshold: nil)
wrapped = ApprovalWrappedRule.new(mr.reload, mr_rule)
wrapped.blockage_reason
# => "Coverage decreased from 90.0% to 70.0%"

4. Test blockage_reason via GraphQL

curl -s -X POST "http://gdk.local:3000/api/graphql" \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query { project(fullPath: \"<PROJECT_PATH>\") { mergeRequest(iid: \"<MR_IID>\") { approvalState { rules { name approved blockageReason } } } } }"
  }' | jq '.data.project.mergeRequest.approvalState.rules'

5. Run the specs

bundle exec rspec ee/spec/models/approval_wrapped_rule_spec.rb -e 'blockage_reason'
# Expected: 8 examples, 0 failures

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.

  • The change is backwards compatible. blockage_reason returns nil when no blocking condition is met.
  • No feature flag needed for this method. It is a read-only display method that does not change merge-blocking behavior.
  • No N+1 queries. blockage_reason uses data already loaded by the approval state (head_pipeline, base_pipeline).
  • GraphQL field follows style guide (no demonstrative "this").
  • 8 RSpec examples cover all edge cases including nil threshold, nil coverage, approved rules, and non-report rules.

Related to #6284

Edited by Norman Debald

Merge request reports

Loading