gitshark

Clone repository

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

← Commits

✨ (ci): Deliver repo secrets and variables to runners

34abb4f9262f702bbac5dd36b239eaf89dd53e0b · Michael Hainz · 2026-07-22T12:43:20Z

Changes

9 files changed, +392 -10

MODIFY docs/admins/ci-runners.md +5 -1
diff --git a/docs/admins/ci-runners.md b/docs/admins/ci-runners.md
index 262d919..27a2ac2 100644
--- a/docs/admins/ci-runners.md
+++ b/docs/admins/ci-runners.md
@@ -91,10 +91,14 @@
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 92 | `action_task` | One job within a run: `seq` (surrogate int64 id handed to runners), `run_id`, `name`, `runs_on` (comma-joined labels for runner matching, empty = any), `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 +| `action_secret` | Per-repo CI secret: `repository_id`, `name`, `value_encrypted` (SecretCrypto envelope), decrypted only when delivered to a runner. Deleted with its repository. |
95 +| `action_variable` | Per-repo CI variable: `repository_id`, `name`, `value` (plaintext config). Deleted with its repository. |
94 96
95 97 `ci_runner*` are introduced by migration `V19__ci_runners.sql`; `action_*` by `V23__action_runs.sql`
96 98 (`V24__action_task_seq.sql` adds `action_task.seq`, `V25__action_task_runs_on.sql` adds
97 -`action_task.runs_on`).
99 +`action_task.runs_on`, `V26__action_secrets_variables.sql` adds `action_secret`/`action_variable`).
100 +Secrets are stored encrypted and require `GITSHARK_SECRET_KEY` to be set (same key as push mirrors);
101 +without it, secrets cannot be decrypted and are omitted from what a runner receives.
98 102 The `ci_runner*` tables hold no repository data (losing them only forces re-registration); the
99 103 `action_*` tables hold run history and logs, tied to their repository by cascade.
100 104
MODIFY docs/admins/getting-started.md +1 -1
diff --git a/docs/admins/getting-started.md b/docs/admins/getting-started.md
index cccf5a9..2b21d1a 100644
--- a/docs/admins/getting-started.md
+++ b/docs/admins/getting-started.md
@@ -357,7 +357,7 @@
357 357 | `GITSHARK_SSH_HOST_KEY` | — | `data/ssh/host-key` | Persistent SSH host key path |
358 358 | `GITSHARK_SSH_PORT` | — | `2222` | Port the embedded SSH server **binds inside the container**; keep >1024 so it needs no root |
359 359 | `GITSHARK_SSH_EXTERNAL_PORT` | — | `22` | Port advertised in clone/push URLs (display only, no runtime effect). Must match the published host port; `22` is omitted from the printed URL |
360 -| `GITSHARK_SECRET_KEY` | — | — | Encrypts push-mirror secrets at rest; required to create mirrors (see [Push mirrors](mirrors.md)) |
360 +| `GITSHARK_SECRET_KEY` | — | — | Encrypts push-mirror secrets and CI secrets at rest; required to create mirrors and to deliver CI secrets to runners (see [Push mirrors](mirrors.md), [CI runners](ci-runners.md)) |
361 361 | `GITSHARK_MIRROR_MAX_ATTEMPTS` | — | `8` | Mirror-sync retry cap before dead-letter |
362 362 | `GITSHARK_MIRROR_ALLOW_INSECURE` | — | `false` | Dev only: allow http/loopback mirror targets |
363 363 | `GITSHARK_MIRROR_DRAIN_INTERVAL` | — | `10s` | How often the async mirror-sync drain worker runs |
MODIFY docs/maintainers/ci-runners.md +13 -3
diff --git a/docs/maintainers/ci-runners.md b/docs/maintainers/ci-runners.md
index ed6fd8a..e23f572 100644
--- a/docs/maintainers/ci-runners.md
+++ b/docs/maintainers/ci-runners.md
@@ -18,6 +18,7 @@
18 18 | Actions UI | `web/ActionResource.java` + `templates/ActionResource/` | Read-only per-repo run list + run detail (jobs and their log rows); sidebar `Actions` tab. |
19 19 | Entities | `model/CiRunner.java`, `model/CiRunnerRegistrationToken.java` | Runner state (migration `V19`). |
20 20 | Run entities | `model/ActionRun.java`, `model/ActionTask.java`, `model/ActionLog.java` | Run/job/log-row persistence (migrations `V23`–`V25`). `ActionTask.seq` (`bigserial`) is the surrogate int64 `Task.id` for the wire; `ActionTask.runs_on` holds the job's labels for matching. |
21 +| Secret/variable entities | `model/ActionSecret.java`, `model/ActionVariable.java` | Per-repo CI secrets (encrypted) and variables (migration `V26`), delivered to runners in FetchTask. |
21 22 | 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 PENDING tasks (drained by FetchTask). |
22 23 | Admin UI | `ci/AdminRunnerResource.java` + `templates/AdminRunnerResource/` | Token generation, runner list, deletion. |
23 24 | Admin gate | `account/AdminAccess.java` | Config-driven instance-admin check. |
@@ -76,6 +77,11 @@
76 77 context (`job`, `ref`, `sha`, `repository`, `run_id`, …) built in `ConnectRunnerResource.toProto` —
77 78 without it the runner cannot select the job from the workflow and nil-derefs. Auth failures return
78 79 the Connect `unauthenticated` error. No long-poll; `tasks_version` is a coarse max-`seq`.
80 +- **Secret & variable delivery:** a claimed task is handed its repository's variables (plaintext) and
81 + secrets (`action_secret`, stored with the `SecretCrypto` envelope, decrypted at delivery — a value
82 + that fails to decrypt is dropped, never sent as ciphertext) in the FetchTask `Task.secrets`/`vars`
83 + maps. Same trust model as GitHub self-hosted runners: secrets go to whatever runner claims the task
84 + (over TLS). No repo/org scoping of secrets and no fork-PR guard yet (no PR triggers exist).
79 85 - **Label matching:** a task carries its job's `runs-on` labels (`action_task.runs_on`, parsed at
80 86 ingest). Dispatch scans PENDING tasks oldest-first and claims the first whose labels are all
81 87 advertised by the fetching runner (empty `runs-on` = any runner); an incompatible task is left for a
@@ -98,7 +104,9 @@
98 104 racing one task → claimed at most once), `TaskProgressTest` (UpdateTask success rolls up task+run
99 105 and frees the runner, UpdateLog append + dedup/resume, cross-runner and bad-credential rejection),
100 106 `LabelMatchingTest` (runner claims a compatible task and skips an incompatible older one, gets
101 - nothing when none match, unconstrained task runs anywhere).
107 + nothing when none match, unconstrained task runs anywhere),
108 + `SecretDeliveryTest` (claimed task receives repo secrets decrypted + variables; empty fetch carries
109 + none).
102 110 - **Zombie reclaim (`ZombieReclaimService`):** a scheduled sweep
103 111 (`gitshark.ci.zombie-reclaim-interval`, default 1m) fails any RUNNING task whose
104 112 `action_task.deadline` has passed — the runner is presumed gone — rolls its run up, and flags the
@@ -125,8 +133,10 @@
125 133 isolated/expanded into its own payload. No `needs`/`matrix` yet.
126 134 - **Non-push events:** only `push` is evaluated; `pull_request`, scheduled and manual triggers are
127 135 not. (`!`-negation within a single pattern list is also not supported.)
128 -- **Later phases:** secrets/variables delivery, concurrency/cancellation, artifacts
129 - (`ACTIONS_RESULTS_URL`), repo/org-scoped and ephemeral runners, commit/MR status.
136 +- **Secrets/variables management UI:** delivery works, but there is no page yet to create/edit/delete
137 + them (tests seed the rows directly).
138 +- **Later phases:** concurrency/cancellation, artifacts (`ACTIONS_RESULTS_URL`), repo/org-scoped and
139 + ephemeral runners, commit/MR status.
130 140
131 141 ## References
132 142
MODIFY src/main/java/de/workaround/ci/ConnectRunnerResource.java +5 -2
diff --git a/src/main/java/de/workaround/ci/ConnectRunnerResource.java b/src/main/java/de/workaround/ci/ConnectRunnerResource.java
index 46b51b6..a21e910 100644
--- a/src/main/java/de/workaround/ci/ConnectRunnerResource.java
+++ b/src/main/java/de/workaround/ci/ConnectRunnerResource.java
@@ -3,6 +3,7 @@
3 3 import java.time.Instant;
4 4 import java.util.Arrays;
5 5 import java.util.List;
6 +import java.util.Map;
6 7
7 8 import com.google.protobuf.ByteString;
8 9 import com.google.protobuf.InvalidProtocolBufferException;
@@ -121,7 +122,7 @@
121 122 TaskDispatchService.Fetched fetched = dispatchService.fetch(uuid, token);
122 123 FetchTaskResponse.Builder response = FetchTaskResponse.newBuilder()
123 124 .setTasksVersion(fetched.tasksVersion());
124 - fetched.task().ifPresent(task -> response.setTask(toProto(task)));
125 + fetched.task().ifPresent(task -> response.setTask(toProto(task, fetched.secrets(), fetched.vars())));
125 126 return ok(response.build().toByteArray());
126 127 }
127 128 catch (RunnerAuthenticationException e)
@@ -177,7 +178,7 @@
177 178 }
178 179 }
179 180
180 - private static Task toProto(ActionTask task)
181 + private static Task toProto(ActionTask task, Map<String, String> secrets, Map<String, String> vars)
181 182 {
182 183 Task.Builder builder = Task.newBuilder().setId(task.seq);
183 184 if (task.payload != null)
@@ -185,6 +186,8 @@
185 186 builder.setWorkflowPayload(ByteString.copyFromUtf8(task.payload));
186 187 }
187 188 builder.setContext(githubContext(task));
189 + builder.putAllSecrets(secrets);
190 + builder.putAllVars(vars);
188 191 return builder.build();
189 192 }
190 193
MODIFY src/main/java/de/workaround/ci/TaskDispatchService.java +54 -3
diff --git a/src/main/java/de/workaround/ci/TaskDispatchService.java b/src/main/java/de/workaround/ci/TaskDispatchService.java
index f9ab64f..0b5b3fe 100644
--- a/src/main/java/de/workaround/ci/TaskDispatchService.java
+++ b/src/main/java/de/workaround/ci/TaskDispatchService.java
@@ -3,7 +3,9 @@
3 3 import java.time.Duration;
4 4 import java.time.Instant;
5 5 import java.util.Arrays;
6 +import java.util.HashMap;
6 7 import java.util.List;
8 +import java.util.Map;
7 9 import java.util.Optional;
8 10 import java.util.Set;
9 11 import java.util.UUID;
@@ -11,9 +13,13 @@
11 13
12 14 import org.eclipse.microprofile.config.inject.ConfigProperty;
13 15
16 +import de.workaround.mirror.SecretCrypto;
14 17 import de.workaround.model.ActionRun;
18 +import de.workaround.model.ActionSecret;
15 19 import de.workaround.model.ActionTask;
20 +import de.workaround.model.ActionVariable;
16 21 import de.workaround.model.CiRunner;
22 +import de.workaround.model.Repository;
17 23 import jakarta.enterprise.context.ApplicationScoped;
18 24 import jakarta.inject.Inject;
19 25 import jakarta.persistence.EntityManager;
@@ -47,14 +53,25 @@
47 53 ActionTask.Repo tasks;
48 54
49 55 @Inject
56 + ActionSecret.Repo secrets;
57 +
58 + @Inject
59 + ActionVariable.Repo variables;
60 +
61 + @Inject
62 + SecretCrypto crypto;
63 +
64 + @Inject
50 65 EntityManager em;
51 66
52 - public record Fetched(Optional<ActionTask> task, long tasksVersion)
67 + public record Fetched(Optional<ActionTask> task, long tasksVersion, Map<String, String> secrets,
68 + Map<String, String> vars)
53 69 {
54 70 }
55 71
56 72 /**
57 - * Authenticate the runner and claim the oldest PENDING task, if any.
73 + * Authenticate the runner and claim the oldest label-compatible PENDING task, if any, delivering
74 + * the owning repository's variables and (decrypted) secrets alongside it.
58 75 *
59 76 * @throws RunnerAuthenticationException if the uuid/token pair is unknown
60 77 */
@@ -70,7 +87,41 @@
70 87 claim(task, runner);
71 88 return task;
72 89 });
73 - return new Fetched(next, tasks.maxSeq());
90 + Map<String, String> secretMap = next.map(task -> secretsFor(task.run.repository)).orElse(Map.of());
91 + Map<String, String> varMap = next.map(task -> variablesFor(task.run.repository)).orElse(Map.of());
92 + return new Fetched(next, tasks.maxSeq(), secretMap, varMap);
93 + }
94 +
95 + /** Decrypted repository secrets by name; a secret that cannot be decrypted is dropped, never leaked as ciphertext. */
96 + private Map<String, String> secretsFor(Repository repository)
97 + {
98 + Map<String, String> result = new HashMap<>();
99 + if (!crypto.available())
100 + {
101 + return result;
102 + }
103 + for (ActionSecret secret : secrets.findByRepository(repository))
104 + {
105 + try
106 + {
107 + result.put(secret.name, crypto.decrypt(secret.valueEncrypted));
108 + }
109 + catch (RuntimeException undecryptable)
110 + {
111 + // key rotated or value corrupt: omit rather than hand the runner unusable/ciphertext data
112 + }
113 + }
114 + return result;
115 + }
116 +
117 + private Map<String, String> variablesFor(Repository repository)
118 + {
119 + Map<String, String> result = new HashMap<>();
120 + for (ActionVariable variable : variables.findByRepository(repository))
121 + {
122 + result.put(variable.name, variable.value);
123 + }
124 + return result;
74 125 }
75 126
76 127 private void claim(ActionTask task, CiRunner runner)
ADD src/main/java/de/workaround/model/ActionSecret.java +51 -0
diff --git a/src/main/java/de/workaround/model/ActionSecret.java b/src/main/java/de/workaround/model/ActionSecret.java
new file mode 100644
index 0000000..2e50101
--- /dev/null
+++ b/src/main/java/de/workaround/model/ActionSecret.java
@@ -0,0 +1,51 @@
1 +package de.workaround.model;
2 +
3 +import java.time.Instant;
4 +import java.util.List;
5 +import java.util.Optional;
6 +import java.util.UUID;
7 +
8 +import org.hibernate.annotations.processing.Find;
9 +import org.hibernate.annotations.processing.HQL;
10 +
11 +import io.quarkus.hibernate.panache.PanacheEntity;
12 +import io.quarkus.hibernate.panache.PanacheRepository;
13 +import jakarta.persistence.Entity;
14 +import jakarta.persistence.GeneratedValue;
15 +import jakarta.persistence.GenerationType;
16 +import jakarta.persistence.Id;
17 +import jakarta.persistence.ManyToOne;
18 +import jakarta.persistence.Table;
19 +
20 +/**
21 + * A repository-scoped CI secret (issue #2, phase 2). The value is stored encrypted (the same
22 + * {@code SecretCrypto} envelope as push-mirror credentials) and decrypted only when delivered to a
23 + * runner in FetchTask. Unique by {@link #name} within a repository; removed with it (cascade).
24 + */
25 +@Entity
26 +@Table(name = "action_secret")
27 +public class ActionSecret implements PanacheEntity.Managed
28 +{
29 + @Id
30 + @GeneratedValue(strategy = GenerationType.UUID)
31 + public UUID id;
32 +
33 + @ManyToOne(optional = false)
34 + public Repository repository;
35 +
36 + public String name;
37 +
38 + public String valueEncrypted;
39 +
40 + public Instant createdAt = Instant.now();
41 +
42 + public interface Repo extends PanacheRepository.Managed<ActionSecret, UUID>
43 + {
44 + @HQL("select s from ActionSecret s where s.repository = :repository order by s.name asc")
45 + List<ActionSecret> findByRepository(Repository repository);
46 +
47 + @Find
48 + Optional<ActionSecret> findByRepositoryAndName(Repository repository, String name);
49 + }
50 +
51 +}
ADD src/main/java/de/workaround/model/ActionVariable.java +50 -0
diff --git a/src/main/java/de/workaround/model/ActionVariable.java b/src/main/java/de/workaround/model/ActionVariable.java
new file mode 100644
index 0000000..2c1c1bb
--- /dev/null
+++ b/src/main/java/de/workaround/model/ActionVariable.java
@@ -0,0 +1,50 @@
1 +package de.workaround.model;
2 +
3 +import java.time.Instant;
4 +import java.util.List;
5 +import java.util.Optional;
6 +import java.util.UUID;
7 +
8 +import org.hibernate.annotations.processing.Find;
9 +import org.hibernate.annotations.processing.HQL;
10 +
11 +import io.quarkus.hibernate.panache.PanacheEntity;
12 +import io.quarkus.hibernate.panache.PanacheRepository;
13 +import jakarta.persistence.Entity;
14 +import jakarta.persistence.GeneratedValue;
15 +import jakarta.persistence.GenerationType;
16 +import jakarta.persistence.Id;
17 +import jakarta.persistence.ManyToOne;
18 +import jakarta.persistence.Table;
19 +
20 +/**
21 + * A repository-scoped CI variable (issue #2, phase 2): plain (non-secret) configuration delivered to
22 + * runners in FetchTask. Unique by {@link #name} within a repository; removed with it (cascade).
23 + */
24 +@Entity
25 +@Table(name = "action_variable")
26 +public class ActionVariable implements PanacheEntity.Managed
27 +{
28 + @Id
29 + @GeneratedValue(strategy = GenerationType.UUID)
30 + public UUID id;
31 +
32 + @ManyToOne(optional = false)
33 + public Repository repository;
34 +
35 + public String name;
36 +
37 + public String value;
38 +
39 + public Instant createdAt = Instant.now();
40 +
41 + public interface Repo extends PanacheRepository.Managed<ActionVariable, UUID>
42 + {
43 + @HQL("select v from ActionVariable v where v.repository = :repository order by v.name asc")
44 + List<ActionVariable> findByRepository(Repository repository);
45 +
46 + @Find
47 + Optional<ActionVariable> findByRepositoryAndName(Repository repository, String name);
48 + }
49 +
50 +}
ADD src/main/resources/db/migration/V26__action_secrets_variables.sql +29 -0
diff --git a/src/main/resources/db/migration/V26__action_secrets_variables.sql b/src/main/resources/db/migration/V26__action_secrets_variables.sql
new file mode 100644
index 0000000..037dd3d
--- /dev/null
+++ b/src/main/resources/db/migration/V26__action_secrets_variables.sql
@@ -0,0 +1,29 @@
1 +-- Repository-level CI secrets and variables (issue #2, phase 2).
2 +--
3 +-- Secrets are stored encrypted (same SecretCrypto envelope as push-mirror credentials) and decrypted
4 +-- only when delivered to a runner in FetchTask. Variables are plain configuration. Both are unique by
5 +-- name within a repository and removed with it.
6 +
7 +create table action_secret
8 +(
9 + id uuid primary key,
10 + repository_id uuid not null references repositories (id) on delete cascade,
11 + name varchar(255) not null,
12 + value_encrypted text not null,
13 + created_at timestamptz not null default now(),
14 + unique (repository_id, name)
15 +);
16 +
17 +create index idx_action_secret_repository on action_secret (repository_id);
18 +
19 +create table action_variable
20 +(
21 + id uuid primary key,
22 + repository_id uuid not null references repositories (id) on delete cascade,
23 + name varchar(255) not null,
24 + value text not null,
25 + created_at timestamptz not null default now(),
26 + unique (repository_id, name)
27 +);
28 +
29 +create index idx_action_variable_repository on action_variable (repository_id);
ADD src/test/java/de/workaround/ci/SecretDeliveryTest.java +184 -0
diff --git a/src/test/java/de/workaround/ci/SecretDeliveryTest.java b/src/test/java/de/workaround/ci/SecretDeliveryTest.java
new file mode 100644
index 0000000..f0568e7
--- /dev/null
+++ b/src/test/java/de/workaround/ci/SecretDeliveryTest.java
@@ -0,0 +1,184 @@
1 +package de.workaround.ci;
2 +
3 +import java.util.List;
4 +import java.util.Map;
5 +import java.util.UUID;
6 +
7 +import org.junit.jupiter.api.Test;
8 +
9 +import de.workaround.git.GitRepositoryService;
10 +import de.workaround.mirror.SecretCrypto;
11 +import de.workaround.model.ActionRun;
12 +import de.workaround.model.ActionSecret;
13 +import de.workaround.model.ActionTask;
14 +import de.workaround.model.ActionVariable;
15 +import de.workaround.model.Repository;
16 +import de.workaround.model.User;
17 +import io.quarkus.test.junit.QuarkusTest;
18 +import jakarta.inject.Inject;
19 +import jakarta.transaction.Transactional;
20 +
21 +import static org.junit.jupiter.api.Assertions.assertEquals;
22 +import static org.junit.jupiter.api.Assertions.assertTrue;
23 +
24 +/**
25 + * Secret and variable delivery (issue #2, phase 2): a claimed task is handed its repository's
26 + * variables (plaintext) and secrets (decrypted from storage) in the FetchTask response.
27 + */
28 +@QuarkusTest
29 +class SecretDeliveryTest
30 +{
31 + @Inject
32 + RunnerRegistrationService runnerService;
33 +
34 + @Inject
35 + TaskDispatchService dispatch;
36 +
37 + @Inject
38 + GitRepositoryService repositories;
39 +
40 + @Inject
41 + SecretCrypto crypto;
42 +
43 + @Inject
44 + ActionRun.Repo runs;
45 +
46 + @Inject
47 + ActionTask.Repo tasks;
48 +
49 + @Inject
50 + ActionSecret.Repo secrets;
51 +
52 + @Inject
53 + ActionVariable.Repo variables;
54 +
55 + @Test
56 + void claimedTaskReceivesRepoSecretsAndVariables()
57 + {
58 + RunnerRegistrationService.RegisteredRunner reg = registerRunner();
59 + seed("sd-a");
60 +
61 + TaskDispatchService.Fetched fetched = dispatch.fetch(reg.runner().uuid, reg.plaintext());
62 +
63 + assertTrue(fetched.task().isPresent());
64 + assertEquals("s3cr3t", fetched.secrets().get("API_TOKEN"), "secret delivered decrypted");
65 + assertEquals("production", fetched.vars().get("DEPLOY_ENV"), "variable delivered as-is");
66 + }
67 +
68 + @Test
69 + void undecryptableSecretIsDroppedNotLeaked()
70 + {
71 + RunnerRegistrationService.RegisteredRunner reg = registerRunner();
72 + seedWithCorruptSecret("sd-c");
73 +
74 + TaskDispatchService.Fetched fetched = dispatch.fetch(reg.runner().uuid, reg.plaintext());
75 +
76 + assertTrue(fetched.task().isPresent());
77 + assertEquals("s3cr3t", fetched.secrets().get("API_TOKEN"), "valid secret still delivered");
78 + assertTrue(fetched.secrets().containsKey("API_TOKEN"));
79 + assertTrue(!fetched.secrets().containsKey("BROKEN"), "undecryptable secret is dropped, never leaked");
80 + }
81 +
82 + @Test
83 + void emptyFetchCarriesNoSecrets()
84 + {
85 + RunnerRegistrationService.RegisteredRunner reg = registerRunner();
86 +
87 + TaskDispatchService.Fetched fetched = dispatch.fetch(reg.runner().uuid, reg.plaintext());
88 +
89 + assertTrue(fetched.task().isEmpty());
90 + assertTrue(fetched.secrets().isEmpty());
91 + assertTrue(fetched.vars().isEmpty());
92 + }
93 +
94 + private RunnerRegistrationService.RegisteredRunner registerRunner()
95 + {
96 + String token = runnerService.createRegistrationToken(persistUser("sd-admin-" + shortId())).plaintext();
97 + return runnerService.register(token, "sd-runner", List.of(), "v4.0.0", false);
98 + }
99 +
100 + @Transactional
101 + void seed(String repoName)
102 + {
103 + User owner = persistUser(repoName + "-" + shortId());
104 + Repository repo = repositories.create(owner, repoName, Repository.Visibility.PUBLIC, null);
105 +
106 + ActionSecret secret = new ActionSecret();
107 + secret.repository = repo;
108 + secret.name = "API_TOKEN";
109 + secret.valueEncrypted = crypto.encrypt("s3cr3t");
110 + secret.persist();
111 +
112 + ActionVariable variable = new ActionVariable();
113 + variable.repository = repo;
114 + variable.name = "DEPLOY_ENV";
115 + variable.value = "production";
116 + variable.persist();
117 +
118 + ActionRun run = new ActionRun();
119 + run.repository = repo;
120 + run.number = runs.maxNumber(repo) + 1;
121 + run.workflowName = "CI";
122 + run.workflowFile = ".forgejo/workflows/ci.yml";
123 + run.event = "push";
124 + run.ref = "refs/heads/main";
125 + run.commitSha = "0000000000000000000000000000000000000000";
126 + run.persist();
127 +
128 + ActionTask task = new ActionTask();
129 + task.run = run;
130 + task.name = "build";
131 + task.payload = "on: push";
132 + task.persist();
133 + }
134 +
135 + @Transactional
136 + void seedWithCorruptSecret(String repoName)
137 + {
138 + User owner = persistUser(repoName + "-" + shortId());
139 + Repository repo = repositories.create(owner, repoName, Repository.Visibility.PUBLIC, null);
140 +
141 + ActionSecret good = new ActionSecret();
142 + good.repository = repo;
143 + good.name = "API_TOKEN";
144 + good.valueEncrypted = crypto.encrypt("s3cr3t");
145 + good.persist();
146 +
147 + ActionSecret broken = new ActionSecret();
148 + broken.repository = repo;
149 + broken.name = "BROKEN";
150 + broken.valueEncrypted = "enc1:not-valid-ciphertext";
151 + broken.persist();
152 +
153 + ActionRun run = new ActionRun();
154 + run.repository = repo;
155 + run.number = runs.maxNumber(repo) + 1;
156 + run.workflowName = "CI";
157 + run.workflowFile = ".forgejo/workflows/ci.yml";
158 + run.event = "push";
159 + run.ref = "refs/heads/main";
160 + run.commitSha = "0000000000000000000000000000000000000000";
161 + run.persist();
162 +
163 + ActionTask task = new ActionTask();
164 + task.run = run;
165 + task.name = "build";
166 + task.payload = "on: push";
167 + task.persist();
168 + }
169 +
170 + @Transactional
171 + User persistUser(String name)
172 + {
173 + User user = new User();
174 + user.oidcSub = name;
175 + user.username = name;
176 + user.persist();
177 + return user;
178 + }
179 +
180 + private static String shortId()
181 + {
182 + return UUID.randomUUID().toString().substring(0, 8);
183 + }
184 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog