1{
2 runTest,
3 forgejoPackage,
4}:
5
6let
7 ## gpg --faked-system-time='20230301T010000!' --quick-generate-key snakeoil ed25519 sign
8 signingPrivateKey = ''
9 -----BEGIN PGP PRIVATE KEY BLOCK-----
10
11 lFgEY/6jkBYJKwYBBAHaRw8BAQdADXiZRV8RJUyC9g0LH04wLMaJL9WTc+szbMi7
12 5fw4yP8AAQCl8EwGfzSLm/P6fCBfA3I9znFb3MEHGCCJhJ6VtKYyRw7ktAhzbmFr
13 ZW9pbIiUBBMWCgA8FiEE+wUM6VW/NLtAdSixTWQt6LZ4x50FAmP+o5ACGwMFCQPC
14 ZwAECwkIBwQVCgkIBRYCAwEAAh4FAheAAAoJEE1kLei2eMedFTgBAKQs1oGFZrCI
15 TZP42hmBTKxGAI1wg7VSdDEWTZxut/2JAQDGgo2sa4VHMfj0aqYGxrIwfP2B7JHO
16 GCqGCRf9O/hzBA==
17 =9Uy3
18 -----END PGP PRIVATE KEY BLOCK-----
19 '';
20 signingPrivateKeyId = "4D642DE8B678C79D";
21
22 metricSecret = "fakesecret";
23
24 base =
25 {
26 lib,
27 pkgs,
28 type,
29 ...
30 }:
31
32 {
33 name = "forgejo-${type}";
34 meta.maintainers = with lib.maintainers; [
35 bendlas
36 emilylange
37 tebriel
38 ];
39
40 nodes = {
41 server =
42 { config, pkgs, ... }:
43 {
44 virtualisation.memorySize = 2047;
45 services.forgejo = {
46 enable = true;
47 package = forgejoPackage;
48 database = { inherit type; };
49 settings.service.DISABLE_REGISTRATION = true;
50 settings."repository.signing".SIGNING_KEY = signingPrivateKeyId;
51 settings.actions.ENABLED = true;
52 settings.repository = {
53 ENABLE_PUSH_CREATE_USER = true;
54 DEFAULT_PUSH_CREATE_PRIVATE = false;
55 };
56 settings.metrics.ENABLED = true;
57 secrets.metrics.TOKEN = pkgs.writeText "metrics_secret" metricSecret;
58 };
59 environment.systemPackages = [
60 config.services.forgejo.package
61 pkgs.gnupg
62 pkgs.jq
63 pkgs.file
64 ];
65 services.openssh.enable = true;
66
67 specialisation.runner = {
68 inheritParentConfig = true;
69 configuration.services.gitea-actions-runner = {
70 package = pkgs.forgejo-runner;
71 instances."test" = {
72 enable = true;
73 name = "ci";
74 url = "http://localhost:3000";
75 labels = [
76 # type ":host" does not depend on docker/podman/lxc
77 "native:host"
78 ];
79 tokenFile = "/var/lib/forgejo/runner_token";
80 };
81 };
82 };
83 specialisation.dump = {
84 inheritParentConfig = true;
85 configuration.services.forgejo.dump = {
86 enable = true;
87 type = "tar.zst";
88 file = "dump.tar.zst";
89 };
90 };
91 };
92 client =
93 { ... }:
94 {
95 programs.git = {
96 enable = true;
97 config = {
98 user.email = "test@localhost";
99 user.name = "test";
100 init.defaultBranch = "main";
101 };
102 };
103 programs.ssh.extraConfig = ''
104 Host *
105 StrictHostKeyChecking no
106 IdentityFile ~/.ssh/privk
107 '';
108 };
109 };
110
111 testScript =
112 { nodes, ... }:
113 let
114 inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
115 serverSystem = nodes.server.system.build.toplevel;
116 dumpFile =
117 with nodes.server.specialisation.dump.configuration.services.forgejo.dump;
118 "${backupDir}/${file}";
119 remoteUri = "forgejo@server:test/repo";
120 remoteUriCheckoutAction = "forgejo@server:test/checkout";
121
122 actionsWorkflowYaml = ''
123 run-name: dummy workflow
124 on:
125 push:
126 jobs:
127 cat:
128 runs-on: native
129 steps:
130 - uses: http://localhost:3000/test/checkout@main
131 - run: cat testfile
132 '';
133 # https://github.com/actions/checkout/releases
134 checkoutActionSource = pkgs.fetchFromGitHub {
135 owner = "actions";
136 repo = "checkout";
137 rev = "v4.1.1";
138 hash = "sha256-h2/UIp8IjPo3eE4Gzx52Fb7pcgG/Ww7u31w5fdKVMos=";
139 };
140 in
141 ''
142 import json
143
144 start_all()
145
146 client.succeed("mkdir -p ~/.ssh")
147 client.succeed("(umask 0077; cat ${snakeOilPrivateKey} > ~/.ssh/privk)")
148
149 client.succeed("mkdir /tmp/repo")
150 client.succeed("git -C /tmp/repo init")
151 client.succeed("echo 'hello world' > /tmp/repo/testfile")
152 client.succeed("git -C /tmp/repo add .")
153 client.succeed("git -C /tmp/repo commit -m 'Initial import'")
154 client.succeed("git -C /tmp/repo remote add origin ${remoteUri}")
155
156 server.wait_for_unit("forgejo.service")
157 server.wait_for_open_port(3000)
158 server.wait_for_open_port(22)
159 server.succeed("curl --fail http://localhost:3000/")
160
161 server.succeed(
162 "su -l forgejo -c 'gpg --homedir /var/lib/forgejo/data/home/.gnupg "
163 + "--import ${toString (pkgs.writeText "forgejo.key" signingPrivateKey)}'"
164 )
165
166 assert "BEGIN PGP PUBLIC KEY BLOCK" in server.succeed("curl http://localhost:3000/api/v1/signing-key.gpg")
167
168 api_version = json.loads(server.succeed("curl http://localhost:3000/api/forgejo/v1/version")).get("version")
169 assert "development" != api_version and "${forgejoPackage.version}+gitea-" in api_version, (
170 "/api/forgejo/v1/version should not return 'development' "
171 + f"but should contain a forgejo+gitea compatibility version string. Got '{api_version}' instead."
172 )
173
174 server.succeed(
175 "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. "
176 + "Please contact your site administrator.'"
177 )
178 server.succeed(
179 "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo forgejo admin user create "
180 + "--username test --password totallysafe --email test@localhost --must-change-password=false'"
181 )
182
183 api_token = server.succeed(
184 "curl --fail -X POST http://test:totallysafe@localhost:3000/api/v1/users/test/tokens "
185 + "-H 'Accept: application/json' -H 'Content-Type: application/json' -d "
186 + "'{\"name\":\"token\",\"scopes\":[\"all\"]}' | jq '.sha1' | xargs echo -n"
187 )
188
189 server.succeed(
190 "curl --fail -X POST http://localhost:3000/api/v1/user/repos "
191 + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
192 + f"-H 'Authorization: token {api_token}'"
193 + ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\'''
194 )
195
196 server.succeed(
197 "curl --fail -X POST http://localhost:3000/api/v1/user/keys "
198 + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
199 + f"-H 'Authorization: token {api_token}'"
200 + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\'''
201 )
202
203 client.succeed("git -C /tmp/repo push origin main")
204
205 client.succeed("git clone ${remoteUri} /tmp/repo-clone")
206 print(client.succeed("ls -lash /tmp/repo-clone"))
207 assert "hello world" == client.succeed("cat /tmp/repo-clone/testfile").strip()
208
209 with subtest("Testing git protocol version=2 over ssh"):
210 git_protocol = client.succeed("GIT_TRACE2_EVENT=true GIT_TRACE2_EVENT_NESTING=3 git -C /tmp/repo-clone fetch |& grep negotiated-version")
211 version = json.loads(git_protocol).get("value")
212 assert version == "2", f"git did not negotiate protocol version 2, but version {version} instead."
213
214 server.wait_until_succeeds(
215 'test "$(curl http://localhost:3000/api/v1/repos/test/repo/commits '
216 + '-H "Accept: application/json" | jq length)" = "1"',
217 timeout=10
218 )
219
220 with subtest("Testing /metrics endpoint with token from cfg.secrets"):
221 server.fail("curl --fail http://localhost:3000/metrics")
222 server.succeed('curl --fail http://localhost:3000/metrics -H "Authorization: Bearer ${metricSecret}"')
223
224 with subtest("Testing runner registration and action workflow"):
225 server.succeed(
226 "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo forgejo actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token"
227 )
228 server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test")
229 server.wait_for_unit("gitea-runner-test.service")
230 server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'")
231
232 # enable actions feature for this repository, defaults to disabled
233 server.succeed(
234 "curl --fail -X PATCH http://localhost:3000/api/v1/repos/test/repo "
235 + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
236 + f"-H 'Authorization: token {api_token}'"
237 + ' -d \'{"has_actions":true}\'''
238 )
239
240 # mirror "actions/checkout" action
241 client.succeed("cp -R ${checkoutActionSource}/ /tmp/checkout")
242 client.succeed("git -C /tmp/checkout init")
243 client.succeed("git -C /tmp/checkout add .")
244 client.succeed("git -C /tmp/checkout commit -m 'Initial import'")
245 client.succeed("git -C /tmp/checkout remote add origin ${remoteUriCheckoutAction}")
246 client.succeed("git -C /tmp/checkout push origin main")
247
248 # push workflow to initial repo
249 client.succeed("mkdir -p /tmp/repo/.forgejo/workflows")
250 client.succeed("cp ${pkgs.writeText "dummy-workflow.yml" actionsWorkflowYaml} /tmp/repo/.forgejo/workflows/")
251 client.succeed("git -C /tmp/repo add .")
252 client.succeed("git -C /tmp/repo commit -m 'Add dummy workflow'")
253 client.succeed("git -C /tmp/repo push origin main")
254
255 def poll_workflow_action_status(_) -> bool:
256 try:
257 response = server.succeed("curl --fail http://localhost:3000/api/v1/repos/test/repo/actions/tasks")
258 status = json.loads(response).get("workflow_runs")[0].get("status")
259
260 except IndexError:
261 status = "???"
262
263 server.log(f"Workflow status: {status}")
264
265 if status == "failure":
266 raise Exception("Workflow failed")
267
268 return status == "success"
269
270 with server.nested("Waiting for the workflow run to be successful"):
271 retry(poll_workflow_action_status, 60)
272
273 with subtest("Testing backup service"):
274 server.succeed("${serverSystem}/specialisation/dump/bin/switch-to-configuration test")
275 server.systemctl("start forgejo-dump")
276 assert "Zstandard compressed data" in server.succeed("file ${dumpFile}")
277 server.copy_from_vm("${dumpFile}")
278 '';
279 };
280in
281{
282 mysql = runTest {
283 imports = [ base ];
284 _module.args.type = "mysql";
285 };
286 sqlite3 = runTest {
287 imports = [ base ];
288 _module.args.type = "sqlite3";
289 };
290 postgres = runTest {
291 imports = [ base ];
292 _module.args.type = "postgres";
293 };
294}