diff options
Diffstat (limited to 'gnu')
| -rw-r--r-- | gnu/local.mk | 2 | ||||
| -rw-r--r-- | gnu/packages/forgejo.scm | 6 | ||||
| -rw-r--r-- | gnu/services/forgejo.scm | 913 | ||||
| -rw-r--r-- | gnu/tests/forgejo.scm | 416 |
4 files changed, 1335 insertions, 2 deletions
diff --git a/gnu/local.mk b/gnu/local.mk index e53fe8b5a0f..3c83861908b 100644 --- a/gnu/local.mk +++ b/gnu/local.mk | |||
| @@ -760,6 +760,7 @@ GNU_SYSTEM_MODULES = \ | |||
| 760 | %D%/services/authentication.scm \ | 760 | %D%/services/authentication.scm \ |
| 761 | %D%/services/file-sharing.scm \ | 761 | %D%/services/file-sharing.scm \ |
| 762 | %D%/services/firmware.scm \ | 762 | %D%/services/firmware.scm \ |
| 763 | %D%/services/forgejo.scm \ | ||
| 763 | %D%/services/games.scm \ | 764 | %D%/services/games.scm \ |
| 764 | %D%/services/ganeti.scm \ | 765 | %D%/services/ganeti.scm \ |
| 765 | %D%/services/getmail.scm \ | 766 | %D%/services/getmail.scm \ |
| @@ -874,6 +875,7 @@ GNU_SYSTEM_MODULES = \ | |||
| 874 | %D%/tests/emacs.scm \ | 875 | %D%/tests/emacs.scm \ |
| 875 | %D%/tests/file-sharing.scm \ | 876 | %D%/tests/file-sharing.scm \ |
| 876 | %D%/tests/foreign.scm \ | 877 | %D%/tests/foreign.scm \ |
| 878 | %D%/tests/forgejo.scm \ | ||
| 877 | %D%/tests/ganeti.scm \ | 879 | %D%/tests/ganeti.scm \ |
| 878 | %D%/tests/gdm.scm \ | 880 | %D%/tests/gdm.scm \ |
| 879 | %D%/tests/guix.scm \ | 881 | %D%/tests/guix.scm \ |
diff --git a/gnu/packages/forgejo.scm b/gnu/packages/forgejo.scm index 65127f1778d..2bd1694b7ee 100644 --- a/gnu/packages/forgejo.scm +++ b/gnu/packages/forgejo.scm | |||
| @@ -31,10 +31,12 @@ | |||
| 31 | #:use-module (gnu packages sqlite) | 31 | #:use-module (gnu packages sqlite) |
| 32 | #:use-module (gnu packages ssh)) | 32 | #:use-module (gnu packages ssh)) |
| 33 | 33 | ||
| 34 | ;;; Do not forget running: | ||
| 35 | ;;; 'make check-system TESTS='forgejo forgejo-mysql forgejo-postgres' when updating. | ||
| 34 | (define-public forgejo | 36 | (define-public forgejo |
| 35 | (package | 37 | (package |
| 36 | (name "forgejo") | 38 | (name "forgejo") |
| 37 | (version "15.0.0") | 39 | (version "15.0.2") |
| 38 | (source | 40 | (source |
| 39 | (origin | 41 | (origin |
| 40 | (method url-fetch) | 42 | (method url-fetch) |
| @@ -48,7 +50,7 @@ | |||
| 48 | "/forgejo-src-" version ".tar.gz")) | 50 | "/forgejo-src-" version ".tar.gz")) |
| 49 | (sha256 | 51 | (sha256 |
| 50 | (base32 | 52 | (base32 |
| 51 | "1b25zhlr81b577swfzxlhc7hiaxnznfygxbiv125vabjmahdmffy")))) | 53 | "16d142hfcz2mf3l0b4nc7dkgn1cf4hvg6vf0gdjjcx6ya7vpsan5")))) |
| 52 | (build-system gnu-build-system) | 54 | (build-system gnu-build-system) |
| 53 | (outputs (list "out" "data")) | 55 | (outputs (list "out" "data")) |
| 54 | (arguments | 56 | (arguments |
diff --git a/gnu/services/forgejo.scm b/gnu/services/forgejo.scm new file mode 100644 index 00000000000..86db26775d2 --- /dev/null +++ b/gnu/services/forgejo.scm | |||
| @@ -0,0 +1,913 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2026 Maxim Cournoyer <maxim@guixotic.coop> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (gnu services forgejo) | ||
| 20 | #:use-module (guix gexp) | ||
| 21 | #:use-module (guix least-authority) | ||
| 22 | #:use-module (guix records) | ||
| 23 | #:use-module (gnu build linux-container) | ||
| 24 | #:use-module (gnu packages forgejo) | ||
| 25 | #:use-module (gnu services) | ||
| 26 | #:use-module (gnu services shepherd) | ||
| 27 | #:use-module (gnu services configuration) | ||
| 28 | #:use-module (gnu system file-systems) | ||
| 29 | #:use-module (gnu system shadow) | ||
| 30 | #:use-module (ice-9 match) | ||
| 31 | #:use-module (srfi srfi-1) | ||
| 32 | #:export (forgejo-service-type | ||
| 33 | forgejo-configuration | ||
| 34 | forgejo-configuration-forgejo | ||
| 35 | forgejo-configuration-application-name | ||
| 36 | forgejo-configuration-application-slogan | ||
| 37 | forgejo-configuration-run-mode | ||
| 38 | forgejo-configuration-configuration-wizard? | ||
| 39 | forgejo-configuration-domain | ||
| 40 | forgejo-configuration-port | ||
| 41 | forgejo-configuration-protocol | ||
| 42 | forgejo-configuration-root-url | ||
| 43 | forgejo-configuration-http-address | ||
| 44 | forgejo-configuration-acme? | ||
| 45 | forgejo-configuration-tls-cert-file | ||
| 46 | forgejo-configuration-tls-key-file | ||
| 47 | forgejo-configuration-ssh-address | ||
| 48 | forgejo-configuration-ssh-port | ||
| 49 | forgejo-configuration-actions? | ||
| 50 | forgejo-configuration-default-actions-url | ||
| 51 | forgejo-configuration-disable-registration? | ||
| 52 | forgejo-configuration-confirm-mail-on-registration? | ||
| 53 | forgejo-configuration-internal-token-file | ||
| 54 | forgejo-configuration-secret-key-file | ||
| 55 | forgejo-configuration-lfs? | ||
| 56 | forgejo-configuration-lfs-secret-file | ||
| 57 | forgejo-configuration-log-level | ||
| 58 | forgejo-configuration-oauth2-secret-file | ||
| 59 | forgejo-configuration-mail-notification? | ||
| 60 | forgejo-configuration-mail-notification-on-new-user-signin? | ||
| 61 | forgejo-configuration-mailer? | ||
| 62 | forgejo-configuration-mail-subject-prefix | ||
| 63 | forgejo-configuration-mail-protocol | ||
| 64 | forgejo-configuration-smtp-address | ||
| 65 | forgejo-configuration-smtp-port | ||
| 66 | forgejo-configuration-smtp-user | ||
| 67 | forgejo-configuration-smtp-password-file | ||
| 68 | forgejo-configuration-mail-plain-text? | ||
| 69 | forgejo-configuration-mail-from | ||
| 70 | forgejo-configuration-sendmail-command | ||
| 71 | forgejo-configuration-sendmail-options | ||
| 72 | forgejo-configuration-sendmail-timeout | ||
| 73 | forgejo-configuration-sendmail-convert-crlf? | ||
| 74 | forgejo-configuration-openid-signin? | ||
| 75 | forgejo-configuration-metrics? | ||
| 76 | forgejo-configuration-metrics-token | ||
| 77 | forgejo-configuration-database-type | ||
| 78 | forgejo-configuration-database-host | ||
| 79 | forgejo-configuration-database-name | ||
| 80 | forgejo-configuration-database-user | ||
| 81 | forgejo-configuration-database-password-file | ||
| 82 | forgejo-configuration-database-tls? | ||
| 83 | forgejo-configuration-user-push-to-create? | ||
| 84 | forgejo-configuration-organization-push-to-create? | ||
| 85 | forgejo-configuration-default-push-to-create-private? | ||
| 86 | forgejo-configuration-require-signin-to-view? | ||
| 87 | forgejo-configuration-default-merge-style | ||
| 88 | forgejo-configuration-default-trust-model | ||
| 89 | forgejo-configuration-default-keep-email-private? | ||
| 90 | forgejo-configuration-offline-mode? | ||
| 91 | forgejo-configuration-user | ||
| 92 | forgejo-configuration-group | ||
| 93 | forgejo-configuration-custom-configuration-file | ||
| 94 | forgejo-configuration-shepherd-provision | ||
| 95 | forgejo-configuration-work-directory)) | ||
| 96 | |||
| 97 | ;;; Copied from (gnu services base) | ||
| 98 | (define (ipv6-address? str) | ||
| 99 | "Return true if STR denotes an IPv6 address." | ||
| 100 | (false-if-exception (->bool (inet-pton AF_INET6 str)))) | ||
| 101 | |||
| 102 | (define (database-type? x) | ||
| 103 | (member x '("sqlite3" "mysql" "postgres"))) | ||
| 104 | |||
| 105 | (define (port? x) | ||
| 106 | (and (number? x) | ||
| 107 | (and (>= x 1) (<= x 65535)))) | ||
| 108 | |||
| 109 | (define (privileged-port? x) | ||
| 110 | (and (number? x) | ||
| 111 | (and (>= x 1) (<= x 1023)))) | ||
| 112 | |||
| 113 | (define (unprivileged-port? x) | ||
| 114 | (and (number? x) | ||
| 115 | (and (>= x 1024) (<= x 65535)))) | ||
| 116 | |||
| 117 | (define (port-or-false? x) | ||
| 118 | (or (not x) | ||
| 119 | (port? x))) | ||
| 120 | |||
| 121 | (define (log-level? x) | ||
| 122 | (member x '("trace" "debug" "info" "warn" "error" "none"))) | ||
| 123 | |||
| 124 | (define (merge-style? x) | ||
| 125 | (member x '("merge" "rebase" "rebase-merge" "squash" "fast-forward-only"))) | ||
| 126 | |||
| 127 | (define (protocol? x) | ||
| 128 | (member x '("http" "https" "fcgi" "http+unix" "fcgi+unix"))) | ||
| 129 | |||
| 130 | (define (run-mode? x) | ||
| 131 | (member x '("dev" "prod"))) | ||
| 132 | |||
| 133 | (define (trust-model? x) | ||
| 134 | (member x '("collaborator" "committer" "collaboratorcommitter"))) | ||
| 135 | |||
| 136 | (define (ssl-mode? x) | ||
| 137 | (member x '(#t #f 'skip-verify))) | ||
| 138 | |||
| 139 | (define (serialize-database-tls type value) | ||
| 140 | (match type | ||
| 141 | ("sqlite3" "disable") ;does not apply | ||
| 142 | ("mysql" (match value | ||
| 143 | (#t "true") | ||
| 144 | (#f "false") | ||
| 145 | ('skip-verify "skip-verify"))) | ||
| 146 | ("postgres" (match value | ||
| 147 | (#t "verify-full") | ||
| 148 | (#f "disable") | ||
| 149 | ('skip-verify "require"))))) | ||
| 150 | |||
| 151 | (define-maybe file-like) | ||
| 152 | |||
| 153 | (define-maybe/no-serialization string) | ||
| 154 | |||
| 155 | (define (mail-protocol? x) | ||
| 156 | (member x '("" "dummy" "sendmail" | ||
| 157 | "smtp" "smtps" "smtp+starttls" "smtp+unix"))) | ||
| 158 | |||
| 159 | (define-configuration/no-serialization forgejo-configuration | ||
| 160 | (forgejo | ||
| 161 | (file-like forgejo) | ||
| 162 | "The forgejo package.") | ||
| 163 | |||
| 164 | (work-directory | ||
| 165 | (string "/var/lib/forgejo") | ||
| 166 | "The directory where Forgejo keeps its state, including its database when | ||
| 167 | using @code{\"sqlite3\"} as the @code{database-type}.") | ||
| 168 | |||
| 169 | (shepherd-provision | ||
| 170 | (list-of-symbols '(forgejo)) | ||
| 171 | "The name(s) of the service.") | ||
| 172 | |||
| 173 | (configuration-wizard? | ||
| 174 | (boolean #f) | ||
| 175 | "Whether to use the initial configuration page (wizard) to setup Forgejo. | ||
| 176 | Setting this to @code{#t} means that none of the declarative configuration | ||
| 177 | values will be honored. The Forgejo-generated configuration file will be kept | ||
| 178 | under @file{custom/conf/app.ini}, relative to the value of the | ||
| 179 | @code{work-directory}.") | ||
| 180 | |||
| 181 | (user | ||
| 182 | (string "forgejo") | ||
| 183 | "The name of the user under which Forgejo will be executed.") | ||
| 184 | |||
| 185 | (group | ||
| 186 | (string "forgejo") | ||
| 187 | "The name of the group under which Forgejo will be executed.") | ||
| 188 | |||
| 189 | (custom-configuration-file | ||
| 190 | maybe-file-like | ||
| 191 | "File-like object to provide a complete Forgejo configuration file as an | ||
| 192 | escape hatch, overriding every other fields of this configuration.") | ||
| 193 | |||
| 194 | ;; Global settings (no section). | ||
| 195 | (application-name | ||
| 196 | (string "Forgejo") | ||
| 197 | "The application name that shows in every page title.") | ||
| 198 | |||
| 199 | (application-slogan | ||
| 200 | (string "") | ||
| 201 | "Slogan to show near the application name in every page title.") | ||
| 202 | |||
| 203 | (run-mode | ||
| 204 | (run-mode "prod") | ||
| 205 | "The run mode to use; either @code{\"dev\"} or @code{\"prod\"}.") | ||
| 206 | |||
| 207 | ;; [server] settings. | ||
| 208 | (domain | ||
| 209 | (string "localhost") | ||
| 210 | "The domain name or host address of the server.") | ||
| 211 | |||
| 212 | (offline-mode? | ||
| 213 | (boolean #f) | ||
| 214 | "Set to @code{#t} to run Forgejo in offline mode, meaning it won't reach | ||
| 215 | out to any external services such as Gravatar, CDNs, etc. and serve everything | ||
| 216 | locally instead.") | ||
| 217 | |||
| 218 | (port | ||
| 219 | (unprivileged-port 3000) | ||
| 220 | "The port Forgejo will listen on. The port should be an unprivileged port; | ||
| 221 | to serve Forgejo on a privileged port like @code{80}, configure a reverse | ||
| 222 | proxy. It is ignored when using a Unix-domain socket.") | ||
| 223 | |||
| 224 | (protocol | ||
| 225 | (protocol "http") | ||
| 226 | "Listen protocol. Either @code{http}, @code{https}, @code{fcgi}, | ||
| 227 | @code{\"http+unix\"} or @code{\"fcgi+unix\"}. @samp{+unix} means @emph{via} a | ||
| 228 | Unix-domain socket, in which case @code{http-address} must be the file name of | ||
| 229 | the socket to use.") | ||
| 230 | |||
| 231 | (root-url | ||
| 232 | maybe-string | ||
| 233 | "Overwrite the automatically generated public URL, | ||
| 234 | which implicitly defaults to @samp{@var{protocol}://@var{domain}:@var{port}}, | ||
| 235 | which is necessary using a reverse proxy.") | ||
| 236 | |||
| 237 | (http-address | ||
| 238 | (string "0.0.0.0") | ||
| 239 | "The address to listen to. It can be an absolute file name when using a | ||
| 240 | Unix-domain socket, for example @file{/run/forgejo.sock}, or a file name | ||
| 241 | relative to the @code{work-directory}, for example @file{forgejo.sock}.") | ||
| 242 | |||
| 243 | (ssh-address | ||
| 244 | (string "0.0.0.0") | ||
| 245 | "The IP address to listen on for the SSH server.") | ||
| 246 | |||
| 247 | (lfs? | ||
| 248 | (boolean #t) | ||
| 249 | "Whether to enable Git LFS support.") | ||
| 250 | |||
| 251 | (lfs-secret-file | ||
| 252 | (string "/etc/forgejo/lfs_jwt_secret") | ||
| 253 | "The file name of the LFS JWT (JSON Web Token) secret, necessary for | ||
| 254 | proper Git LFS operation, which can be generated with @samp{forgejo generate | ||
| 255 | secret JWT_SECRET}. Automatically generated the first time Forgejo runs if it | ||
| 256 | does not yet exist.") | ||
| 257 | |||
| 258 | (mail-notification? | ||
| 259 | (boolean #f) | ||
| 260 | "Whether to enable email notifications.") | ||
| 261 | |||
| 262 | (acme? | ||
| 263 | (boolean #f) | ||
| 264 | "Whether to enable @acronym{ACME, Automatic Certificate Management | ||
| 265 | Environment}, to automatically provision TLS certificates, which can be useful | ||
| 266 | if you use the @code{https} or @code{https+unix} protocols.") | ||
| 267 | |||
| 268 | (tls-cert-file | ||
| 269 | (string "https/cert.pem") | ||
| 270 | "File name of a TLS certificate file. Ignored if @code{acme?} is enabled. | ||
| 271 | A relative file name is located under @file{@var{work-directory}/custom}.") | ||
| 272 | |||
| 273 | (tls-key-file | ||
| 274 | (string "https/key.pem") | ||
| 275 | "File name of a TLS key. Ignored if @code{acme?} is enabled. A relative | ||
| 276 | file name is located under @file{@var{work-directory}/custom}.") | ||
| 277 | |||
| 278 | (ssh-port | ||
| 279 | (port-or-false 22) | ||
| 280 | "The SSH port Forgejo will use; if you already run an OpenSSH as the SSH | ||
| 281 | server, specify the port used by the SSH daemon. Otherwise, you need to use | ||
| 282 | an unprivileged port (greater or equal to 1024) which Forgejo will use with | ||
| 283 | its built-in SSH server. Set to @code{#f} to disable SSH support.") | ||
| 284 | |||
| 285 | ;; [actions] settings. | ||
| 286 | (actions? | ||
| 287 | (boolean #t) | ||
| 288 | "Whether to enable actions capabilities.") | ||
| 289 | |||
| 290 | (default-actions-url | ||
| 291 | (string "https://code.forgejo.org") | ||
| 292 | "The default address to fetch action plugins from.") | ||
| 293 | |||
| 294 | ;; [security] settings. | ||
| 295 | (internal-token-file | ||
| 296 | (string "/etc/forgejo/internal_token") | ||
| 297 | "The file name of the internal token used to validate communication within | ||
| 298 | Forgejo, which can be generated with @samp{forgejo generate | ||
| 299 | secret INTERNAL_TOKEN}") | ||
| 300 | |||
| 301 | (secret-key-file | ||
| 302 | (string "/etc/forgejo/secret_key") | ||
| 303 | "The file name containing the global secret key used for encrypting data | ||
| 304 | like 2FA secrets; it is thus very important to back it up somewhere safe to | ||
| 305 | avoid losing access to encrypted data.") | ||
| 306 | |||
| 307 | ;; [oauth2] settings. | ||
| 308 | (oauth2-jwt-secret-file | ||
| 309 | (string "/etc/forgejo/oauth2_jwt_secret") | ||
| 310 | "The file name of the OAuth2 JWT secret, which Forgejo expects to exist.") | ||
| 311 | |||
| 312 | ;; [admin] settings. | ||
| 313 | (mail-notification-on-new-user-signin? | ||
| 314 | (boolean #f) | ||
| 315 | "Whether to notify administrators by email when a new user signs in for the | ||
| 316 | first time.") | ||
| 317 | |||
| 318 | ;; [mailer] settings. | ||
| 319 | (mailer? | ||
| 320 | (boolean #f) | ||
| 321 | "Whether to enable the mail server integration, used for sending email | ||
| 322 | notifications for example.") | ||
| 323 | |||
| 324 | (mail-from | ||
| 325 | (string "") | ||
| 326 | "The FROM email address used for sent emails, per the RFC 5322 | ||
| 327 | specification. This can be for example just an email address, or something | ||
| 328 | like @samp{\"Name\" <email@@example.com>}. This field @emph{must} be specified | ||
| 329 | if @code{mailer?} is set to @code{#t}.") | ||
| 330 | |||
| 331 | (mail-subject-prefix | ||
| 332 | (string "") | ||
| 333 | "The prefix displayed before the subject in emails.") | ||
| 334 | |||
| 335 | (mail-protocol | ||
| 336 | (mail-protocol "") | ||
| 337 | "The mail server protocol. One of @code{\"smtp\"}, @code{\"smtps\"}, | ||
| 338 | @code{\"smtp+starttls\"}, @code{\"smtp+unix\"}, @code{\"sendmail\"}, | ||
| 339 | @code{\"dummy\"} or @code{\"\"}. The empty string defaults means the protocol | ||
| 340 | is inferred from the value of the @code{smtp-port} value, meaning its | ||
| 341 | effective implicit value is @code{\"smtps\"}. @code{\"dummy\"} causes emails | ||
| 342 | to be sent to the log, useful for testing. When @code{\"sendmail\"} is used, | ||
| 343 | it is expected to be fully and correctly externally configured, with the other | ||
| 344 | @samp{smtp-} prefixed options of this configuration ignored.") | ||
| 345 | |||
| 346 | (smtp-address | ||
| 347 | (string "") | ||
| 348 | "The mail server address, for example @code{\"smtp.example.com\"} or an | ||
| 349 | absolute file name, if using @code{\"smtp+unix\"} for the | ||
| 350 | @code{mail-protocol}.") | ||
| 351 | |||
| 352 | (smtp-port | ||
| 353 | (port 465) ;implies smtps protocol | ||
| 354 | "The mail server port. This field affects the implicit value of the | ||
| 355 | @code{mail-protocol}.") | ||
| 356 | |||
| 357 | (smtp-user | ||
| 358 | (string "") | ||
| 359 | "The SMTP server user name, if required ") | ||
| 360 | |||
| 361 | (smtp-password-file | ||
| 362 | (string "") | ||
| 363 | "An absolute file name containing the password of the SMTP server.") | ||
| 364 | |||
| 365 | (mail-plain-text? | ||
| 366 | (boolean #f) | ||
| 367 | "Send emails only in plain text, without HTML alternative.") | ||
| 368 | |||
| 369 | (sendmail-command | ||
| 370 | (string "sendmail") | ||
| 371 | "The file name of the @command{sendmail} command to use, which can be | ||
| 372 | either an absolute file name or a command name to be looked from | ||
| 373 | @env{\"PATH\"}.") | ||
| 374 | |||
| 375 | (sendmail-options | ||
| 376 | (list-of-strings '()) | ||
| 377 | "Optional options to pass to the @command{sendmail} command. If your | ||
| 378 | @code{sendmail} program, like that of Postfix, interprets options, you should | ||
| 379 | provide @code{\"--\"} as the first option.") | ||
| 380 | |||
| 381 | (sendmail-timeout | ||
| 382 | (string "5m") | ||
| 383 | "The timeout for Sendmail, provided as a | ||
| 384 | @url{https://pkg.go.dev/time#ParseDuration, Go @code{time.Duration} string}, | ||
| 385 | for example @code{\"300ms\"}, @code{\"1.5h\"} or @code{\"2h45m\"}.") | ||
| 386 | |||
| 387 | (sendmail-convert-crlf? | ||
| 388 | (boolean #t) | ||
| 389 | "Whether to convert @samp{\\r\\n} to @samp{\\n} for Sendmail.") | ||
| 390 | |||
| 391 | ;; [log] settings. | ||
| 392 | (log-level | ||
| 393 | (log-level "info") | ||
| 394 | "The log level to use. Either @code{\"trace\"}, @code{\"debug\"}, | ||
| 395 | @code{\"info\"}, @code{\"warn\"}, @code{\"error\"} or @code{\"none\"}.") | ||
| 396 | |||
| 397 | ;; [openid] settings. | ||
| 398 | (openid-signin? | ||
| 399 | (boolean #t) | ||
| 400 | "Whether to allow signing in using OpenID.") | ||
| 401 | |||
| 402 | ;; [service] settings. | ||
| 403 | (disable-registration? | ||
| 404 | (boolean #f) | ||
| 405 | "Disallow registration, only allowing administrators to create accounts.") | ||
| 406 | |||
| 407 | (confirm-mail-on-registration? | ||
| 408 | (boolean #f) | ||
| 409 | "Whether to require email confirmation when new users register.") | ||
| 410 | |||
| 411 | (require-signin-to-view? | ||
| 412 | (boolean #f) | ||
| 413 | "Whether users must sign in before they can view the explore pages.") | ||
| 414 | |||
| 415 | (default-keep-email-private? | ||
| 416 | (boolean #f) | ||
| 417 | "Whether by default a user email is displayed on their profile.") | ||
| 418 | |||
| 419 | (user-push-to-create? | ||
| 420 | (boolean #f) | ||
| 421 | "Allow users to push local repositories to Forgejo and have them | ||
| 422 | automatically created for a user.") | ||
| 423 | |||
| 424 | (organization-push-to-create? | ||
| 425 | (boolean #f) | ||
| 426 | "Allow users to push local repositories to Forgejo and have them | ||
| 427 | automatically created for an organization.") | ||
| 428 | |||
| 429 | (default-push-to-create-private? | ||
| 430 | (boolean #t) | ||
| 431 | "Whether to make newly created repositories private by default when they | ||
| 432 | were created via push.") | ||
| 433 | |||
| 434 | ;; [repository.pull-request] settings. | ||
| 435 | (default-merge-style | ||
| 436 | (merge-style "merge") | ||
| 437 | "The default merge style to use. Either @code{\"merge\"}, | ||
| 438 | @code{\"rebase\"}, @code{\"rebase-merge\"}, @code{\"squash\"} or | ||
| 439 | @code{\"fast-forward-only\"}.") | ||
| 440 | |||
| 441 | ;; [repository.signing] settings. | ||
| 442 | (default-trust-model | ||
| 443 | (trust-model "committer") | ||
| 444 | "The default trust model for repositories. Either | ||
| 445 | @code{\"collaborator\"}, @code{\"committer\"} or | ||
| 446 | @code{\"collaboratorcommitter\"}") | ||
| 447 | |||
| 448 | ;; [metrics] settings. | ||
| 449 | (metrics? | ||
| 450 | (boolean #f) | ||
| 451 | "Whether to enable the metrics endpoint.") | ||
| 452 | |||
| 453 | (metrics-token | ||
| 454 | (string "") | ||
| 455 | "A secret to use, if authorization to the metrics endpoint is desired.") | ||
| 456 | |||
| 457 | ;; [database] settings. | ||
| 458 | (database-type | ||
| 459 | (database-type "sqlite3") | ||
| 460 | "The database type to use. Either @code{\"sqlite3\"}, @code{\"mysql\"} or | ||
| 461 | @code{\"postgres\"}. The sqlite3 type is the simplest to use, but a | ||
| 462 | full-fledged database like PostgreSQL may scale better for multiple users. If | ||
| 463 | you change this, make sure to adjust the other @samp{database-*} values | ||
| 464 | accordingly. Also note that currently, the PostgreSQL and MySQL databases | ||
| 465 | must be manually created: refer to the official Forgejo documentation for | ||
| 466 | @url{https://forgejo.org/docs/latest/admin/installation/database-preparation, | ||
| 467 | database preparation}.") | ||
| 468 | |||
| 469 | (database-host | ||
| 470 | (string "/var/run/postgresql") | ||
| 471 | "This is the @samp{@var{host}:@var{port}} tuple for remote database | ||
| 472 | MySQL/PostgreSQL hosts, or a local Unix-domain socket file. If using MySQL, | ||
| 473 | this should be adjusted to @file{/run/mysqld/mysqld.sock}") | ||
| 474 | |||
| 475 | (database-name | ||
| 476 | ;; Keep the default database name in sync with the database user name, so | ||
| 477 | ;; that it can be easily provisioned via a the | ||
| 478 | ;; postgresql-role-service-type, which assumes both match. | ||
| 479 | (string "forgejo") | ||
| 480 | "The MySQL/PostgreSQL database name to use.") | ||
| 481 | |||
| 482 | (database-user | ||
| 483 | (string "forgejo") | ||
| 484 | "The MySQL/PostgreSQL database user to use.") | ||
| 485 | |||
| 486 | (database-password-file | ||
| 487 | maybe-string | ||
| 488 | "A file name containing the password to access the MySQL/PostgreSQL | ||
| 489 | database, for example @file{\"/etc/forgejo/db_passwd\"}.") | ||
| 490 | |||
| 491 | (database-tls? | ||
| 492 | (ssl-mode #f) | ||
| 493 | "Whether the connection to a MySQL/PostgreSQL database should use TLS. Set | ||
| 494 | to @code{#t} to enable TLS with full verification or to @code{'skip-verify} to | ||
| 495 | enable TLS without verification.")) | ||
| 496 | |||
| 497 | ;;; TODO: Allow enabling/configuring email integration | ||
| 498 | ;;; See custom/conf/app.example.ini for a documentation of the fields. | ||
| 499 | (define forgejo-configuration->file | ||
| 500 | (match-record-lambda <forgejo-configuration> | ||
| 501 | ( application-name application-slogan work-directory | ||
| 502 | run-mode domain protocol http-address port root-url | ||
| 503 | acme? tls-cert-file tls-key-file | ||
| 504 | ssh-address ssh-port | ||
| 505 | default-keep-email-private? | ||
| 506 | default-merge-style default-trust-model | ||
| 507 | actions? default-actions-url | ||
| 508 | disable-registration? confirm-mail-on-registration? | ||
| 509 | require-signin-to-view? mail-notification? | ||
| 510 | internal-token-file secret-key-file | ||
| 511 | lfs? lfs-secret-file oauth2-jwt-secret-file | ||
| 512 | mail-notification-on-new-user-signin? | ||
| 513 | mailer? mail-subject-prefix mail-protocol | ||
| 514 | smtp-address smtp-port smtp-user smtp-password-file | ||
| 515 | mail-plain-text? mail-from | ||
| 516 | sendmail-command sendmail-options sendmail-timeout | ||
| 517 | sendmail-convert-crlf? | ||
| 518 | log-level offline-mode? openid-signin? | ||
| 519 | metrics? metrics-token | ||
| 520 | user-push-to-create? organization-push-to-create? | ||
| 521 | default-push-to-create-private? | ||
| 522 | user group | ||
| 523 | custom-configuration-file | ||
| 524 | database-type database-host database-name database-user | ||
| 525 | database-password-file database-tls?) | ||
| 526 | (if (maybe-value-set? custom-configuration-file) | ||
| 527 | custom-configuration-file | ||
| 528 | (mixed-text-file "forgejo.ini" " | ||
| 529 | APP_NAME = " application-name " | ||
| 530 | APP_SLOGAN = " application-slogan " | ||
| 531 | RUN_USER = " user " | ||
| 532 | WORK_PATH = " work-directory " | ||
| 533 | RUN_MODE = " run-mode " | ||
| 534 | |||
| 535 | [database] | ||
| 536 | DB_TYPE = " database-type " | ||
| 537 | HOST = " database-host " | ||
| 538 | NAME = " database-name " | ||
| 539 | USER = " database-user "\n" | ||
| 540 | (if (maybe-value-set? database-password-file) | ||
| 541 | (string-append "PASSWD_URI = file://" database-password-file "\n") | ||
| 542 | "") | ||
| 543 | "SSL_MODE = " (serialize-database-tls database-type database-tls?) " | ||
| 544 | |||
| 545 | [repository] | ||
| 546 | ROOT = " work-directory "/data/forgejo-repositories | ||
| 547 | DEFAULT_PUSH_CREATE_PRIVATE = " | ||
| 548 | (if default-push-to-create-private? "true" "false") " | ||
| 549 | ENABLE_PUSH_CREATE_USER = " (if user-push-to-create? "true" "false") " | ||
| 550 | ENABLE_PUSH_CREATE_ORG = " (if organization-push-to-create? "true" "false") " | ||
| 551 | |||
| 552 | [server] | ||
| 553 | SSH_DOMAIN = " domain " | ||
| 554 | SSH_LISTEN_HOST = " ssh-address " | ||
| 555 | PROTOCOL = " protocol " | ||
| 556 | DOMAIN = " domain " | ||
| 557 | HTTP_ADDR = " http-address " | ||
| 558 | HTTP_PORT = " (number->string port) "\n" | ||
| 559 | (if (maybe-value-set? root-url) | ||
| 560 | (string-append "ROOT_URL = " root-url "\n") | ||
| 561 | "") | ||
| 562 | "ENABLE_ACME = " (if acme? "true" "false") " | ||
| 563 | CERT_FILE = " tls-cert-file " | ||
| 564 | KEY_FILE = " tls-key-file " | ||
| 565 | APP_DATA_PATH = " work-directory "/data | ||
| 566 | DISABLE_SSH = " (if (not ssh-port) "true" "false") " | ||
| 567 | SSH_PORT = " (if (not ssh-port) "" (number->string ssh-port)) " | ||
| 568 | LFS_START_SERVER = " (if lfs? "true" "false") " | ||
| 569 | LFS_JWT_SECRET_URI = file://" lfs-secret-file " | ||
| 570 | OFFLINE_MODE = " (if offline-mode? "true" "false") " | ||
| 571 | |||
| 572 | [lfs] | ||
| 573 | PATH = " work-directory "/data/lfs | ||
| 574 | |||
| 575 | [service] | ||
| 576 | REGISTER_EMAIL_CONFIRM = false | ||
| 577 | ENABLE_NOTIFY_MAIL = " (if mail-notification? "true" "false") " | ||
| 578 | DISABLE_REGISTRATION = " (if disable-registration? "true" "false") " | ||
| 579 | REGISTER_EMAIL_CONFIRM = " (if confirm-mail-on-registration? "true" "false") " | ||
| 580 | ENABLE_CAPTCHA = false | ||
| 581 | REQUIRE_SIGNIN_VIEW = " (if require-signin-to-view? "true" "false") " | ||
| 582 | DEFAULT_KEEP_EMAIL_PRIVATE = " (if default-keep-email-private? "true" "false") " | ||
| 583 | |||
| 584 | [openid] | ||
| 585 | ENABLE_OPENID_SIGNIN = " (if openid-signin? "true" "false") " | ||
| 586 | |||
| 587 | [cron.update_checker] | ||
| 588 | ENABLED = false | ||
| 589 | |||
| 590 | [log] | ||
| 591 | LEVEL = " log-level " | ||
| 592 | ROOT_PATH = " work-directory "/log | ||
| 593 | |||
| 594 | [repository.pull-request] | ||
| 595 | DEFAULT_MERGE_STYLE = " default-merge-style " | ||
| 596 | |||
| 597 | [repository.signing] | ||
| 598 | DEFAULT_TRUST_MODEL = " default-trust-model " | ||
| 599 | |||
| 600 | [security] | ||
| 601 | INSTALL_LOCK = true | ||
| 602 | SECRET_KEY = file://" secret-key-file " | ||
| 603 | INTERNAL_TOKEN_URI = file://" internal-token-file " | ||
| 604 | |||
| 605 | [oauth2] | ||
| 606 | JWT_SECRET_URI = file://" oauth2-jwt-secret-file " | ||
| 607 | |||
| 608 | [admin] | ||
| 609 | SEND_NOTIFICATION_EMAIL_ON_NEW_USER = " | ||
| 610 | (if mail-notification-on-new-user-signin? "true" "false") " | ||
| 611 | |||
| 612 | [mailer] | ||
| 613 | ENABLED = " (if mailer? "true" "false") " | ||
| 614 | SUBJECT_PREFIX = " mail-subject-prefix " | ||
| 615 | PROTOCOL = " mail-protocol " | ||
| 616 | SMTP_ADDR = " smtp-address " | ||
| 617 | SMTP_PORT = " (number->string smtp-port) " | ||
| 618 | USER = " smtp-user " | ||
| 619 | PASSWD_URI = " (if (string-null? smtp-password-file) | ||
| 620 | "" | ||
| 621 | (string-append "file://" smtp-password-file)) " | ||
| 622 | SEND_AS_PLAIN_TEXT = " (if mail-plain-text? "true" "false") " | ||
| 623 | FROM = " mail-from " | ||
| 624 | SENDMAIL_PATH = " sendmail-command " | ||
| 625 | SENDMAIL_ARGS = " (string-join sendmail-options " ") " | ||
| 626 | SENDMAIL_TIMEOUT = " sendmail-timeout " | ||
| 627 | SENDMAIL_CONVERT_CRLF = " (if sendmail-convert-crlf? "true" "false") " | ||
| 628 | |||
| 629 | [metrics] | ||
| 630 | ENABLED = " (if metrics? "true" "false") " | ||
| 631 | TOKEN = " metrics-token " | ||
| 632 | |||
| 633 | [actions] | ||
| 634 | ENABLED = " (if actions? "true" "false") " | ||
| 635 | DEFAULT_ACTIONS_URL = " default-actions-url " | ||
| 636 | ")))) | ||
| 637 | |||
| 638 | (define (forgejo-shepherd-service config) | ||
| 639 | (match-record config <forgejo-configuration> | ||
| 640 | ( forgejo work-directory user group port | ||
| 641 | http-address protocol ssh-port | ||
| 642 | database-type database-host | ||
| 643 | configuration-wizard? | ||
| 644 | internal-token-file lfs-secret-file | ||
| 645 | smtp-password-file | ||
| 646 | tls-cert-file tls-key-file | ||
| 647 | oauth2-jwt-secret-file secret-key-file | ||
| 648 | shepherd-provision) | ||
| 649 | (let* ((mappings (append | ||
| 650 | %network-file-mappings | ||
| 651 | (list %store-mapping ;XXX: coarse-grained | ||
| 652 | (file-system-mapping | ||
| 653 | (source "/etc/ssl/certs") | ||
| 654 | (target source)) | ||
| 655 | (file-system-mapping | ||
| 656 | (source work-directory) | ||
| 657 | (target source) | ||
| 658 | (writable? #t)) | ||
| 659 | (file-system-mapping | ||
| 660 | (source internal-token-file) | ||
| 661 | (target source)) | ||
| 662 | (file-system-mapping | ||
| 663 | (source lfs-secret-file) | ||
| 664 | (target source)) | ||
| 665 | (file-system-mapping | ||
| 666 | (source oauth2-jwt-secret-file) | ||
| 667 | (target source)) | ||
| 668 | (file-system-mapping | ||
| 669 | (source secret-key-file) | ||
| 670 | (target source))) | ||
| 671 | (if (absolute-file-name? smtp-password-file) | ||
| 672 | (list (file-system-mapping | ||
| 673 | (source smtp-password-file) | ||
| 674 | (target source))) | ||
| 675 | '()) | ||
| 676 | (if (absolute-file-name? tls-cert-file) | ||
| 677 | (list (file-system-mapping | ||
| 678 | (source tls-cert-file) | ||
| 679 | (target source))) | ||
| 680 | '()) | ||
| 681 | (if (absolute-file-name? tls-key-file) | ||
| 682 | (list (file-system-mapping | ||
| 683 | (source tls-key-file) | ||
| 684 | (target source))) | ||
| 685 | '()) | ||
| 686 | (if (and (not (string=? "sqlite3" database-type)) | ||
| 687 | (absolute-file-name? database-host)) ;socket | ||
| 688 | (list (file-system-mapping | ||
| 689 | (source database-host) | ||
| 690 | (target source) | ||
| 691 | (writable? #t))) | ||
| 692 | '()))) | ||
| 693 | (forgejo (file-append forgejo "/bin/forgejo")) | ||
| 694 | (forgejo-wrapper (least-authority-wrapper | ||
| 695 | forgejo | ||
| 696 | #:name "forgejo-pola-wrapper" | ||
| 697 | #:user user #:group group | ||
| 698 | #:namespaces | ||
| 699 | ;; The wrapper needs access to the forgejo user | ||
| 700 | ;; to be able to mount | ||
| 701 | ;; /etc/forgejo/internal_token for example. | ||
| 702 | (fold delq %namespaces '(net user)) | ||
| 703 | #:mappings mappings | ||
| 704 | #:preserved-environment-variables | ||
| 705 | (cons* "FORGEJO_WORK_DIR" | ||
| 706 | "SSL_CERT_DIR" | ||
| 707 | %default-preserved-environment-variables))) | ||
| 708 | (config (forgejo-configuration->file config)) | ||
| 709 | (init-provision (list (symbol-append (first shepherd-provision) | ||
| 710 | '-init)))) | ||
| 711 | (list (shepherd-service | ||
| 712 | (documentation "Run Forgejo pre-start actions.") | ||
| 713 | (requirement '(networking user-processes)) | ||
| 714 | (provision init-provision) | ||
| 715 | (one-shot? #t) | ||
| 716 | (modules (cons '(srfi srfi-19) %default-modules)) | ||
| 717 | (start | ||
| 718 | #~(lambda _ | ||
| 719 | (chdir #$work-directory) | ||
| 720 | |||
| 721 | (define app.ini "custom/conf/app.ini") | ||
| 722 | |||
| 723 | (define (symlink? x) | ||
| 724 | (eq? (stat:type (stat x)) 'symlink)) | ||
| 725 | |||
| 726 | (define (delete-file/backup x) | ||
| 727 | (let* ((timestamp (date->string | ||
| 728 | (current-date) "~4")) | ||
| 729 | (bak (string-append | ||
| 730 | x "-" timestamp ".bak"))) | ||
| 731 | (copy-file x bak) | ||
| 732 | (delete-file x))) | ||
| 733 | |||
| 734 | (define (fork+exec-forgejo . options) | ||
| 735 | ;; We can't change the environment in a one-shot | ||
| 736 | ;; service/Shepherd (is this a bug?), so use this wrapper | ||
| 737 | ;; of fork+exec-command that passes the needed | ||
| 738 | ;; environment variables and raises on unclean exit. | ||
| 739 | (let ((pid | ||
| 740 | (apply fork+exec-command | ||
| 741 | (cons #$forgejo-wrapper options) | ||
| 742 | (list #:environment-variables | ||
| 743 | (list #$(string-append | ||
| 744 | "FORGEJO_WORK_DIR=" | ||
| 745 | work-directory) | ||
| 746 | #$(string-append | ||
| 747 | "HOME=" work-directory)))))) | ||
| 748 | (unless (zero? (cdr (waitpid pid))) | ||
| 749 | (error "failed to run forgejo command" | ||
| 750 | #$forgejo-wrapper options)))) | ||
| 751 | |||
| 752 | (unless #$configuration-wizard? | ||
| 753 | ;; When users transition from configuration-wizard? #t to | ||
| 754 | ;; #f, the stale wizard configuration is backed up. | ||
| 755 | (when (file-exists? app.ini) | ||
| 756 | (if (symlink? app.ini) | ||
| 757 | (delete-file app.ini) | ||
| 758 | (delete-file/backup app.ini))) | ||
| 759 | (symlink #$config app.ini)) | ||
| 760 | |||
| 761 | (when (file-exists? app.ini) | ||
| 762 | ;; Initialize Forgejo's database. | ||
| 763 | (fork+exec-forgejo "migrate") | ||
| 764 | ;; Refresh binary file names captured in git hooks. | ||
| 765 | (fork+exec-forgejo "admin" "regenerate" "hooks") | ||
| 766 | ;; Update command option in 'authorized_keys' file. | ||
| 767 | (fork+exec-forgejo "admin" "regenerate" "keys") | ||
| 768 | ;; Sanity check configuration/environment. | ||
| 769 | (fork+exec-forgejo "doctor" "check" "--all"))))) | ||
| 770 | (shepherd-service | ||
| 771 | (documentation "Run the Forgejo Git forge") | ||
| 772 | (requirement (append init-provision | ||
| 773 | '(networking | ||
| 774 | user-processes) | ||
| 775 | (if (privileged-port? ssh-port) | ||
| 776 | '(ssh-daemon) | ||
| 777 | '()) | ||
| 778 | (if (string=? "postgres" database-type) | ||
| 779 | '(postgres) | ||
| 780 | '()) | ||
| 781 | (if (string=? "mysql" database-type) | ||
| 782 | '(mysql) | ||
| 783 | '()))) | ||
| 784 | (provision shepherd-provision) | ||
| 785 | (actions | ||
| 786 | (list (shepherd-configuration-action | ||
| 787 | (if configuration-wizard? | ||
| 788 | (string-append work-directory "/custom/conf/app.ini") | ||
| 789 | config)))) | ||
| 790 | (start | ||
| 791 | #~(make-systemd-constructor | ||
| 792 | (list #$forgejo-wrapper "web" | ||
| 793 | #$@(if configuration-wizard? | ||
| 794 | '() | ||
| 795 | #~("--config" #$config))) | ||
| 796 | (cond | ||
| 797 | (#$(string-suffix? "+unix" protocol) | ||
| 798 | (list (endpoint (make-socket-address AF_UNIX | ||
| 799 | #$http-address) | ||
| 800 | #:socket-owner #$user | ||
| 801 | #:socket-group #$group))) | ||
| 802 | (#$(string=? "0.0.0.0" http-address) | ||
| 803 | (list (endpoint (make-socket-address AF_INET | ||
| 804 | INADDR_ANY #$port)) | ||
| 805 | (endpoint (make-socket-address AF_INET6 | ||
| 806 | INADDR_ANY #$port)))) | ||
| 807 | (#$(ipv6-address? http-address) | ||
| 808 | (list (endpoint (make-socket-address | ||
| 809 | AF_INET6 | ||
| 810 | (inet-pton AF_INET6 #$http-address) | ||
| 811 | #$port)))) | ||
| 812 | (else ;assumed ipv4 | ||
| 813 | (list (endpoint (make-socket-address | ||
| 814 | AF_INET | ||
| 815 | (inet-pton AF_INET #$http-address) | ||
| 816 | #$port))))) | ||
| 817 | #:log-file "/var/log/forgejo.log" | ||
| 818 | #:environment-variables | ||
| 819 | (list (string-append "HOME=" #$work-directory) | ||
| 820 | "SSL_CERT_DIR=/etc/ssl/certs"))) | ||
| 821 | (stop #~(make-systemd-destructor))))))) | ||
| 822 | |||
| 823 | (define %forgejo-accounts | ||
| 824 | (match-record-lambda <forgejo-configuration> | ||
| 825 | (user group work-directory) | ||
| 826 | (list (user-group (name user) | ||
| 827 | (system? #t)) | ||
| 828 | (user-account | ||
| 829 | (name user) | ||
| 830 | (group group) | ||
| 831 | (system? #t) | ||
| 832 | (comment "Forgejo User") | ||
| 833 | (home-directory work-directory))))) | ||
| 834 | |||
| 835 | (define %forgejo-activation | ||
| 836 | (match-record-lambda <forgejo-configuration> | ||
| 837 | ( forgejo user configuration-wizard? work-directory | ||
| 838 | protocol http-address | ||
| 839 | secret-key-file internal-token-file lfs-secret-file | ||
| 840 | oauth2-jwt-secret-file) | ||
| 841 | #~(begin | ||
| 842 | (use-modules (gnu build activation) | ||
| 843 | (guix build utils) | ||
| 844 | (ice-9 match)) | ||
| 845 | |||
| 846 | (define secret-files `(("SECRET_KEY" . ,#$secret-key-file) | ||
| 847 | ("INTERNAL_TOKEN" . ,#$internal-token-file) | ||
| 848 | ("LFS_JWT_SECRET" . ,#$lfs-secret-file) | ||
| 849 | ("JWT_SECRET" . ,#$oauth2-jwt-secret-file))) | ||
| 850 | |||
| 851 | (define forgejo #$(file-append forgejo "/bin/forgejo")) | ||
| 852 | |||
| 853 | (define (generate-secret type file) | ||
| 854 | (with-exception-handler | ||
| 855 | (lambda (e) | ||
| 856 | (false-if-exception (delete-file file)) | ||
| 857 | (raise-exception e)) | ||
| 858 | (lambda () | ||
| 859 | (call-with-output-file file | ||
| 860 | (lambda (port) | ||
| 861 | (let ((pid (spawn forgejo | ||
| 862 | (list forgejo "generate" "secret" type) | ||
| 863 | #:output port))) | ||
| 864 | (match (waitpid pid) | ||
| 865 | ((_ . status) | ||
| 866 | (unless (zero? status) | ||
| 867 | (error "failed to generate forgejo secret" type)))))))))) | ||
| 868 | |||
| 869 | (let* ((owner (getpw #$user)) | ||
| 870 | (uid (passwd:uid owner)) | ||
| 871 | (gid (passwd:gid owner))) | ||
| 872 | ;; Top-level directories of secrets; needed even when not using the | ||
| 873 | ;; wizard. | ||
| 874 | (for-each (match-lambda | ||
| 875 | ((_ . file) | ||
| 876 | (mkdir-p/perms (dirname file) owner #o750))) | ||
| 877 | secret-files) | ||
| 878 | (when (and (string-suffix? "-unix" #$protocol) | ||
| 879 | (absolute-file-name? http-address)) | ||
| 880 | (mkdir-p/perms (dirname http-address) owner #o750)) | ||
| 881 | ;; To please 'forgejo doctor check --all' out of the box. | ||
| 882 | (mkdir-p/perms #$work-directory owner #o750) | ||
| 883 | (with-directory-excursion #$work-directory | ||
| 884 | (mkdir-p/perms ".ssh" owner #o700) | ||
| 885 | (mkdir-p/perms "custom" owner #o750) | ||
| 886 | (mkdir-p/perms "custom/conf" owner #o750) | ||
| 887 | (mkdir-p/perms "data" owner #o750) | ||
| 888 | (mkdir-p/perms "data/forgejo-repositories" owner #o750) | ||
| 889 | (mkdir-p/perms "log" owner #o750)) | ||
| 890 | (unless #$configuration-wizard? | ||
| 891 | ;; Bootstrap secret files. | ||
| 892 | (for-each (match-lambda | ||
| 893 | ((type . file) | ||
| 894 | (unless (file-exists? file) | ||
| 895 | (generate-secret type file)) | ||
| 896 | (chown file uid gid) | ||
| 897 | (chmod file #o400))) | ||
| 898 | secret-files)))))) | ||
| 899 | |||
| 900 | (define forgejo-service-type | ||
| 901 | (service-type | ||
| 902 | (name 'forgejo) | ||
| 903 | (extensions | ||
| 904 | (list (service-extension shepherd-root-service-type | ||
| 905 | forgejo-shepherd-service) | ||
| 906 | (service-extension account-service-type | ||
| 907 | %forgejo-accounts) | ||
| 908 | (service-extension activation-service-type | ||
| 909 | %forgejo-activation) | ||
| 910 | (service-extension profile-service-type | ||
| 911 | (compose list forgejo-configuration-forgejo)))) | ||
| 912 | (default-value (forgejo-configuration)) | ||
| 913 | (description "Run Forgejo."))) | ||
diff --git a/gnu/tests/forgejo.scm b/gnu/tests/forgejo.scm new file mode 100644 index 00000000000..d13114228f7 --- /dev/null +++ b/gnu/tests/forgejo.scm | |||
| @@ -0,0 +1,416 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2026 Maxim Cournoyer <maxim@guixotic.coop> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (gnu tests forgejo) | ||
| 20 | #:use-module (gnu packages databases) | ||
| 21 | #:use-module (gnu packages guile) | ||
| 22 | #:use-module (gnu packages linux) | ||
| 23 | #:use-module (gnu packages sqlite) | ||
| 24 | #:use-module (gnu packages version-control) | ||
| 25 | #:use-module (gnu tests) | ||
| 26 | #:use-module (gnu system) | ||
| 27 | #:use-module (gnu system vm) | ||
| 28 | #:use-module (gnu services) | ||
| 29 | #:use-module (gnu services base) | ||
| 30 | #:use-module (gnu services databases) | ||
| 31 | #:use-module (gnu services forgejo) | ||
| 32 | #:use-module (gnu services networking) | ||
| 33 | #:use-module (gnu services ssh) | ||
| 34 | #:use-module (guix gexp) | ||
| 35 | #:use-module (ice-9 match) | ||
| 36 | #:use-module (srfi srfi-1) | ||
| 37 | #:export (%test-forgejo | ||
| 38 | %test-forgejo-mysql | ||
| 39 | %test-forgejo-postgres)) | ||
| 40 | |||
| 41 | ;;; Sample SSH key generated with 'ssh-keygen -t ed25519'. | ||
| 42 | (define dummy-ssh-private-key "\ | ||
| 43 | -----BEGIN OPENSSH PRIVATE KEY----- | ||
| 44 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | ||
| 45 | QyNTUxOQAAACAsDitIHHy6wlmXz7cJO0UQQbrszdpLvBSiTZk1l08z0AAAAJBl4y2eZeMt | ||
| 46 | ngAAAAtzc2gtZWQyNTUxOQAAACAsDitIHHy6wlmXz7cJO0UQQbrszdpLvBSiTZk1l08z0A | ||
| 47 | AAAEDxsrZIsEgI6cehDgFNsP/FQ3aefvb7fUtp/RF6sZf3GiwOK0gcfLrCWZfPtwk7RRBB | ||
| 48 | uuzN2ku8FKJNmTWXTzPQAAAAC21heGltQHRlcnJhAQI= | ||
| 49 | -----END OPENSSH PRIVATE KEY----- | ||
| 50 | ") | ||
| 51 | |||
| 52 | (define dummy-ssh-public-key "\ | ||
| 53 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwOK0gcfLrCWZfPtwk7RRBBuuzN2ku8FK\ | ||
| 54 | JNmTWXTzPQ dummy") | ||
| 55 | |||
| 56 | ;;; For a manual test/inspection: | ||
| 57 | #;( | ||
| 58 | |||
| 59 | $(./pre-inst-env guix system vm \ | ||
| 60 | -e '(@@ (gnu tests forgejo) %forgejo-os)' --no-graphic) \ | ||
| 61 | -nic user,model=virtio-net-pci,hostfwd=tcp::3000-:3000 -m 1024 | ||
| 62 | |||
| 63 | ) | ||
| 64 | |||
| 65 | (define %forgejo-os | ||
| 66 | (let ((base %simple-os)) | ||
| 67 | (operating-system | ||
| 68 | (inherit %simple-os) | ||
| 69 | (packages (cons* git-minimal git-lfs | ||
| 70 | ;;sqlite strace ;for debug | ||
| 71 | %base-packages)) | ||
| 72 | (services | ||
| 73 | (cons* (service dhcpcd-service-type) | ||
| 74 | (service openssh-service-type) | ||
| 75 | (service forgejo-service-type | ||
| 76 | (forgejo-configuration | ||
| 77 | (application-slogan "In Code We Trust") | ||
| 78 | (run-mode "dev") | ||
| 79 | (log-level "debug") | ||
| 80 | (user-push-to-create? #t) | ||
| 81 | (default-push-to-create-private? #f) | ||
| 82 | (mailer? #t) | ||
| 83 | (mail-from "Forgejo Test <test@example.com>") | ||
| 84 | (mail-protocol "dummy") | ||
| 85 | (mail-notification? #t) | ||
| 86 | (mail-notification-on-new-user-signin? #t))) | ||
| 87 | %base-services))))) | ||
| 88 | |||
| 89 | (define %forgejo-os/postgres | ||
| 90 | (operating-system | ||
| 91 | (inherit %forgejo-os) | ||
| 92 | (services | ||
| 93 | (cons* | ||
| 94 | (service postgresql-service-type | ||
| 95 | (postgresql-configuration | ||
| 96 | (postgresql postgresql))) | ||
| 97 | (service postgresql-role-service-type | ||
| 98 | (postgresql-role-configuration | ||
| 99 | (roles (list (postgresql-role | ||
| 100 | (name "forgejo") | ||
| 101 | (create-database? #t)))))) | ||
| 102 | (modify-services (operating-system-user-services %forgejo-os) | ||
| 103 | (forgejo-service-type | ||
| 104 | config => | ||
| 105 | (forgejo-configuration | ||
| 106 | (inherit config) | ||
| 107 | (database-type "postgres")))))))) | ||
| 108 | |||
| 109 | (define %forgejo-os/mysql | ||
| 110 | (operating-system | ||
| 111 | (inherit %forgejo-os) | ||
| 112 | (services | ||
| 113 | (cons | ||
| 114 | (service mysql-service-type) | ||
| 115 | (modify-services (operating-system-user-services %forgejo-os) | ||
| 116 | (forgejo-service-type | ||
| 117 | config => | ||
| 118 | (forgejo-configuration | ||
| 119 | (inherit config) | ||
| 120 | (database-type "mysql") | ||
| 121 | (database-host "/run/mysqld/mysqld.sock")))))))) | ||
| 122 | |||
| 123 | (define (os->forgejo-configuration os) | ||
| 124 | (let ((forgejo-service | ||
| 125 | (find (lambda (x) | ||
| 126 | (eq? forgejo-service-type (service-kind x))) | ||
| 127 | (operating-system-user-services os)))) | ||
| 128 | (service-value forgejo-service))) | ||
| 129 | |||
| 130 | (define* (make-forgejo-test name #:key (os %forgejo-os)) | ||
| 131 | "Return a test of an OS running the Forgejo service." | ||
| 132 | |||
| 133 | (define forgejo-config | ||
| 134 | (os->forgejo-configuration os)) | ||
| 135 | |||
| 136 | (define database-type | ||
| 137 | (forgejo-configuration-database-type forgejo-config)) | ||
| 138 | |||
| 139 | (define database-name | ||
| 140 | (forgejo-configuration-database-name forgejo-config)) | ||
| 141 | |||
| 142 | (define database-user | ||
| 143 | (forgejo-configuration-database-user forgejo-config)) | ||
| 144 | |||
| 145 | (define vm | ||
| 146 | (virtual-machine | ||
| 147 | (operating-system (marionette-operating-system | ||
| 148 | os | ||
| 149 | #:imported-modules '((gnu services herd)) | ||
| 150 | #:requirements (match database-type | ||
| 151 | ("mysql" '(mysql)) | ||
| 152 | ("postgres" '(postgres)) | ||
| 153 | ("sqlite3" '())))) | ||
| 154 | (memory-size 1024) | ||
| 155 | (port-forwardings '((3333 . 3000))))) ;3333 on host | ||
| 156 | |||
| 157 | (define test | ||
| 158 | (with-extensions (list guile-json-4) | ||
| 159 | (with-imported-modules '((gnu build dbus-service) | ||
| 160 | (gnu build marionette) | ||
| 161 | (guix base64)) | ||
| 162 | #~(begin | ||
| 163 | (use-modules (gnu build dbus-service) | ||
| 164 | (gnu build marionette) | ||
| 165 | (guix base64) | ||
| 166 | (json) | ||
| 167 | (rnrs base) | ||
| 168 | (rnrs bytevectors) | ||
| 169 | (srfi srfi-64) | ||
| 170 | (srfi srfi-71) | ||
| 171 | (web client) | ||
| 172 | (web response) | ||
| 173 | (web uri)) | ||
| 174 | |||
| 175 | (define marionette | ||
| 176 | (make-marionette (list #$vm))) | ||
| 177 | |||
| 178 | (test-runner-current (system-test-runner #$output)) | ||
| 179 | (test-begin "forgejo") | ||
| 180 | |||
| 181 | (unless (string=? "mysql" #$database-type) | ||
| 182 | (test-skip 2)) | ||
| 183 | (test-assert "wait for mysql socket" | ||
| 184 | ;; This can be removed after Shepherd gains systemd notify | ||
| 185 | ;; support (mysql uses it to notify when it's ready). | ||
| 186 | (wait-for-unix-socket | ||
| 187 | #$(forgejo-configuration-database-host forgejo-config) | ||
| 188 | marionette)) | ||
| 189 | (test-assert "mysql setup" | ||
| 190 | (marionette-eval | ||
| 191 | '(begin | ||
| 192 | (use-modules (guix build utils)) | ||
| 193 | (invoke "mysql" "-e" (string-append "\ | ||
| 194 | CREATE USER '" #$database-user "'@'localhost'; | ||
| 195 | CREATE DATABASE " #$database-name "; | ||
| 196 | GRANT ALL PRIVILEGES ON " #$database-name ".*" | ||
| 197 | "TO '" #$database-user "'@'localhost'; | ||
| 198 | FLUSH PRIVILEGES;"))) | ||
| 199 | marionette)) | ||
| 200 | |||
| 201 | (test-assert "forgejo can be restarted" | ||
| 202 | (marionette-eval | ||
| 203 | '(begin | ||
| 204 | (use-modules (gnu services herd)) | ||
| 205 | (restart-service 'forgejo)) | ||
| 206 | marionette)) | ||
| 207 | |||
| 208 | (test-assert "forgejo runs" | ||
| 209 | (marionette-eval | ||
| 210 | '(begin | ||
| 211 | (use-modules (gnu services herd)) | ||
| 212 | (start-service 'forgejo)) | ||
| 213 | marionette)) | ||
| 214 | |||
| 215 | (test-assert "shepherd listens on tcp port 3000" | ||
| 216 | (wait-for-tcp-port 3000 marionette)) | ||
| 217 | |||
| 218 | (test-assert "slogan can be retrieved from main page" | ||
| 219 | ;; This socket-activates Forgejo, so we must wait a bit while it | ||
| 220 | ;; becomes ready. | ||
| 221 | (with-retries 20 1 | ||
| 222 | (let (((values response body) | ||
| 223 | (http-get "http://localhost:3333"))) | ||
| 224 | (assert (= 200 (response-code response))) | ||
| 225 | (string-contains body "In Code We Trust")))) | ||
| 226 | |||
| 227 | (test-assert "create test forgejo user" | ||
| 228 | (marionette-eval | ||
| 229 | '(begin | ||
| 230 | (use-modules (guix build utils)) | ||
| 231 | (invoke "su" "-l" "forgejo" "-c" | ||
| 232 | "FORGEJO_WORK_DIR=/var/lib/forgejo forgejo \ | ||
| 233 | admin user create --username dummy --password dummy \ | ||
| 234 | --email dummy@localhost --must-change-password=false")) | ||
| 235 | marionette)) | ||
| 236 | |||
| 237 | (test-assert "upload ssh key for dummy forgejo user" | ||
| 238 | ;; We use the Forgejo API to get a token, then use it to set the | ||
| 239 | ;; SSH public key of the user, which is needed for | ||
| 240 | ;; non-interactively pushing to a repository. | ||
| 241 | (let (((values response body) | ||
| 242 | (http-post "http://localhost:3333/api/v1/user/keys" | ||
| 243 | #:headers | ||
| 244 | `((Accept . "application/json") | ||
| 245 | (Authorization | ||
| 246 | . ,(string-append | ||
| 247 | "Basic " (base64-encode | ||
| 248 | (string->utf8 "dummy:dummy")))) | ||
| 249 | (Content-Type . "application/json")) | ||
| 250 | #:body (string->utf8 | ||
| 251 | (scm->json-string | ||
| 252 | '(("key" . #$dummy-ssh-public-key) | ||
| 253 | ("read_only" . #t) | ||
| 254 | ("title" . "Dummy's SSH key"))))))) | ||
| 255 | (= 201 (response-code response)))) | ||
| 256 | |||
| 257 | (test-assert "configure git client for dummy user" | ||
| 258 | (marionette-eval | ||
| 259 | '(begin | ||
| 260 | (setenv "HOME" "/root") | ||
| 261 | (chdir "/root") | ||
| 262 | |||
| 263 | ;; Populate private SSH key. | ||
| 264 | (mkdir ".ssh") | ||
| 265 | (call-with-output-file ".ssh/id_ed25519" | ||
| 266 | (lambda (port) | ||
| 267 | (display #$dummy-ssh-private-key port))) | ||
| 268 | (chmod ".ssh/id_ed25519" #o700) | ||
| 269 | |||
| 270 | ;; To avoid interactive prompts. | ||
| 271 | (call-with-output-file ".ssh/config" | ||
| 272 | (lambda (port) | ||
| 273 | (display "StrictHostKeyChecking no\n" port))) | ||
| 274 | |||
| 275 | ;; Init git. | ||
| 276 | (invoke "git" "config" "--global" "user.name" "dummy") | ||
| 277 | (invoke "git" "config" "--global" "user.email" | ||
| 278 | "dummy@localhost")) | ||
| 279 | marionette)) | ||
| 280 | |||
| 281 | (test-assert "git create dummy local repo" | ||
| 282 | (marionette-eval | ||
| 283 | '(begin | ||
| 284 | (mkdir "dummy-repo") | ||
| 285 | (chdir "dummy-repo") | ||
| 286 | (invoke "git" "init") | ||
| 287 | (invoke "git" "remote" "add" "origin" | ||
| 288 | "forgejo@localhost:dummy/hello") ;SSH remote | ||
| 289 | (call-with-output-file "hello.txt" | ||
| 290 | (lambda (port) | ||
| 291 | (display "is anybody here?\n" port))) | ||
| 292 | (invoke "git" "add" "hello.txt") | ||
| 293 | (invoke "git" "commit" "-m" "init repo")) | ||
| 294 | marionette)) | ||
| 295 | |||
| 296 | (test-assert "git push repo to forgejo via ssh" | ||
| 297 | (marionette-eval | ||
| 298 | '(begin | ||
| 299 | (invoke "git" "push" "origin" "master")) | ||
| 300 | marionette)) | ||
| 301 | |||
| 302 | (test-assert "git clone repo from forgejo via http" | ||
| 303 | (marionette-eval | ||
| 304 | '(begin | ||
| 305 | (use-modules (ice-9 textual-ports)) | ||
| 306 | ;; FIXME: Switch back 127.0.0.1 to localhost after | ||
| 307 | ;; <https://codeberg.org/shepherd/shepherd/issues/122> is | ||
| 308 | ;; resolved. | ||
| 309 | (invoke "git" "clone" "http://127.0.0.1:3000/dummy/hello" | ||
| 310 | "/tmp/hello-copy") | ||
| 311 | (call-with-input-file "/tmp/hello-copy/hello.txt" | ||
| 312 | (lambda (port) | ||
| 313 | (string-contains (get-string-all port) | ||
| 314 | "is anybody here?")))) | ||
| 315 | marionette)) | ||
| 316 | |||
| 317 | (test-assert "setup git lfs" | ||
| 318 | (marionette-eval | ||
| 319 | '(begin | ||
| 320 | (invoke "git" "lfs" "install") | ||
| 321 | (invoke "git" "lfs" "track" "*.blob") | ||
| 322 | (invoke "git" "add" ".gitattributes")) | ||
| 323 | marionette)) | ||
| 324 | |||
| 325 | (test-assert "commit blob file with git lfs" | ||
| 326 | (marionette-eval | ||
| 327 | '(begin | ||
| 328 | (use-modules (ice-9 binary-ports) | ||
| 329 | (rnrs base) | ||
| 330 | ((scheme base) #:select (read-bytevector | ||
| 331 | write-bytevector))) | ||
| 332 | |||
| 333 | (call-with-output-file "one-mebibyte.blob" | ||
| 334 | (lambda (out) | ||
| 335 | (write-bytevector | ||
| 336 | (call-with-input-file "/dev/random" | ||
| 337 | (lambda (in) | ||
| 338 | (read-bytevector (expt 2 20) in)) | ||
| 339 | #:binary #t) | ||
| 340 | out)) | ||
| 341 | #:binary #t) | ||
| 342 | |||
| 343 | (invoke "git" "add" "one-mebibyte.blob") | ||
| 344 | (invoke "git" "commit" "-m" "add binary blob") | ||
| 345 | (zero? | ||
| 346 | (system "git lfs ls-files|grep -Fq one-mebibyte.blob"))) | ||
| 347 | marionette)) | ||
| 348 | |||
| 349 | (test-assert "push lfs tracked files to forgejo" | ||
| 350 | (marionette-eval | ||
| 351 | '(begin | ||
| 352 | (invoke "git" "push" "origin" "master")) | ||
| 353 | marionette)) | ||
| 354 | |||
| 355 | (test-assert "cloned repo contains lfs tracked files" | ||
| 356 | (marionette-eval | ||
| 357 | '(begin | ||
| 358 | ;; FIXME: Switch back 127.0.0.1 to localhost after | ||
| 359 | ;; <https://codeberg.org/shepherd/shepherd/issues/122> is | ||
| 360 | ;; resolved. | ||
| 361 | (invoke "git" "clone" "http://127.0.0.1:3000/dummy/hello" | ||
| 362 | "/tmp/hello-with-blob") | ||
| 363 | (= (expt 2 20) | ||
| 364 | (stat:size | ||
| 365 | (stat "/tmp/hello-with-blob/one-mebibyte.blob")))) | ||
| 366 | marionette)) | ||
| 367 | |||
| 368 | (test-assert "email notification works" | ||
| 369 | (begin | ||
| 370 | (let (((values response body) | ||
| 371 | (http-post | ||
| 372 | "http://localhost:3333/user/forgot_password" | ||
| 373 | #:headers `((Content-Type | ||
| 374 | . "application/x-www-form-urlencoded")) | ||
| 375 | #:body (string->utf8 | ||
| 376 | (string-append | ||
| 377 | "email=" (uri-encode "dummy@localhost")))))) | ||
| 378 | (assert (= 200 (response-code response)))) | ||
| 379 | |||
| 380 | (with-retries 10 1 | ||
| 381 | (marionette-eval | ||
| 382 | '(begin | ||
| 383 | (use-modules (ice-9 textual-ports)) | ||
| 384 | (string-contains (call-with-input-file | ||
| 385 | "/var/log/forgejo.log" | ||
| 386 | get-string-all) | ||
| 387 | "Subject: Recover your account")) | ||
| 388 | marionette)))) | ||
| 389 | |||
| 390 | (test-end))))) | ||
| 391 | |||
| 392 | (gexp->derivation (string-append "forgejo-test-" name) test)) | ||
| 393 | |||
| 394 | (define %test-forgejo | ||
| 395 | (system-test | ||
| 396 | (name "forgejo") | ||
| 397 | (description "Basic tests for the Forgejo service.") | ||
| 398 | (value (make-forgejo-test "sqlite")))) | ||
| 399 | |||
| 400 | (define %test-forgejo-mysql | ||
| 401 | (system-test | ||
| 402 | (name "forgejo-mysql") | ||
| 403 | (description "Basic tests for the Forgejo service, using a MySQL | ||
| 404 | database.") | ||
| 405 | (value (make-forgejo-test "mysql" #:os %forgejo-os/mysql)))) | ||
| 406 | |||
| 407 | (define %test-forgejo-postgres | ||
| 408 | (system-test | ||
| 409 | (name "forgejo-postgres") | ||
| 410 | (description "Basic tests for the Forgejo service, using a PostgreSQL | ||
| 411 | database.") | ||
| 412 | (value (make-forgejo-test "postgres" #:os %forgejo-os/postgres)))) | ||
| 413 | |||
| 414 | ;; Local Variables: | ||
| 415 | ;; eval: (put 'with-retries 'scheme-indent-function 2) | ||
| 416 | ;; End: | ||
