tweaks
296556b · gmorell · 2026-05-22 18:40
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 timings — and 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