defmodule GitGud.Workflows.PathDispatchTest do
@moduledoc """
`paths:` / `paths-ignore:` decide a push by the files it changed.
Written for bigtrippr, whose Android workflow says `paths: ["android/**"]`
and ran — and re-published an identical APK under a new versionCode — on
every server-only push, because the filter was never read.
On disk, with real commits, because what matters here is the diff the
dispatcher computes (from the ref's old value, or for a new branch from
where it left the default branch), not only the matching, which
`TriggerFiltersTest` covers.
"""
use GitGud.DataCase, async: false
alias GitGud.Repositories.Repository
alias GitGud.Workflows
import GitGud.ForgeFixtures
setup do
{_user, repo} = repository_fixture()
{:ok, repo: repo}
end
defp workflow(filters) do
"""
name: ci
on:
push:
#{filters}
jobs:
build:
runs-on: self-hosted
steps:
- run: echo hello
"""
end
# A bare repository built with plumbing: `files` is a map of path =>
# contents, written as one commit on top of `parents`.
defp commit!(%Repository{disk_path: path}, files, parents \\ []) do
index = Path.join(System.tmp_dir!(), "gitidx-#{System.unique_integer([:positive])}")
env = [{"GIT_INDEX_FILE", index}]
try do
for {file, contents} <- files do
blob = git!(path, ["hash-object", "-w", "--stdin"], contents)
git!(path, ["update-index", "--add", "--cacheinfo", "100644,#{blob},#{file}"], nil, env)
end
tree = git!(path, ["write-tree"], nil, env)
parent_args = Enum.flat_map(parents, &["-p", &1])
git!(path, ["commit-tree", tree | parent_args] ++ ["-m", "commit"])
after
File.rm(index)
end
end
# Each commit carries the whole tree, so a child is its parent's files
# plus the changes.
defp child!(repo, parent_files, changes, parent) do
commit!(repo, Map.merge(parent_files, changes), [parent])
end
defp git!(path, args, input \\ nil, env \\ []) do
{out, 0} =
case input do
nil ->
System.cmd("git", args, cd: path, stderr_to_stdout: true, env: env)
_ ->
file = Path.join(System.tmp_dir!(), "gitin-#{System.unique_integer([:positive])}")
File.write!(file, input)
try do
command = Enum.map_join(["git" | args] ++ ["<", file], " ", "e_arg/1)
System.cmd("sh", ["-c", command], cd: path, stderr_to_stdout: true, env: env)
after
File.rm(file)
end
end
String.trim(out)
end
defp quote_arg("<"), do: "<"
defp quote_arg(arg), do: "'" <> String.replace(arg, "'", "'\\''") <> "'"
defp sha!(hex), do: elem(GitGud.Git.from_hex(hex), 1)
defp push!(repo, ref, base, head) do
git!(repo.disk_path, ["update-ref", ref, head])
{:ok, runs} =
Workflows.dispatch(repo, %{
trigger: "push",
head_sha: sha!(head),
base_sha: base && sha!(base),
ref: ref
})
runs
end
# A base commit on `main` (the fixture's default branch) holding the
# workflow, and a function that pushes one more commit on top of it.
defp on_main(repo, filters) do
files = %{".forgejo/workflows/ci.yml" => workflow(filters), "README.md" => "hi\n"}
base = commit!(repo, files)
git!(repo.disk_path, ["update-ref", "refs/heads/main", base])
{files, base}
end
describe "paths:" do
test "skips a push that changes nothing under it, runs one that does", %{repo: repo} do
{files, base} = on_main(repo, ~s( paths: ["android/**"]))
readme = child!(repo, files, %{"README.md" => "changed\n"}, base)
assert push!(repo, "refs/heads/main", base, readme) == []
android = child!(repo, files, %{"android/x" => "x\n"}, readme)
assert [_run] = push!(repo, "refs/heads/main", readme, android)
end
end
describe "paths-ignore:" do
test "skips a push that only touches ignored files", %{repo: repo} do
{files, base} = on_main(repo, ~s( paths-ignore: ["docs/**"]))
docs = child!(repo, files, %{"docs/a" => "a\n"}, base)
assert push!(repo, "refs/heads/main", base, docs) == []
both = child!(repo, files, %{"docs/b" => "b\n", "lib/a" => "a\n"}, docs)
assert [_run] = push!(repo, "refs/heads/main", docs, both)
end
end
describe "a new branch" do
test "is compared with where it left the default branch", %{repo: repo} do
{files, base} = on_main(repo, ~s( paths: ["lib/**"]))
topic = child!(repo, files, %{"docs/a" => "a\n"}, base)
assert push!(repo, "refs/heads/topic", nil, topic) == []
zero = String.duplicate("0", 40)
assert push!(repo, "refs/heads/topic", zero, topic) == []
end
test "runs when there is no default branch to compare with", %{repo: repo} do
{files, base} = on_main(repo, ~s( paths: ["lib/**"]))
topic = child!(repo, files, %{"docs/a" => "a\n"}, base)
git!(repo.disk_path, ["update-ref", "-d", "refs/heads/main"])
assert [_run] = push!(repo, "refs/heads/topic", nil, topic)
end
test "runs when it is the default branch itself, pushed new", %{repo: repo} do
{_files, base} = on_main(repo, ~s( paths: ["lib/**"]))
assert [_run] = push!(repo, "refs/heads/main", nil, base)
end
end
test "a tag push ignores path filters", %{repo: repo} do
{_files, base} =
on_main(repo, """
tags: ['v*']
paths: ["nothing/**"]
""")
assert [_run] = push!(repo, "refs/tags/v1.0.0", nil, base)
end
test "an unresolvable base fails open", %{repo: repo} do
{files, base} = on_main(repo, ~s( paths: ["lib/**"]))
docs = child!(repo, files, %{"docs/a" => "a\n"}, base)
bogus = Base.encode16(:crypto.strong_rand_bytes(20), case: :lower)
assert [_run] = push!(repo, "refs/heads/main", bogus, docs)
end
describe "the event payload" do
test "lists the pushed commits back to the old value, head first", %{repo: repo} do
{files, base} = on_main(repo, " branches: [main]")
one = child!(repo, files, %{"lib/a" => "a\n"}, base)
two = child!(repo, files, %{"lib/b" => "b\n"}, one)
assert [run] = push!(repo, "refs/heads/main", base, two)
payload = run.event_payload
assert payload["ref"] == "refs/heads/main"
assert payload["before"] == base
assert payload["after"] == two
assert payload["created"] == false
assert payload["head_commit"]["id"] == two
assert payload["head_commit"]["message"] =~ "commit"
# Oldest first, as on GitHub, and the old value itself isn't listed.
assert Enum.map(payload["commits"], & &1["id"]) == [one, two]
assert payload["compare"] =~ "/compare?base=#{base}&head=#{two}"
assert payload["repository"]["name"] == repo.name
end
test "a new branch's before is 40 zeros and lists only its own commits", %{repo: repo} do
{files, base} = on_main(repo, " branches: [topic]")
topic = child!(repo, files, %{"lib/a" => "a\n"}, base)
assert [run] = push!(repo, "refs/heads/topic", nil, topic)
assert run.event_payload["before"] == String.duplicate("0", 40)
assert run.event_payload["created"] == true
assert Enum.map(run.event_payload["commits"], & &1["id"]) == [topic]
end
end
test "a workflow without path filters runs on every push", %{repo: repo} do
{files, base} = on_main(repo, " branches: [main]")
docs = child!(repo, files, %{"docs/a" => "a\n"}, base)
assert [_run] = push!(repo, "refs/heads/main", base, docs)
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
7.5 KiB · text
History
b868907