prepared_at is documented as waiting for preparation steps that are not awaited

Problem

The API documentation states that prepared_at populates only after all five preparation steps complete:

  • Create the diff.
  • Create the pipelines.
  • Check mergeability.
  • Link all Git LFS objects.
  • Send notifications.

Two of those are scheduled asynchronously and nothing waits for them before prepared_at is set, so the field does not mean what the documentation promises:

  • Create the pipelines - MergeRequests::AfterCreateService#prepare_for_mergeability calls create_pipeline_for(merge_request, current_user, async: true), which enqueues the work.
  • Check mergeability - the same method calls merge_request.check_mergeability(async: true), which enqueues MergeRequestMergeabilityCheckWorker.

mark_merge_request_as_prepared then runs in the same job, without waiting for either. On a quiet instance the mergeability check often finishes after prepared_at is already set, and it can finish minutes later when Sidekiq is busy.

Proposal

Correct the documentation rather than the ordering. Split the list into the steps that are guaranteed to have finished and the steps that are only scheduled, so API consumers can rely on what prepared_at actually tells them.

Alternative considered, and why not

Delaying prepared_at until the async steps finish looks like the "correct" fix but breaks two things:

  1. It is the retry guard for NewMergeRequestWorker. That worker returns early when issuable&.prepared? is true, and deduplicate :until_executed does not cover Sidekiq retries because retries skip client middleware. Moving prepared_at later means a retried job re-runs the whole of AfterCreateService: duplicate notifications, todos, cross-references and webhooks. A replacement durable marker would be needed first.
  2. It changes the merge request create webhook payload. prepared_at is in Gitlab::HookData::MergeRequestBuilder.safe_hook_attributes and execute_hooks runs after mark_merge_request_as_prepared, so consumers would start receiving prepared_at: null on create.

Both are worth discussing on their own merits, but neither should be a prerequisite for the documentation matching the behaviour.

Context

Came up in review of !242698 (merged), which adds a Duo Agent Platform trigger that fires once the diff is built and code-owner approval rules are synced against it. While reviewing when that event should fire, we found the documented meaning of prepared_at and its implementation disagree.