defmodule GitGud.Workflows.JobIfTest do
@moduledoc """
A job's `if:`, decided by the forge as the job would be queued, when
it can be: a false one skips the job (and so its dependents, and maybe
finishes the run) instead of leaving it queued for a runner that may
never come — a `macos-latest` leg of a release matrix on a branch push,
say. Anything the forge can't evaluate is queued for the runner, as
before.
"""
use GitGud.DataCase, async: false
alias GitGud.Workflows
alias GitGud.Workflows.WorkflowJob
alias GitGud.Workflows.WorkflowRun
alias GitGud.Workflows.Yaml
import GitGud.ForgeFixtures
setup do
{_user, repo} = repository_fixture()
{:ok, repo: repo}
end
# As in JobGraphTest: a seed row plus a re-run creates the jobs through
# the real path without a repository on disk.
defp run_workflow(repo, yaml, ref) do
{:ok, doc} = Yaml.parse(yaml)
seed =
%WorkflowRun{repository_id: repo.id}
|> WorkflowRun.create_changeset(%{
"number" => System.unique_integer([:positive]),
"workflow_path" => ".forgejo/workflows/ci.yml",
"workflow_name" => doc.name,
"trigger" => "push",
"head_ref" => ref,
"head_sha" => :crypto.strong_rand_bytes(20),
"workflow_doc" => doc.raw
})
|> Repo.insert!()
{:ok, run} = Workflows.rerun(seed)
run
end
defp jobs(run, job_id) do
Repo.all(
from j in WorkflowJob,
where: j.workflow_run_id == ^run.id and j.job_id == ^job_id,
order_by: j.id
)
end
defp job(run, job_id), do: run |> jobs(job_id) |> hd()
defp run_status(run), do: Repo.get!(WorkflowRun, run.id).status
# The shape of neiam/git-friends' rust-ci.yml: a release matrix across
# runners nobody here carries, only for tags.
@release """
name: ci
on: push
jobs:
build:
runs-on: self-hosted
steps:
- run: true
release:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
include:
- runner: ubuntu-latest
- runner: ubuntu-24.04-arm
- runner: macos-latest
- runner: windows-latest
runs-on: ${{ matrix.runner }}
steps:
- run: true
create-release:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: self-hosted
steps:
- run: true
"""
describe "a release matrix only for tags" do
test "on a branch push, every leg and its dependent is skipped, and the run succeeds",
%{repo: repo} do
run = run_workflow(repo, @release, "refs/heads/master")
assert Enum.all?(jobs(run, "release"), &(&1.status == "blocked"))
{:ok, _} = Workflows.update_job_status(job(run, "build"), "success")
legs = jobs(run, "release")
assert length(legs) == 4
assert Enum.all?(legs, &(&1.status == "skipped"))
assert Enum.all?(legs, &(&1.status_reason == "Skipped: `if:` is false"))
assert job(run, "create-release").status == "skipped"
assert run_status(run) == "success"
end
test "on a tag push, the legs are queued", %{repo: repo} do
run = run_workflow(repo, @release, "refs/tags/v1.2.0")
{:ok, _} = Workflows.update_job_status(job(run, "build"), "success")
assert Enum.all?(jobs(run, "release"), &(&1.status == "queued"))
assert job(run, "create-release").status == "blocked"
assert run_status(run) in ~w(queued running)
end
end
test "a job without needs is skipped as the run is created", %{repo: repo} do
run =
run_workflow(
repo,
"""
name: ci
on: push
jobs:
publish:
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
strategy:
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- run: true
""",
"refs/heads/main"
)
assert Enum.all?(jobs(run, "publish"), &(&1.status == "skipped"))
assert run_status(run) == "success"
end
test "a matrix value can decide a leg", %{repo: repo} do
run =
run_workflow(
repo,
"""
name: ci
on: push
jobs:
test:
if: matrix.os != 'macos-latest'
strategy:
matrix:
os: [self-hosted, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- run: true
""",
"refs/heads/main"
)
assert %{"self-hosted" => "queued", "macos-latest" => "skipped"} ==
Map.new(jobs(run, "test"), &{&1.matrix["os"], &1.status})
end
test "a true `if:` is queued", %{repo: repo} do
run =
run_workflow(
repo,
"""
name: ci
on: push
jobs:
lint:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: self-hosted
steps:
- run: true
""",
"refs/heads/main"
)
assert job(run, "lint").status == "queued"
end
describe "what only the runner can decide is left to it" do
for condition <- [
"steps.x.outputs.y == 'z'",
"always() && startsWith(github.ref, 'refs/tags/v')",
"${{ success() && false }}",
"env.DEPLOY == 'true'",
"secrets.TOKEN != ''",
"${{ github.ref }} == 'refs/tags/v1'"
] do
test "if: #{condition}", %{repo: repo} do
run =
run_workflow(
repo,
"""
name: ci
on: push
jobs:
build:
if: #{inspect(unquote(condition))}
runs-on: self-hosted
steps:
- run: true
""",
"refs/heads/main"
)
assert job(run, "build").status == "queued"
end
end
end
test "re-running a job stuck queued decides its `if:` then", %{repo: repo} do
run = run_workflow(repo, @release, "refs/heads/master")
{:ok, _} = Workflows.update_job_status(job(run, "build"), "success")
# As though it was queued before the forge decided `if:` itself.
for leg <- jobs(run, "release") do
leg |> Ecto.Changeset.change(status: "queued", status_reason: nil) |> Repo.update!()
end
run |> WorkflowRun.status_changeset("running") |> Repo.update!()
job(run, "create-release") |> Ecto.Changeset.change(status: "blocked") |> Repo.update!()
{:ok, _} = Workflows.rerun_job(Repo.get!(WorkflowRun, run.id), "release")
assert Enum.all?(jobs(run, "release"), &(&1.status == "skipped"))
assert job(run, "create-release").status == "skipped"
assert run_status(run) == "success"
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
6.6 KiB · text
History
b868907