Add CD RolloutStep Model

What does this MR do?

Adds a Cd::RolloutStep model backed by a new cd_rollout_steps table. One row is created per node in a rollout's flow definition tree - including stage containers, not just leaf steps - addressable by a path/parent_path pair (e.g. "0", "0.0", "1"). This gives the flow visualization (and future step-execution workers) a place to read and write per-step execution state as a rollout progresses. States are pending/running/success/failed/skipped/cancelled for ordinary steps, or pending/awaiting_approval/approved/rejected/skipped/cancelled for approval-gate steps - the legal set depends on step_type, enforced by a model validation.

Rows are built and bulk-inserted synchronously when a rollout is created, inside the same transaction that already creates rollout_environments and deployments - not later at rollout start. A new Cd::RolloutSteps::Builder service turns a flow definition's step tree into unsaved Cd::RolloutStep records ready for bulk insert. Cd::ApplicationFlowDefinitions::Document gains a #steps_with_paths method that walks the step tree and returns every node (stage containers included) with its position path and parent path (nil at top level). Cd::Rollouts::CreateService now builds rollout_steps right after rollout_environments, and memoizes its flow-definition YAML parse (into one Document instance) since both steps need it within the same #execute call. Cd::Rollout and Cd::RolloutEnvironment each gain a rollout_steps has_many association.

Changes

  • New cd_rollout_steps table: organization_id, rollout_id, rollout_environment_id (nullable, FK to cd_rollout_environments), timestamps, started_at, finished_at, state (smallint enum, default pending), path, parent_path, step_type, name, error, params (jsonb, JSON-schema-validated, holds step-specific config e.g. wait seconds, canary service weights)
  • Unique index on (rollout_id, path); separate FK migrations to organizations, cd_rollouts, cd_rollout_environments (one FK per migration, this codebase's convention)
  • New Cd::RolloutSteps::Builder service builds unsaved Cd::RolloutStep records from a flow definition's step tree
  • New Cd::ApplicationFlowDefinitions::Document#steps_with_paths returns every tree node with its path/parent_path
  • Cd::Rollouts::CreateService builds rollout_steps alongside rollout_environments/deployments in one transaction; memoizes the parsed flow Document
  • Cd::Rollout/Cd::RolloutEnvironment gain rollout_steps associations
  • Database dictionary entry db/docs/cd_rollout_steps.yml, feature category continuous_delivery

One new table, 4 migrations (create + 3 FK migrations). No existing tables or schema are touched, no breaking changes. Gated by the existing ai_native_deploy feature flag on the CdRolloutCreate GraphQL mutation - that flag is not new, not introduced here. This MR is backend/data-model only - nothing exposes rollout_steps via GraphQL or REST yet (the CdRolloutCreate mutation's response shape is unchanged); exposing the step tree in the API is a follow-up. This MR does not change flow-definition validation - an invalid or environment-less flow definition still fails rollout creation the same way as before, via the pre-existing InvalidFlowDefinitionError path.

How to test

1. Seed an organization, application, flow definition, and version set (Rails console)
Feature.enable(:ai_native_deploy)

org = Organizations::Organization.create!(path: "cd-rollout-steps-demo", name: "CD Rollout Steps Demo")
user = User.find_by(username: "root")
Organizations::OrganizationUser.create!(organization: org, user: user, access_level: Gitlab::Access::OWNER)

app = Cd::Application.create!(organization: org, name: "demo-app")
env = Cd::Environment.create!(organization: org, name: "production", tier: :production)
service = Cd::Service.create!(organization: org, application: app, name: "web")

Cd::EnvironmentDriverBinding.create!(organization: org, environment: env,
  driver_ref: "argo-rollouts", driver_config: { "cluster_agent_id" => "1" })

flow_yaml = <<~YAML
  steps:
    - type: com.gitlab.cd.steps.stage
      name: production
      steps:
        - type: com.gitlab.cd.argo.rolling.deploy
          environment: production
          services:
            - name: web
    - type: com.gitlab.cd.steps.wait
      seconds: 30
YAML
Cd::ApplicationFlowDefinition.create!(application: app, organization_id: org.id, definition: flow_yaml)

artifact_source = Cd::ArtifactSource.create!(organization: org, service: service,
  name: "web-image", source_ref: "registry.example.com/web", source_config: {})
version = Cd::Version.create!(organization: org, artifact_source: artifact_source, name: "v1_0_0")

vset = Cd::VersionSet.create!(organization: org, application: app, name: "1.0.0")
Cd::VersionSetEntry.create!(organization: org, version_set: vset, version: version)

puts "organization_gid=#{org.to_global_id}"
puts "version_set_gid=#{vset.to_global_id}"
2. Run the CdRolloutCreate mutation (GraphQL Explorer at /-/graphql-explorer)
mutation {
  cdRolloutCreate(input: {
    organizationId: "<organization_gid from step 1>",
    versionSetId: "<version_set_gid from step 1>"
  }) {
    rollout {
      id
      iid
      state
    }
    errors
  }
}

Expected (verified live against a running GDK): the mutation succeeds and returns the created rollout. rollout_steps are not yet exposed in the API, so verify them separately via Rails console.

Sample response (cdRolloutCreate success):

{
  "data": {
    "cdRolloutCreate": {
      "rollout": {
        "id": "gid://gitlab/Cd::Rollout/78",
        "iid": 1,
        "state": "PENDING"
      },
      "errors": []
    }
  }
}

Sample response (rollout_steps, via rollout.rollout_steps.each { |s| puts [s.path, s.parent_path, s.step_type, s.state, s.name, s.params, s.rollout_environment_id].inspect } in a Rails console):

["0", nil, "com.gitlab.cd.steps.stage", "pending", "production", nil, nil]
["0.0", "0", "com.gitlab.cd.argo.rolling.deploy", "pending", nil, {"services"=>[{"name"=>"web"}]}, 66]
["1", nil, "com.gitlab.cd.steps.wait", "pending", nil, {"seconds"=>30}, nil]

The stage container has no environment link, its nested deploy step links to the rollout_environment it targets, the standalone wait step also has no environment link, and all three start pending.

References

https://gitlab.com/gitlab-org/gitlab/-/work_items/610547

Screenshots or screen recordings

No UI change - backend/data-model only.

How to set up and validate locally

  1. Run the seed script above in a Rails console.
  2. Run the cdRolloutCreate mutation in GraphQL Explorer and confirm the rollout is returned.
  3. In a Rails console, confirm rollout.rollout_steps.count matches the number of tree nodes in the flow definition and each step is pending.
  4. Note that flow-definition validation is unchanged: an invalid or environment-less flow definition still fails rollout creation the same pre-existing way.

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.

Merge request reports

Loading
Loading