defmodule OpenAgents.Forge.Assignment do
@moduledoc "A repository-scoped assignment of one issue to one target."
use Ecto.Schema
import Ecto.Changeset
@states ~w(admitted running completed failed cancelled)
@terminal_states ~w(completed failed cancelled)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@timestamps_opts [type: :utc_datetime_usec]
schema "forge_assignments" do
belongs_to :conversation, OpenAgents.Conversations.Conversation
belongs_to :conversation_box, OpenAgents.Box.ConversationBox
belongs_to :machine, OpenAgents.Machines.Machine
belongs_to :repository, OpenAgents.Repositories.Repository
belongs_to :issue, OpenAgents.Issues.Issue, type: :id
belongs_to :run, OpenAgents.Box.Run
belongs_to :work_job, OpenAgents.Work.Job
field :target_kind, :string, default: "box"
field :credential_delivery_status, :string, default: "not_applicable"
field :credential_delivery_reason, :string
field :requesting_principal, :map
field :branch, :string
field :state, :string, default: "admitted"
field :terminal_branch, :string
field :terminal_commit, :string
field :failure_reason, :string
field :deadline_at, :utc_datetime_usec
field :admitted_at, :utc_datetime_usec
field :started_at, :utc_datetime_usec
field :finished_at, :utc_datetime_usec
field :transparency_tier, :string, default: "ledger"
belongs_to :artifact_link, OpenAgents.Transparency.ArtifactLink
timestamps()
end
@type t :: %__MODULE__{}
def states, do: @states
def terminal_states, do: @terminal_states
def terminal?(%__MODULE__{state: state}), do: state in @terminal_states
def changeset(assignment, attrs) do
assignment
|> cast(attrs, [
:requesting_principal,
:branch,
:state,
:terminal_branch,
:terminal_commit,
:failure_reason,
:deadline_at,
:admitted_at,
:started_at,
:finished_at,
:run_id,
:work_job_id,
:conversation_id,
:target_kind,
:credential_delivery_status,
:credential_delivery_reason,
:transparency_tier,
:artifact_link_id
])
|> put_programmatic(attrs, :conversation_box_id)
|> put_programmatic(attrs, :machine_id)
|> put_programmatic(attrs, :repository_id)
|> put_programmatic(attrs, :issue_id)
|> put_programmatic(attrs, :conversation_id)
|> validate_required([
:repository_id,
:issue_id,
:requesting_principal,
:branch,
:deadline_at,
:admitted_at
])
|> validate_inclusion(:state, @states)
|> validate_inclusion(:transparency_tier, OpenAgents.Transparency.ArtifactLink.tiers())
|> validate_format(
:branch,
~r/\A(?![.-])(?!.*(?:\.\.|@\{|[ ~^:?*\[\\]))[^\s:]+(?<!\.)(?<!\/)(?<!\.lock)\z/
)
|> foreign_key_constraint(:conversation_box_id)
|> foreign_key_constraint(:conversation_id)
|> foreign_key_constraint(:machine_id)
|> foreign_key_constraint(:repository_id)
|> foreign_key_constraint(:issue_id)
|> foreign_key_constraint(:run_id)
|> foreign_key_constraint(:work_job_id)
|> unique_constraint(:conversation_box_id, name: :forge_assignments_one_active_box_index)
|> unique_constraint(:machine_id, name: :forge_assignments_one_active_machine_index)
|> unique_constraint(:issue_id, name: :forge_assignments_one_active_issue_index)
|> validate_target()
end
defp validate_target(changeset) do
case get_field(changeset, :target_kind) do
"box" ->
if get_field(changeset, :conversation_box_id),
do: changeset,
else: add_error(changeset, :conversation_box_id, "can't be blank")
"computer" ->
if get_field(changeset, :machine_id),
do: changeset,
else: add_error(changeset, :machine_id, "can't be blank")
_ ->
changeset
end
end
defp put_programmatic(changeset, attrs, field) do
case Map.fetch(attrs, field) do
{:ok, value} -> put_change(changeset, field, value)
:error -> changeset
end
end
end