Actions: a warning conclusion for jobs that passed but want a look

d8a7c7f · Gabriel Morell · 2026-10-27 18:57

37 files +450 -44
Message
{commit_body(@commit)}

Files changed

modified README.md
+3 −3
@@ -103,11 +103,11 @@ reinvent them.
103 103 - **Filter patterns** in `branches:`, `tags:`, `paths:` and their `-ignore` lists follow GitHub's cheat sheet: `*` is any run of characters but `/`, `**` any run at all (`**/` also no directory), `?` zero or one and `+` one or more of the **preceding** character, `[a-z0-9]` one listed character (alphanumerics and `a-z`/`A-Z`/`0-9` ranges only), `\x` a literal `x`. In `branches:`, `tags:` and `paths:` the last matching pattern decides, so `!pattern` excludes and a later positive pattern includes again; a list with only `!` patterns matches nothing. `*` no longer crosses `/` in `branches:` / `tags:` (`branches: ['*']` doesn't run for `feature/x`), and `!` works there now. Branch protection and required-check patterns are separate: their `*` still crosses `/`
104 104 - **`paths:` / `paths-ignore:`**: a push runs if any changed file is selected by `paths:`, or if not every changed file is matched by `paths-ignore:`; both set → `paths:` wins, with a warning. Changed files are the ref's old → new diff (renames count as both paths); a new branch is diffed from where it left the default branch. Tag pushes and manual runs ignore path filters, and when the changed files can't be worked out (no default branch to compare with, an unknown base, a git error, more than 3000 files) the filter passes — a run is never skipped because the forge couldn't look
105 105 - **The job's context** follows Forgejo's: `github.event_name`, `ref`, `ref_name`, `ref_type`, `sha`, `repository` (`owner/name`), `repository_id`, `repository_owner(_id)`, `actor(_id)`, `run_id`, `run_number`, `run_attempt` (1, bumped by re-running the job; re-running the whole run makes a new run and a new `run_id`), `workflow`, `workflow_ref` (`owner/name/<path>@<ref>`), `job`, `server_url`, `api_url`, `token` (`repository_id` is sent, but act doesn't map it, so `${{ github.repository_id }}` reads blank). **`github.event`** is GitHub's webhook payload, trimmed: every event has `repository` and `sender`; `push` has `ref`, `before` (40 zeros for a new ref), `after`, `created`, `deleted`, `forced` (always `false`: not tracked), `compare`, `commits` (at most 20) and `head_commit`, `pusher`; `pull_request` has `action`, `number` and `pull_request` (`title`, `body`, `html_url`, `user`, `head`/`base` with `ref`, `sha`, `label`, `repo`); `workflow_dispatch` has `inputs`, `ref`, `workflow`. **`secrets.GITHUB_TOKEN`** (and `GITEA_TOKEN`, `FORGEJO_TOKEN`) is the job token. The workflow's top-level `env:`, `defaults:`, `permissions:`, `run-name:` and `on:` reach the runner with the job
106 − - **Jobs that depend on jobs**, as on GitHub: `${{ needs.<id>.outputs.* }}` and `needs.<id>.result` (`success`, `failure`, `cancelled`, `skipped`; for a matrix, the worst leg's, with the legs' outputs merged) reach the dependent job. A job waits until all its needs finish; if any didn't succeed it is **skipped** — and its own dependents with it — unless its `if:` calls `always()`, `failure()` or `cancelled()`, so cleanup and "report the failure" jobs run. A job its own `if:` skipped shows as skipped, not green. Job-level `continue-on-error: true` (or `${{ matrix.* }}` that comes out `true`) keeps a failure from skipping its dependents, failing the run or fail-fasting its matrix siblings, though the job still shows as failed; any other expression there counts as false. Skipped jobs don't fail a run. Unlike GitHub, **cancelling a run cancels its waiting jobs too**, `always()` or not
106 + - **Jobs that depend on jobs**, as on GitHub: `${{ needs.<id>.outputs.* }}` and `needs.<id>.result` (`success`, `failure`, `cancelled`, `skipped`; for a matrix, the worst leg's, with the legs' outputs merged) reach the dependent job. A job waits until all its needs finish; if any didn't succeed it is **skipped** — and its own dependents with it — unless its `if:` calls `always()`, `failure()` or `cancelled()`, so cleanup and "report the failure" jobs run. A job its own `if:` skipped shows as skipped, not green. Job-level `continue-on-error: true` (or `${{ matrix.* }}` that comes out `true`) keeps a failure from skipping its dependents, failing the run or fail-fasting its matrix siblings, and the job concludes **warning** (an amber ⚠ badge) rather than failed; any other expression there counts as false. A job can also conclude warning though it succeeded, by setting the output `gitgud-conclusion` to `warning` (`outputs: { gitgud-conclusion: ${{ steps.scan.outputs.conclusion }} }`; any case, surrounding whitespace ignored, any other value ignored) — a gitgud extension GitHub and Forgejo read as an ordinary output, so there the job just passes. A job that failed without `continue-on-error` stays failed whatever its outputs say. Dependents see a warning job's `needs.<id>.result` as `success`, as GitHub does for `continue-on-error`; a run whose jobs passed, were skipped or warned concludes success, and its page says how many warnings it had. The mobile API (and `gg ci`) report the job's status as `warning`; the `workflow_run` webhook carries the run's conclusion, `success` (there's no per-job webhook, so nothing says GitHub's `neutral`). A re-run starts the job's outputs afresh. Skipped jobs don't fail a run. Unlike GitHub, **cancelling a run cancels its waiting jobs too**, `always()` or not
107 107 - Re-run / cancel buttons on the workflow show page (a re-run keeps the original run's actor). **Cancelling stops running jobs**: the runner hears it in the reply to its next progress report and stops the job, still uploading its log; a cancelled job stays cancelled whatever its runner reports afterwards
108 108 - **`concurrency:`**, workflow and job level, as a group name or `{group:, cancel-in-progress:}`: within a group (per repository) one run or job is in progress and at most one waits (shown as "Waiting for <group>"); a newer arrival cancels the waiting one, or with `cancel-in-progress` the one in progress too, and when the one in progress finishes the waiting one starts. A run (or job) cancelled only by a newer one in its group concludes `cancelled`, not failed. A workflow's group is decided when the run is created, a job's when it would be queued, so it can read `matrix` and `needs`. Group and `cancel-in-progress` may use `${{ }}` with the forge's own expression subset: property paths into `github`, `inputs`, `vars`, `matrix`, `needs` (`a.b`, `a['b']`), string/number/`true`/`false`/`null` literals, `||`, `&&`, `==`, `!=`, `!`, parentheses, `format()`, `fromJSON()` and `toJSON()`. A group outside that subset (any other function, say) is ignored with a warning, shown on the job — the run goes ahead **without** concurrency control rather than being held on a guess. Re-running a single job puts it back through its job-level group, not its run's
109 − - **A commit status per job**, named as Forgejo names them — `<workflow> / <job> (<event>)`, e.g. `ci / test (push)` or `ci / build (arm64, 28) (pull_request)` for a matrix leg — so a pull request's `push` run and `pull_request` run on the same head (or two jobs of one workflow) don't overwrite each other. Queued, blocked, waiting, awaiting-approval and running jobs are pending; success and skipped are success; failure and cancelled are failure, except a job cancelled by a newer one in its concurrency group, which shows as cancelled. Each pill links to its job on the run page, and the pull request page updates them live
110 − - **Required status checks** in branch protection: with "Require status checks to pass before merging", a pull request merges into the branch only when every required check (one per line; `*` matches anything, `/` and spaces included, so `ci / test *` covers both events) matches at least one status on its head and every status it matches is success. An empty list means every status on the head, and there must be one. The merge box lists what's missing or not passing and disables the merge; an admin's override merges past them, recorded like a review-rule override. Direct pushes aren't checked — there's no head to have checks on, as on Forgejo
109 + - **A commit status per job**, named as Forgejo names them — `<workflow> / <job> (<event>)`, e.g. `ci / test (push)` or `ci / build (arm64, 28) (pull_request)` for a matrix leg — so a pull request's `push` run and `pull_request` run on the same head (or two jobs of one workflow) don't overwrite each other. Queued, blocked, waiting, awaiting-approval and running jobs are pending; success and skipped are success; warning is `warning` (Forgejo's state, an amber pill); failure and cancelled are failure, except a job cancelled by a newer one in its concurrency group, which shows as cancelled. Each pill links to its job on the run page, and the pull request page updates them live
110 + - **Required status checks** in branch protection: with "Require status checks to pass before merging", a pull request merges into the branch only when every required check (one per line; `*` matches anything, `/` and spaces included, so `ci / test *` covers both events) matches at least one status on its head and every status it matches passed: success, or warning (a skipped job reports success). An empty list means every status on the head, and there must be one. The merge box lists what's missing or not passing and disables the merge; an admin's override merges past them, recorded like a review-rule override. Direct pushes aren't checked — there's no head to have checks on, as on Forgejo
111 111 - **The job token** (`${{ github.token }}`, an HS256 JWT minted per job on `FetchTask`) works only while its job runs and for five minutes after (for the runner's post-job cache save and artifact upload), and only for that attempt of the job — a leaked token no longer stays valid for a day. It clones and fetches its own repository, reads and publishes its packages, reads its metadata over the API, dispatches workflows (its own repository's, or one that granted it), and reaches the artifact + cache endpoints of its run. **`permissions:`** narrows it as on GitHub: workflow level, replaced by a job's own; `read-all`, `write-all`, `{}` or a map in which unnamed scopes are `none`. `contents` (`read` fetch, `write` also push — to its own repository, subject to branch protection like any direct push, attributed to whoever started the run), `packages` (`read` / `write`) and `actions` (`write` to dispatch) mean something; `metadata` is always read; the others (`issues`, `pull-requests`, `statuses`, `checks`, `deployments`, `id-token`, …) are accepted and ignored, since the token can't do those things here anyway. With no `permissions:` the token keeps `contents: read, packages: write, actions: write` — GitHub's permissive default would also let it push. **A push made with the job token starts no workflows** (no `push` run, no pull request `synchronize`), as on GitHub, so a workflow that pushes can't trigger itself forever; webhooks and pull request heads still update
112 112 - **GitHub Actions cache server v1**, scoped to the repository. Restore order: the primary key exactly, then each fallback key as a prefix (newest first), each tried on the run's own ref, then — for a `pull_request` run — the PR's base branch, then the default branch; a save goes to the run's own ref. Upload chunks are placed at their `Content-Range` offsets (`actions/cache` sends four at once) and assembled on finalize; a finalized entry is immutable, and only the job that reserved an entry can upload to or finalize it (so a fork run can't fill in an entry a default-branch job reserved). Age/size eviction worker, which also clears uploads abandoned for a day. **Not reached by a default forgejo-runner**: with `cache.enabled` (its default) the runner starts its own cache server and points `ACTIONS_CACHE_URL` at it, ignoring the URL gitgud sends; jobs only use this one if the runner has `cache.enabled: false` and `ACTIONS_CACHE_URL` set in `runner.envs`. `actions/cache@v4`'s cache service v2 isn't implemented
113 113 - A simplified **v1 "pipelines" artifact API**, scoped to the run: a job creates, lists and downloads its own run's artifacts only (so jobs of one run can share files). Not yet usable from the stock actions: forgejo-runner points `ACTIONS_RUNTIME_URL` at `/api/actions_pipeline/`, which isn't mounted, and `actions/upload-artifact@v3` / `download-artifact@v3` speak a different shape of that API (chunked `PUT`s with `itemPath`, container item listing). `@v4` (the Twirp `ArtifactService` under `ACTIONS_RESULTS_URL`) isn't supported
modified lib/git_gud/branch_protections.ex
+7 −3
@@ -115,11 +115,15 @@ defmodule GitGud.BranchProtections do
115 115 end
116 116 end
117 117
118 + # The commit status states a required check passes with.
119 + @passing_states ~w(success warning)
120 +
118 121 @doc """
119 122 Whether `head_sha`'s commit statuses satisfy the rule's required
120 123 checks, as GitHub and Forgejo decide it: every pattern in
121 124 `required_checks` matches at least one status, and every status a
122 − pattern matches is `success` (a skipped job reports success). An
125 + pattern matches passes: `success` (a skipped job reports success), or
126 + `warning` (a job that passed but wants a look). An
123 127 empty `required_checks` means every status on the commit, of which
124 128 there must be at least one.
125 129
@@ -129,7 +133,7 @@ defmodule GitGud.BranchProtections do
129 133
130 134 Returns `:ok` (also when the rule doesn't require checks), or
131 135 `{:error, {:status_checks, missing_patterns, failing_contexts}}`, where
132 − a failing context is one that isn't `success` yet — pending included.
136 + a failing context is one that hasn't passed yet — pending included.
133 137 """
134 138 def check_statuses(%BranchProtection{require_status_checks: false}, _repo, _sha), do: :ok
135 139
@@ -151,7 +155,7 @@ defmodule GitGud.BranchProtections do
151 155 matches
152 156 |> Map.values()
153 157 |> List.flatten()
154 − |> Enum.reject(&(&1.state == "success"))
158 + |> Enum.reject(&(&1.state in @passing_states))
155 159 |> Enum.map(& &1.context)
156 160 |> Enum.uniq()
157 161 |> Enum.sort()
modified lib/git_gud/workflows.ex
+103 −20
@@ -2117,6 +2117,9 @@ defmodule GitGud.Workflows do
2117 2117 copy: a finished job doesn't go back (`WorkflowJob.status_changeset/3`),
2118 2118 and `{:error, changeset}` says it was refused.
2119 2119
2120 + A `failure` or `success` may land as `warning` instead
2121 + (`conclusion/2`): the stored row, outputs included, is what decides.
2122 +
2120 2123 `opts`: `:conclusion`; `:reason`, recorded as the job's `status_reason`;
2121 2124 `fail_fast: false` to leave its matrix siblings alone (a job cancelled by
2122 2125 its concurrency group is no verdict on the matrix).
@@ -2127,13 +2130,21 @@ defmodule GitGud.Workflows do
2127 2130
2128 2131 {:ok, result} =
2129 2132 Repo.transaction(fn ->
2130 − WorkflowJob
2131 − |> where([j], j.id == ^job.id)
2132 − |> lock("FOR UPDATE")
2133 − |> Repo.one!()
2134 − |> WorkflowJob.status_changeset(status, opts)
2133 + locked =
2134 + WorkflowJob
2135 + |> where([j], j.id == ^job.id)
2136 + |> lock("FOR UPDATE")
2137 + |> Repo.one!()
2138 +
2139 + {concluded, why} = conclusion(locked, status)
2140 +
2141 + locked
2142 + |> WorkflowJob.status_changeset(concluded, opts)
2135 2143 |> then(fn cs ->
2136 − if reason, do: Ecto.Changeset.change(cs, status_reason: reason), else: cs
2144 + case reason || why do
2145 + nil -> cs
2146 + reason -> Ecto.Changeset.change(cs, status_reason: reason)
2147 + end
2137 2148 end)
2138 2149 |> Repo.update()
2139 2150 end)
@@ -2305,7 +2316,11 @@ defmodule GitGud.Workflows do
2305 2316 # * step never started → skipped
2306 2317 # * step was in flight → inherit the job's terminal status (a skipped
2307 2318 # job's steps are skipped)
2308 − defp finalize_steps(%WorkflowJob{id: jid, status: job_status}) do
2319 + #
2320 + # A `warning` job's steps read as what happened to them: failed under
2321 + # `continue-on-error`, else succeeded.
2322 + defp finalize_steps(%WorkflowJob{id: jid} = job) do
2323 + job_status = runner_result(job)
2309 2324 now = DateTime.utc_now() |> DateTime.truncate(:second)
2310 2325
2311 2326 from(s in WorkflowStep,
@@ -2530,23 +2545,75 @@ defmodule GitGud.Workflows do
2530 2545 end
2531 2546
2532 2547 @doc """
2533 − The result a job's dependents see: `success` for a job that failed
2534 − under `continue-on-error: true`, its status otherwise. The job itself
2535 − still shows as failed.
2548 + The result a job's dependents see: `success` for a `warning` job, its
2549 + status otherwise — as on GitHub, where a job that failed under
2550 + `continue-on-error` reads as `success` to `needs.<id>.result`. The job
2551 + itself shows as `warning`.
2552 +
2553 + A `failure` under `continue-on-error` reads as `success` too: rows
2554 + finished before `warning` existed are stored that way.
2555 + """
2556 + def effective_result(%WorkflowJob{status: "warning"}), do: "success"
2557 +
2558 + def effective_result(%WorkflowJob{status: "failure"} = job) do
2559 + if continue_on_error?(job), do: "success", else: "failure"
2560 + end
2561 +
2562 + def effective_result(%WorkflowJob{status: status}), do: status
2536 2563
2537 − `continue-on-error` is honoured when it's literally true, or `"true"`
2564 + @doc """
2565 + Whether a job's `continue-on-error` is on: literally true, or `"true"`
2538 2566 once `${{ matrix.* }}` is substituted (`continue-on-error: ${{
2539 2567 matrix.experimental }}`). Any other expression is the runner's to
2540 2568 evaluate, and can't be seen from here: it counts as false.
2541 2569 """
2542 − def effective_result(%WorkflowJob{status: "failure"} = job) do
2543 − case Matrix.substitute(job.definition["continue-on-error"], job.matrix || %{}) do
2544 − flag when flag in [true, "true"] -> "success"
2545 − _ -> "failure"
2546 − end
2570 + def continue_on_error?(%WorkflowJob{} = job) do
2571 + Matrix.substitute(job.definition["continue-on-error"], job.matrix || %{}) in [true, "true"]
2547 2572 end
2548 2573
2549 − def effective_result(%WorkflowJob{status: status}), do: status
2574 + @doc """
2575 + What the runner reported for a job, which for a `warning` job isn't
2576 + its status: `failure` when it failed under `continue-on-error`, else
2577 + `success`.
2578 + """
2579 + def runner_result(%WorkflowJob{status: "warning"} = job),
2580 + do: if(continue_on_error?(job), do: "failure", else: "success")
2581 +
2582 + def runner_result(%WorkflowJob{status: status}), do: status
2583 +
2584 + # The output a job sets to conclude `warning` though it succeeded:
2585 + #
2586 + # outputs:
2587 + # gitgud-conclusion: ${{ steps.scan.outputs.conclusion }}
2588 + #
2589 + # A gitgud extension. GitHub and Forgejo treat it as any other output,
2590 + # so there the job just passes.
2591 + @conclusion_output "gitgud-conclusion"
2592 +
2593 + # What a job given `status` concludes, and why when that isn't
2594 + # `status` itself (for its `status_reason`):
2595 + #
2596 + # * failed under `continue-on-error` → `warning`: it did fail, but
2597 + # was allowed to, which is neither a pass nor a failure to show
2598 + # * succeeded with `gitgud-conclusion` set to `warning` (any case,
2599 + # trimmed; other values are ignored) → `warning`
2600 + #
2601 + # A job that failed without `continue-on-error` stays failed whatever
2602 + # its outputs say: an output can soften a pass, not excuse a failure.
2603 + defp conclusion(%WorkflowJob{} = job, "failure") do
2604 + if continue_on_error?(job),
2605 + do: {"warning", "Failed, but continue-on-error allows it"},
2606 + else: {"failure", nil}
2607 + end
2608 +
2609 + defp conclusion(%WorkflowJob{outputs: %{@conclusion_output => value}}, "success")
2610 + when is_binary(value) do
2611 + if value |> String.trim() |> String.downcase() == "warning",
2612 + do: {"warning", "Succeeded, and reported #{@conclusion_output}: warning"},
2613 + else: {"success", nil}
2614 + end
2615 +
2616 + defp conclusion(_job, status), do: {status, nil}
2550 2617
2551 2618 @doc """
2552 2619 Whether a job's `if:` calls a status-check function — `always()`,
@@ -2572,7 +2639,8 @@ defmodule GitGud.Workflows do
2572 2639 end
2573 2640 end
2574 2641
2575 − # A job failed the run when it failed without `continue-on-error`. A
2642 + # A job failed the run when it failed without `continue-on-error` (a
2643 + # `warning` job passes it, as `effective_result/1` says). A
2576 2644 # job cancelled on its own (fail-fast, a runner that gave up) fails it
2577 2645 # too, as on GitHub; only a cancelled run, or one superseded in a
2578 2646 # concurrency group, concludes `cancelled`.
@@ -2691,13 +2759,16 @@ defmodule GitGud.Workflows do
2691 2759 queued, blocked, waiting, running -> "pending"
2692 2760 success, skipped -> "success" (a skipped required
2693 2761 check passes, as on GitHub)
2762 + warning -> "warning" (Forgejo's state for
2763 + it; a required check passes)
2694 2764 failure, cancelled -> "failure"
2695 2765 cancelled by a newer run or job
2696 2766 in its concurrency group -> "cancelled" (superseded, not
2697 2767 failed: the newer one decides)
2698 2768
2699 − A job that fails under `continue-on-error` still reports `failure`: the
2700 − job did fail, whatever its dependents make of it.
2769 + A job that fails under `continue-on-error` is `warning`: it did fail,
2770 + but was allowed to, and a pill that said `failure` sent people to look
2771 + at a run that passed.
2701 2772
2702 2773 Also wakes the CI dashboards (`broadcast_ci_change/1`).
2703 2774 """
@@ -2776,6 +2847,7 @@ defmodule GitGud.Workflows do
2776 2847 cond do
2777 2848 status in ~w(queued blocked waiting awaiting_approval running) -> "pending"
2778 2849 status in ~w(success skipped) -> "success"
2850 + status == "warning" -> "warning"
2779 2851 status == "cancelled" and superseded_by_group?(job) -> "cancelled"
2780 2852 status in ~w(failure cancelled) -> "failure"
2781 2853 true -> "error"
@@ -2810,12 +2882,19 @@ defmodule GitGud.Workflows do
2810 2882 "running" -> "Running"
2811 2883 "success" -> "Successful" <> took(job, " in ")
2812 2884 "failure" -> "Failing" <> took(job, " after ")
2885 + "warning" -> warning_description(job)
2813 2886 "cancelled" -> job.status_reason || "Cancelled"
2814 2887 "skipped" -> "Skipped"
2815 2888 other -> other
2816 2889 end
2817 2890 end
2818 2891
2892 + defp warning_description(%WorkflowJob{} = job) do
2893 + if continue_on_error?(job),
2894 + do: "Failing" <> took(job, " after ") <> " (continue-on-error)",
2895 + else: "Warning" <> took(job, " after ")
2896 + end
2897 +
2819 2898 defp took(%WorkflowJob{started_at: %DateTime{} = from, completed_at: %DateTime{} = to}, prefix) do
2820 2899 secs = max(DateTime.diff(to, from), 0)
2821 2900
@@ -2952,6 +3031,10 @@ defmodule GitGud.Workflows do
2952 3031 completed_at: nil,
2953 3032 runner_id: nil,
2954 3033 attempt: job.attempt + 1,
3034 + # The attempt's outputs are its own: a stale
3035 + # `gitgud-conclusion: warning` would conclude the
3036 + # re-run before it reported anything.
3037 + outputs: %{},
2955 3038 concurrency_group: nil,
2956 3039 cancel_in_progress: false,
2957 3040 status_reason: nil
modified lib/git_gud/workflows/commit_status.ex
+4 −1
@@ -5,6 +5,9 @@ defmodule GitGud.Workflows.CommitStatus do
5 5 `state` is the GitHub-flavored summary:
6 6 * `pending` — the job is queued, blocked, waiting, awaiting approval or running
7 7 * `success` — the job succeeded, or was skipped
8 + * `warning` — the job passed, but wants a look: it failed under
9 + `continue-on-error`, or reported `gitgud-conclusion: warning`
10 + (Forgejo's state; a required check it matches passes)
8 11 * `failure` — the job failed, or was cancelled
9 12 * `cancelled` — the job was cancelled by a newer run or job in its
10 13 concurrency group: superseded rather than failed (not `success`
@@ -28,7 +31,7 @@ defmodule GitGud.Workflows.CommitStatus do
28 31
29 32 alias GitGud.Repositories.Repository
30 33
31 − @valid_states ~w(pending success failure cancelled error)
34 + @valid_states ~w(pending success warning failure cancelled error)
32 35
33 36 schema "commit_statuses" do
34 37 field :sha, :binary
modified lib/git_gud/workflows/workflow_job.ex
+7 −3
@@ -23,7 +23,11 @@ defmodule GitGud.Workflows.WorkflowJob do
23 23 # `awaiting_approval` = a fork pull request's job that would be queued,
24 24 # held until someone with write access approves the run
25 25 # (`Workflows.approve_pull_request_runs/2`). Not claimable, not final.
26 − @valid_statuses ~w(queued running success failure cancelled blocked skipped waiting awaiting_approval)
26 + # `warning` = finished, and passes, but someone should look: it failed
27 + # under `continue-on-error`, or it succeeded and set the output
28 + # `gitgud-conclusion: warning` (`Workflows.update_job_status/3` decides).
29 + # Dependents and the run read it as `success`.
30 + @valid_statuses ~w(queued running success failure cancelled blocked skipped waiting awaiting_approval warning)
27 31
28 32 schema "workflow_jobs" do
29 33 field :job_id, :string
@@ -105,7 +109,7 @@ defmodule GitGud.Workflows.WorkflowJob do
105 109 status == "running" ->
106 110 fields
107 111
108 − status in ~w(success failure cancelled skipped) ->
112 + status in ~w(success failure cancelled skipped warning) ->
109 113 fields
110 114 |> Map.put(:completed_at, now)
111 115 |> Map.put(:conclusion, Keyword.get(opts, :conclusion, status))
@@ -118,7 +122,7 @@ defmodule GitGud.Workflows.WorkflowJob do
118 122 end
119 123
120 124 @doc "Terminal statuses — job won't transition out of these."
121 − def terminal?(s) when s in ~w(success failure cancelled skipped), do: true
125 + def terminal?(s) when s in ~w(success failure cancelled skipped warning), do: true
122 126 def terminal?(_), do: false
123 127
124 128 def claim_changeset(job, runner_id) do
modified lib/git_gud_web/components/core_components.ex
+2 −0

Click to load diff…

modified lib/git_gud_web/controllers/api/mobile/json.ex
+3 −0

Click to load diff…

modified lib/git_gud_web/controllers/runner_controller.ex
+11 −5

Click to load diff…

modified lib/git_gud_web/live/ci_components.ex
+6 −1

Click to load diff…

modified lib/git_gud_web/live/workflow_live/index.ex
+1 −0

Click to load diff…

modified lib/git_gud_web/live/workflow_live/show.ex
+26 −2

Click to load diff…

added priv/repo/migrations/20261027184400_add_warning_to_commit_status_states.exs
+27 −0

Click to load diff…

modified test/git_gud/branch_protections/status_checks_test.exs
+9 −1

Click to load diff…

modified test/git_gud/workflows/commit_status_test.exs
+36 −0

Click to load diff…

modified test/git_gud/workflows/job_graph_test.exs
+84 −2

Click to load diff…

modified test/git_gud_web/controllers/runner_controller_test.exs
+45 −3

Click to load diff…

added test/git_gud_web/live/workflow_run_warning_test.exs
+76 −0

Click to load diff…

Parents: 8cfafe0