gitshark

Clone repository

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

← Commits

✨ (ci): Serve FetchTask so runners claim pending tasks

c32e1806f232a91d2fc18e6ae1d101ba7b703b7e · Michael Hainz · 2026-07-22T09:58:43Z

Changes

7 files changed, +387 -8

MODIFY docs/admins/ci-runners.md +3 -2
diff --git a/docs/admins/ci-runners.md b/docs/admins/ci-runners.md
index 45932b5..92d227d 100644
--- a/docs/admins/ci-runners.md
+++ b/docs/admins/ci-runners.md
@@ -89,10 +89,11 @@
89 89 | `ci_runner_registration_token` | Reusable registration tokens: `token_hash`, `created_by_id`, `created_at`, `last_used`. |
90 90 | `ci_runner` | Registered runners: `uuid` (the `x-runner-uuid` value), `token_hash`, `name`, `labels` (comma-joined), `version`, `status` (`IDLE`/`ACTIVE`/`OFFLINE`/`UNSPECIFIED`), `ephemeral`, `last_seen`, `created_at`. |
91 91 | `action_run` | One workflow run per repository: `number` (per-repo sequential), `workflow_name`, `workflow_file`, `event`, `ref`, `commit_sha`, `triggered_by_id`, `status` (`PENDING`/`RUNNING`/`SUCCESS`/`FAILURE`/`CANCELLED`), timestamps. Deleted with its repository. |
92 -| `action_task` | One job within a run: `run_id`, `name`, `payload`, `runner_id` (the claiming runner, null while pending), `status`, `log_length` (durable log-row count = UpdateLog resume offset), `deadline` (zombie timeout), timestamps. Deleted with its run. |
92 +| `action_task` | One job within a run: `seq` (surrogate int64 id handed to runners), `run_id`, `name`, `payload`, `runner_id` (the claiming runner, null while pending), `status`, `log_length` (durable log-row count = UpdateLog resume offset), `deadline` (zombie timeout), timestamps. Deleted with its run. |
93 93 | `action_log` | One log row of a task: `task_id`, `line_index` (0-based), `content`, `timestamp`. Deleted with its task. |
94 94
95 -`ci_runner*` are introduced by migration `V19__ci_runners.sql`; `action_*` by `V23__action_runs.sql`.
95 +`ci_runner*` are introduced by migration `V19__ci_runners.sql`; `action_*` by `V23__action_runs.sql`
96 +(and `V24__action_task_seq.sql` adds the `action_task.seq` surrogate id).
96 97 The `ci_runner*` tables hold no repository data (losing them only forces re-registration); the
97 98 `action_*` tables hold run history and logs, tied to their repository by cascade.
98 99
MODIFY docs/maintainers/ci-runners.md +16 -6
diff --git a/docs/maintainers/ci-runners.md b/docs/maintainers/ci-runners.md
index c3fc63e..7fee4e5 100644
--- a/docs/maintainers/ci-runners.md
+++ b/docs/maintainers/ci-runners.md
@@ -12,8 +12,9 @@
12 12 | Generated messages | `de.workaround.ci.proto.*` (build output) | Protobuf message classes generated by `protobuf-maven-plugin` at `generate-sources`. |
13 13 | Connect endpoint | `ci/ConnectRunnerResource.java` | JAX-RS resource serving the Connect unary RPCs under `/api/actions`. |
14 14 | Registration/presence | `ci/RunnerRegistrationService.java` | Token issue, runner register/declare/authenticate, list/delete. |
15 +| Task dispatch | `ci/TaskDispatchService.java` | FetchTask: authenticate, claim the oldest PENDING task, flip task+run to RUNNING, set the runner ACTIVE and the task deadline — atomically. |
15 16 | Entities | `model/CiRunner.java`, `model/CiRunnerRegistrationToken.java` | Runner state (migration `V19`). |
16 -| Run entities | `model/ActionRun.java`, `model/ActionTask.java`, `model/ActionLog.java` | Run/job/log-row persistence (migration `V23`). |
17 +| Run entities | `model/ActionRun.java`, `model/ActionTask.java`, `model/ActionLog.java` | Run/job/log-row persistence (migrations `V23`, `V24`). `ActionTask.seq` (`bigserial`) is the surrogate int64 `Task.id` for the wire. |
17 18 | Workflow ingest | `ci/WorkflowIngestService.java`, `ci/WorkflowRunFactory.java` | Post-receive hook: parse `.forgejo`/`.gitea` workflows at the pushed head, evaluate `on: push`, persist a run + its tasks. Rows are created but not yet dispatched. |
18 19 | Admin UI | `ci/AdminRunnerResource.java` + `templates/AdminRunnerResource/` | Token generation, runner list, deletion. |
19 20 | Admin gate | `account/AdminAccess.java` | Config-driven instance-admin check. |
@@ -57,17 +58,26 @@
57 58 updated branch, parses them (Jackson `YAMLMapper`), and for those triggered by `push` persists one
58 59 `action_run` (per-repo `number`, PENDING) with one PENDING `action_task` per job via
59 60 `WorkflowRunFactory` (`@Transactional`). Handles the YAML-1.1 `on:`→boolean-`true` key coercion.
61 +- **`FetchTask` dispatch:** a registered runner claims the oldest PENDING task (`TaskDispatchService`,
62 + one transaction) — task+run flip to RUNNING, the runner goes ACTIVE, `action_task.deadline` is set,
63 + and the task is delivered with its surrogate int64 `seq` id and `workflow_payload`. The candidate
64 + row is locked `FOR UPDATE SKIP LOCKED` (id-only select, to keep the lock off the nullable `runner`
65 + join) so concurrent fetchers never claim the same task. Auth failures return the Connect
66 + `unauthenticated` error. No long-poll; `tasks_version` is a coarse max-`seq`.
60 67 - Tests: `RunnerRegistrationServiceTest` (service), `ConnectRunnerResourceTest` (protobuf-over-HTTP
61 68 round-trip for Ping/Register/Declare + auth failures), `AdminAccessTest` (admin gate),
62 69 `ActionRunPersistenceTest` (run/task/log persistence, per-repo run numbering, pending-task lookup),
63 - `WorkflowIngestServiceTest` (push → run/task creation, non-push trigger and no-workflow are no-ops).
70 + `WorkflowIngestServiceTest` (push → run/task creation, non-push trigger and no-workflow are no-ops),
71 + `FetchTaskTest` (claim oldest pending over the wire, empty queue, bad credentials, and two runners
72 + racing one task → claimed at most once).
64 73
65 74 ## What still needs to be implemented
66 75
67 -- **Run loop:** `FetchTask` (long-poll with `tasks_version`), `UpdateTask`, `UpdateLog` (offset /
68 - `ack_index` resume). Not served yet — the ingested PENDING tasks are the queue it will drain.
69 -- **Per-job payload expansion:** ingest stores the raw workflow YAML in `action_task.payload`;
70 - `Task.workflow_payload` needs the single job isolated/expanded. No `needs`/`matrix` yet.
76 +- **Run loop (remaining):** `UpdateTask` (step/task state) and `UpdateLog` (offset / `ack_index`
77 + resume). `FetchTask` is served; these two report progress back. Add long-poll and a real
78 + state-driven `tasks_version` when they land.
79 +- **Per-job payload expansion:** ingest/dispatch deliver the raw workflow YAML as `workflow_payload`;
80 + it needs the single job isolated/expanded. No `needs`/`matrix` yet.
71 81 - **Trigger refinement:** only bare `on: push` is honored; branch/tag/path filters and other events
72 82 (tag push, `pull_request`) are not evaluated.
73 83 - **Run UI:** per-repository run list + run detail with live per-step status and logs.
MODIFY src/main/java/de/workaround/ci/ConnectRunnerResource.java +38 -0
diff --git a/src/main/java/de/workaround/ci/ConnectRunnerResource.java b/src/main/java/de/workaround/ci/ConnectRunnerResource.java
index 41b41b2..082d51b 100644
--- a/src/main/java/de/workaround/ci/ConnectRunnerResource.java
+++ b/src/main/java/de/workaround/ci/ConnectRunnerResource.java
@@ -3,16 +3,21 @@
3 3 import java.util.Arrays;
4 4 import java.util.List;
5 5
6 +import com.google.protobuf.ByteString;
6 7 import com.google.protobuf.InvalidProtocolBufferException;
7 8
8 9 import de.workaround.ci.proto.ping.v1.PingRequest;
9 10 import de.workaround.ci.proto.ping.v1.PingResponse;
10 11 import de.workaround.ci.proto.runner.v1.DeclareRequest;
11 12 import de.workaround.ci.proto.runner.v1.DeclareResponse;
13 +import de.workaround.ci.proto.runner.v1.FetchTaskRequest;
14 +import de.workaround.ci.proto.runner.v1.FetchTaskResponse;
12 15 import de.workaround.ci.proto.runner.v1.RegisterRequest;
13 16 import de.workaround.ci.proto.runner.v1.RegisterResponse;
14 17 import de.workaround.ci.proto.runner.v1.Runner;
15 18 import de.workaround.ci.proto.runner.v1.RunnerStatus;
19 +import de.workaround.ci.proto.runner.v1.Task;
20 +import de.workaround.model.ActionTask;
16 21 import de.workaround.model.CiRunner;
17 22 import jakarta.inject.Inject;
18 23 import jakarta.ws.rs.Consumes;
@@ -43,6 +48,9 @@
43 48 @Inject
44 49 RunnerRegistrationService runnerService;
45 50
51 + @Inject
52 + TaskDispatchService dispatchService;
53 +
46 54 @POST
47 55 @Path("/ping.v1.PingService/Ping")
48 56 public Response ping(byte[] body) throws InvalidProtocolBufferException
@@ -89,6 +97,36 @@
89 97 }
90 98 }
91 99
100 + @POST
101 + @Path("/runner.v1.RunnerService/FetchTask")
102 + public Response fetchTask(@HeaderParam("x-runner-uuid") String uuid, @HeaderParam("x-runner-token") String token,
103 + byte[] body) throws InvalidProtocolBufferException
104 + {
105 + FetchTaskRequest.parseFrom(body); // tasks_version is advisory; phase 1 always checks the queue
106 + try
107 + {
108 + TaskDispatchService.Fetched fetched = dispatchService.fetch(uuid, token);
109 + FetchTaskResponse.Builder response = FetchTaskResponse.newBuilder()
110 + .setTasksVersion(fetched.tasksVersion());
111 + fetched.task().ifPresent(task -> response.setTask(toProto(task)));
112 + return ok(response.build().toByteArray());
113 + }
114 + catch (RunnerAuthenticationException e)
115 + {
116 + return connectError(Response.Status.UNAUTHORIZED, "unauthenticated", e.getMessage());
117 + }
118 + }
119 +
120 + private static Task toProto(ActionTask task)
121 + {
122 + Task.Builder builder = Task.newBuilder().setId(task.seq);
123 + if (task.payload != null)
124 + {
125 + builder.setWorkflowPayload(ByteString.copyFromUtf8(task.payload));
126 + }
127 + return builder.build();
128 + }
129 +
92 130 private static Runner toProto(CiRunner runner, String plaintextSecret)
93 131 {
94 132 Runner.Builder builder = Runner.newBuilder()
ADD src/main/java/de/workaround/ci/TaskDispatchService.java +98 -0
diff --git a/src/main/java/de/workaround/ci/TaskDispatchService.java b/src/main/java/de/workaround/ci/TaskDispatchService.java
new file mode 100644
index 0000000..717f893
--- /dev/null
+++ b/src/main/java/de/workaround/ci/TaskDispatchService.java
@@ -0,0 +1,98 @@
1 +package de.workaround.ci;
2 +
3 +import java.time.Duration;
4 +import java.time.Instant;
5 +import java.util.List;
6 +import java.util.Optional;
7 +import java.util.UUID;
8 +
9 +import de.workaround.model.ActionRun;
10 +import de.workaround.model.ActionTask;
11 +import de.workaround.model.CiRunner;
12 +import jakarta.enterprise.context.ApplicationScoped;
13 +import jakarta.inject.Inject;
14 +import jakarta.persistence.EntityManager;
15 +import jakarta.transaction.Transactional;
16 +
17 +/**
18 + * Hands PENDING tasks to runners over FetchTask (issue #2, phase 1). Claiming a task flips it (and its
19 + * run) to RUNNING, records the claiming runner and a {@link #TASK_TIMEOUT}-based {@link
20 + * ActionTask#deadline} for later zombie reclaim, and marks the runner ACTIVE — all in one transaction
21 + * so a task is never handed to two runners: the candidate row is selected {@code FOR UPDATE SKIP
22 + * LOCKED}, so concurrent fetchers pick distinct rows (or none) rather than racing on the same one.
23 + *
24 + * <p>Phase-1 scope: no long-poll (returns immediately, empty when the queue is drained) and a coarse
25 + * {@code tasks_version} = highest task id issued (bumps on task creation, not on state change).
26 + */
27 +@ApplicationScoped
28 +public class TaskDispatchService
29 +{
30 + /** How long a claimed task may run before it is eligible for zombie reclaim (reclaim itself is a later slice). */
31 + static final Duration TASK_TIMEOUT = Duration.ofHours(1);
32 +
33 + @Inject
34 + RunnerRegistrationService runnerService;
35 +
36 + @Inject
37 + ActionTask.Repo tasks;
38 +
39 + @Inject
40 + EntityManager em;
41 +
42 + public record Fetched(Optional<ActionTask> task, long tasksVersion)
43 + {
44 + }
45 +
46 + /**
47 + * Authenticate the runner and claim the oldest PENDING task, if any.
48 + *
49 + * @throws RunnerAuthenticationException if the uuid/token pair is unknown
50 + */
51 + @Transactional
52 + public Fetched fetch(String uuid, String token)
53 + {
54 + CiRunner runner = runnerService.authenticate(uuid, token);
55 + runner.lastSeen = Instant.now();
56 +
57 + Optional<ActionTask> next = lockOldestPending().map(id ->
58 + {
59 + ActionTask task = tasks.findById(id);
60 + claim(task, runner);
61 + return task;
62 + });
63 + return new Fetched(next, tasks.maxSeq());
64 + }
65 +
66 + /**
67 + * Lock the oldest PENDING task's id with {@code FOR UPDATE SKIP LOCKED} so a concurrent fetcher in
68 + * another transaction cannot claim the same row. Selecting only the id keeps the {@code FOR UPDATE}
69 + * off the nullable {@code runner} association (Postgres rejects it on the nullable side of a join).
70 + */
71 + private Optional<UUID> lockOldestPending()
72 + {
73 + @SuppressWarnings("unchecked")
74 + List<UUID> ids = em.createNativeQuery(
75 + "select id from action_task where status = 'PENDING' order by created_at asc limit 1 for update skip locked")
76 + .getResultList();
77 + return ids.isEmpty() ? Optional.empty() : Optional.of(ids.get(0));
78 + }
79 +
80 + private static void claim(ActionTask task, CiRunner runner)
81 + {
82 + Instant now = Instant.now();
83 + task.runner = runner;
84 + task.status = ActionRun.Status.RUNNING;
85 + task.startedAt = now;
86 + task.deadline = now.plus(TASK_TIMEOUT);
87 +
88 + runner.status = CiRunner.Status.ACTIVE;
89 +
90 + ActionRun run = task.run;
91 + if (run.status == ActionRun.Status.PENDING)
92 + {
93 + run.status = ActionRun.Status.RUNNING;
94 + run.startedAt = now;
95 + }
96 + }
97 +
98 +}
MODIFY src/main/java/de/workaround/model/ActionTask.java +15 -0
diff --git a/src/main/java/de/workaround/model/ActionTask.java b/src/main/java/de/workaround/model/ActionTask.java
index 5560e5c..3a4fcef 100644
--- a/src/main/java/de/workaround/model/ActionTask.java
+++ b/src/main/java/de/workaround/model/ActionTask.java
@@ -5,11 +5,14 @@
5 5 import java.util.Optional;
6 6 import java.util.UUID;
7 7
8 +import org.hibernate.annotations.Generated;
8 9 import org.hibernate.annotations.processing.Find;
9 10 import org.hibernate.annotations.processing.HQL;
11 +import org.hibernate.generator.EventType;
10 12
11 13 import io.quarkus.hibernate.panache.PanacheEntity;
12 14 import io.quarkus.hibernate.panache.PanacheRepository;
15 +import jakarta.persistence.Column;
13 16 import jakarta.persistence.Entity;
14 17 import jakarta.persistence.EnumType;
15 18 import jakarta.persistence.Enumerated;
@@ -37,6 +40,14 @@
37 40 @ManyToOne(optional = false)
38 41 public ActionRun run;
39 42
43 + /**
44 + * Surrogate, globally-unique int64 exposed as the runner.v1 {@code Task.id}; the runner echoes it
45 + * in UpdateTask/UpdateLog. DB-generated ({@code bigserial}); read back after insert.
46 + */
47 + @Column(insertable = false, updatable = false)
48 + @Generated(event = EventType.INSERT)
49 + public long seq;
50 +
40 51 /** Job identifier from the workflow file, e.g. {@code build}. */
41 52 public String name;
42 53
@@ -77,6 +88,10 @@
77 88
78 89 @Find
79 90 Optional<ActionTask> findByIdAndRunner(UUID id, CiRunner runner);
91 +
92 + /** The highest surrogate id issued so far; the coarse {@code tasks_version} handed to runners. */
93 + @HQL("select coalesce(max(t.seq), 0) from ActionTask t")
94 + long maxSeq();
80 95 }
81 96
82 97 }
ADD src/main/resources/db/migration/V24__action_task_seq.sql +11 -0
diff --git a/src/main/resources/db/migration/V24__action_task_seq.sql b/src/main/resources/db/migration/V24__action_task_seq.sql
new file mode 100644
index 0000000..095cc6f
--- /dev/null
+++ b/src/main/resources/db/migration/V24__action_task_seq.sql
@@ -0,0 +1,11 @@
1 +-- Surrogate int64 task id for the runner.v1 protocol (issue #2, phase 1).
2 +--
3 +-- The Forgejo/Gitea Task message identifies a task by an int64 `id` that the runner echoes back in
4 +-- UpdateTask/UpdateLog. action_task uses a UUID primary key, so a separate stable, globally-unique
5 +-- int64 is exposed to the wire. bigserial gives us a NOT NULL sequence-backed column; existing rows
6 +-- (none in practice) are backfilled by the default.
7 +
8 +alter table action_task
9 + add column seq bigserial;
10 +
11 +create unique index idx_action_task_seq on action_task (seq);
ADD src/test/java/de/workaround/ci/FetchTaskTest.java +206 -0
diff --git a/src/test/java/de/workaround/ci/FetchTaskTest.java b/src/test/java/de/workaround/ci/FetchTaskTest.java
new file mode 100644
index 0000000..22d8b93
--- /dev/null
+++ b/src/test/java/de/workaround/ci/FetchTaskTest.java
@@ -0,0 +1,206 @@
1 +package de.workaround.ci;
2 +
3 +import java.util.List;
4 +import java.util.UUID;
5 +import java.util.concurrent.Callable;
6 +import java.util.concurrent.CyclicBarrier;
7 +import java.util.concurrent.ExecutorService;
8 +import java.util.concurrent.Executors;
9 +import java.util.concurrent.Future;
10 +
11 +import org.junit.jupiter.api.Test;
12 +
13 +import com.google.protobuf.InvalidProtocolBufferException;
14 +
15 +import de.workaround.ci.proto.runner.v1.FetchTaskRequest;
16 +import de.workaround.ci.proto.runner.v1.FetchTaskResponse;
17 +import de.workaround.git.GitRepositoryService;
18 +import de.workaround.model.ActionRun;
19 +import de.workaround.model.ActionTask;
20 +import de.workaround.model.CiRunner;
21 +import de.workaround.model.Repository;
22 +import de.workaround.model.User;
23 +import io.quarkus.test.junit.QuarkusTest;
24 +import jakarta.inject.Inject;
25 +import jakarta.transaction.Transactional;
26 +
27 +import static io.restassured.RestAssured.given;
28 +import static org.junit.jupiter.api.Assertions.assertEquals;
29 +import static org.junit.jupiter.api.Assertions.assertFalse;
30 +import static org.junit.jupiter.api.Assertions.assertTrue;
31 +
32 +/**
33 + * Drives FetchTask over the Connect wire (issue #2, phase 1): a registered runner claims the oldest
34 + * PENDING task, which flips to RUNNING and is delivered with its surrogate int64 id and payload;
35 + * an empty queue yields no task; bad credentials are rejected.
36 + */
37 +@QuarkusTest
38 +class FetchTaskTest
39 +{
40 + private static final String PROTO = "application/proto";
41 +
42 + @Inject
43 + RunnerRegistrationService runnerService;
44 +
45 + @Inject
46 + GitRepositoryService repositories;
47 +
48 + @Inject
49 + User.Repo users;
50 +
51 + @Inject
52 + ActionRun.Repo runs;
53 +
54 + @Inject
55 + ActionTask.Repo tasks;
56 +
57 + @Test
58 + void fetchClaimsOldestPendingTask() throws InvalidProtocolBufferException
59 + {
60 + RunnerRegistrationService.RegisteredRunner reg = registerRunner();
61 + Repository repo = newRepo("ft-a");
62 + UUID taskId = seedPendingTask(repo, "build", "on: push\njobs:\n build:\n runs-on: ubuntu-latest");
63 +
64 + byte[] bytes = given()
65 + .contentType(PROTO)
66 + .header("x-runner-uuid", reg.runner().uuid)
67 + .header("x-runner-token", reg.plaintext())
68 + .body(FetchTaskRequest.newBuilder().setTasksVersion(0).build().toByteArray())
69 + .when().post("/api/actions/runner.v1.RunnerService/FetchTask")
70 + .then().statusCode(200)
71 + .extract().asByteArray();
72 +
73 + FetchTaskResponse response = FetchTaskResponse.parseFrom(bytes);
74 + assertTrue(response.hasTask(), "a pending task must be handed out");
75 + assertTrue(response.getTask().getId() > 0, "task carries its surrogate int64 id");
76 + assertEquals("on: push\njobs:\n build:\n runs-on: ubuntu-latest",
77 + response.getTask().getWorkflowPayload().toStringUtf8());
78 +
79 + ActionTask claimed = tasks.findById(taskId);
80 + assertEquals(ActionRun.Status.RUNNING, claimed.status);
81 + assertEquals(response.getTask().getId(), claimed.seq);
82 + assertEquals(reg.runner().uuid, claimed.runner.uuid);
83 + assertEquals(ActionRun.Status.RUNNING, claimed.run.status);
84 + assertEquals(CiRunner.Status.ACTIVE, claimed.runner.status);
85 + }
86 +
87 + @Test
88 + void fetchWithEmptyQueueReturnsNoTask() throws InvalidProtocolBufferException
89 + {
90 + RunnerRegistrationService.RegisteredRunner reg = registerRunner();
91 +
92 + byte[] bytes = given()
93 + .contentType(PROTO)
94 + .header("x-runner-uuid", reg.runner().uuid)
95 + .header("x-runner-token", reg.plaintext())
96 + .body(FetchTaskRequest.newBuilder().setTasksVersion(0).build().toByteArray())
97 + .when().post("/api/actions/runner.v1.RunnerService/FetchTask")
98 + .then().statusCode(200)
99 + .extract().asByteArray();
100 +
101 + FetchTaskResponse response = FetchTaskResponse.parseFrom(bytes);
102 + assertFalse(response.hasTask(), "no pending task means an empty response");
103 + }
104 +
105 + @Test
106 + void concurrentFetchesClaimTheSameTaskAtMostOnce() throws Exception
107 + {
108 + RunnerRegistrationService.RegisteredRunner a = registerRunner();
109 + RunnerRegistrationService.RegisteredRunner b = registerRunner();
110 + Repository repo = newRepo("ft-race");
111 + seedPendingTask(repo, "build", "on: push\njobs:\n build:\n runs-on: ubuntu-latest");
112 +
113 + CyclicBarrier start = new CyclicBarrier(2);
114 + ExecutorService pool = Executors.newFixedThreadPool(2);
115 + try
116 + {
117 + Future<Boolean> f1 = pool.submit(fetchGotTask(a, start));
118 + Future<Boolean> f2 = pool.submit(fetchGotTask(b, start));
119 + int handedOut = (f1.get() ? 1 : 0) + (f2.get() ? 1 : 0);
120 + assertEquals(1, handedOut, "exactly one runner may claim the single pending task");
121 + }
122 + finally
123 + {
124 + pool.shutdownNow();
125 + }
126 + }
127 +
128 + private Callable<Boolean> fetchGotTask(RunnerRegistrationService.RegisteredRunner runner, CyclicBarrier start)
129 + {
130 + return () ->
131 + {
132 + start.await();
133 + byte[] bytes = given()
134 + .contentType(PROTO)
135 + .header("x-runner-uuid", runner.runner().uuid)
136 + .header("x-runner-token", runner.plaintext())
137 + .body(FetchTaskRequest.newBuilder().setTasksVersion(0).build().toByteArray())
138 + .when().post("/api/actions/runner.v1.RunnerService/FetchTask")
139 + .then().statusCode(200)
140 + .extract().asByteArray();
141 + return FetchTaskResponse.parseFrom(bytes).hasTask();
142 + };
143 + }
144 +
145 + @Test
146 + void fetchWithBadCredentialsIsUnauthenticated()
147 + {
148 + given()
149 + .contentType(PROTO)
150 + .header("x-runner-uuid", "nope")
151 + .header("x-runner-token", "gsrt_bogus")
152 + .body(FetchTaskRequest.newBuilder().setTasksVersion(0).build().toByteArray())
153 + .when().post("/api/actions/runner.v1.RunnerService/FetchTask")
154 + .then().statusCode(401);
155 + }
156 +
157 + private RunnerRegistrationService.RegisteredRunner registerRunner()
158 + {
159 + String regToken = runnerService.createRegistrationToken(persistUser("ft-admin-" + shortId())).plaintext();
160 + return runnerService.register(regToken, "ft-runner", List.of("ubuntu-latest"), "v4.0.0", false);
161 + }
162 +
163 + private Repository newRepo(String name)
164 + {
165 + User owner = persistUser(name + "-" + shortId());
166 + return repositories.create(owner, name, Repository.Visibility.PUBLIC, null);
167 + }
168 +
169 + @Transactional
170 + UUID seedPendingTask(Repository repository, String jobName, String payload)
171 + {
172 + Repository repo = repositories.find(repository.ownerHandle(), repository.name).orElseThrow();
173 + ActionRun run = new ActionRun();
174 + run.repository = repo;
175 + run.number = runs.maxNumber(repo) + 1;
176 + run.workflowName = "CI";
177 + run.workflowFile = ".forgejo/workflows/ci.yml";
178 + run.event = "push";
179 + run.ref = "refs/heads/main";
180 + run.commitSha = "0000000000000000000000000000000000000000";
181 + run.persist();
182 +
183 + ActionTask task = new ActionTask();
184 + task.run = run;
185 + task.name = jobName;
186 + task.payload = payload;
187 + task.persist();
188 + return task.id;
189 + }
190 +
191 + @Transactional
192 + User persistUser(String name)
193 + {
194 + User user = new User();
195 + user.oidcSub = name;
196 + user.username = name;
197 + user.persist();
198 + return user;
199 + }
200 +
201 + private static String shortId()
202 + {
203 + return UUID.randomUUID().toString().substring(0, 8);
204 + }
205 +
206 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog