Give jobs github.ref_name and github.ref_type

7d9322a · Gabriel Morell · 2026-09-16 19:40

12 files +132 -5
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud/workflows.ex
+2 −5
@@ -18,6 +18,7 @@ defmodule GitGud.Workflows do
18 18 alias GitGud.Repositories
19 19 alias GitGud.Repositories.Repository
20 20 alias GitGud.Workflows.Matrix
21 + alias GitGud.Workflows.Ref
21 22 alias GitGud.Workflows.Runner
22 23 alias GitGud.Workflows.WorkflowJob
23 24 alias GitGud.Workflows.WorkflowRun
@@ -39,7 +40,7 @@ defmodule GitGud.Workflows do
39 40 def dispatch(%Repository{} = repo, event) do
40 41 %{trigger: trigger, head_sha: head_sha, ref: ref} = event
41 42 triggered_by_id = Map.get(event, :triggered_by_id)
42 − ctx = ref_context(ref)
43 + ctx = Ref.context(ref)
43 44 head_sha = peel(repo, ref, head_sha)
44 45
45 46 workflows = discover_workflows(repo, head_sha)
@@ -64,10 +65,6 @@ defmodule GitGud.Workflows do
64 65 {:ok, runs}
65 66 end
66 67
67 − defp ref_context("refs/heads/" <> branch), do: %{branch: branch}
68 − defp ref_context("refs/tags/" <> tag), do: %{tag: tag}
69 − defp ref_context(_), do: %{}
70 −
71 68 # An annotated tag's ref points at a *tag object*, not at a commit, and
72 69 # everything downstream wants the commit: `discover_workflows` reads the
73 70 # tree, the run records `head_sha`, and the runner checks it out. Before this,
added lib/git_gud/workflows/ref.ex
+62 −0
@@ -0,0 +1,62 @@
1 +defmodule GitGud.Workflows.Ref do
2 + @moduledoc """
3 + What a pushed ref is, in the terms workflows are written in.
4 +
5 + A ref name is the only thing separating "push to master" from "release
6 + v1.2.3", and three places need to agree about it: dispatch, which decides
7 + whether a workflow's `branches:`/`tags:` filter claims the push; the runner
8 + context, which carries `github.ref_name` and `github.ref_type` into a job;
9 + and anything later that wants to say "this run was a tag".
10 +
11 + They agreed by coincidence until they didn't. `github.ref_type` was simply
12 + absent from the context, so a release workflow's `if` read it as empty, took
13 + its "not a tag" branch on a tag push, published nothing, and reported
14 + success — which is worse than failing, because nothing asks to be looked at.
15 + """
16 +
17 + @doc """
18 + The short name: `refs/heads/main` -> `main`, `refs/tags/v1.0` -> `v1.0`.
19 +
20 + Anything else passes through unchanged. A caller is better served by a
21 + surprising value it can see than by a blank one it cannot.
22 +
23 + iex> GitGud.Workflows.Ref.name("refs/tags/v1.0")
24 + "v1.0"
25 + """
26 + @spec name(String.t() | nil) :: String.t()
27 + def name("refs/heads/" <> name), do: name
28 + def name("refs/tags/" <> name), do: name
29 + def name(ref) when is_binary(ref), do: ref
30 + def name(_), do: ""
31 +
32 + @doc """
33 + `"branch"`, `"tag"`, or `""` for a ref that is neither.
34 +
35 + These are GitHub's spellings, because `github.ref_type` is compared against
36 + them in workflows people copy from its documentation.
37 +
38 + iex> GitGud.Workflows.Ref.type("refs/tags/v1.0")
39 + "tag"
40 + """
41 + @spec type(String.t() | nil) :: String.t()
42 + def type("refs/heads/" <> _), do: "branch"
43 + def type("refs/tags/" <> _), do: "tag"
44 + def type(_), do: ""
45 +
46 + @doc """
47 + The filter context for `GitGud.Workflows.Yaml.matches?/3`.
48 +
49 + `%{branch: name}` or `%{tag: name}`, and `%{}` for a ref that is neither —
50 + which filters on nothing rather than guessing.
51 +
52 + iex> GitGud.Workflows.Ref.context("refs/heads/main")
53 + %{branch: "main"}
54 + """
55 + @spec context(String.t() | nil) :: %{
56 + optional(:branch) => String.t(),
57 + optional(:tag) => String.t()
58 + }
59 + def context("refs/heads/" <> branch), do: %{branch: branch}
60 + def context("refs/tags/" <> tag), do: %{tag: tag}
61 + def context(_), do: %{}
62 +end
modified lib/git_gud_web/controllers/runner_controller.ex
+10 −0
@@ -24,6 +24,7 @@ defmodule GitGudWeb.RunnerController do
24 24 import GitGudWeb.Plugs.Twirp
25 25
26 26 alias GitGud.Workflows
27 + alias GitGud.Workflows.Ref
27 28 alias GitGud.Workflows.Runner, as: RunnerSchema
28 29 alias GitGud.Workflows.Runners
29 30 alias GitGudWeb.Plugs.CacheRawBody
@@ -286,6 +287,15 @@ defmodule GitGudWeb.RunnerController do
286 287 %{
287 288 "event" => run.trigger || "push",
288 289 "ref" => run.head_ref || "",
290 + # `github.ref_name` / `github.ref_type`, and the GITHUB_REF_NAME and
291 + # GITHUB_REF_TYPE the runner exports from them. Every workflow written
292 + # against GitHub's documentation reaches for these, and their absence
293 + # does not fail — it reads as empty, which is how a release workflow
294 + # here took its "not a tag" branch on a tag push, published nothing, and
295 + # reported success. A CI failure that claims to have worked is the
296 + # expensive kind.
297 + "ref_name" => Ref.name(run.head_ref),
298 + "ref_type" => Ref.type(run.head_ref),
289 299 "sha" => if(run.head_sha, do: GitGud.Git.to_hex(run.head_sha), else: ""),
290 300 "repository" => owner <> "/" <> repo.name,
291 301 "repository_owner" => owner,
added test/git_gud/workflows/ref_test.exs
+58 −0
@@ -0,0 +1,58 @@
1 +defmodule GitGud.Workflows.RefTest do
2 + @moduledoc """
3 + The ref parsing three places used to do separately.
4 +
5 + `github.ref_type` being absent from the runner context is what made a
6 + release workflow take its "not a tag" branch on a tag push and report
7 + success for publishing nothing, so the spellings here are GitHub's — a
8 + workflow copied from its documentation compares against exactly these
9 + strings.
10 + """
11 + use ExUnit.Case, async: true
12 +
13 + doctest GitGud.Workflows.Ref
14 +
15 + alias GitGud.Workflows.Ref
16 +
17 + describe "name/1" do
18 + test "strips the ref prefix" do
19 + assert Ref.name("refs/heads/master") == "master"
20 + assert Ref.name("refs/tags/v0.1.0") == "v0.1.0"
21 + end
22 +
23 + test "keeps a slash inside the name" do
24 + assert Ref.name("refs/heads/topic/thing") == "topic/thing"
25 + end
26 +
27 + test "passes anything else through rather than blanking it" do
28 + assert Ref.name("HEAD") == "HEAD"
29 + assert Ref.name(nil) == ""
30 + end
31 + end
32 +
33 + describe "type/1" do
34 + test "is GitHub's spelling" do
35 + assert Ref.type("refs/heads/master") == "branch"
36 + assert Ref.type("refs/tags/v0.1.0") == "tag"
37 + end
38 +
39 + test "is empty for a ref that is neither" do
40 + assert Ref.type("refs/pull/7/head") == ""
41 + assert Ref.type(nil) == ""
42 + end
43 + end
44 +
45 + describe "context/1" do
46 + test "names the one key the filters look at" do
47 + assert Ref.context("refs/heads/master") == %{branch: "master"}
48 + assert Ref.context("refs/tags/v0.1.0") == %{tag: "v0.1.0"}
49 + end
50 +
51 + # A context that guessed would make `branches:` and `tags:` filters
52 + # decide on a ref that is neither; an empty one filters on nothing.
53 + test "is empty for a ref that is neither" do
54 + assert Ref.context("refs/pull/7/head") == %{}
55 + assert Ref.context(nil) == %{}
56 + end
57 + end
58 +end

Parents: efc4874