Fix 500 on the run page for anyone who isn't an admin

570765b · Gabriel Morell · 2026-09-12 05:21

14 files +98 -7
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud_web/live/org_live/actions.ex
+1 −1
@@ -30,7 +30,7 @@ defmodule GitGudWeb.OrgLive.Actions do
30 30 socket
31 31 |> assign(:org, org)
32 32 |> assign(:viewer, user)
33 − |> assign(:is_admin?, user && Organizations.org_admin?(org, user))
33 + |> assign(:is_admin?, !!(user && Organizations.org_admin?(org, user)))
34 34 |> assign(:repo_ids, repo_ids)
35 35 |> assign(:filter, :active)
36 36 |> assign(:page_title, "CI — @#{org.handle}")
modified lib/git_gud_web/live/org_live/show.ex
+2 −2
@@ -31,8 +31,8 @@ defmodule GitGudWeb.OrgLive.Show do
31 31 socket
32 32 |> assign(:org, org)
33 33 |> assign(:viewer, user)
34 − |> assign(:is_admin?, user && Organizations.org_admin?(org, user))
35 − |> assign(:is_member?, user && Organizations.member?(org, user))
34 + |> assign(:is_admin?, !!(user && Organizations.org_admin?(org, user)))
35 + |> assign(:is_member?, !!(user && Organizations.member?(org, user)))
36 36 |> assign(:teams, Organizations.list_teams(org))
37 37 |> assign(:repos, repos)
38 38 |> assign(:ci_status, Workflows.latest_run_status_by_repo(Enum.map(repos, & &1.id)))
modified lib/git_gud_web/live/repo_live/header.ex
+5 −1
@@ -179,7 +179,11 @@ defmodule GitGudWeb.RepoLive.Header do
179 179
180 180 socket
181 181 |> Phoenix.Component.assign(:has_packages?, Packages.list_packages(repo) != [])
182 − |> Phoenix.Component.assign(:can_admin?, user && admin?(user, repo))
182 + # `!!` is load-bearing: `user && admin?(...)` is nil for a signed-out
183 + # visitor, and a page that writes `@can_admin? and ...` then raises
184 + # BadBooleanError for exactly the people who are allowed to look but
185 + # not to touch. The attr is declared boolean; assign one.
186 + |> Phoenix.Component.assign(:can_admin?, !!(user && admin?(user, repo)))
183 187 |> Phoenix.Component.assign(:latest_run, Workflows.latest_run(repo))
184 188 |> Phoenix.Component.assign(:open_pulls, PullRequests.count_open(repo))
185 189 |> Phoenix.Component.assign(:repo_tags, GitGud.Tags.tag_names(repo))
modified lib/git_gud_web/live/workflow_live/show.ex
+6 −3
@@ -27,7 +27,10 @@ defmodule GitGudWeb.WorkflowLive.Show do
27 27 |> assign(:run, run)
28 28 |> assign(:artifacts, Workflows.list_artifacts(run))
29 29 |> assign(:expanded_step, nil)
30 − |> assign(:can_admin?, user && admin?(user, repo))
30 + # Boolean, not `nil` — this page writes `@can_admin? and ...`,
31 + # which raises BadBooleanError on a nil. See
32 + # `RepoLive.Header.assign_chrome/2`.
33 + |> assign(:can_admin?, !!(user && admin?(user, repo)))
31 34 |> assign(:page_title, "Run ##{run.number}")
32 35 |> assign(:now, DateTime.utc_now())
33 36 |> assign(:ticking?, false)
@@ -391,8 +394,8 @@ defmodule GitGudWeb.WorkflowLive.Show do
391 394 <span class="font-mono">{a.name}</span>
392 395 <span class="text-xs opacity-60">
393 396 {format_size(a.size)}
394 − <span :if={a.content_type}> · {a.content_type}</span>
395 − <span :if={a.inserted_at}> · {format_dt(a.inserted_at)}</span>
397 + <span :if={a.content_type}> · {a.content_type}</span>
398 + <span :if={a.inserted_at}> · {format_dt(a.inserted_at)}</span>
396 399 </span>
397 400 </div>
398 401 <a
added test/git_gud_web/live/workflow_run_anonymous_test.exs
+84 −0
@@ -0,0 +1,84 @@
1 +defmodule GitGudWeb.WorkflowRunAnonymousTest do
2 + @moduledoc """
3 + A signed-out visitor loading a public repository's run page.
4 +
5 + This 500'd in production: `assign(:can_admin?, user && admin?(...))`
6 + is `nil` — not `false` — with no user, and the page writes
7 + `@can_admin? and job.status in [...]`, which raises BadBooleanError on
8 + a nil left-hand side. The people who could see it were exactly the
9 + people who couldn't log in.
10 + """
11 + use GitGudWeb.ConnCase, async: false
12 +
13 + import Phoenix.LiveViewTest
14 + import GitGud.ForgeFixtures
15 +
16 + alias GitGud.Repo
17 + alias GitGud.Repositories.Storage
18 + alias GitGud.Workflows.WorkflowJob
19 + alias GitGud.Workflows.WorkflowRun
20 +
21 + setup do
22 + {user, repo} = repository_fixture(%{visibility: "public"})
23 + repo = Repo.preload(repo, [:owner, :organization])
24 +
25 + run =
26 + %WorkflowRun{repository_id: repo.id}
27 + |> WorkflowRun.create_changeset(%{
28 + "number" => 1,
29 + "workflow_path" => ".forgejo/workflows/build.yml",
30 + "workflow_name" => "build",
31 + "trigger" => "push",
32 + "head_ref" => "refs/heads/main",
33 + "head_sha" => :crypto.strong_rand_bytes(20),
34 + "workflow_doc" => %{"name" => "build", "jobs" => %{"build" => %{"steps" => []}}}
35 + })
36 + |> Repo.insert!()
37 +
38 + # A finished job: the re-run button is only rendered for terminal
39 + # statuses, which is where the nil got used as a boolean.
40 + %WorkflowJob{workflow_run_id: run.id}
41 + |> WorkflowJob.create_changeset(%{
42 + job_id: "build",
43 + name: "build",
44 + runs_on: %{"labels" => ["self-hosted"]},
45 + needs: [],
46 + definition: %{"steps" => []}
47 + })
48 + |> Ecto.Changeset.change(status: "failure")
49 + |> Repo.insert!()
50 +
51 + %{
52 + user: user,
53 + repo: repo,
54 + path: "/r/#{Storage.repo_handle(repo)}/#{repo.name}/actions/#{run.number}"
55 + }
56 + end
57 +
58 + test "renders for a signed-out visitor", %{conn: conn, path: path} do
59 + {:ok, _lv, html} = live(conn, path)
60 +
61 + assert html =~ "build"
62 + # Visible, but not offered the admin-only action.
63 + refute html =~ "Re-run job"
64 + end
65 +
66 + test "renders for a signed-in non-admin", %{conn: conn, path: path} do
67 + other = GitGud.AccountsFixtures.user_fixture()
68 +
69 + {:ok, _lv, html} = conn |> log_in_user(other) |> live(path)
70 +
71 + assert html =~ "build"
72 + refute html =~ "Re-run job"
73 + end
74 +
75 + test "offers the re-run to someone who can administer the repo", %{
76 + conn: conn,
77 + user: user,
78 + path: path
79 + } do
80 + {:ok, _lv, html} = conn |> log_in_user(user) |> live(path)
81 +
82 + assert html =~ "Re-run job"
83 + end
84 +end

Parents: af8d19b