neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Workflows: run on: pull_request for same-repository pull requests
5e29d5c · Gabriel Morell · 2026-10-17 04:33
Message
{commit_body(@commit)}
Files changed
modified
PROGRESS.md
+2
−0
| 35 | 35 | ### Workflow triggers |
| 36 | 36 | |
| 37 | 37 | - **`paths:` / `paths-ignore:`** are honoured for `push`, with GitHub's semantics and pattern syntax (`GitGud.Workflows.Glob`). Every push used to run every workflow: bigtrippr's Android workflow, filtered to `android/**`, ran its tests and re-published an identical APK under a new versionCode on each server-only push. The push worker now passes the ref's old SHA; dispatch lists the changed files once (`Git.changed_paths/3`, `git diff --name-only --no-renames -z`), only when some workflow filters on paths. A new branch is diffed from its merge-base with the default branch. Tags and manual runs ignore path filters; anything that can't be determined (or > 3000 files) fails open. |
| 38 | +- **`github.event_name`** is set in every job's context. It was empty, so a bigtrippr job gated on `github.event_name == 'push'` never ran. | |
| 39 | +- **`on: pull_request`** for same-repository pull requests: `opened` (PR creation), `synchronize` (a push that moves an open PR's head, attributed to the pusher) and `reopened` (which first catches the head up with the branch, since closed PRs aren't refreshed on push), each through the `Workflows.Workers.PullRequestDispatch` Oban job so the LiveView and the push worker never wait on it. `Workflows.dispatch_pull_request/3` reads workflows from the PR head, pins `refs/pull/<N>/head`, and records the PR number and branches on the run (migration `add_pull_request_to_workflow_runs`) for the job's `head_ref` / `base_ref`. `types:`, `branches:` / `branches-ignore:` (the target branch) and `paths:` are honoured; `branches-ignore:` (and `tags-ignore:`) now work for `push` too. Fork and federated PRs are not dispatched — they'd run with the target's secrets — and the head is tested, not a merge result. | |
| 38 | 40 | - Follow-up: `branches:` / `tags:` still use the old matcher, whose `*` crosses `/` (GitHub's doesn't). Left alone because changing it changes which existing branch filters fire. |
| 39 | 41 | |
| 40 | 42 | ### Runner protocol |
modified
README.md
+2
−1
| 88 | 88 | - Repo-scoped + org-scoped runner registration with one-shot tokens minted from the settings UI |
| 89 | 89 | - **Matrix builds**, with `runs-on` as a real label set — `strategy.matrix` expands into one job per combination, each claimed independently, so `runs-on: [self-hosted, "${{ matrix.arch }}"]` sends the arm64 leg to an arm64 machine and the amd64 leg to an amd64 one. `include`/`exclude` follow GitHub's semantics, `fail-fast` defaults on, and `needs:` names the group rather than a leg |
| 90 | 90 | - **Five deployment styles** offered in the registration UI: Native binary, Docker, Podman, Podman Quadlet, Kubernetes (Deployment + dind sidecar) — copy-pasteable `register` + `daemon` snippets per style |
| 91 | − - **Triggers**: `push` (with `branches:` / `tags:`, GitHub's rule that naming one excludes the other kind of ref) and `workflow_dispatch` | |
| 91 | + - **Triggers**: `push` (with `branches:` / `branches-ignore:` / `tags:` / `tags-ignore:`, GitHub's rule that naming one kind excludes the other kind of ref, and a list beating its `-ignore` if both are set), `pull_request` and `workflow_dispatch` | |
| 92 | + - **`on: pull_request`** for pull requests within one repository: `types:` `opened`, `synchronize` (the head moved) and `reopened` (the default; other activity types never fire). `branches:` / `branches-ignore:` filter the **target** branch, and `paths:` the PR's diff from its merge-base. Workflows are read from the PR's head commit, so a PR can change its own CI, and the job tests the head itself at `refs/pull/<N>/head` — not a merge with the target, as GitHub's `refs/pull/<N>/merge` does. The job gets `github.event_name`, `head_ref` and `base_ref` (the source and target branches), `ref_name` `<N>/head` and an empty `ref_type`. **Fork and federated pull requests don't run yet**: their head is code the target's maintainers haven't reviewed, and a job is handed the target repository's secrets and a token that can publish its packages; GitHub runs them without secrets and with a read-only token, and until that exists here they don't run at all. `pull_request_target` isn't supported | |
| 92 | 93 | - **`paths:` / `paths-ignore:`** with GitHub's pattern rules (`*` stays within a directory, `**` crosses them, `?`, `[a-z]`; in `paths:`, `!pattern` un-matches and a later positive pattern re-matches). 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 |
| 93 | 94 | - Re-run / cancel buttons on the workflow show page |
| 94 | 95 | - Per-task HS256 JWT minted on `FetchTask`, gates artifact + cache endpoints |
added
docs/plans/workflow-pull-request.md
+186
−0
| 1 | +# Plan: run workflows for pull requests (`on: pull_request`) | |
| 2 | + | |
| 3 | +Status: **executed** (2026-10-17), on top of | |
| 4 | +[workflow-path-filters.md](workflow-path-filters.md) (this reuses its | |
| 5 | +changed-file listing and `paths` matching). Written to be | |
| 6 | +carried out by one agent without further questions. | |
| 7 | + | |
| 8 | +## Why | |
| 9 | + | |
| 10 | +`on: pull_request` is accepted by the parser (`Yaml.matches?/3` would match | |
| 11 | +it) but nothing ever dispatches that trigger: the only caller of | |
| 12 | +`Workflows.dispatch/2` is the push worker | |
| 13 | +(`Repositories.Workers.CommitBackfill.dispatch_workflows/4`), which always | |
| 14 | +sends `"push"`. Workflows written for GitHub/Forgejo that test pull | |
| 15 | +requests never run here, and the PR page's checks (it already reads commit | |
| 16 | +statuses, `lib/git_gud_web/live/pr_live/show.ex`) only show whatever a | |
| 17 | +push to the same SHA happened to produce. | |
| 18 | + | |
| 19 | +There is also a context bug to fix first: the runner context carries the | |
| 20 | +trigger as `event` but not `event_name`, so `github.event_name` is empty in | |
| 21 | +every job. A bigtrippr job with | |
| 22 | +`if: github.event_name == 'push' && …` was skipped on every push because of | |
| 23 | +it. | |
| 24 | + | |
| 25 | +## Scope | |
| 26 | + | |
| 27 | +In: | |
| 28 | +- `github.event_name` in the runner context. | |
| 29 | +- Same-repository pull requests (source and target branches in one repo): | |
| 30 | + runs on `opened`, `synchronize` (head moved), `reopened`. | |
| 31 | +- `types:` filter, `branches:` / `branches-ignore:` (against the **target** | |
| 32 | + branch, as on GitHub), `paths:` / `paths-ignore:` (against the PR's diff | |
| 33 | + from its merge-base). | |
| 34 | +- The runner context a PR job expects: `ref`, `ref_name`, `head_ref`, | |
| 35 | + `base_ref`, `sha`, `event_name`. | |
| 36 | + | |
| 37 | +Out (state it in the docs, don't build it): | |
| 38 | +- **Cross-repository (fork) and federated pull requests.** Their head is | |
| 39 | + code the target's maintainers haven't reviewed, and a job is handed the | |
| 40 | + **target** repository's secrets (`runner_controller.ex`: | |
| 41 | + `GitGud.Secrets.resolve_for(repo)`) and a token that can publish its | |
| 42 | + packages. GitHub runs fork PRs without secrets and with a read-only | |
| 43 | + token; doing that safely here is its own piece of work. Until then, do | |
| 44 | + not dispatch for them at all, and log at debug level why. | |
| 45 | +- `pull_request_target`, the `closed` / `edited` / `labeled` / `assigned` | |
| 46 | + activity types, merge refs (`refs/pull/N/merge`; GitHub tests the merge | |
| 47 | + result, this tests the head — say so in the docs). | |
| 48 | + | |
| 49 | +## Where things are | |
| 50 | + | |
| 51 | +| | | | |
| 52 | +|---|---| | |
| 53 | +| Runner context | `lib/git_gud_web/controllers/runner_controller.ex`, the map with `"event" => run.trigger` (~line 288) | | |
| 54 | +| Ref → filter ctx | `lib/git_gud/workflows/ref.ex` (`Ref.context/1`, `Ref.name/1`, `Ref.type/1`) | | |
| 55 | +| Dispatch | `lib/git_gud/workflows.ex` — `dispatch/2`, `create_run/2`; runs store `trigger`, `head_sha`, `head_ref` | | |
| 56 | +| Trigger matching | `lib/git_gud/workflows/yaml.ex` | | |
| 57 | +| PRs | `lib/git_gud/pull_requests.ex`: `do_create_pull_request/3` (opened), `set_state/2` (reopen: `"open"`), `refresh_cross_repo_heads/3` (called on every push via `CommitBackfill.refresh_cross_repo_prs/3`; despite the name it also covers same-repo PRs, whose `source_repository_id` is the repo itself — that is the `synchronize` moment); `PullRequest.cross_repo?/1`, `PullRequest.federated?/1` | | |
| 58 | +| Commit statuses | `lib/git_gud/workflows/commit_status.ex`, `Workflows.update_run_commit_status/1` (keyed on the run's head SHA, which the PR page reads) | | |
| 59 | +| Tests to copy from | `test/git_gud/workflows/tag_dispatch_test.exs` (on-disk repo + dispatch); find the existing PR tests under `test/git_gud/pull_requests*` for creating PRs in tests | | |
| 60 | + | |
| 61 | +## Steps | |
| 62 | + | |
| 63 | +### 1. `github.event_name` | |
| 64 | + | |
| 65 | +In the runner context map add `"event_name" => run.trigger || "push"` | |
| 66 | +next to `"event"`. Test: the existing runner controller test that asserts | |
| 67 | +on the context (find it: grep `ref_name` under `test/`) gets an assertion | |
| 68 | +for `event_name`. This step is independent: commit it on its own first. | |
| 69 | + | |
| 70 | +### 2. Trigger matching for pull requests | |
| 71 | + | |
| 72 | +In `Yaml`: | |
| 73 | +- `pull_request` config's `types:` — default `["opened", "synchronize", | |
| 74 | + "reopened"]` when absent; the event's action must be in the list. | |
| 75 | +- `branches:` / `branches-ignore:` match the **target** branch (ctx | |
| 76 | + `:branch` is the target's short name). `branches-ignore` may not be | |
| 77 | + supported for push either — add it for both if missing, with GitHub's | |
| 78 | + rule that `branches` and `branches-ignore` can't both be set (if both: | |
| 79 | + use `branches`, log a warning; same choice as paths). | |
| 80 | +- `paths` / `paths-ignore`: unchanged code from the path-filters plan; | |
| 81 | + ctx `:changed_paths` is the PR's diff (step 3). | |
| 82 | +- `tags:` never applies to pull requests. | |
| 83 | + | |
| 84 | +`matches?/3` gains the action through ctx (`:action`), not a new arity. | |
| 85 | +Pure tests in `trigger_filters_test.exs`: types default and explicit; | |
| 86 | +target-branch filter; `branches-ignore`; paths on a PR. | |
| 87 | + | |
| 88 | +### 3. `Workflows.dispatch_pull_request(pr, action, opts)` | |
| 89 | + | |
| 90 | +New public function in `GitGud.Workflows`: | |
| 91 | + | |
| 92 | +- Return `{:ok, []}` without doing anything when `pr.state != "open"`, | |
| 93 | + `PullRequest.cross_repo?(pr)` or `PullRequest.federated?(pr)`. | |
| 94 | +- Workflows are read from the PR **head** commit (`pr.head_sha`) — that is | |
| 95 | + GitHub's behaviour for `pull_request` and what lets a PR change its own | |
| 96 | + CI. (Fork PRs are excluded above, so this is code from the repo's own | |
| 97 | + writers.) | |
| 98 | +- Pin `refs/pull/<N>/head` to `pr.head_sha` in the repo | |
| 99 | + (`Git.update_ref/4`, as `pin_pull_head`/`refresh_*` already do for | |
| 100 | + cross-repo PRs), so the ref the job is told about exists and | |
| 101 | + `actions/checkout` can fetch it. | |
| 102 | +- Changed paths: `Git.changed_paths(repo_path, merge_base(target head, | |
| 103 | + pr.head_sha), pr.head_sha)` — only if some workflow is path-filtered; | |
| 104 | + fail open as in the path plan. | |
| 105 | +- ctx: `%{branch: target short name, action: action, changed_paths: …}`. | |
| 106 | +- Create runs with `trigger: "pull_request"`, `head_sha: pr.head_sha`, | |
| 107 | + `head_ref: "refs/pull/<N>/head"`, and store the PR's number, source and | |
| 108 | + target refs on the run. Add a migration | |
| 109 | + (`mix ecto.gen.migration add_pull_request_to_workflow_runs`) with | |
| 110 | + nullable `pull_request_number :integer`, `pr_head_ref :string`, | |
| 111 | + `pr_base_ref :string` on `workflow_runs`, and cast them in | |
| 112 | + `WorkflowRun`'s changeset. Refactor `dispatch/2`'s inner loop into a | |
| 113 | + shared private function rather than duplicating it. | |
| 114 | +- Return `{:ok, runs}`. | |
| 115 | + | |
| 116 | +### 4. The runner context for a PR job | |
| 117 | + | |
| 118 | +In `runner_controller.ex`, when `run.trigger == "pull_request"`: | |
| 119 | +- `"ref" => "refs/pull/<N>/head"` (already the run's `head_ref`), | |
| 120 | + `"ref_name" => "<N>/head"`, `"ref_type" => "branch"`? — GitHub sets | |
| 121 | + `ref_type` empty for PRs; use `""`. | |
| 122 | +- `"head_ref" => run.pr_head_ref`, `"base_ref" => run.pr_base_ref` (short | |
| 123 | + branch names, as GitHub gives them). | |
| 124 | +- `"event_name" => "pull_request"` (from step 1). | |
| 125 | +- `"event"` stays the trigger string — check what forgejo-runner/act does | |
| 126 | + with `event` (it may expect the event *payload* object there; if the | |
| 127 | + existing push jobs work with a string, keep it, and do not change push | |
| 128 | + behaviour in this plan). | |
| 129 | + | |
| 130 | +Make sure `Ref.name/1` / `Ref.type/1` are not fed `refs/pull/...` paths | |
| 131 | +that make them crash or misreport; add clauses or bypass them for PR runs. | |
| 132 | + | |
| 133 | +### 5. Call sites | |
| 134 | + | |
| 135 | +- **opened**: at the end of `do_create_pull_request/3`, after the | |
| 136 | + transaction succeeds, call `Workflows.dispatch_pull_request(pr, "opened", | |
| 137 | + triggered_by_id: author.id)`. Do it outside the transaction; a dispatch | |
| 138 | + failure must never fail PR creation (rescue + log). | |
| 139 | +- **synchronize**: in `refresh_cross_repo_heads/3`, for each PR whose | |
| 140 | + `head_sha` actually changed (it has `old_sha`), call it with | |
| 141 | + `"synchronize"` and the pusher if available (thread `pusher_id` from | |
| 142 | + `CommitBackfill.refresh_cross_repo_prs/3` if it's cheap; else nil). | |
| 143 | + This function already handles same-repo PRs; cross-repo ones are dropped | |
| 144 | + inside `dispatch_pull_request/3`. | |
| 145 | +- **reopened**: in `set_state/2` when moving `"closed"` → `"open"` | |
| 146 | + successfully. Check whether the head moved while closed (re-resolve the | |
| 147 | + source ref) — if `refresh_*` already keeps `head_sha` current for closed | |
| 148 | + PRs, no extra work; otherwise refresh `head_sha` before dispatching. | |
| 149 | +- Dispatch is DB + git work; if PR creation runs inside a LiveView | |
| 150 | + process, enqueue it (Oban, like `CommitBackfill`) instead of running it | |
| 151 | + inline. Look at how `CommitBackfill` is enqueued and copy that shape | |
| 152 | + (`Workers.PullRequestDispatch`, args: pr id, action, actor id). | |
| 153 | + | |
| 154 | +### 6. Tests | |
| 155 | + | |
| 156 | +On disk, modelled on `tag_dispatch_test.exs`: | |
| 157 | +- same-repo PR opened → a `pull_request` run on the PR head SHA, with | |
| 158 | + `head_ref` `refs/pull/N/head`, and that ref exists in the repo. | |
| 159 | +- push to the source branch → a `synchronize` run (and the usual push run). | |
| 160 | +- `types: [opened]` → no run on synchronize. | |
| 161 | +- `branches: [master]` → runs for a PR targeting master, not one targeting | |
| 162 | + `release`. | |
| 163 | +- `paths: ["lib/**"]` → PR that only changes `docs/` → no run. | |
| 164 | +- workflow present only on the PR branch (added by the PR) → it runs | |
| 165 | + (workflows come from the head). | |
| 166 | +- cross-repo PR (fork fixture, if the test helpers have one; else build | |
| 167 | + the struct with a different `source_repository_id`) → no run, and the | |
| 168 | + target's secrets are never resolved for it. | |
| 169 | +- closed then reopened → a `reopened` run. | |
| 170 | +- runner context for a PR job (controller test): `event_name`, | |
| 171 | + `head_ref`, `base_ref`, `ref`, `ref_name`. | |
| 172 | + | |
| 173 | +### 7. Docs | |
| 174 | + | |
| 175 | +Where workflow support is documented (see the path plan's step 7): `on: | |
| 176 | +pull_request` with the supported `types`, that it tests the PR head (not a | |
| 177 | +merge result), that `branches` means the target branch, and that fork and | |
| 178 | +federated PRs don't run yet and why (secrets). | |
| 179 | + | |
| 180 | +## Done when | |
| 181 | + | |
| 182 | +- `mix precommit` passes. | |
| 183 | +- Commits, in the repository's message style: (1) `event_name`, (2) the | |
| 184 | + pull request trigger (migration included). Separate from the path-filter | |
| 185 | + commit. | |
| 186 | +- **Not pushed.** Pushing gitgud deploys the forge. |
modified
lib/git_gud/pull_requests.ex
+35
−4
| 24 | 24 | alias GitGud.Repositories |
| 25 | 25 | alias GitGud.Repositories.Numbering |
| 26 | 26 | alias GitGud.Repositories.Repository |
| 27 | + alias GitGud.Workflows.Workers.PullRequestDispatch | |
| 27 | 28 | |
| 28 | 29 | @target_type "pull_request" |
| 29 | 30 |
| 245 | 246 | |
| 246 | 247 | with {:ok, pr} <- result do |
| 247 | 248 | _ = GitGud.Federation.Publishing.publish_pr_opened(repo, pr, author) |
| 249 | + :ok = PullRequestDispatch.enqueue(pr, "opened", uid) | |
| 248 | 250 | {:ok, pr} |
| 249 | 251 | end |
| 250 | 252 | end |
| 464 | 466 | the target repo for every open cross-repo PR that has `source_repo` + |
| 465 | 467 | `branch` as its source, and re-snapshots `head_sha` on the PR row. |
| 466 | 468 | """ |
| 467 | − def refresh_cross_repo_heads(%Repository{} = source_repo, branch, new_sha) do | |
| 469 | + def refresh_cross_repo_heads(%Repository{} = source_repo, branch, new_sha, pusher_id \\ nil) do | |
| 468 | 470 | prs = |
| 469 | 471 | Repo.all( |
| 470 | 472 | from p in PullRequest, |
| 507 | 509 | to: Git.to_hex(new_sha), |
| 508 | 510 | commits: count_new_commits(source_repo, old_sha, new_sha) |
| 509 | 511 | }) |
| 512 | + | |
| 513 | + # `pull_request` workflows' `synchronize`. Fork PRs pass through | |
| 514 | + # here too and are turned away by the dispatcher. | |
| 515 | + :ok = PullRequestDispatch.enqueue(updated, "synchronize", pusher_id) | |
| 510 | 516 | end |
| 511 | 517 | end |
| 512 | 518 |
| 573 | 579 | def set_state(%PullRequest{state: "merged"}, "open"), do: {:error, :merged} |
| 574 | 580 | |
| 575 | 581 | def set_state(%PullRequest{} = pr, state) when state in ["open", "closed"] do |
| 576 | − pr | |
| 577 | − |> PullRequest.state_changeset(state) | |
| 578 | − |> Repo.update() | |
| 582 | + result = | |
| 583 | + pr | |
| 584 | + |> PullRequest.state_changeset(state) | |
| 585 | + |> Repo.update() | |
| 586 | + | |
| 587 | + case {pr.state, result} do | |
| 588 | + {"closed", {:ok, %PullRequest{state: "open"} = reopened}} -> | |
| 589 | + # Pushes to a closed PR's branch don't move its head (only open PRs | |
| 590 | + # are refreshed on push), so catch up before its CI runs. | |
| 591 | + reopened = refresh_head_on_reopen(reopened) | |
| 592 | + :ok = PullRequestDispatch.enqueue(reopened, "reopened", nil) | |
| 593 | + {:ok, reopened} | |
| 594 | + | |
| 595 | + _ -> | |
| 596 | + result | |
| 597 | + end | |
| 598 | + end | |
| 599 | + | |
| 600 | + # Same-repository PRs only: a fork's branch lives in another repository | |
| 601 | + # and would need fetching, and its CI doesn't run here anyway. | |
| 602 | + defp refresh_head_on_reopen(%PullRequest{} = pr) do | |
| 603 | + with false <- PullRequest.cross_repo?(pr) or PullRequest.federated?(pr), | |
| 604 | + %Repository{} = repo <- Repo.get(Repository, pr.repository_id), | |
| 605 | + {:ok, head} when head != pr.head_sha <- Repositories.resolve(repo, pr.source_ref) do | |
| 606 | + pr |> Ecto.Changeset.change(head_sha: head) |> Repo.update!() | |
| 607 | + else | |
| 608 | + _ -> pr | |
| 609 | + end | |
| 579 | 610 | end |
| 580 | 611 | |
| 581 | 612 | # ── mergeability ───────────────────────────────────────────────────── |
modified
lib/git_gud/repositories/workers/commit_backfill.ex
+3
−3
| 42 | 42 | Webhooks.notify(repo, "push", push_payload(repo, ref, old_sha, new_sha, :update)) |
| 43 | 43 | dispatch_workflows(repo, ref, old_sha, new_sha, pusher_id) |
| 44 | 44 | publish_push(repo, ref, old_sha, new_sha) |
| 45 | − refresh_cross_repo_prs(repo, ref, new_sha) | |
| 45 | + refresh_cross_repo_prs(repo, ref, new_sha, pusher_id) | |
| 46 | 46 | enqueue_language_stats(repo, ref, new_sha) |
| 47 | 47 | :ok |
| 48 | 48 | end |
| 62 | 62 | |
| 63 | 63 | defp enqueue_language_stats(_repo, _ref, _sha), do: :ok |
| 64 | 64 | |
| 65 | − defp refresh_cross_repo_prs(repo, ref, new_sha) do | |
| 65 | + defp refresh_cross_repo_prs(repo, ref, new_sha, pusher_id) do | |
| 66 | 66 | branch = |
| 67 | 67 | case ref do |
| 68 | 68 | "refs/heads/" <> b -> b |
| 70 | 70 | end |
| 71 | 71 | |
| 72 | 72 | if branch do |
| 73 | − GitGud.PullRequests.refresh_cross_repo_heads(repo, branch, new_sha) | |
| 73 | + GitGud.PullRequests.refresh_cross_repo_heads(repo, branch, new_sha, pusher_id) | |
| 74 | 74 | end |
| 75 | 75 | end |
| 76 | 76 |
modified
lib/git_gud/workflows.ex
+123
−27
Click to load diff…
added
lib/git_gud/workflows/workers/pull_request_dispatch.ex
+73
−0
Click to load diff…
modified
lib/git_gud/workflows/workflow_run.ex
+11
−1
Click to load diff…
modified
lib/git_gud/workflows/yaml.ex
+75
−39
Click to load diff…
modified
lib/git_gud_web/controllers/runner_controller.ex
+23
−0
Click to load diff…
added
priv/repo/migrations/20261017042727_add_pull_request_to_workflow_runs.exs
+14
−0
Click to load diff…
added
test/git_gud/workflows/pull_request_dispatch_test.exs
+317
−0
Click to load diff…
modified
test/git_gud/workflows/trigger_filters_test.exs
+111
−0
Click to load diff…
modified
test/git_gud_web/controllers/runner_controller_test.exs
+18
−0
Click to load diff…
Parents: 0239060