Add minimum coverage threshold (1/6): DB migration, model & feature flag

What does this MR do and why?

This is MR 1 of 6 in a stacked series that adds a minimum coverage threshold to the Coverage-Check approval rule, allowing teams to enforce an absolute minimum coverage percentage before merging. This is a rework of !224343 (closed), split into smaller MRs as discussed with @kherman1 and @hfyngvason.

This MR establishes the database foundation and model layer. Subsequent MRs build on top of this:

MR Branch Base Content
1 (this MR) 6284-coverage-threshold-1-db-model-ff master DB migration & model validation
2 6284-coverage-threshold-2-service-rest 6284-coverage-threshold-1-db-model-ff Service logic, REST API & feature flag
3 6284-coverage-threshold-3-graphql 6284-coverage-threshold-2-service-rest GraphQL mutations & fields
4 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)

MRs 5 and 6 are reworks of the existing !224368 (closed) (Frontend) and !224375 (closed) (Docs).

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

This MR includes:

  • Two database migrations adding coverage_minimum_threshold (float, nullable) to approval_project_rules and approval_merge_request_rules (one migration per table to avoid multi-table locking)
  • A separate migration adding DB-level CHECK constraints (0-100 range) on both tables to ensure data integrity at the schema layer
  • Model validation (0-100 range) on ApprovalProjectRule, scoped to code_coverage report type, kept consistent with the DB CHECK constraint (no feature flag gating, per @SamWord's review)
  • Addition of coverage_minimum_threshold to REPORT_APPROVER_ATTRIBUTES for automatic propagation from project rules to MR rules

Note: The coverage_minimum_threshold feature flag (gitlab_com_derisk) was originally introduced in this MR but moved to MR 2 (!226370 (merged)), where it is first consumed in the critical mergeability path. Splitting it off here keeps MR 1 free of unused FF definitions on master.

Files to review in this MR

  • db/migrate/20260425171811_add_coverage_minimum_threshold_to_approval_project_rules.rb - Migration (approval_project_rules)
  • db/migrate/20260425171813_add_coverage_minimum_threshold_to_approval_merge_request_rules.rb - Migration (approval_merge_request_rules)
  • db/migrate/20260425171817_add_coverage_minimum_threshold_check_constraints.rb - CHECK constraints migration
  • db/schema_migrations/20260425171811
  • db/schema_migrations/20260425171813
  • db/schema_migrations/20260425171817
  • db/structure.sql - Schema changes for both tables
  • ee/app/models/approval_project_rule.rb - REPORT_APPROVER_ATTRIBUTES + validation
  • ee/spec/models/approval_project_rule_spec.rb - Validation tests

Related to #6284 Extracted from !224343 (closed)

How to set up and validate locally

  1. Run the migration bin/rails db:migrate

  2. Verify in Rails Console bin/rails console

# Verify the columns exist
ApprovalProjectRule.column_names.include?('coverage_minimum_threshold')
# => true
ApprovalMergeRequestRule.column_names.include?('coverage_minimum_threshold')
# => true

# Create a test rule with valid threshold
project = Project.first
rule = ApprovalProjectRule.new(
  project: project,
  name: 'Coverage-Check-Test',
  rule_type: :report_approver,
  report_type: :code_coverage,
  approvals_required: 1,
  coverage_minimum_threshold: 80.0
)
rule.valid?
# => true

# Test validation: value above 100 is rejected
rule.coverage_minimum_threshold = 150.0
rule.valid?
# => false
rule.errors[:coverage_minimum_threshold]
# => ["must be less than or equal to 100"]

# Test validation: value below 0 is rejected
rule.coverage_minimum_threshold = -5.0
rule.valid?
# => false

# Test validation: nil is allowed (feature is optional)
rule.coverage_minimum_threshold = nil
rule.valid?
# => true

# Validation is scoped to code_coverage rules only - other report types skip the check
non_coverage_rule = build(:approval_project_rule, :license_scanning, coverage_minimum_threshold: 150.0)
non_coverage_rule.valid?
# => true (validation skipped, value is semantically irrelevant for non-code_coverage rules;
#         the DB CHECK constraint still enforces NULL or 0..100 at the schema layer)
  1. Run the specs bundle exec rspec ee/spec/models/approval_project_rule_spec.rb -e 'coverage_minimum_threshold' # Expected: 2 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. When coverage_minimum_threshold is nil (default), behavior is unchanged.
  • Database migrations are safe: separate add_column per table with null: true, no default, no index needed. CHECK constraints added in a dedicated migration.
  • Model validation is consistent with the DB CHECK constraint and applies regardless of feature flag state.
  • Feature is behind the existing coverage_check_approval_rule licensed feature, no new licensing flag needed.

Related to #6284

Edited by Norman Debald

Merge request reports

Loading