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_reasonmethod 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 configuredcoverage_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)
- Threshold case:
- Private helper
coverage_blockage_reasonthat checks threshold first, then relative decrease blockageReasonfield onApprovalRuleTypeGraphQL 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_reasonmethodsee/app/graphql/types/approval_rule_type.rb-blockageReasonfieldee/spec/models/approval_wrapped_rule_spec.rb- 8 test cases forblockage_reasonee/spec/graphql/types/approval_rule_type_spec.rb- Field list updatedoc/api/graphql/reference/_index.md- Generated GraphQL docspublic/-/graphql/introspection_result.json- Generated introspection schemapublic/-/graphql/introspection_result_no_deprecated.json- Generated introspection schema
Related to #6284 Extracted from !224343 (closed)
How to set up and validate locally
Prerequisites
- MR 1 (!226363 (merged)), MR 2 (!226370 (merged)), and MR 3 (!226372 (merged)) must be applied first
- Run
bin/rails db:migrate - Enable the feature flag in Rails console:
Feature.enable(:coverage_minimum_threshold)
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 failuresMR 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_reasonreturnsnilwhen 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_reasonuses 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