neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Runners have an architecture; only x86_64 ones take bare self-hosted
cd6ad80 · Gabriel Morell · 2026-10-23 19:57
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/audit/catalog.ex
+5
−0
| 136 | 136 | "runner.registered" => %{category: "runners", target: "runner", keys: ~w(labels version)}, |
| 137 | 137 | "runner.created" => %{category: "runners", target: "runner", keys: ~w(labels)}, |
| 138 | 138 | "runner.deleted" => %{category: "runners", target: "runner", keys: ~w()}, |
| 139 | + "runner.architecture_changed" => %{ | |
| 140 | + category: "runners", | |
| 141 | + target: "runner", | |
| 142 | + keys: ~w(from to) | |
| 143 | + }, | |
| 139 | 144 | # Approving a fork's runs lets code nobody here wrote run on the |
| 140 | 145 | # repository's runners. |
| 141 | 146 | "ci.fork_runs_approved" => %{ |
modified
lib/git_gud/release.ex
+46
−0
| 66 | 66 | result |
| 67 | 67 | end |
| 68 | 68 | |
| 69 | + @doc """ | |
| 70 | + Set the architecture of the runner named `name` — the way to reach a | |
| 71 | + global runner, which no settings page manages. Only an x86_64 runner | |
| 72 | + takes `runs-on: self-hosted` work without listing the label. | |
| 73 | + | |
| 74 | + bin/git_gud eval 'GitGud.Release.set_runner_architecture("e52c", "aarch64")' | |
| 75 | + """ | |
| 76 | + def set_runner_architecture(name, architecture) | |
| 77 | + when is_binary(name) and is_binary(architecture) do | |
| 78 | + load_app() | |
| 79 | + | |
| 80 | + {:ok, result, _} = | |
| 81 | + Ecto.Migrator.with_repo(GitGud.Repo, fn repo -> | |
| 82 | + import Ecto.Query, warn: false | |
| 83 | + | |
| 84 | + case repo.all(from r in GitGud.Workflows.Runner, where: r.name == ^name) do | |
| 85 | + [runner] -> | |
| 86 | + runner | |
| 87 | + |> GitGud.Workflows.Runner.architecture_changeset(architecture) | |
| 88 | + |> repo.update() | |
| 89 | + | |
| 90 | + [] -> | |
| 91 | + :not_found | |
| 92 | + | |
| 93 | + _ -> | |
| 94 | + :ambiguous | |
| 95 | + end | |
| 96 | + end) | |
| 97 | + | |
| 98 | + case result do | |
| 99 | + {:ok, runner} -> | |
| 100 | + IO.puts("#{runner.name}: #{runner.architecture}") | |
| 101 | + | |
| 102 | + {:error, cs} -> | |
| 103 | + IO.puts("not changed: #{inspect(cs.errors)}") | |
| 104 | + | |
| 105 | + :not_found -> | |
| 106 | + IO.puts("no runner named: #{name}") | |
| 107 | + | |
| 108 | + :ambiguous -> | |
| 109 | + IO.puts("more than one runner is named #{name}; change it on its settings page") | |
| 110 | + end | |
| 111 | + | |
| 112 | + result | |
| 113 | + end | |
| 114 | + | |
| 69 | 115 | def rollback(repo, version) do |
| 70 | 116 | load_app() |
| 71 | 117 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) |
modified
lib/git_gud/workflows.ex
+10
−8
| 1488 | 1488 | def claim_job_by_handle(runner, _), do: claim_next_job(runner) |
| 1489 | 1489 | |
| 1490 | 1490 | defp do_claim_specific_job(%Runner{id: rid} = runner, job_id) do |
| 1491 | − match_labels = runner |> effective_labels() |> match_labels() | |
| 1491 | + match_labels = match_labels(runner, effective_labels(runner)) | |
| 1492 | 1492 | |
| 1493 | 1493 | Repo.transaction(fn -> |
| 1494 | 1494 | job = |
| 1534 | 1534 | # `passed_over` are jobs found at their `max-parallel` once locked |
| 1535 | 1535 | # (`max_parallel_slot?/1`), which the next attempt looks past. |
| 1536 | 1536 | defp do_claim_next_job(%Runner{id: rid} = runner, labels, passed_over \\ []) do |
| 1537 | − match_labels = match_labels(labels) | |
| 1537 | + match_labels = match_labels(runner, labels) | |
| 1538 | 1538 | |
| 1539 | 1539 | Repo.transaction(fn -> |
| 1540 | 1540 | job = |
| 1650 | 1650 | # `runs-on: [self-hosted, arm64]` reaches an arm64 machine and only |
| 1651 | 1651 | # an arm64 machine. |
| 1652 | 1652 | # |
| 1653 | − # `self-hosted` is added implicitly: `runs-on: self-hosted` is meant | |
| 1654 | − # to match any self-registered runner regardless of its other | |
| 1655 | − # labels, and everything this codebase mints is by definition | |
| 1656 | − # self-hosted. | |
| 1653 | + # A runner's architecture is a label too (`[self-hosted, aarch64]`). | |
| 1654 | + # `self-hosted` is implied for an x86_64 runner only: the workflows | |
| 1655 | + # that say `runs-on: self-hosted` were written when every runner was | |
| 1656 | + # x86_64, and some build images that deploy to x86_64 machines. Any | |
| 1657 | + # other runner takes `self-hosted` work only if its labels list it. | |
| 1657 | 1658 | # |
| 1658 | 1659 | # Built into jsonb on the SQL side from a text array rather than |
| 1659 | 1660 | # interpolated as a JSON string: a string parameter bound to a |
| 1660 | 1661 | # `::jsonb` placeholder is encoded as a JSON *string*, so containment |
| 1661 | 1662 | # compares an array against `"[\"a\",\"b\"]"` and quietly matches |
| 1662 | 1663 | # nothing. `to_jsonb(?::text[])` has one unambiguous reading. |
| 1663 | − defp match_labels(labels) do | |
| 1664 | − if "self-hosted" in labels, do: labels, else: ["self-hosted" | labels] | |
| 1664 | + defp match_labels(%Runner{architecture: arch}, labels) do | |
| 1665 | + implied = if arch == "x86_64", do: ["self-hosted", arch], else: [arch] | |
| 1666 | + Enum.uniq(implied ++ labels) | |
| 1665 | 1667 | end |
| 1666 | 1668 | |
| 1667 | 1669 | # A runner only claims jobs in its scope: |
modified
lib/git_gud/workflows/runner.ex
+16
−0
| 7 | 7 | alias GitGud.Repositories.Repository |
| 8 | 8 | |
| 9 | 9 | @valid_statuses ~w(active offline disabled) |
| 10 | + @architectures ~w(x86_64 aarch64) | |
| 10 | 11 | |
| 11 | 12 | schema "runners" do |
| 12 | 13 | field :uuid, Ecto.UUID |
| 20 | 21 | # `Declare`; we record but don't route on them. Surfaced in |
| 21 | 22 | # the UI for operator visibility. |
| 22 | 23 | field :agent_labels, :map, default: %{"labels" => []} |
| 24 | + # Operator-assigned, like `labels`. Only an x86_64 runner takes | |
| 25 | + # `runs-on: self-hosted` without carrying the label: every runner | |
| 26 | + # was x86_64 when that was decided, and the workflows that say | |
| 27 | + # `self-hosted` build x86_64 images. | |
| 28 | + field :architecture, :string, default: "x86_64" | |
| 23 | 29 | field :token_hash, :binary |
| 24 | 30 | field :last_seen_at, :utc_datetime |
| 25 | 31 | field :status, :string, default: "active" |
| 52 | 58 | ) |
| 53 | 59 | end |
| 54 | 60 | |
| 61 | + @doc "Set the runner's architecture." | |
| 62 | + def architecture_changeset(runner, architecture) do | |
| 63 | + runner | |
| 64 | + |> change(architecture: architecture) | |
| 65 | + |> validate_inclusion(:architecture, @architectures) | |
| 66 | + |> check_constraint(:architecture, name: :runners_architecture) | |
| 67 | + end | |
| 68 | + | |
| 69 | + def architectures, do: @architectures | |
| 70 | + | |
| 55 | 71 | @doc "Human label for the runner's scope." |
| 56 | 72 | def scope_label(%__MODULE__{repository_id: r}) when not is_nil(r), do: "repo" |
| 57 | 73 | def scope_label(%__MODULE__{organization_id: o}) when not is_nil(o), do: "org" |
modified
lib/git_gud/workflows/runners.ex
+9
−0
| 398 | 398 | """ |
| 399 | 399 | def delete_runner(%Runner{} = runner), do: Repo.delete(runner) |
| 400 | 400 | |
| 401 | + @doc """ | |
| 402 | + Set a runner's architecture (`Runner.architectures/0`). Only an | |
| 403 | + x86_64 runner takes `runs-on: self-hosted` work without listing the | |
| 404 | + label. Same authorization rule as `delete_runner/1`. | |
| 405 | + """ | |
| 406 | + def set_architecture(%Runner{} = runner, architecture) do | |
| 407 | + runner |> Runner.architecture_changeset(architecture) |> Repo.update() | |
| 408 | + end | |
| 409 | + | |
| 401 | 410 | @doc """ |
| 402 | 411 | List runners visible at `scope`. |
| 403 | 412 |
modified
lib/git_gud_web/components/audit_components.ex
+3
−0
Click to load diff…
modified
lib/git_gud_web/live/org_live/settings_runners.ex
+75
−12
Click to load diff…
modified
lib/git_gud_web/live/repo_live/settings_runners.ex
+75
−12
Click to load diff…
added
priv/repo/migrations/20261023195053_add_runner_architecture.exs
+20
−0
Click to load diff…
modified
test/git_gud/workflows/matrix_dispatch_test.exs
+85
−0
Click to load diff…
modified
test/git_gud_web/live/runner_audit_test.exs
+55
−0
Click to load diff…
Parents: 990181a