Expose acting user on CD entities via GraphQL

What this MR does

The CD Rails design keeps attribution in the transition journal rather than as columns on the entities themselves, so status and attribution stay derived and are never authored directly by clients. But nothing captured that attribution at creation time, so there was no data to derive from.

This MR adds the missing capture points to expose the acting user:

  • Cd::Rollouts::CreateService now writes the initial transition journal row, since it is the only place the requesting user is known synchronously (StartWorker/StartService run async).
  • Cd::VersionSets::CreateService now sets created_by. Version sets have no transition journal to derive from, so this is the one case that needed a new column (created_by_id, write-once, consistent with version sets being immutable).
  • Cd::Concerns::PrincipalReference centralizes parsing the "user:<id>" principal format so callers never do it themselves.

New GraphQL fields (all batched, N+1-safe):

  • CdRollout.triggeredByUser
  • CdVersionSet.author

CdDeployment.triggeredByUser and CdService.lastDeployedBy were dropped from this MR: deployment creation has no production write path yet, so cd_deployment_transitions is never populated and those fields would always resolve to null. Follow-up once deployment creation/attribution is designed.

Query Plans

This MR introduces one new query. This CD table carries no meaningful GitLab.com production data yet (Beta feature, gated by the ai_native_deploy feature flag), so per the database review guidelines, the plan below was generated from a local development environment (seeded with representative rows, reverted afterwards) rather than postgres.ai.

This reuses an index that already existed before this change; no new indexes were required beyond the one added for cd_version_sets.created_by_id.

Cd::RolloutTransition.first_acting_user_id_by_rollout

SELECT "cd_rollout_transitions"."rollout_id", "cd_rollout_transitions"."principal",
       "cd_rollout_transitions"."on_behalf_of", "cd_rollout_transitions"."created_at"
FROM "cd_rollout_transitions"
WHERE "cd_rollout_transitions"."rollout_id" IN (1, 2, ..., 50)
ORDER BY "cd_rollout_transitions"."created_at" ASC, "cd_rollout_transitions"."id" ASC

Query plan (local, 50 rollout ids):

Sort  (cost=15.53..15.75 rows=88 width=88) (actual time=0.028..0.029 rows=50 loops=1)
  Sort Key: created_at, id
  Sort Method: quicksort  Memory: 27kB
  ->  Index Scan using index_cd_rollout_transitions_on_rollout_id_and_created_at on cd_rollout_transitions  (cost=0.15..12.69 rows=88 width=88) (actual time=0.012..0.016 rows=50 loops=1)
        Index Cond: (rollout_id = ANY ('{1,2,...,50}'::bigint[]))
Planning Time: 0.192 ms
Execution Time: 0.034 ms

Testing GraphQl queries

  1. Discover an organization ID and any existing CD entities
query {
  organization(id: "gid://gitlab/Organizations::Organization/1") {
    id
    cdApplications {
      nodes {
        id
        name
        services { nodes { id name } }
        versionSets { nodes { id name } }
        rollouts { nodes { id iid } }
      }
    }
  }
}
  1. CdVersionSet.author
query {
  organization(id: "gid://gitlab/Organizations::Organization/1") {
    cdVersionSet(id: "gid://gitlab/Cd::VersionSet/<ID>") {
      id
      name
      author {
        id
        username
      }
    }
  }
}

Screenshot_2026-07-27_at_18.36.41

  1. CdRollout.triggeredByUser
query {
  organization(id: "gid://gitlab/Organizations::Organization/1") {
    cdRollout(id: "gid://gitlab/Cd::Rollout/<ID>") {
      id
      iid
      triggeredByUser {
        id
        username
      }
    }
  }
}

Screenshot_2026-07-27_at_18.38.47

Note: reads are gated behind the ai_native_deploy feature flag — enable it first with Feature.enable(:ai_native_deploy) in a rails console if it isn't already on.

Note: triggeredByUser/author are derived from data written at creation time (the transition journal, or created_by_id for version sets). Rollouts and version sets that already existed before this MR will have no attribution to derive from and will correctly return null — this is expected, not a bug. The fields only populate for entities created after this change, via the CdRolloutCreate/CdVersionSetCreate mutations.

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.

Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/606062

Edited by Carla Drago

Merge request reports

Loading
Loading