neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Give jobs github.ref_name and github.ref_type
7d9322a · Gabriel Morell · 2026-09-16 19:40
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