Plan: one commit status per job, and branch protection that can require them
Status: ready to execute. Written to be carried out start to finish by
one agent without further questions. Independent of the other CI plans;
if workflow-job-graph.md has been executed, jobs
can also be skipped — map that as step 1 says.
Why
- Two runs on one commit overwrite each other’s status.
Workflows.update_run_commit_status/1writes one row per run with context"gitgud-actions/" <> (run.workflow_name || run.workflow_path), upserting on(repository_id, sha, context). A same-repository pull request’s head commit gets apushrun (the push to the branch) and apull_requestrun (synchronize) of the same workflow — one context, so whichever finished last is what the PR page shows: a failing PR run can be hidden by a passing push run. Two workflow files that share aname:collide the same way. - Statuses are per workflow, not per job. GitHub and Forgejo report
each job:
<workflow> / <job> (<event>)(Forgejo:services/actions/commit_status.go). A workflow withtestandlintshows one pill here, so you can’t see which job failed, and branch protection (below) couldn’t requirelintalone. - Branch protection can’t require checks.
BranchProtectionhasrequire_status_checksandrequired_checkscolumns (lib/git_gud/branch_protections/branch_protection.ex), cast by its changeset, but nothing reads them:BranchProtections.evaluate/5checks force-push and approvals only, and the settings UI (lib/git_gud_web/live/branch_protection_live/index.ex) doesn’t offer them. Anything that sets them through the changeset gets a rule that silently enforces nothing. - The PR page doesn’t update when CI finishes.
lib/git_gud_web/live/pr_live/show.exrenders<.commit_status_badges repo={@repo} sha={@pr.head_sha} />(line ~533) but doesn’t subscribe toWorkflows.ci_topic/0, so pills stay “pending” until a reload.
Semantics to match
- Context per job:
"#{workflow_name} / #{job_name} (#{event})", whereworkflow_name=run.workflow_name || Path.basename(run.workflow_path),job_name= the job row’sname(alreadybuild (arm64, 28)for a matrix leg),event=run.trigger. That is Forgejo’s format, so required-check names people copy from Forgejo match. - State: queued/blocked/waiting/running →
pending; success →success; failure and cancelled →failure(today’s mapping, and Forgejo’s inservices/actions/commit_status.go,toCommitStatus); skipped →successwith description “Skipped” (Forgejo maps skipped to success, and GitHub counts a skipped required check as passing). - Required checks (GitHub/Forgejo): a PR may be merged into a branch whose
rule has
require_status_checksonly when, for the PR head commit, every pattern inrequired_checksmatches at least one status and every matching status issuccess. Patterns are globs (*matches any run of characters, including/and spaces — Forgejo uses glob patterns for this field). An emptyrequired_checkswithrequire_status_checkson means “every status on the head commit must be success, and there must be at least one”. - Direct pushes to a protected branch are not blocked by status checks (no PR, no head to have checks on — Forgejo’s behaviour); say so in the UI hint.
- Admin override:
merge!/3’soverride: truealready bypasses review rules; let it bypass status checks too (same audit trail).
Where things are
| Status writer | lib/git_gud/workflows.ex: update_run_commit_status/1 (called from create_run/2, maybe_complete_run/1, cancel/1), update_job_status/3, create_job_leg/3 |
| Status schema | lib/git_gud/workflows/commit_status.ex; reader Workflows.list_commit_statuses/2 |
| Pills | lib/git_gud_web/components/core_components.ex commit_status_badges/1, display_context/1 (strips gitgud-actions/) |
| PR page | lib/git_gud_web/live/pr_live/show.ex (mount, handle_info/2, merge box), PullRequests.merge_blockers/2 (review rules, shown on the page) |
| Protection | lib/git_gud/branch_protections.ex check_ref_update/5, evaluate/5; schema above; callers PullRequests.merge!/3, GitGud.Merges (check_protection/5), GitGudWeb.PreReceiveController |
| Settings UI | lib/git_gud_web/live/branch_protection_live/index.ex |
| Tests | test/git_gud/branch_protections_test.exs, test/git_gud/workflows/rerun_cancel_test.exs, test/git_gud_web/live/pr_live/ |
Steps
1. Per-job statuses
Add Workflows.update_job_commit_status(job, run) writing the context and
state above, target_url = the run page anchored to the job if the run
page has job anchors (check workflow_live/show.ex; else the run page),
description = e.g. “Successful in 2m”, “Failing after 40s”, “Waiting to
run”, “Skipped”. Call it:
- for every job row created in
create_job_leg/3(initial pending), - in
update_job_status/3after the update, - in
cancel/1for each job it cancels (it usesupdate_all; select the affected rows first, or re-read them after).
Stop writing the per-run gitgud-actions/<workflow> status (delete the
writer, keep broadcast_ci_change/1 — move that call into the new
function). Old rows stay on old commits; display_context/1 keeps
stripping their prefix.
2. PR page refreshes
In pr_live/show.ex mount/3 (when connected?), subscribe to
Workflows.ci_topic/0; on {:ci_run_changed, run_id} re-render the
status pills if the run is in this repository (load the run’s
repository_id — cheap) — e.g. bump an assign the badges component reads
so it reloads.
3. Required checks: evaluation
In BranchProtections, add check_statuses(rule, repo, head_sha)
returning :ok or {:error, {:status_checks, missing :: [pattern], failing :: [context]}}, using Workflows.list_commit_statuses/2 and a
glob matcher where * matches anything (a private regex helper; don’t use
GitGud.Workflows.Glob, whose * stops at /).
Restructure evaluate/5 from a cond into a with chain so all rules
apply in turn (today the approvals clause returns early and nothing after
it could run): force-push → approvals → status checks. Status checks apply
only when opts[:pull_request_id] is given (a merge); pass the PR head
sha in opts (head_sha:) from merge!/3 and Merges (check what
Merges has in hand: analysis.theirs_sha is the head being merged).
merge!/3: override: true skips the status-check error as it skips
review blocks. Find how override is threaded into check_ref_update today
(it isn’t — it’s only used by check_review_block/3) and pass it.
4. Required checks: on the PR page
Extend what the merge box shows (next to PullRequests.merge_blockers/2’s
review blockers): “Required checks: missing”, “
failing”, and disable the merge button the same way review blocks do. Put
the lookup in PullRequests (e.g. status_check_blockers(repo, pr)
calling BranchProtections.rule_for_branch/2 + check_statuses/3) so the
LiveView and merge!/3 agree.
5. Required checks: settings UI
In branch_protection_live/index.ex: a checkbox “Require status checks to
pass before merging” (require_status_checks) and a text input “Required
checks” (comma- or newline-separated → required_checks list; parse in
the LiveView before casting), with a hint listing the contexts seen on the
default branch’s latest commit (Workflows.list_commit_statuses/2) so
people can copy exact names. Show them in the rule list.
6. Docs
README CI list: per-job statuses and their names; required checks and their glob rule; that direct pushes aren’t checked.
Tests
- A run with jobs
testandlintcreates two pending statuses with the Forgejo-format contexts; failinglint→ its statusfailure,testunaffected. - A
pushrun and apull_requestrun on the same sha → four distinct statuses (… (push)and… (pull_request)), neither overwrites the other. - Cancel → each cancelled job’s status becomes
failurewith description “Cancelled”. BranchProtectionswithrequire_status_checks: true, required_checks: ["ci / test *"]:- head has
ci / test (push)success → merge allowed; - head has it
failure→{:status_checks, [], ["ci / test (push)"]}; - no matching status →
{:status_checks, ["ci / test *"], []}; required_approvals: 1plus checks: both enforced (the oldcondwould have stopped at approvals);- a direct push (no
pull_request_id) is not blocked by checks; merge!(…, override: true)merges despite a failing check.
- head has
- LiveView: the PR page shows the missing/failing checks and the merge
button is disabled; a broadcast on
ci_topicre-renders the pills. - Branch protection settings: saving the checkbox and a two-line
required_checksfield persists a two-element list.
Done when
mix testpasses; no new compile warnings (comparemix compile --forcewarning lists before/after);mix formatonly touched files.- One commit in the repository’s message style.
- Not pushed.