Add merge readiness to the replacement MR overview

Fills the empty overview container from the data layer below with a merge readiness panel: a hero stating whether the merge request can merge and what it is waiting on, a progress bar, and one row per merge check.

Row state and the hero verdict both come from the mergeabilityChecks GraphQL field rather than being computed from raw fields, so each of the five check statuses keeps its own meaning - a check the project has turned off, one passing on an override, and one still running all read differently instead of being lumped in with a plain pass. A merged or closed merge request gets its state rather than a readiness score. The panel follows the merge status subscription, so it updates in place when a pipeline finishes or a thread is resolved.

Chain, each part building on the one above:

  1. !252481 (merged) - flag, toggle, empty container (merged)
  2. !255057 - data layer: mount point, Apollo provider, query and subscription
  3. !252997 - layout shell and merge readiness panel (this MR)
  4. !252998 - reviewers panel
  5. !252999 - description, labels and milestone

mr-252997-readiness-draft-row

Detailed context for AI agents

What this MR adds

  • The layout shell for the replacement overview. Single column for now - the two-column grid and the sidebar arrive with the reviewers panel, so the grid-template-columns rule is not here yet.
  • A merge readiness panel: a hero, a progress bar (passing checks over total checks), and one row per merge check.
  • Four rows always render, in this order: Approvals, Discussions, Pipeline, Merge conflicts. Any other merge check earns a row only while it is blocking, still being checked, or passing on an override.
  • No GraphQL, view or wiring changes. The mrAiOverview query, the merge status subscription, the mount point data attributes and the Apollo provider all live in the data layer MR below (!255057). This MR consumes the merge request it already fetches.

Files touched: ee/app/assets/javascripts/merge_requests/ai_overview/{constants.js,utils.js,components/merge_readiness.vue,components/readiness_row.vue} plus three lines in components/app.vue to render the panel, app/assets/stylesheets/page_bundles/merge_request.scss, the specs under ee/spec/frontend/merge_requests/ai_overview/ and ee/spec/features/merge_request/user_toggles_ai_overview_spec.rb, and locale/gitlab.pot.

Status to row presentation

mergeabilityChecks status Counts as passing Icon and accessible name
SUCCESS yes check-circle-filled, "Passing"
INACTIVE yes status-neutral, "Not required"
WARNING yes warning, "Override added"
CHECKING no status-running, "In progress"
FAILED no error, "Blocked"

The row reads its presentation straight off the GraphQL status enum - there is no intermediate state name. CHECK_STATUS in constants.js is the single map, and the passing and extraRow flags on each entry drive the progress count and whether a non-default check earns a row.

INACTIVE means the project has turned that check off, so it cannot block the merge. WARNING means it passed only because someone added an override - requested_changes, security_policy_violations and security_policy_pipeline_check are the three checks that can return it.

done, inactive and warning all count towards the passing total, because none of them can hold the merge up. They are kept apart in the row so that the icon never contradicts the detail text beside it: the detail comes from the raw count fields, so a turned-off pipeline check renders "Failed" next to a neutral "Not required" icon rather than next to a green tick.

A pending row takes its detail from the already-translated CHECKING_REASONS map in ~/vue_merge_request_widget/components/checks/constants, so the conflicts row reads "Checking for merge conflicts." rather than reporting a raw field that has not settled yet.

A check identifier missing from the response defaults to pending, not done. A gap in the data has to read as unfinished, otherwise project.mergeRequest coming back null without a GraphQL error would render a confident "Ready to merge" off no data at all.

GraphQL returns the identifier as an enum name, in upper case, so lookups lower-case it. That is what the existing merge checks component does. The four default rows map to the identifiers not_approved, discussions_not_resolved, ci_must_pass and conflict; row labels come from a CHECK_LABELS map that shares its keys with FAILURE_REASONS, which supplies the detail for a check without a default row.

The hero verdict

Four verdicts, in precedence order:

Verdict Title Subtitle
blocked This merge request is not ready to merge Waiting on: the blocked rows only
checking Checking whether this merge request can be merged Still checking: ...
warning Ready to merge, with caution Passing with an override: ...
ready Ready to merge Every merge check is passing.

checking is separate from blocked because the merge gate has not reached a negative verdict yet, and the blocked subtitle names only the rows that are actually blocked rather than everything that is not yet passing. warning is separate from ready so an override is never indistinguishable from an unconditional pass - the classic widget says "Merge with caution: Override added" in the same situation.

The verdict, the progress bar and the "N of M checks passing" count are all computed over the rows that actually render, so the hero and the rows cannot disagree.

Merge requests that are not open

The query and subscription select MergeRequest.state. When it is anything but opened, the readiness rows and the progress bar are replaced by a hero stating the real state:

  • merged - "Merged" / "This merge request has been merged."
  • closed - "Closed" / "This merge request was closed without being merged."
  • locked - "Merge in progress" / "This merge request is being merged."

The merge gate reports not_open as a failing check, so without this the overview on a merged merge request read "This merge request is not ready to merge / Waiting on: Merge conflicts, Merge request state". The conflict check also returns checking indefinitely once the source branch is gone, so that row would have sat at "In progress" forever.

Cost

mergeabilityChecks calls Gitaly and runs every check, but this does not add a page load: when the overview is enabled, _page.html.haml does not render projects/merge_requests/widget, the widget Vue app never mounts, and so the widget's own mergeChecks query cannot run. Against the classic overview it replaces, this path drops that query, a polled detailedMergeStatus query that ran the whole check suite again, two subscriptions and three startup REST calls, and adds one query and one subscription. The new query does not poll.

The panel subscribes to mergeRequestMergeStatusUpdated, the same signal the widget uses (mr_widget_options.vue). Because the payload is the same merge request, cache normalisation re-renders the rows and no updateQuery is needed. Without it the rows sat stale until a reload, which is worst for exactly the rows that change on their own - a running pipeline or a checking conflict.

Row actions

The discussions row links to the diffs tab and the pipeline row links to the pipeline. The approvals row deliberately has no link: in the old tab layout it pointed at the Overview tab, which is the page it now lives on.

Accessibility

  • Each row's icon carries its state as an accessible name. GlIcon renders aria-hidden unless it is given a label, so without it a screen reader hears "Approvals, 0 of 1 required" with nothing to say whether that row is the blocker.
  • GlProgressBar gets an aria-label of the "N of M checks passing" text, which it otherwise reports as the generic "Progress bar".
  • The pending state reads as active rather than dormant: status-running with the info variant.
  • Readiness rows are a ul and li list rather than nested divs.

Feature flag

Everything user-facing is behind mr_ai_overview (type: :wip) plus an opt-in cookie. _page.html.haml renders the partial only under ai_overview_enabled?, which requires both the flag and cookies[:mr_ai_overview_enabled]; the CE helpers stub ai_overview_available? and ai_overview_enabled? to false; and the JS chunk is dynamic-imported only when #js-ai-overview is in the DOM, so it is not even downloaded with the flag off. No server-side type or field was added, so nothing new is reachable through the API.

The one ungated piece is the two-declaration .ai-overview-hero rule in the merge_request.scss page bundle, which ships to every merge request page because page bundles cannot be loaded conditionally.

Verification

  • ee/spec/frontend/merge_requests/ai_overview/ - 34 examples, passing. Covers the override state, the unfinished state, a turned-off check both with and without a default row, an empty check list, the subscription updating the panel with no refetch, the per-row accessible names, and merged, closed and locked through describe.each.
  • ee/spec/features/merge_request/user_toggles_ai_overview_spec.rb - 9 examples, passing. One asserts that a draft merge request renders five readiness rows including the draft one, and one marks the merge request as merged and asserts the panel says so and renders no readiness rows. Both exercise the identifier and state mapping end to end against the real GraphQL API.
  • Both GraphQL documents validate against a dumped schema. MergeRequest.state is MergeRequestState! and its wire values are lower case (opened, closed, locked, merged), which is what the state lookup is keyed on.
  • eslint and prettier are clean, and locale/gitlab.pot is regenerated - 50 AiOverview| strings in this MR, the rest in the data layer MR below.

Out of scope

  • Internal event tracking on the overview toggle, carried over from the first MR in the chain.
  • The two-column grid and the reviewers sidebar, which arrive in !252998.
  • Any merge action. The classic widget owns merge, auto-merge and "Mark as ready", and it is not rendered while the toggle is on, so the replacement overview can show the verdict but not act on it. That wants a later MR in the chain, along with an approve action on the approvals row and a first-class security findings row - all three exist in the earlier own-tab prototype (!233471).
  • Pipeline job progress in the row detail ("Running, 6 of 9 jobs"). Pipeline.totalJobs exists but there is no finished-count field, so it needs a second jobs(statuses: [...]) { count } connection on a query that already touches Gitaly. Not worth it for a cosmetic string.
Edited by Marc Shaw

Merge request reports

Loading
Loading