lib/openagents/deployments/run.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Deployments.Run do
  @moduledoc """
  The admitted, immutable execution of one deployment request.

  The run carries the lifecycle state, the durable policy explanation, the lease
  a worker holds while it executes, and the provider receipt. It is the only
  record a provider ever sees, and it holds secret references rather than secret
  values so a provider crash, an event stream, or an audit export cannot spill a
  tenant credential.
  """

  use Ecto.Schema

  import Ecto.Changeset

  @states ~w(requested checking waiting_for_approval queued deploying succeeded failed cancelled superseded)
  @terminal_states ~w(succeeded failed cancelled superseded)
  @active_states ~w(requested checking waiting_for_approval queued deploying)

  @type t :: %__MODULE__{}

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime_usec]

  schema "deployment_runs" do
    field :input_digest, :string
    field :state, :string, default: "requested"
    field :result_reason, :string
    field :provider, :string
    field :provider_receipt, :map, default: %{}
    field :policy_explanation, {:array, :map}, default: []
    field :attempt_count, :integer, default: 0
    field :lease_owner, :string
    field :lease_expires_at, :utc_datetime_usec
    field :cancel_requested_at, :utc_datetime_usec
    field :started_at, :utc_datetime_usec
    field :finished_at, :utc_datetime_usec
    field :superseded_by_run_id, :binary_id

    belongs_to :repository, OpenAgents.Repositories.Repository
    belongs_to :environment, OpenAgents.Deployments.Environment
    belongs_to :deployment_request, OpenAgents.Deployments.Request
    belongs_to :cancel_requested_by_user, OpenAgents.Accounts.User

    timestamps()
  end

  @doc false
  def changeset(run, attrs) do
    run
    |> cast(attrs, [
      :state,
      :result_reason,
      :provider_receipt,
      :policy_explanation,
      :attempt_count,
      :lease_owner,
      :lease_expires_at,
      :cancel_requested_at,
      :started_at,
      :finished_at,
      :superseded_by_run_id
    ])
    |> validate_required([:input_digest, :state, :provider])
    |> validate_inclusion(:state, @states)
    |> validate_length(:result_reason, max: 80)
    |> validate_length(:lease_owner, max: 120)
    |> validate_number(:attempt_count, greater_than_or_equal_to: 0)
    |> unique_constraint(:deployment_request_id)
  end

  @doc "Every legal lifecycle state."
  @spec states() :: [String.t()]
  def states, do: @states

  @doc "The states from which no further transition is legal."
  @spec terminal_states() :: [String.t()]
  def terminal_states, do: @terminal_states

  @doc "The states in which a run still holds or awaits work."
  @spec active_states() :: [String.t()]
  def active_states, do: @active_states

  @doc "Whether the run reached a terminal state."
  @spec terminal?(t()) :: boolean()
  def terminal?(%__MODULE__{state: state}), do: state in @terminal_states
end