3.2 KiB · text 5af55d9
defmodule GitGud.Workflows.WorkflowJob do
use Ecto.Schema
import Ecto.Changeset
alias GitGud.Workflows.Runner
alias GitGud.Workflows.WorkflowRun
alias GitGud.Workflows.WorkflowStep
# `blocked` = waiting on upstream `needs:`. Promoted to `queued` (or
# `cancelled`) by `Workflows.resolve_dependents/1` when any sibling
# job completes. `claim_next_job/1` only picks `queued`, so blocked
# jobs are never accidentally claimed by a runner before their
# prerequisites finish.
# `skipped` = upstream that the GitHub-Actions `if:` evaluated false
# for. Treated as success for the purpose of resolving downstream
# `needs:` (matches GitHub semantics).
@valid_statuses ~w(queued running success failure cancelled blocked skipped)
schema "workflow_jobs" do
field :job_id, :string
field :name, :string
field :runs_on, :map
field :needs, {:array, :string}, default: []
field :status, :string, default: "queued"
field :conclusion, :string
field :started_at, :utc_datetime
field :completed_at, :utc_datetime
field :definition, :map, default: %{}
# Populated by `update_task/2` from `UpdateTaskRequest.outputs`.
# Downstream jobs in the same run read this via
# `${{ needs.<id>.outputs.<k> }}` — surfaced in their Task.needs
# by `build_task/1`.
field :outputs, :map, default: %{}
# Snapshot of upstream `needs:` results/outputs taken when this job is
# promoted from `blocked` -> `queued` (upstream then terminal). Read by
# `build_needs/1`; avoids a racy re-query of upstream status at fetch.
# Shape: %{"<need_job_id>" => %{"status" => "...", "outputs" => %{}}}.
field :needs_snapshot, :map, default: %{}
belongs_to :workflow_run, WorkflowRun
belongs_to :runner, Runner
has_many :steps, WorkflowStep
timestamps(type: :utc_datetime)
end
def create_changeset(job, attrs) do
job
|> cast(attrs, [:job_id, :name, :runs_on, :needs, :definition])
|> validate_required([:job_id, :definition])
end
def status_changeset(job, status, opts \\ []) when status in @valid_statuses do
now = DateTime.utc_now(:second)
fields = %{status: status}
fields =
cond do
status == "running" ->
Map.put(fields, :started_at, now)
status in ~w(success failure cancelled skipped) ->
fields
|> Map.put(:completed_at, now)
|> Map.put(:conclusion, Keyword.get(opts, :conclusion, status))
true ->
fields
end
change(job, fields)
end
@doc "Terminal statuses — job won't transition out of these."
def terminal?(s) when s in ~w(success failure cancelled skipped), do: true
def terminal?(_), do: false
@doc """
Did this job's terminal state count as 'satisfying' a downstream
`needs:` declaration? Mirrors GitHub Actions semantics: a need is
satisfied by `success` or `skipped`, never by `failure` or
`cancelled` (the downstream gets cascade-cancelled instead).
"""
def satisfies_dependency?(s) when s in ~w(success skipped), do: true
def satisfies_dependency?(_), do: false
def claim_changeset(job, runner_id) do
change(job, status: "running", runner_id: runner_id, started_at: DateTime.utc_now(:second))
end
def valid_statuses, do: @valid_statuses
end