lib/openagents/deployments/request.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Deployments.Request do
  @moduledoc """
  One durable intent to deploy exact bytes to one environment.

  A request records what was asked for and who asked: the exact commit, the
  exact artifact digest, the source ref and workflow the intent came from, the
  principal kind, and the idempotency key the caller spent. It never records a
  decision — admission lives on the run — so a replayed key can be answered from
  the request alone.
  """

  use Ecto.Schema

  import Ecto.Changeset

  @principal_types ~w(user workflow operator)
  @commit_pattern ~r/\A[0-9a-f]{40}\z/
  @artifact_pattern ~r/\A[a-z0-9]+:[0-9a-f]{32,89}\z/
  @ref_pattern ~r/\A(?:refs\/(?:heads|tags)\/)[\x21-\x7e]{1,240}\z/

  @type t :: %__MODULE__{}

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

  schema "deployment_requests" do
    field :commit_sha, :string
    field :artifact_digest, :string
    field :artifact_created_at, :utc_datetime_usec
    field :source_ref, :string
    field :source_workflow, :string
    field :principal_type, :string
    field :requested_by_grant_id, :binary_id
    field :idempotency_key, :string
    field :request_digest, :string
    field :input_digest, :string
    field :requested_at, :utc_datetime_usec

    belongs_to :repository, OpenAgents.Repositories.Repository
    belongs_to :environment, OpenAgents.Deployments.Environment
    belongs_to :requested_by_user, OpenAgents.Accounts.User
    has_one :run, OpenAgents.Deployments.Run, foreign_key: :deployment_request_id

    timestamps()
  end

  @doc false
  def changeset(request, attrs) do
    request
    |> cast(attrs, [
      :commit_sha,
      :artifact_digest,
      :artifact_created_at,
      :source_ref,
      :source_workflow,
      :idempotency_key
    ])
    |> validate_required([
      :commit_sha,
      :artifact_digest,
      :source_ref,
      :idempotency_key,
      :principal_type,
      :request_digest,
      :input_digest,
      :requested_at
    ])
    |> update_change(:commit_sha, &String.downcase/1)
    |> validate_format(:commit_sha, @commit_pattern)
    |> validate_format(:artifact_digest, @artifact_pattern)
    |> validate_format(:source_ref, @ref_pattern)
    |> validate_length(:source_workflow, min: 1, max: 120)
    |> validate_length(:idempotency_key, min: 8, max: 255)
    |> validate_inclusion(:principal_type, @principal_types)
    |> unique_constraint(:idempotency_key,
      name: :deployment_requests_idempotency_index
    )
    |> foreign_key_constraint(:repository_id)
    |> foreign_key_constraint(:environment_id)
  end

  @doc "The principal kinds that can hold a deployment intent."
  @spec principal_types() :: [String.t()]
  def principal_types, do: @principal_types
end