tweaks

296556b · gmorell · 2026-05-22 18:40

2 files +99 -0

Files changed

modified lib/git_gud/workflows.ex
+92 −0
@@ -617,6 +617,82 @@ defmodule GitGud.Workflows do
617 617 |> Repo.update()
618 618 end
619 619
620 + @doc """
621 + Apply runner-reported step states to our WorkflowStep rows.
622 +
623 + The runner sends a `StepState` per step in `TaskState.steps` —
624 + identified by a 0-based `id` (its execution-order index). We
625 + create steps with `number = 1..n` at dispatch time, so the
626 + mapping is `runner_id + 1 == our number`. (Some runner versions
627 + use 1-based; we also try a direct `number == runner_id` match.)
628 +
629 + Per-step we update status (from the `Result` enum), conclusion,
630 + and timingsand broadcast each change so the workflow LiveView
631 + can light up steps in real time as the runner progresses.
632 + """
633 + def apply_step_states(%WorkflowJob{id: job_id}, step_states) when is_list(step_states) do
634 + steps =
635 + from(s in WorkflowStep, where: s.workflow_job_id == ^job_id)
636 + |> Repo.all()
637 +
638 + by_number = Map.new(steps, fn s -> {s.number, s} end)
639 +
640 + Enum.each(step_states, fn st ->
641 + target = Map.get(by_number, st.id + 1) || Map.get(by_number, st.id)
642 +
643 + case target do
644 + nil ->
645 + :ok
646 +
647 + %WorkflowStep{} = step ->
648 + status = step_result_to_status(st.result)
649 +
650 + changes =
651 + %{status: status}
652 + |> maybe_put(:conclusion, step_status_conclusion(status))
653 + |> maybe_put(:started_at, timestamp_to_dt(st.started_at))
654 + |> maybe_put(:completed_at, timestamp_to_dt(st.stopped_at))
655 +
656 + {:ok, updated} =
657 + step
658 + |> Ecto.Changeset.change(changes)
659 + |> Repo.update()
660 +
661 + broadcast({:step_status, job_id, updated})
662 + end
663 + end)
664 +
665 + :ok
666 + end
667 +
668 + def apply_step_states(_job, _), do: :ok
669 +
670 + defp step_result_to_status(:RESULT_SUCCESS), do: "success"
671 + defp step_result_to_status(:RESULT_FAILURE), do: "failure"
672 + defp step_result_to_status(:RESULT_CANCELLED), do: "cancelled"
673 + defp step_result_to_status(:RESULT_SKIPPED), do: "skipped"
674 + # `RESULT_UNSPECIFIED` with a `started_at` timestamp means the
675 + # step is currently running.
676 + defp step_result_to_status(_), do: "running"
677 +
678 + defp step_status_conclusion(status) when status in ~w(success failure cancelled skipped),
679 + do: status
680 +
681 + defp step_status_conclusion(_), do: nil
682 +
683 + defp maybe_put(map, _key, nil), do: map
684 + defp maybe_put(map, key, value), do: Map.put(map, key, value)
685 +
686 + defp timestamp_to_dt(nil), do: nil
687 +
688 + defp timestamp_to_dt(%Google.Protobuf.Timestamp{seconds: 0, nanos: 0}), do: nil
689 +
690 + defp timestamp_to_dt(%Google.Protobuf.Timestamp{seconds: secs}) when is_integer(secs) do
691 + DateTime.from_unix!(secs) |> DateTime.truncate(:second)
692 + end
693 +
694 + defp timestamp_to_dt(_), do: nil
695 +
620 696 def update_job_status(%WorkflowJob{} = job, status, opts \\ []) do
621 697 {:ok, updated} =
622 698 job |> WorkflowJob.status_changeset(status, opts) |> Repo.update()
@@ -860,4 +936,20 @@ defmodule GitGud.Workflows do
860 936 {:workflow_job_status, job}
861 937 )
862 938 end
939 +
940 + defp broadcast({:step_status, job_id, step}) do
941 + # Re-derive the parent run id so subscribers (the workflow LV
942 + # which subscribes per-run) get the update on the right topic.
943 + case Repo.get(WorkflowJob, job_id) do
944 + %WorkflowJob{workflow_run_id: rid} ->
945 + Phoenix.PubSub.broadcast(
946 + GitGud.PubSub,
947 + topic_for_run(rid),
948 + {:workflow_step_status, step}
949 + )
950 +
951 + _ ->
952 + :ok
953 + end
954 + end
863 955 end
modified lib/git_gud_web/controllers/runner_controller.ex
+7 −0
@@ -358,6 +358,13 @@ defmodule GitGudWeb.RunnerController do
358 358 job ->
359 359 mapped = map_result(state && state.result)
360 360
361 + # Per-step status BEFORE the job-level transition. We get
362 + # one row in state.steps per declared step with its
363 + # individual result + timing; without this, WorkflowStep
364 + # rows stay forever in `pending`.
365 + if state && is_list(state.steps),
366 + do: Workflows.apply_step_states(job, state.steps)
367 +
361 368 # Persist runner-reported outputs before we transition status —
362 369 # downstream `resolve_dependents/1` may pick up dependents
363 370 # that read them via `needs.<id>.outputs.<k>`.

Parents: 499c875