gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ (ci): Show commit and MR CI status

ac5ca27345aa9ecd070e9453c27c4e2ca56c5439 · Michael Hainz · 2026-07-23T09:50:08Z

Changes

12 files changed, +338 -24

MODIFY README.md +3 -2
diff --git a/README.md b/README.md
index 305a76b..5db64c1 100644
--- a/README.md
+++ b/README.md
@@ -90,8 +90,9 @@
90 90 which a runner claims, executes, and streams logs for — visible on the repository's **Actions** tab;
91 91 a vanished runner's task is reclaimed after a timeout. Jobs are matched to runners by `runs-on`
92 92 labels, ordered by `needs` dependencies, and repository owners manage encrypted secrets and
93 - variables that are delivered to runners, jobs support `needs` ordering and `strategy.matrix`, and
94 - runs can be cancelled or re-run from the UI. Non-push events and artifacts are follow-up phases. Guides: [for users](docs/users/ci-runners.md), [for admins](docs/admins/ci-runners.md),
93 + variables that are delivered to runners, jobs support `needs` ordering and `strategy.matrix`, runs
94 + can be cancelled or re-run from the UI, and a commit's CI result shows on its commit and merge-request
95 + pages. Non-push events, artifacts, and scoped/ephemeral runners are follow-up phases. Guides: [for users](docs/users/ci-runners.md), [for admins](docs/admins/ci-runners.md),
95 96 [architecture](docs/maintainers/ci-runners.md)
96 97 activities from; local users can in turn follow a remote repository — or a whole remote user, whose
97 98 public repositories are then followed and shown grouped — and read their pushes (see below)
MODIFY docs/maintainers/ci-runners.md +10 -2
diff --git a/docs/maintainers/ci-runners.md b/docs/maintainers/ci-runners.md
index f9a2102..af09463 100644
--- a/docs/maintainers/ci-runners.md
+++ b/docs/maintainers/ci-runners.md
@@ -16,6 +16,7 @@
16 16 | Task progress | `ci/TaskProgressService.java` | UpdateTask (result → task status + run roll-up, runner back to IDLE) and UpdateLog (resume-safe log-row append, `ack_index`). |
17 17 | Zombie reclaim | `ci/ZombieReclaimService.java` | Scheduled sweep failing RUNNING tasks past their deadline (vanished runner) and rolling up their runs. |
18 18 | Run controls | `ci/ActionRunService.java` | Cancel a run (settle run + unfinished tasks) and re-run a finished run (reset tasks to PENDING, clear logs/outputs). |
19 +| Commit status | `ci/CommitStatusService.java` | Aggregate a commit's runs into one status; shown on commit/MR pages and via the Gitea commit-status API. |
19 20 | Actions UI | `web/ActionResource.java` + `templates/ActionResource/` | Read-only per-repo run list + run detail (jobs and their log rows); sidebar `Actions` tab. |
20 21 | Secrets/variables UI | `web/ActionSettingsResource.java` + `ci/ActionSecretService.java` + `templates/ActionSettingsResource/` | Owner-only CRUD for CI secrets (write-only, encrypted) and variables at `settings/actions`. |
21 22 | Entities | `model/CiRunner.java`, `model/CiRunnerRegistrationToken.java` | Runner state (migration `V19`). |
@@ -134,7 +135,8 @@
134 135 running run but leaves other branches alone), `MatrixExpansionTest` (single- and two-dimension
135 136 matrices expand to one task per cell with a reduced payload; a non-matrix job stays single),
136 137 `MatrixNeedsTest` (a dependent waits for every cell of a needed matrix job, and one failed cell
137 - cancels the dependent).
138 + cancels the dependent), `CommitCiStatusTest` (commit page + MR page show the aggregate badge, the
139 + commit-status API reflects failure, and a commit with no runs stays all-clear).
138 140 - **Zombie reclaim (`ZombieReclaimService`):** a scheduled sweep
139 141 (`gitshark.ci.zombie-reclaim-interval`, default 1m) fails any RUNNING task whose
140 142 `action_task.deadline` has passed — the runner is presumed gone — rolls its run up, and flags the
@@ -150,6 +152,12 @@
150 152 - **Superseded runs:** after ingest creates the run(s) for a push, `ActionRunService.cancelSuperseded`
151 153 cancels that branch's other still-active runs (keeping the just-created ones), so an in-flight run
152 154 is abandoned when a newer commit lands on the same ref. Other branches are unaffected.
155 +- **Commit / MR status:** `CommitStatusService.aggregate` folds a commit's runs
156 + (`ActionRun.Repo.findByRepositoryAndCommitSha`) into one status (worst-of: FAILURE > RUNNING >
157 + CANCELLED > SUCCESS; no runs → none). Shown as a badge on the commit-detail page and on the MR page
158 + (for the source branch's head commit, resolved live). The Gitea `commits/{ref}/status` API now
159 + returns the real aggregate (mapped to `success`/`failure`/`pending`) with one entry per run — a
160 + commit with no runs still reports `success` so Renovate proceeds.
153 161 - **Actions UI:** a read-only `Actions` tab on each repository — `ActionResource` renders a run list
154 162 (workflow, run number, status, event, short commit) and a run detail page with each job and its
155 163 streamed log rows. Read-gated like the rest of the repo UI (404 for a hidden repo). Tested by
@@ -170,7 +178,7 @@
170 178 - **Matrix advanced options:** `include`/`exclude` and `fail-fast`/`max-parallel` are not honored
171 179 (plain dimension cross-product only).
172 180 - **Later phases:** artifacts (`ACTIONS_RESULTS_URL`), repo/org-scoped and ephemeral runners,
173 - commit/MR status, non-push events.
181 + non-push events.
174 182
175 183 ## References
176 184
MODIFY docs/users/ci-runners.md +3 -0
diff --git a/docs/users/ci-runners.md b/docs/users/ci-runners.md
index cb9b05e..8e7d602 100644
--- a/docs/users/ci-runners.md
+++ b/docs/users/ci-runners.md
@@ -43,6 +43,9 @@
43 43 Pushing a new commit to a branch automatically cancels that branch's earlier still-running run, so
44 44 only the latest push keeps running.
45 45
46 +A commit's overall CI result appears as a status badge on its commit page, and a merge request shows
47 +the CI status of its source branch's latest commit.
48 +
46 49 ## Trigger filters
47 50
48 51 Beyond a bare `on: push` (which runs on every branch push), you can scope runs to specific refs:
MODIFY src/main/java/de/workaround/api/ApiModels.java +6 -5
diff --git a/src/main/java/de/workaround/api/ApiModels.java b/src/main/java/de/workaround/api/ApiModels.java
index 85456fd..b1641ee 100644
--- a/src/main/java/de/workaround/api/ApiModels.java
+++ b/src/main/java/de/workaround/api/ApiModels.java
@@ -213,8 +213,9 @@
213 213 }
214 214
215 215 /**
216 - * A Gitea commit status. git-shark has no status store yet, so {@code POST /statuses/{sha}} echoes the
217 - * posted status back (nothing is persisted) and the list/ combined endpoints report an empty, all-clear set.
216 + * A Gitea commit status. The list/combined endpoints report one status per CI run on the commit;
217 + * {@code POST /statuses/{sha}} still only echoes the posted status back (git-shark's statuses come
218 + * from its own runs, not external posts).
218 219 */
219 220 public record CommitStatusView(long id, String state, String context, String description,
220 221 @JsonProperty("target_url") String targetUrl)
@@ -222,9 +223,9 @@
222 223 }
223 224
224 225 /**
225 - * The Gitea combined commit status. With no status store, the combined {@code state} is reported as
226 - * {@code success} (both under the current field name and the legacy {@code worstStatus}) so a Gitea client
227 - * treats the ref as passing and proceeds; the merge endpoint still rejects a real conflict.
226 + * The Gitea combined commit status: the worst-of the commit's CI runs (mapped to {@code success}/
227 + * {@code failure}/{@code pending}). A commit with no runs reports {@code success} (via {@link
228 + * #allClear}) so a Gitea client like Renovate treats the ref as passing and proceeds.
228 229 */
229 230 public record CombinedStatusView(String state, @JsonProperty("worstStatus") String worstStatus, String sha,
230 231 @JsonProperty("total_count") int totalCount, List<CommitStatusView> statuses)
MODIFY src/main/java/de/workaround/api/CommitStatusApiResource.java +45 -10
diff --git a/src/main/java/de/workaround/api/CommitStatusApiResource.java b/src/main/java/de/workaround/api/CommitStatusApiResource.java
index a63514a..5371b91 100644
--- a/src/main/java/de/workaround/api/CommitStatusApiResource.java
+++ b/src/main/java/de/workaround/api/CommitStatusApiResource.java
@@ -2,9 +2,12 @@
2 2
3 3 import java.util.List;
4 4
5 +import de.workaround.ci.CommitStatusService;
5 6 import de.workaround.git.AccessPolicy;
6 7 import de.workaround.git.ForbiddenOperationException;
8 +import de.workaround.git.GitBrowseService;
7 9 import de.workaround.git.GitRepositoryService;
10 +import de.workaround.model.ActionRun;
8 11 import de.workaround.model.Repository;
9 12 import de.workaround.model.User;
10 13 import jakarta.inject.Inject;
@@ -19,11 +22,11 @@
19 22 import jakarta.ws.rs.core.Response;
20 23
21 24 /**
22 - * Gitea commit-status endpoints. git-shark has no status store yet (CI runners exist but are unwired), so the
23 - * combined/list reads report an empty, all-clear set and {@code POST /statuses/{sha}} echoes the posted status
24 - * without persisting it — enough for Renovate to treat a branch as passing and proceed. The {@code ref} segment
25 - * is matched greedily so slash-bearing branch names resolve. Reads follow repository visibility; posting a
26 - * status needs a token and write access.
25 + * Gitea commit-status endpoints. The combined/list reads report the repository's CI runs for the resolved
26 + * commit (one status per run); a commit with no runs stays all-clear {@code success} so Renovate proceeds.
27 + * {@code POST /statuses/{sha}} still only echoes the posted status (git-shark statuses come from its own runs,
28 + * not external posts). The {@code ref} segment is matched greedily so slash-bearing branch names resolve.
29 + * Reads follow repository visibility; posting a status needs a token and write access.
27 30 */
28 31 @Path("/api/v1/repos/{owner}/{name}")
29 32 @Produces(MediaType.APPLICATION_JSON)
@@ -38,13 +41,29 @@
38 41 @Inject
39 42 ApiPrincipal principal;
40 43
44 + @Inject
45 + GitBrowseService browse;
46 +
47 + @Inject
48 + CommitStatusService commitStatus;
49 +
41 50 @GET
42 51 @Path("commits/{ref:.+}/status")
43 52 public ApiModels.CombinedStatusView combined(@PathParam("owner") String owner, @PathParam("name") String name,
44 53 @PathParam("ref") String ref)
45 54 {
46 - requireReadable(owner, name);
47 - return ApiModels.CombinedStatusView.allClear(ref);
55 + Repository repo = readable(owner, name);
56 + String sha = resolveSha(repo, ref);
57 + List<ActionRun> found = commitStatus.runsFor(repo, sha);
58 + if (found.isEmpty())
59 + {
60 + // no runs for this commit — stay all-clear so Renovate and other clients proceed
61 + return ApiModels.CombinedStatusView.allClear(sha);
62 + }
63 + String state = CommitStatusService.toGiteaState(
64 + commitStatus.aggregate(found).orElse(ActionRun.Status.SUCCESS));
65 + List<ApiModels.CommitStatusView> statuses = toStatusViews(found);
66 + return new ApiModels.CombinedStatusView(state, state, sha, statuses.size(), statuses);
48 67 }
49 68
50 69 @GET
@@ -52,8 +71,23 @@
52 71 public List<ApiModels.CommitStatusView> list(@PathParam("owner") String owner, @PathParam("name") String name,
53 72 @PathParam("ref") String ref)
54 73 {
55 - requireReadable(owner, name);
56 - return List.of();
74 + Repository repo = readable(owner, name);
75 + return toStatusViews(commitStatus.runsFor(repo, resolveSha(repo, ref)));
76 + }
77 +
78 + private static List<ApiModels.CommitStatusView> toStatusViews(List<ActionRun> runs)
79 + {
80 + return runs.stream()
81 + .map(run -> new ApiModels.CommitStatusView(run.number, CommitStatusService.toGiteaState(run.status),
82 + "ci/" + run.workflowName, run.status.label, ""))
83 + .toList();
84 + }
85 +
86 + private String resolveSha(Repository repo, String ref)
87 + {
88 + return browse.commit(repositories.repositoryPath(repo), ref)
89 + .map(GitBrowseService.CommitInfo::id)
90 + .orElse(ref);
57 91 }
58 92
59 93 @POST
@@ -77,12 +111,13 @@
77 111 return Response.status(Response.Status.CREATED).entity(echoed).build();
78 112 }
79 113
80 - private void requireReadable(String owner, String name)
114 + private Repository readable(String owner, String name)
81 115 {
82 116 Repository repo = repositories.find(owner, name).orElseThrow(NotFoundException::new);
83 117 if (!accessPolicy.canRead(principal.orNull(), repo))
84 118 {
85 119 throw new NotFoundException();
86 120 }
121 + return repo;
87 122 }
88 123 }
ADD src/main/java/de/workaround/ci/CommitStatusService.java +67 -0
diff --git a/src/main/java/de/workaround/ci/CommitStatusService.java b/src/main/java/de/workaround/ci/CommitStatusService.java
new file mode 100644
index 0000000..3c8cc45
--- /dev/null
+++ b/src/main/java/de/workaround/ci/CommitStatusService.java
@@ -0,0 +1,67 @@
1 +package de.workaround.ci;
2 +
3 +import java.util.List;
4 +import java.util.Optional;
5 +
6 +import de.workaround.model.ActionRun;
7 +import de.workaround.model.Repository;
8 +import jakarta.enterprise.context.ApplicationScoped;
9 +import jakarta.inject.Inject;
10 +
11 +/**
12 + * The overall CI status of a commit (issue #2, phase 3): the aggregate of every run for that commit
13 + * SHA, shown on commit and merge-request pages and exposed through the Gitea commit-status API.
14 + */
15 +@ApplicationScoped
16 +public class CommitStatusService
17 +{
18 + @Inject
19 + ActionRun.Repo runs;
20 +
21 + public List<ActionRun> runsFor(Repository repository, String commitSha)
22 + {
23 + return runs.findByRepositoryAndCommitSha(repository, commitSha);
24 + }
25 +
26 + /**
27 + * The aggregate status of a commit, or empty when it has no runs. Worst-of: any FAILURE →
28 + * FAILURE; else anything still running/pending → RUNNING; else any CANCELLED → CANCELLED; else
29 + * SUCCESS.
30 + */
31 + public Optional<ActionRun.Status> aggregate(Repository repository, String commitSha)
32 + {
33 + return aggregate(runsFor(repository, commitSha));
34 + }
35 +
36 + public Optional<ActionRun.Status> aggregate(List<ActionRun> forCommit)
37 + {
38 + if (forCommit.isEmpty())
39 + {
40 + return Optional.empty();
41 + }
42 + if (forCommit.stream().anyMatch(r -> r.status == ActionRun.Status.FAILURE))
43 + {
44 + return Optional.of(ActionRun.Status.FAILURE);
45 + }
46 + if (forCommit.stream().anyMatch(r -> !r.status.isTerminal()))
47 + {
48 + return Optional.of(ActionRun.Status.RUNNING);
49 + }
50 + if (forCommit.stream().anyMatch(r -> r.status == ActionRun.Status.CANCELLED))
51 + {
52 + return Optional.of(ActionRun.Status.CANCELLED);
53 + }
54 + return Optional.of(ActionRun.Status.SUCCESS);
55 + }
56 +
57 + /** Map an aggregate run status to a Gitea commit-status state. */
58 + public static String toGiteaState(ActionRun.Status status)
59 + {
60 + return switch (status)
61 + {
62 + case SUCCESS -> "success";
63 + case FAILURE, CANCELLED -> "failure";
64 + case PENDING, RUNNING -> "pending";
65 + };
66 + }
67 +}
MODIFY src/main/java/de/workaround/model/ActionRun.java +4 -0
diff --git a/src/main/java/de/workaround/model/ActionRun.java b/src/main/java/de/workaround/model/ActionRun.java
index 355bae9..bf5fc15 100644
--- a/src/main/java/de/workaround/model/ActionRun.java
+++ b/src/main/java/de/workaround/model/ActionRun.java
@@ -104,6 +104,10 @@
104 104 @HQL("select r from ActionRun r where r.repository = :repository and r.ref = :ref "
105 105 + "and r.status in (PENDING, RUNNING)")
106 106 List<ActionRun> findActiveByRepositoryAndRef(Repository repository, String ref);
107 +
108 + @HQL("select r from ActionRun r where r.repository = :repository and r.commitSha = :commitSha "
109 + + "order by r.number desc")
110 + List<ActionRun> findByRepositoryAndCommitSha(Repository repository, String commitSha);
107 111 }
108 112
109 113 }
MODIFY src/main/java/de/workaround/web/MergeRequestResource.java +10 -2
diff --git a/src/main/java/de/workaround/web/MergeRequestResource.java b/src/main/java/de/workaround/web/MergeRequestResource.java
index 2ad532b..d14a815 100644
--- a/src/main/java/de/workaround/web/MergeRequestResource.java
+++ b/src/main/java/de/workaround/web/MergeRequestResource.java
@@ -52,7 +52,7 @@
52 52
53 53 static native TemplateInstance mergeRequest(Repository repo, RepoNav nav, boolean owner, boolean loggedIn,
54 54 UUID currentUserId, boolean canModerate, MergeRequest mr, List<FileDiffView> files, int additions,
55 - int deletions, List<User> assignees, List<MergeRequestComment> discussion);
55 + int deletions, List<User> assignees, List<MergeRequestComment> discussion, String ciStatus);
56 56 }
57 57
58 58 /**
@@ -79,6 +79,9 @@
79 79 GitBrowseService browse;
80 80
81 81 @Inject
82 + de.workaround.ci.CommitStatusService commitStatusService;
83 +
84 + @Inject
82 85 AccessPolicy accessPolicy;
83 86
84 87 @Inject
@@ -186,8 +189,13 @@
186 189 fileIndex++;
187 190 }
188 191 }
192 + String headSha = browse.commits(service.repositoryPath(repo), mr.sourceBranch, 0, 1)
193 + .flatMap(page -> page.commits().stream().findFirst())
194 + .map(GitBrowseService.CommitInfo::id)
195 + .orElse(null);
196 + String ciStatus = headSha == null ? null : commitStatusService.aggregate(repo, headSha).map(Enum::name).orElse(null);
189 197 return Templates.mergeRequest(repo, repoNav.build(repo, uriInfo), isOwner(repo), loggedIn, currentUserId,
190 - canModerate, mr, files, additions, deletions, assignableUsers(repo), discussion);
198 + canModerate, mr, files, additions, deletions, assignableUsers(repo), discussion, ciStatus);
191 199 }
192 200
193 201 /** How many of a repository's top commit authors the pickers offer as suggestions. */
MODIFY src/main/java/de/workaround/web/RepositoryResource.java +6 -2
diff --git a/src/main/java/de/workaround/web/RepositoryResource.java b/src/main/java/de/workaround/web/RepositoryResource.java
index 3e369a2..645df3a 100644
--- a/src/main/java/de/workaround/web/RepositoryResource.java
+++ b/src/main/java/de/workaround/web/RepositoryResource.java
@@ -68,7 +68,7 @@
68 68 boolean hasNext);
69 69
70 70 static native TemplateInstance commit(Repository repo, RepoNav nav, GitBrowseService.CommitInfo commit,
71 - List<GitMergeService.FileDiff> files, int additions, int deletions);
71 + List<GitMergeService.FileDiff> files, int additions, int deletions, String ciStatus);
72 72
73 73 static native TemplateInstance branches(Repository repo, RepoNav nav,
74 74 List<GitBrowseService.BranchInfo> branches);
@@ -92,6 +92,9 @@
92 92 GitMergeService mergeService;
93 93
94 94 @Inject
95 + de.workaround.ci.CommitStatusService commitStatusService;
96 +
97 + @Inject
95 98 AccessPolicy accessPolicy;
96 99
97 100 @Inject
@@ -256,7 +259,8 @@
256 259 Path path = service.repositoryPath(repo);
257 260 GitBrowseService.CommitInfo info = browse.commit(path, id).orElseThrow(NotFoundException::new);
258 261 GitMergeService.DiffView diff = mergeService.commitDiff(path, id).orElseThrow(NotFoundException::new);
259 - return Templates.commit(repo, nav, info, diff.files(), diff.additions(), diff.deletions());
262 + String ciStatus = commitStatusService.aggregate(repo, info.id()).map(Enum::name).orElse(null);
263 + return Templates.commit(repo, nav, info, diff.files(), diff.additions(), diff.deletions(), ciStatus);
260 264 }
261 265
262 266 @GET
MODIFY src/main/resources/templates/MergeRequestResource/mergeRequest.html +1 -0
diff --git a/src/main/resources/templates/MergeRequestResource/mergeRequest.html b/src/main/resources/templates/MergeRequestResource/mergeRequest.html
index 9a7b088..a604a06 100644
--- a/src/main/resources/templates/MergeRequestResource/mergeRequest.html
+++ b/src/main/resources/templates/MergeRequestResource/mergeRequest.html
@@ -7,6 +7,7 @@
7 7 <h2>{mr.title} <span class="issue-no">!{mr.number}</span></h2>
8 8 <p>
9 9 <span class="badge status-{mr.status}">{mr.status.label}</span>
10 + {#if ciStatus}<span class="badge status-{ciStatus}" title="CI status of the source branch head">{ciStatus}</span>{/if}
10 11 <span class="mr-branches"><code>{mr.sourceBranch}</code> &rarr; <code>{mr.targetBranch}</code></span>
11 12 <span class="muted">opened by {#avatar user=mr.author /} {mr.author.username}</span>
12 13 </p>
MODIFY src/main/resources/templates/RepositoryResource/commit.html +3 -1
diff --git a/src/main/resources/templates/RepositoryResource/commit.html b/src/main/resources/templates/RepositoryResource/commit.html
index 1dc5710..2dc796b 100644
--- a/src/main/resources/templates/RepositoryResource/commit.html
+++ b/src/main/resources/templates/RepositoryResource/commit.html
@@ -5,7 +5,9 @@
5 5 <section class="repo-main">
6 6 <p><a href="/repos/{repo.ownerHandle}/{repo.name}/commits/{nav.defaultBranch}">&larr; Commits</a></p>
7 7 <div class="commit-detail">
8 - <h2 class="commit-subject">{commit.message}</h2>
8 + <h2 class="commit-subject">{commit.message}
9 + {#if ciStatus}<span class="badge status-{ciStatus}" title="CI status">{ciStatus}</span>{/if}
10 + </h2>
9 11 <p class="commit-meta muted">
10 12 <code>{commit.id}</code> · {commit.author} · {commit.date}
11 13 </p>
ADD src/test/java/de/workaround/web/CommitCiStatusTest.java +180 -0
diff --git a/src/test/java/de/workaround/web/CommitCiStatusTest.java b/src/test/java/de/workaround/web/CommitCiStatusTest.java
new file mode 100644
index 0000000..4279906
--- /dev/null
+++ b/src/test/java/de/workaround/web/CommitCiStatusTest.java
@@ -0,0 +1,180 @@
1 +package de.workaround.web;
2 +
3 +import java.nio.charset.StandardCharsets;
4 +import java.nio.file.Path;
5 +import java.util.Map;
6 +import java.util.UUID;
7 +
8 +import org.junit.jupiter.api.Test;
9 +
10 +import de.workaround.git.GitBrowseService;
11 +import de.workaround.git.GitRepositoryService;
12 +import de.workaround.git.GitTestSeeder;
13 +import de.workaround.model.ActionRun;
14 +import de.workaround.model.Repository;
15 +import de.workaround.model.User;
16 +import io.quarkus.test.junit.QuarkusTest;
17 +import jakarta.inject.Inject;
18 +import jakarta.transaction.Transactional;
19 +
20 +import static io.restassured.RestAssured.given;
21 +import static org.hamcrest.Matchers.equalTo;
22 +import static org.hamcrest.Matchers.containsString;
23 +import static org.hamcrest.Matchers.not;
24 +
25 +/**
26 + * CI status on commits (issue #2, phase 3): a commit's aggregate run status shows on its page and is
27 + * exposed through the Gitea commit-status API; a commit with no runs stays all-clear.
28 + */
29 +@QuarkusTest
30 +class CommitCiStatusTest
31 +{
32 + @Inject
33 + GitRepositoryService repositories;
34 +
35 + @Inject
36 + GitBrowseService browse;
37 +
38 + @Inject
39 + ActionRun.Repo runs;
40 +
41 + @Inject
42 + de.workaround.git.MergeRequestService mergeRequests;
43 +
44 + @Inject
45 + User.Repo users;
46 +
47 + @Test
48 + void commitPageShowsAggregateBadgeAndApiReflectsFailure()
49 + {
50 + Fixture f = seedRepoWithCommit("cs-a");
51 + seedRun(f.repo, f.head, ActionRun.Status.FAILURE);
52 +
53 + given().when().get("/repos/" + f.owner + "/cs-a/commit/" + f.head)
54 + .then().statusCode(200)
55 + .body(containsString("badge status-FAILURE"));
56 +
57 + given().when().get("/api/v1/repos/" + f.owner + "/cs-a/commits/" + f.head + "/status")
58 + .then().statusCode(200)
59 + .body("state", equalTo("failure"))
60 + .body("total_count", equalTo(1));
61 + }
62 +
63 + @Test
64 + void successRunShowsSuccessBadge()
65 + {
66 + Fixture f = seedRepoWithCommit("cs-b");
67 + seedRun(f.repo, f.head, ActionRun.Status.SUCCESS);
68 +
69 + given().when().get("/repos/" + f.owner + "/cs-b/commit/" + f.head)
70 + .then().statusCode(200)
71 + .body(containsString("badge status-SUCCESS"));
72 + }
73 +
74 + @Test
75 + void commitWithoutRunsHasNoBadgeAndApiStaysSuccess()
76 + {
77 + Fixture f = seedRepoWithCommit("cs-c");
78 +
79 + given().when().get("/repos/" + f.owner + "/cs-c/commit/" + f.head)
80 + .then().statusCode(200)
81 + .body(not(containsString("badge status-")));
82 +
83 + given().when().get("/api/v1/repos/" + f.owner + "/cs-c/commits/" + f.head + "/status")
84 + .then().statusCode(200)
85 + .body("state", equalTo("success"));
86 + }
87 +
88 + @Test
89 + void mergeRequestPageShowsSourceHeadCiStatus()
90 + {
91 + String owner = mergeRequestWithRun("cs-mr", ActionRun.Status.FAILURE);
92 +
93 + given().when().get("/repos/" + owner + "/cs-mr/merge-requests/1")
94 + .then().statusCode(200)
95 + .body(containsString("badge status-FAILURE"));
96 + }
97 +
98 + @Transactional
99 + String mergeRequestWithRun(String repoName, ActionRun.Status status)
100 + {
101 + String username = repoName + "-" + UUID.randomUUID().toString().substring(0, 8);
102 + User owner = new User();
103 + owner.oidcSub = username;
104 + owner.username = username;
105 + owner.persist();
106 + Repository repo = repositories.create(owner, repoName, Repository.Visibility.PUBLIC, null);
107 + try
108 + {
109 + Path bare = repositories.repositoryPath(repo);
110 + GitTestSeeder.seed(bare, Map.of("README.md", "# hi\n".getBytes(StandardCharsets.UTF_8)));
111 + GitTestSeeder.seedBranch(bare, "feature",
112 + Map.of("feature.txt", "x\n".getBytes(StandardCharsets.UTF_8)));
113 + String head = browse.commits(bare, "feature", 0, 1).orElseThrow().commits().get(0).id();
114 + mergeRequests.create(owner, repo, "add feature", null, "feature", "main");
115 +
116 + ActionRun run = new ActionRun();
117 + run.repository = repo;
118 + run.number = runs.maxNumber(repo) + 1;
119 + run.workflowName = "CI";
120 + run.workflowFile = ".forgejo/workflows/ci.yml";
121 + run.event = "push";
122 + run.ref = "refs/heads/feature";
123 + run.commitSha = head;
124 + run.status = status;
125 + run.persist();
126 + return username;
127 + }
128 + catch (Exception e)
129 + {
130 + throw new RuntimeException(e);
131 + }
132 + }
133 +
134 + private record Fixture(String owner, Repository repo, String head)
135 + {
136 + }
137 +
138 + private Fixture seedRepoWithCommit(String repoName)
139 + {
140 + String username = repoName + "-" + UUID.randomUUID().toString().substring(0, 8);
141 + Repository repo = createRepo(username, repoName);
142 + try
143 + {
144 + Path bare = repositories.repositoryPath(repo);
145 + GitTestSeeder.seed(bare, Map.of("README.md", "# hi\n".getBytes(StandardCharsets.UTF_8)));
146 + String head = browse.commits(bare, "main", 0, 1).orElseThrow().commits().get(0).id();
147 + return new Fixture(username, repo, head);
148 + }
149 + catch (Exception e)
150 + {
151 + throw new RuntimeException(e);
152 + }
153 + }
154 +
155 + @Transactional
156 + Repository createRepo(String username, String repoName)
157 + {
158 + User owner = new User();
159 + owner.oidcSub = username;
160 + owner.username = username;
161 + owner.persist();
162 + return repositories.create(owner, repoName, Repository.Visibility.PUBLIC, null);
163 + }
164 +
165 + @Transactional
166 + void seedRun(Repository repository, String sha, ActionRun.Status status)
167 + {
168 + Repository repo = repositories.find(repository.ownerHandle(), repository.name).orElseThrow();
169 + ActionRun run = new ActionRun();
170 + run.repository = repo;
171 + run.number = runs.maxNumber(repo) + 1;
172 + run.workflowName = "CI";
173 + run.workflowFile = ".forgejo/workflows/ci.yml";
174 + run.event = "push";
175 + run.ref = "refs/heads/main";
176 + run.commitSha = sha;
177 + run.status = status;
178 + run.persist();
179 + }
180 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog