Slice per-step logs from the job's flat stream (log_index/log_length)
268bd5d · gmorell · 2026-06-25 19:57
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/workflows.ex
+46
−2
@@ -543,16 +543,48 @@ defmodule GitGud.Workflows do
| 543 | 543 | Persisted log rows for a step, ascending by sequence. Hydrates the log |
| 544 | 544 | view when a step is opened (live appends arrive separately via PubSub). |
| 545 | 545 | Returns `%{seq, body}` maps — the shape the log stream renders. |
| 546 | + | |
| 547 | + The runner streams one flat log per job (stored on the first step, see | |
| 548 | + `ensure_log_step/1`); each step owns the slice `[log_index, log_index + | |
| 549 | + log_length)` of it. When those offsets are known we return that slice; | |
| 550 | + otherwise we fall back to rows stored directly under this step (the | |
| 551 | + whole stream for the holder step, empty for the rest — the old | |
| 552 | + behavior, e.g. while a step is still running). | |
| 546 | 553 | """ |
| 547 | − def list_step_logs(step_id) when is_integer(step_id) do | |
| 554 | + def list_step_logs(%WorkflowStep{workflow_job_id: jid, log_index: idx, log_length: len}) | |
| 555 | + when is_integer(idx) and is_integer(len) and len > 0 do | |
| 556 | + holder_id = log_holder_id(jid) | |
| 557 | + upper = idx + len | |
| 558 | + | |
| 559 | + from(l in "workflow_step_logs", | |
| 560 | + where: l.workflow_step_id == ^holder_id and l.seq >= ^idx and l.seq < ^upper, | |
| 561 | + order_by: [asc: l.seq], | |
| 562 | + select: %{seq: l.seq, body: l.body} | |
| 563 | + ) | |
| 564 | + |> Repo.all() | |
| 565 | + end | |
| 566 | + | |
| 567 | + def list_step_logs(%WorkflowStep{id: id}) do | |
| 548 | 568 | from(l in "workflow_step_logs", |
| 549 | − where: l.workflow_step_id == ^step_id, | |
| 569 | + where: l.workflow_step_id == ^id, | |
| 550 | 570 | order_by: [asc: l.seq], |
| 551 | 571 | select: %{seq: l.seq, body: l.body} |
| 552 | 572 | ) |
| 553 | 573 | |> Repo.all() |
| 554 | 574 | end |
| 555 | 575 | |
| 576 | + # Step that physically holds the job's flat log stream — the lowest | |
| 577 | + # `number`, matching `ensure_log_step/1`. | |
| 578 | + defp log_holder_id(job_id) do | |
| 579 | + Repo.one( | |
| 580 | + from s in WorkflowStep, | |
| 581 | + where: s.workflow_job_id == ^job_id, | |
| 582 | + order_by: [asc: s.number], | |
| 583 | + limit: 1, | |
| 584 | + select: s.id | |
| 585 | + ) | |
| 586 | + end | |
| 587 | + | |
| 556 | 588 | defp next_log_seq(step_id) do |
| 557 | 589 | max_seq = |
| 558 | 590 | Repo.one( |
@@ -729,6 +761,7 @@ defmodule GitGud.Workflows do
| 729 | 761 | |> maybe_put(:conclusion, step_status_conclusion(status)) |
| 730 | 762 | |> maybe_put(:started_at, timestamp_to_dt(st.started_at)) |
| 731 | 763 | |> maybe_put(:completed_at, timestamp_to_dt(st.stopped_at)) |
| 764 | + |> maybe_put_log_offsets(st) | |
| 732 | 765 | |
| 733 | 766 | {:ok, updated} = |
| 734 | 767 | step |
@@ -744,6 +777,17 @@ defmodule GitGud.Workflows do
| 744 | 777 | |
| 745 | 778 | def apply_step_states(_job, _), do: :ok |
| 746 | 779 | |
| 780 | + # The runner reports each step's slice of the job's flat log as | |
| 781 | + # [log_index, log_index + log_length). Only persist when the step | |
| 782 | + # actually has lines, so a 0-length intermediate update doesn't clobber | |
| 783 | + # offsets set by an earlier one. | |
| 784 | + defp maybe_put_log_offsets(changes, %{log_length: len, log_index: idx}) | |
| 785 | + when is_integer(len) and len > 0 and is_integer(idx) do | |
| 786 | + changes |> Map.put(:log_index, idx) |> Map.put(:log_length, len) | |
| 787 | + end | |
| 788 | + | |
| 789 | + defp maybe_put_log_offsets(changes, _), do: changes | |
| 790 | + | |
| 747 | 791 | defp step_result_to_status(:RESULT_SUCCESS), do: "success" |
| 748 | 792 | defp step_result_to_status(:RESULT_FAILURE), do: "failure" |
| 749 | 793 | defp step_result_to_status(:RESULT_CANCELLED), do: "cancelled" |
modified
lib/git_gud/workflows/workflow_step.ex
+4
−0
@@ -13,6 +13,10 @@ defmodule GitGud.Workflows.WorkflowStep do
| 13 | 13 | field :conclusion, :string |
| 14 | 14 | field :started_at, :utc_datetime |
| 15 | 15 | field :completed_at, :utc_datetime |
| 16 | + # Offsets into the job's flat log stream (stored on the first step): | |
| 17 | + # this step's lines are [log_index, log_index + log_length). | |
| 18 | + field :log_index, :integer | |
| 19 | + field :log_length, :integer | |
| 16 | 20 | |
| 17 | 21 | belongs_to :workflow_job, WorkflowJob |
| 18 | 22 |
modified
lib/git_gud_web/live/workflow_live/show.ex
+9
−1
@@ -101,7 +101,11 @@ defmodule GitGudWeb.WorkflowLive.Show do
| 101 | 101 | # finished (or reloaded) run shows nothing until we load them here. |
| 102 | 102 | # `reset: true` replaces the stream with the authoritative set; |
| 103 | 103 | # dom_id keys on `seq` so any in-flight live appends dedupe. |
| 104 | − logs = Workflows.list_step_logs(id) | |
| 104 | + logs = | |
| 105 | + case find_step(socket.assigns.run, id) do | |
| 106 | + %{} = step -> Workflows.list_step_logs(step) | |
| 107 | + _ -> [] | |
| 108 | + end | |
| 105 | 109 | |
| 106 | 110 | {:noreply, |
| 107 | 111 | socket |
@@ -110,6 +114,10 @@ defmodule GitGudWeb.WorkflowLive.Show do
| 110 | 114 | end |
| 111 | 115 | end |
| 112 | 116 | |
| 117 | + defp find_step(run, id) do | |
| 118 | + Enum.find_value(run.jobs, fn job -> Enum.find(job.steps, &(&1.id == id)) end) | |
| 119 | + end | |
| 120 | + | |
| 113 | 121 | def handle_event("rerun", _params, socket) do |
| 114 | 122 | cond do |
| 115 | 123 | not socket.assigns.can_admin? -> |
added
priv/repo/migrations/20260625000000_add_step_log_offsets.exs
+14
−0
@@ -0,0 +1,14 @@
| 1 | +defmodule GitGud.Repo.Migrations.AddStepLogOffsets do | |
| 2 | + use Ecto.Migration | |
| 3 | + | |
| 4 | + # The runner streams one flat log per job (UpdateLog) and reports each | |
| 5 | + # step's slice as [log_index, log_index + log_length) via StepState. | |
| 6 | + # Persist those offsets so the run view can show per-step logs instead | |
| 7 | + # of dumping the whole stream onto the first step. | |
| 8 | + def change do | |
| 9 | + alter table(:workflow_steps) do | |
| 10 | + add :log_index, :integer | |
| 11 | + add :log_length, :integer | |
| 12 | + end | |
| 13 | + end | |
| 14 | +end |
Parents: eaaaea6