neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Audit: runners and fork-run approvals
5db6178 · Gabriel Morell · 2026-10-22 16:34
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/audit.ex
+20
−0
| 224 | 224 | } |
| 225 | 225 | end |
| 226 | 226 | |
| 227 | + @doc """ | |
| 228 | + Record approving or rejecting a pull request's fork runs, once it's | |
| 229 | + done and only if there were runs. After the fact rather than in a | |
| 230 | + transaction: each run is approved or cancelled in its own, and | |
| 231 | + runners are told as each one commits. | |
| 232 | + """ | |
| 233 | + @spec fork_runs(actor(), :approved | :rejected, struct(), list()) :: Entry.t() | nil | |
| 234 | + def fork_runs(_actor, _verdict, _pr, []), do: nil | |
| 235 | + | |
| 236 | + def fork_runs(actor, verdict, pr, runs) do | |
| 237 | + repo = Repo.get!(Repository, pr.repository_id) | |
| 238 | + | |
| 239 | + record(actor, "ci.fork_runs_#{verdict}", %{ | |
| 240 | + repository: repo, | |
| 241 | + target_id: pr.id, | |
| 242 | + target_label: "##{pr.number}", | |
| 243 | + metadata: %{pull_request: pr.number, runs: length(runs)} | |
| 244 | + }) | |
| 245 | + end | |
| 246 | + | |
| 227 | 247 | # ── reading ────────────────────────────────────────────────────────── |
| 228 | 248 | |
| 229 | 249 | @default_limit 50 |
modified
lib/git_gud/audit/actor.ex
+6
−1
| 34 | 34 | def job(%{id: job_id, job_id: name}, %{number: number}), |
| 35 | 35 | do: %__MODULE__{type: "job", ref: job_id, label: "run ##{number} / job #{name}", via: "job"} |
| 36 | 36 | |
| 37 | − @doc "The forge itself: a cron job, an import, an incoming webhook." | |
| 37 | + @doc """ | |
| 38 | + The forge itself: a cron job, an import, an incoming webhook — or an | |
| 39 | + operator at the console, running a mix task or release command. | |
| 40 | + """ | |
| 41 | + def system(:console), do: %__MODULE__{type: "system", label: "console"} | |
| 42 | + | |
| 38 | 43 | def system(kind) when kind in [:cron, :import, :webhook], |
| 39 | 44 | do: %__MODULE__{ |
| 40 | 45 | type: "system", |
modified
lib/git_gud/audit/catalog.ex
+21
−0
| 127 | 127 | target: "job_token_grant", |
| 128 | 128 | keys: ~w() |
| 129 | 129 | }, |
| 130 | + # Runners: never a registration token or a runner's own token. | |
| 131 | + "runner.registration_token_minted" => %{ | |
| 132 | + category: "runners", | |
| 133 | + target: nil, | |
| 134 | + keys: ~w(expires_at) | |
| 135 | + }, | |
| 136 | + "runner.registered" => %{category: "runners", target: "runner", keys: ~w(labels version)}, | |
| 137 | + "runner.created" => %{category: "runners", target: "runner", keys: ~w(labels)}, | |
| 138 | + "runner.deleted" => %{category: "runners", target: "runner", keys: ~w()}, | |
| 139 | + # Approving a fork's runs lets code nobody here wrote run on the | |
| 140 | + # repository's runners. | |
| 141 | + "ci.fork_runs_approved" => %{ | |
| 142 | + category: "ci", | |
| 143 | + target: "pull_request", | |
| 144 | + keys: ~w(pull_request runs) | |
| 145 | + }, | |
| 146 | + "ci.fork_runs_rejected" => %{ | |
| 147 | + category: "ci", | |
| 148 | + target: "pull_request", | |
| 149 | + keys: ~w(pull_request runs) | |
| 150 | + }, | |
| 130 | 151 | # The pruner's own trace: how much went, from before when. |
| 131 | 152 | "audit.pruned" => %{category: "instance", target: nil, keys: ~w(count cutoff)} |
| 132 | 153 | } |
modified
lib/git_gud/workflows/runners.ex
+37
−5
| 87 | 87 | Redeem `token` and register a runner with `attrs` in its scope, all or |
| 88 | 88 | nothing. `{:ok, runner, bearer}`, `:error` for a bad token, or |
| 89 | 89 | `{:error, changeset}`. |
| 90 | + | |
| 91 | + `audit: ip` records `runner.registered` in the same transaction, with | |
| 92 | + the new runner as the actor and `ip` as its address. Here rather than | |
| 93 | + at the call site: the actor doesn't exist until the runner does, and | |
| 94 | + only this transaction knows which scope the token was for. | |
| 90 | 95 | """ |
| 91 | − def register_with_token(token, attrs) do | |
| 96 | + def register_with_token(token, attrs, opts \\ []) do | |
| 92 | 97 | Repo.transact(fn -> |
| 93 | 98 | with {:ok, scope} <- consume_registration_token(token), |
| 94 | 99 | {:ok, runner, bearer} <- register(attrs, scope) do |
| 95 | − {:ok, {runner, bearer}} | |
| 100 | + entry = | |
| 101 | + if Keyword.has_key?(opts, :audit) do | |
| 102 | + GitGud.Audit.insert!( | |
| 103 | + GitGud.Audit.Actor.runner(runner, opts[:audit]), | |
| 104 | + "runner.registered", | |
| 105 | + Map.merge(audit_scope(scope), %{ | |
| 106 | + target_id: runner.id, | |
| 107 | + metadata: %{labels: runner_labels(runner), version: runner.version} | |
| 108 | + }) | |
| 109 | + ) | |
| 110 | + end | |
| 111 | + | |
| 112 | + {:ok, {runner, bearer, entry}} | |
| 96 | 113 | else |
| 97 | 114 | :error -> {:error, :bad_token} |
| 98 | 115 | {:error, _} = err -> err |
| 99 | 116 | end |
| 100 | 117 | end) |
| 101 | 118 | |> case do |
| 102 | − {:ok, {runner, bearer}} -> {:ok, runner, bearer} | |
| 103 | − {:error, :bad_token} -> :error | |
| 104 | − {:error, _} = err -> err | |
| 119 | + {:ok, {runner, bearer, entry}} -> | |
| 120 | + if entry, do: GitGud.Audit.notice(entry) | |
| 121 | + {:ok, runner, bearer} | |
| 122 | + | |
| 123 | + {:error, :bad_token} -> | |
| 124 | + :error | |
| 125 | + | |
| 126 | + {:error, _} = err -> | |
| 127 | + err | |
| 105 | 128 | end |
| 106 | 129 | end |
| 107 | 130 |
| 203 | 226 | end |
| 204 | 227 | end |
| 205 | 228 | |
| 229 | + @doc "Where a runner of `scope` belongs in the audit log." | |
| 230 | + def audit_scope(%Repository{} = repo), do: %{repository: repo} | |
| 231 | + def audit_scope(%Organization{} = org), do: %{organization: org} | |
| 232 | + def audit_scope(_global), do: %{} | |
| 233 | + | |
| 234 | + @doc "A runner's advertised labels, as a list." | |
| 235 | + def runner_labels(%Runner{labels: %{"labels" => labels}}) when is_list(labels), do: labels | |
| 236 | + def runner_labels(_), do: [] | |
| 237 | + | |
| 206 | 238 | defp scope_attrs(:global), do: %{} |
| 207 | 239 | defp scope_attrs(%Repository{id: id}), do: %{repository_id: id} |
| 208 | 240 | defp scope_attrs(%Organization{id: id}), do: %{organization_id: id} |
modified
lib/git_gud_web/components/audit_components.ex
+21
−0
| 136 | 136 | "job_token_grant.deleted" -> |
| 137 | 137 | "removed job-token access for" |
| 138 | 138 | |
| 139 | + "runner.registration_token_minted" -> | |
| 140 | + "minted a runner registration token" | |
| 141 | + | |
| 142 | + "runner.registered" -> | |
| 143 | + "registered" | |
| 144 | + | |
| 145 | + "runner.created" -> | |
| 146 | + "created the runner" | |
| 147 | + | |
| 148 | + "runner.deleted" -> | |
| 149 | + "deleted the runner" | |
| 150 | + | |
| 151 | + "ci.fork_runs_approved" -> | |
| 152 | + fork_runs("approved", entry.metadata) | |
| 153 | + | |
| 154 | + "ci.fork_runs_rejected" -> | |
| 155 | + fork_runs("rejected", entry.metadata) | |
| 156 | + | |
| 139 | 157 | "audit.pruned" -> |
| 140 | 158 | "pruned #{entry.metadata["count"]} audit entries" |
| 141 | 159 |
| 179 | 197 | defp joined(values) when is_list(values), do: Enum.join(values, ", ") |
| 180 | 198 | defp joined(value), do: to_string(value) |
| 181 | 199 | |
| 200 | + defp fork_runs(verb, metadata), | |
| 201 | + do: "#{verb} #{metadata["runs"]} fork run(s) on pull request ##{metadata["pull_request"]}" | |
| 202 | + | |
| 182 | 203 | defp keys(metadata), do: metadata |> Map.keys() |> Enum.sort() |> Enum.join(", ") |
| 183 | 204 | |
| 184 | 205 | defp from_to(metadata), do: "(#{metadata["from"]} → #{metadata["to"]})" |
modified
lib/git_gud_web/controllers/api/mobile/pull_request_controller.ex
+12
−3
Click to load diff…
modified
lib/git_gud_web/controllers/runner_controller.ex
+1
−1
Click to load diff…
modified
lib/git_gud_web/live/org_live/settings_runners.ex
+41
−6
Click to load diff…
modified
lib/git_gud_web/live/pr_live/show.ex
+4
−0
Click to load diff…
modified
lib/git_gud_web/live/repo_live/settings_runners.ex
+41
−6
Click to load diff…
modified
lib/mix/tasks/gitgud.runner.create.ex
+23
−4
Click to load diff…
modified
test/git_gud/workflows/fork_pull_request_test.exs
+5
−0
Click to load diff…
modified
test/git_gud_web/controllers/runner_controller_test.exs
+9
−0
Click to load diff…
added
test/git_gud_web/live/runner_audit_test.exs
+76
−0
Click to load diff…
Parents: d423d5a