1 { config, pkgs, lib, ... }:
5 cfg = config.services.sourcehut;
6 rcfg = config.services.redis;
8 settingsFormat = pkgs.formats.ini {
11 else generators.mkKeyValueDefault {
13 if v == true then "yes"
14 else if v == false then "no"
15 else generators.mkValueStringDefault {} v;
18 commonServiceSettings = service: {
20 description = "URL ${service}.sr.ht is being served at (protocol://domain)";
22 default = "https://${service}.${cfg.originBase}";
24 debug-host = mkOption {
25 description = "Address to bind the debug server to.";
29 debug-port = mkOption {
30 description = "Port to bind the debug server to.";
32 default = cfg.${service}.port;
34 connection-string = mkOption {
35 description = "SQLAlchemy connection string for the database.";
37 default = "postgresql:///${cfg.${service}.database}?user=${cfg.${service}.user}&host=/var/run/postgresql";
39 migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade";
40 oauth-client-id = mkOptionNullOrStr "OAUTH client id for meta.sr.ht.";
41 oauth-client-secret = mkOptionNullOrStr "OAUTH client secret for meta.sr.ht.";
44 # Specialized python containing all the modules
45 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
61 mkOptionNullOrStr = description: mkOption {
63 type = with types; nullOr str;
80 (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
81 The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
82 support other reverse-proxies officially.
84 However it's possible to use an alternative reverse-proxy by
87 * adjusting the relevant settings for server addresses and ports directly
89 Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
93 options.services.sourcehut = {
94 enable = mkEnableOption ''
95 sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
96 task dispatching, wiki and account management services
100 type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
101 default = [ "man" "meta" "paste" ];
102 example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
104 Services to enable on the sourcehut network.
108 originBase = mkOption {
110 default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
112 Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
118 default = "127.0.0.1";
126 type = types.package;
129 The python package to use. It should contain references to the *srht modules and also
134 settings = mkOption {
135 type = lib.types.submodule {
136 freeformType = settingsFormat.type;
138 environment = mkOption {
139 description = "Values other than \"production\" adds a banner to each page.";
140 type = types.enum [ "development" "production" ];
141 default = "development";
143 global-domain = mkOptionNullOrStr "Global domain name.";
144 owner-email = mkOption {
145 description = "Owner's email.";
147 default = "contact@example.com";
149 owner-name = mkOption {
150 description = "Owner's name.";
152 default = "John Doe";
154 secret-key = mkOptionNullOrStr "Secret key to encrypt session cookies with.";
155 site-blurb = mkOption {
156 description = "Blurb for your site.";
158 default = "the hacker's forge";
160 site-info = mkOption {
161 description = "The top-level info page for your site.";
163 default = "https://sourcehut.org";
165 site-name = mkOption {
166 description = "The name of your network of sr.ht-based sites.";
168 default = "sourcehut";
170 source-url = mkOption {
171 description = "The source code for your fork of sr.ht.";
173 default = "https://git.sr.ht/~sircmpwn/srht";
177 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
178 smtp-port = mkOption {
179 description = "Outgoing SMTP port.";
180 type = with types; nullOr port;
183 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
184 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
185 smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
186 error-to = mkOptionNullOrStr "Address receiving application exceptions";
187 error-from = mkOptionNullOrStr "Address sending application exceptions";
188 pgp-privkey = mkOptionNullOrStr ''
191 Your PGP key information (DO NOT mix up pub and priv here)
192 You must remove the password from your secret key, if present.
193 You can do this with <code>gpg --edit-key [key-id]</code>, then use the <code>passwd</code> command and do not enter a new password.
195 pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
196 pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
199 private-key = mkOptionNullOrStr ''
200 base64-encoded Ed25519 key for signing webhook payloads.
201 This should be consistent for all *.sr.ht sites,
202 as this key will be used to verify signatures
203 from other sites in your network.
204 Use the <code>srht-webhook-keygen</code> command to generate a key.
207 options."dispatch.sr.ht" = commonServiceSettings "dispatch";
208 options."dispatch.sr.ht::github" = {
209 oauth-client-id = mkOptionNullOrStr "OAUTH client id.";
210 oauth-client-secret = mkOptionNullOrStr "OAUTH client secret.";
212 options."dispatch.sr.ht::gitlab" = {
213 enabled = mkEnableOption "GitLab integration";
214 canonical-upstream = mkOption {
216 description = "Canonical upstream.";
217 default = "gitlab.com";
219 repo-cache = mkOption {
221 description = "Repository cache directory.";
222 default = "./repo-cache";
224 "gitlab.com" = mkOption {
226 description = "GitLab id and secret.";
228 example = "GitLab:application id:secret";
231 options."builds.sr.ht" = commonServiceSettings "builds" // {
233 description = "The redis connection used for the celery worker.";
235 default = "redis://${rcfg.bind}:${toString rcfg.port}/3";
238 description = "The shell used for ssh.";
240 default = "runner-shell";
243 options."git.sr.ht" = commonServiceSettings "git" // {
244 outgoing-domain = mkOption {
245 description = "Outgoing domain.";
247 default = "http://git.${cfg.originBase}";
249 post-update-script = mkOption {
250 description = "A post-update script which is installed in every git repo.";
252 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
255 description = "Path to git repositories on disk.";
257 default = "/var/lib/git";
259 webhooks = mkOption {
260 description = "The redis connection used for the webhooks worker.";
262 default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
265 options."hg.sr.ht" = commonServiceSettings "hg" // {
266 changegroup-script = mkOption {
267 description = "A post-update script which is installed in every mercurial repo..";
269 default = "${cfg.python}/bin/hgsrht-hook-changegroup";
272 description = "Path to mercurial repositories on disk.";
274 default = "/var/lib/hg";
276 srhtext = mkOptionNullOrStr ''
277 Path to the srht mercurial extension
278 (defaults to where the hgsrht code is)
280 clone_bundle_threshold = mkOption {
281 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
282 type = types.ints.unsigned;
286 description = "Path to hg-ssh (if not in $PATH).";
288 default = "${pkgs.mercurial}/bin/hg-ssh";
290 webhooks = mkOption {
291 description = "The redis connection used for the webhooks worker.";
293 default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
296 options."hub.sr.ht" = commonServiceSettings "hub" // {
298 options."lists.sr.ht" = commonServiceSettings "lists" // {
299 allow-new-lists = mkEnableOption "Allow creation of new lists.";
300 network-key = mkOptionNullOrStr "Network key.";
301 notify-from = mkOption {
302 description = "Outgoing email for notifications generated by users.";
304 default = "lists-notify@${cfg.originBase}";
306 posting-domain = mkOption {
307 description = "Posting domain.";
309 default = "lists.${cfg.originBase}";
312 description = "The redis connection used for the celery worker.";
314 default = "redis://${rcfg.bind}:${toString rcfg.port}/4";
316 webhooks = mkOption {
317 description = "The redis connection used for the webhooks worker.";
319 default = "redis://${rcfg.bind}:${toString rcfg.port}/2";
322 options."lists.sr.ht::worker" = {
323 reject-mimetypes = mkOption {
324 type = with types; listOf str;
325 default = ["text/html"];
327 reject-url = mkOption {
328 description = "Reject URL.";
329 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
334 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
335 Alternatively, specify IP:PORT and an SMTP server will be run instead.
338 default = "/tmp/lists.sr.ht-lmtp.sock";
340 sock-group = mkOption {
342 The lmtp daemon will make the unix socket group-read/write
343 for users in this group.
349 options."man.sr.ht" = commonServiceSettings "man" // {
351 options."meta.sr.ht" = commonServiceSettings "meta" // {
352 api-origin = mkOption {
353 description = "Origin URL for API, 100 more than web.";
355 default = "http://localhost:5100";
357 webhooks = mkOption {
358 description = "The redis connection used for the webhooks worker.";
360 default = "redis://${rcfg.bind}:${toString rcfg.port}/6";
362 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
364 options."meta.sr.ht::settings" = {
365 registration = mkEnableOption "public registration";
366 onboarding-redirect = mkOption {
367 description = "Where to redirect new users upon registration.";
369 default = "https://meta.${cfg.originBase}";
371 user-invites = mkOption {
373 How many invites each user is issued upon registration
374 (only applicable if open registration is disabled).
376 type = types.ints.unsigned;
380 options."meta.sr.ht::aliases" = mkOption {
381 description = "Aliases for the client IDs of commonly used OAuth clients.";
382 type = with types; attrsOf int;
384 example = { "git.sr.ht" = 12345; };
386 options."meta.sr.ht::billing" = {
387 enabled = mkEnableOption "the billing system";
388 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
389 stripe-secret-key = mkOptionNullOrStr "Secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
391 options."paste.sr.ht" = commonServiceSettings "paste" // {
392 webhooks = mkOption {
394 default = "redis://${rcfg.bind}:${toString rcfg.port}/5";
397 options."todo.sr.ht" = commonServiceSettings "todo" // {
398 network-key = mkOptionNullOrStr "Network key.";
399 notify-from = mkOption {
400 description = "Outgoing email for notifications generated by users.";
402 default = "todo-notify@${cfg.originBase}";
404 webhooks = mkOption {
405 description = "The redis connection used for the webhooks worker.";
407 default = "redis://${rcfg.bind}:${toString rcfg.port}/7";
410 options."todo.sr.ht::mail" = {
411 posting-domain = mkOption {
412 description = "Posting domain.";
414 default = "todo.${cfg.originBase}";
418 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
419 Alternatively, specify IP:PORT and an SMTP server will be run instead.
422 default = "/tmp/todo.sr.ht-lmtp.sock";
424 sock-group = mkOption {
426 The lmtp daemon will make the unix socket group-read/write
427 for users in this group.
436 The configuration for the sourcehut network.
441 config = mkIf cfg.enable {
445 assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
446 message = "The webhook's private key must be defined and of a 44 byte length.";
450 assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
451 message = "meta.sr.ht's origin must be defined.";
455 environment.etc."sr.ht/config.ini".source =
456 settingsFormat.generate "sourcehut-config.ini"
457 # Disabled services must be removed from the config
458 # to be effectively disabled.
460 let srv = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" k; in
461 srv == null || elem (head srv) cfg.services
464 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
467 services.postgresql.enable = mkOverride 999 true;
469 services.postfix.enable = mkOverride 999 true;
471 services.cron.enable = mkOverride 999 true;
473 services.redis.enable = mkOverride 999 true;
474 services.redis.bind = mkOverride 999 "127.0.0.1";
477 meta.doc = ./sourcehut.xml;
478 meta.maintainers = with maintainers; [ tomberek ];