1 { config, pkgs, lib, ... }:
4 inherit (config.services) postfix postgresql redis;
5 inherit (config.users) users groups;
6 cfg = config.services.sourcehut;
7 domain = cfg.settings."sr.ht".global-domain;
8 settingsFormat = pkgs.formats.ini {
9 listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
12 else generators.mkKeyValueDefault {
14 if v == true then "yes"
15 else if v == false then "no"
16 else generators.mkValueStringDefault {} v;
19 configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
20 # Each service needs access to only a subset of sections (and secrets).
21 (filterAttrs (k: v: v != null)
22 (mapAttrs (section: v:
23 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
24 if srvMatch == null # Include sections shared by all services
25 || head srvMatch == srv # Include sections for the service being configured
27 # *srht-{dispatch,keys,shell,update-hook} share the same config.ini
28 else if srv == "ssh" && elem (head srvMatch) ["builds" "git" "hg"] && cfg.${head srvMatch}.enable then v
29 # Enable Web links and integrations between services.
30 else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
33 # mansrht crashes without it
34 oauth-client-id = v.oauth-client-id or null;
36 # Drop sub-sections of other services
38 (recursiveUpdate cfg.settings {
39 # Those paths are mounted using BindPaths= or BindReadOnlyPaths=
40 # for services needing access to them.
41 "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
42 "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
43 "git.sr.ht".post-update-script = "/var/lib/sourcehut/gitsrht/bin/post-update-script";
44 "hg.sr.ht".changegroup-script = "/var/lib/sourcehut/hgsrht/bin/changegroup-script";
46 commonServiceSettings = srv: {
48 description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
50 default = "https://${srv}.localhost.localdomain";
52 debug-host = mkOption {
53 description = "Address to bind the debug server to.";
54 type = with types; nullOr str;
57 debug-port = mkOption {
58 description = "Port to bind the debug server to.";
59 type = with types; nullOr str;
62 connection-string = mkOption {
63 description = "SQLAlchemy connection string for the database.";
65 default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
67 migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
68 oauth-client-id = mkOption {
69 description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
72 oauth-client-secret = mkOption {
73 description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
75 apply = s: "<" + toString s;
79 # Specialized python containing all the modules
80 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
93 # Not a python package
98 mkOptionNullOrStr = description: mkOption {
100 type = with types; nullOr str;
105 options.services.sourcehut = {
106 enable = mkEnableOption ''
107 sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
108 task dispatching, wiki and account management services
111 services = mkOption {
112 type = with types; listOf (enum
113 [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
114 defaultText = "locally enabled services";
116 Services that may be displayed as links in the title bar of the Web interface.
120 listenAddress = mkOption {
122 default = "localhost";
123 description = "Address to bind to.";
128 type = types.package;
131 The python package to use. It should contain references to the *srht modules and also
137 enable = mkEnableOption ''local minio integration'';
141 enable = mkEnableOption ''local nginx integration'';
145 enable = mkEnableOption ''local postfix integration'';
149 enable = mkEnableOption ''local postgresql integration'';
153 enable = mkEnableOption ''local redis integration'';
154 firstDatabase = mkOption {
158 Number of the first Redis database to use.
159 At most 9 consecutive databases are currently used.
164 settings = mkOption {
165 type = lib.types.submodule {
166 freeformType = settingsFormat.type;
168 global-domain = mkOption {
169 description = "Global domain name.";
171 example = "example.com";
173 environment = mkOption {
174 description = "Values other than \"production\" adds a banner to each page.";
175 type = types.enum [ "development" "production" ];
176 default = "development";
178 network-key = mkOption {
180 An absolute file path (which should be outside the Nix-store)
181 to a secret key to encrypt internal messages with. Use <code>srht-keygen network</code> to
182 generate this key. It must be consistent between all services and nodes.
185 apply = s: "<" + toString s;
187 owner-email = mkOption {
188 description = "Owner's email.";
190 default = "contact@example.com";
192 owner-name = mkOption {
193 description = "Owner's name.";
195 default = "John Doe";
197 redis-host = mkOption {
198 type = with types; nullOr str;
200 The redis host URL. This is used for caching and temporary storage, and must
201 be shared between nodes (e.g. g - 1it1.sr.ht and git2.sr.ht), but need not be
202 shared between services. It may be shared between services, however, with no
203 ill effect, if this better suits your infrastructure.
206 site-blurb = mkOption {
207 description = "Blurb for your site.";
209 default = "the hacker's forge";
211 site-info = mkOption {
212 description = "The top-level info page for your site.";
214 default = "https://sourcehut.org";
216 service-key = mkOption {
218 An absolute file path (which should be outside the Nix-store)
219 to a key used for encrypting session cookies. Use <code>srht-keygen service</code> to
220 generate the service key. This must be shared between each node of the same
221 service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
222 different keys. If you configure all of your services with the same
223 config.ini, you may use the same service-key for all of them.
226 apply = s: "<" + toString s;
228 site-name = mkOption {
229 description = "The name of your network of sr.ht-based sites.";
231 default = "sourcehut";
233 source-url = mkOption {
234 description = "The source code for your fork of sr.ht.";
236 default = "https://git.sr.ht/~sircmpwn/srht";
240 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
241 smtp-port = mkOption {
242 description = "Outgoing SMTP port.";
243 type = with types; nullOr port;
246 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
247 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
248 smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
249 error-to = mkOptionNullOrStr "Address receiving application exceptions";
250 error-from = mkOptionNullOrStr "Address sending application exceptions";
251 pgp-privkey = mkOptionNullOrStr ''
252 An absolute file path (which should be outside the Nix-store)
253 to an OpenPGP private key.
255 Your PGP key information (DO NOT mix up pub and priv here)
256 You must remove the password from your secret key, if present.
257 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.
259 pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
260 pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
263 s3-upstream = mkOption {
264 description = "Configure the S3-compatible object storage service.";
265 type = with types; nullOr str;
268 s3-access-key = mkOption {
269 description = "Access key to the S3-compatible object storage service";
270 type = with types; nullOr str;
273 s3-secret-key = mkOption {
275 An absolute file path (which should be outside the Nix-store)
276 to the secret key of the S3-compatible object storage service.
278 type = with types; nullOr path;
280 apply = mapNullable (s: "<" + toString s);
284 private-key = mkOption {
286 An absolute file path (which should be outside the Nix-store)
287 to a base64-encoded Ed25519 key for signing webhook payloads.
288 This should be consistent for all *.sr.ht sites,
289 as this key will be used to verify signatures
290 from other sites in your network.
291 Use the <code>srht-keygen webhook</code> command to generate a key.
294 apply = s: "<" + toString s;
298 options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
300 options."dispatch.sr.ht::github" = {
301 oauth-client-id = mkOptionNullOrStr "OAuth client id.";
302 oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
304 options."dispatch.sr.ht::gitlab" = {
305 enabled = mkEnableOption "GitLab integration";
306 canonical-upstream = mkOption {
308 description = "Canonical upstream.";
309 default = "gitlab.com";
311 repo-cache = mkOption {
313 description = "Repository cache directory.";
314 default = "./repo-cache";
316 "gitlab.com" = mkOption {
317 type = with types; nullOr str;
318 description = "GitLab id and secret.";
320 example = "GitLab:application id:secret";
324 options."builds.sr.ht" = commonServiceSettings "builds" // {
326 description = "The redis connection used for the celery worker.";
328 default = "redis://localhost:6379/3";
331 description = "The shell used for ssh.";
333 default = "runner-shell";
337 options."git.sr.ht" = commonServiceSettings "git" // {
338 outgoing-domain = mkOption {
339 description = "Outgoing domain.";
341 default = "https://git.localhost.localdomain";
343 post-update-script = mkOption {
345 A post-update script which is installed in every git repo.
346 This setting is propagated to newer and existing repositories.
349 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
350 defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
351 # Git hooks are run relative to their repository's directory,
352 # but gitsrht-update-hook looks up ../config.ini
353 apply = p: pkgs.writeShellScript "update-hook" ''
354 test -e "''${PWD%/*}"/config.ini ||
355 ln -s ${users."sshsrht".home}/../config.ini "''${PWD%/*}"/config.ini
356 exec -a "$0" '${p}' "$@"
361 Path to git repositories on disk.
362 If changing the default, you must ensure that
363 the gitsrht's user as read and write access to it.
366 default = "/var/lib/sourcehut/gitsrht/repos";
368 webhooks = mkOption {
369 description = "The redis connection used for the webhooks worker.";
371 default = "redis://localhost:6379/1";
375 options."hg.sr.ht" = commonServiceSettings "hg" // {
376 changegroup-script = mkOption {
378 A changegroup script which is installed in every mercurial repo.
379 This setting is propagated to newer and existing repositories.
382 default = "${cfg.python}/bin/hgsrht-hook-changegroup";
386 Path to mercurial repositories on disk.
387 If changing the default, you must ensure that
388 the hgsrht's user as read and write access to it.
391 default = "/var/lib/sourcehut/hgsrht/repos";
393 srhtext = mkOptionNullOrStr ''
394 Path to the srht mercurial extension
395 (defaults to where the hgsrht code is)
397 clone_bundle_threshold = mkOption {
398 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
399 type = types.ints.unsigned;
403 description = "Path to hg-ssh (if not in $PATH).";
405 default = "${pkgs.mercurial}/bin/hg-ssh";
407 webhooks = mkOption {
408 description = "The redis connection used for the webhooks worker.";
410 default = "redis://localhost:6379/8";
414 options."hub.sr.ht" = commonServiceSettings "hub" // {
417 options."lists.sr.ht" = commonServiceSettings "lists" // {
418 allow-new-lists = mkEnableOption "Allow creation of new lists.";
419 notify-from = mkOption {
420 description = "Outgoing email for notifications generated by users.";
422 default = "lists-notify@localhost.localdomain";
424 posting-domain = mkOption {
425 description = "Posting domain.";
427 default = "lists.localhost.localdomain";
430 description = "The redis connection used for the celery worker.";
432 default = "redis://localhost:6379/4";
434 webhooks = mkOption {
435 description = "The redis connection used for the webhooks worker.";
437 default = "redis://localhost:6379/2";
440 options."lists.sr.ht::worker" = {
441 reject-mimetypes = mkOption {
442 type = with types; listOf str;
443 default = ["text/html"];
445 reject-url = mkOption {
446 description = "Reject URL.";
448 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
452 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
453 Alternatively, specify IP:PORT and an SMTP server will be run instead.
456 default = "/tmp/lists.sr.ht-lmtp.sock";
458 sock-group = mkOption {
460 The lmtp daemon will make the unix socket group-read/write
461 for users in this group.
468 options."man.sr.ht" = commonServiceSettings "man" // {
471 options."meta.sr.ht" =
472 removeAttrs (commonServiceSettings "meta")
473 ["oauth-client-id" "oauth-client-secret"] // {
474 api-origin = mkOption {
475 description = "Origin URL for API, 100 more than web.";
477 default = "http://localhost:5100";
479 webhooks = mkOption {
480 description = "The redis connection used for the webhooks worker.";
482 default = "redis://localhost:6379/6";
484 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
486 options."meta.sr.ht::settings" = {
487 registration = mkEnableOption "public registration";
488 onboarding-redirect = mkOption {
489 description = "Where to redirect new users upon registration.";
491 default = "https://meta.localhost.localdomain";
493 user-invites = mkOption {
495 How many invites each user is issued upon registration
496 (only applicable if open registration is disabled).
498 type = types.ints.unsigned;
502 options."meta.sr.ht::aliases" = mkOption {
503 description = "Aliases for the client IDs of commonly used OAuth clients.";
504 type = with types; attrsOf int;
506 example = { "git.sr.ht" = 12345; };
508 options."meta.sr.ht::billing" = {
509 enabled = mkEnableOption "the billing system";
510 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
511 stripe-secret-key = mkOptionNullOrStr ''
512 An absolute file path (which should be outside the Nix-store)
513 to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
515 apply = mapNullable (s: "<" + toString s);
519 options."pages.sr.ht" = commonServiceSettings "pages" // {
520 gemini-certs = mkOption {
522 An absolute file path (which should be outside the Nix-store)
523 to Gemini certificates.
525 type = with types; nullOr path;
528 max-site-size = mkOption {
529 description = "Maximum size of any given site (post-gunzip), in MiB.";
533 user-domain = mkOption {
535 Configures the user domain, if enabled.
536 All users are given <username>.this.domain.
538 type = with types; nullOr str;
542 options."pages.sr.ht::api" = {
545 options."paste.sr.ht" = commonServiceSettings "paste" // {
546 webhooks = mkOption {
548 default = "redis://localhost:6379/5";
552 options."todo.sr.ht" = commonServiceSettings "todo" // {
553 notify-from = mkOption {
554 description = "Outgoing email for notifications generated by users.";
556 default = "todo-notify@localhost.localdomain";
558 webhooks = mkOption {
559 description = "The redis connection used for the webhooks worker.";
561 default = "redis://localhost:6379/7";
564 options."todo.sr.ht::mail" = {
565 posting-domain = mkOption {
566 description = "Posting domain.";
568 default = "todo.localhost.localdomain";
572 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
573 Alternatively, specify IP:PORT and an SMTP server will be run instead.
576 default = "/tmp/todo.sr.ht-lmtp.sock";
578 sock-group = mkOption {
580 The lmtp daemon will make the unix socket group-read/write
581 for users in this group.
590 The configuration for the sourcehut network.
595 enableWorker = mkEnableOption "worker for builds.sr.ht";
598 type = with types; attrsOf (attrsOf (attrsOf package));
600 example = lib.literalExample ''(let
601 # Pinning unstable to allow usage with flakes and limit rebuilds.
602 pkgs_unstable = builtins.fetchGit {
603 url = "https://github.com/NixOS/nixpkgs";
604 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
605 ref = "nixos-unstable";
607 image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
608 pkgs = (import pkgs_unstable {});
612 nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
616 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
623 type = types.package;
625 example = literalExample "pkgs.gitFull";
627 Git package for git.sr.ht. This can help silence collisions.
634 type = types.package;
635 default = pkgs.mercurial;
637 Mercurial package for hg.sr.ht. This can help silence collisions.
640 cloneBundles = mkOption {
644 Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
650 config = mkIf cfg.enable (mkMerge [
652 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
654 services.sourcehut.settings = {
655 "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
656 "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
657 "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
658 "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
659 "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
660 "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
663 (mkIf cfg.postgresql.enable {
664 services.postgresql.enable = true;
666 (mkIf cfg.postfix.enable {
667 services.postfix.enable = true;
668 # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
669 systemd.services.postfix.serviceConfig.PrivateTmp = true;
671 (mkIf cfg.redis.enable {
672 services.redis.enable = true;
673 services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
675 (mkIf cfg.nginx.enable {
676 services.nginx.enable = true;
678 (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
680 # Note that sshd will continue to honor AuthorizedKeysFile
681 authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
682 # The sshsrht-dispatch user needs:
683 # 1. to read ${users."sshsrht".home}/../config.ini,
684 # 2. to access the redis server in redis-host,
685 # 3. to access the postgresql server in the service's connection-string,
686 # 4. to query metasrht-api (through the HTTP API).
687 # Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
688 # to write their log on /var/log with that user, and will fallback to stderr,
689 # making their log visible in sshd's log when sshd is in debug mode (-d).
690 # Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
692 authorizedKeysCommandUser = users."sshsrht".name;
694 PermitUserEnvironment SRHT_*
697 environment.etc."ssh/srht-dispatch" = {
698 # sshd_config(5): The program must be owned by root, not writable by group or others
700 source = pkgs.writeShellScript "srht-dispatch" ''
702 cd ${users."sshsrht".home}
703 exec ${cfg.python}/bin/gitsrht-dispatch "$@"
706 systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
707 #path = optional cfg.git.enable [ cfg.git.package ];
708 restartTriggers = [ configIni ];
710 RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
712 # Note that the path /usr/bin/*srht-* are hardcoded in multiple places in *.sr.ht,
713 # for instance to get the user from the [*.sr.ht::dispatch] settings.
714 optionals cfg.builds.enable [
715 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
716 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-shell:/usr/bin/buildsrht-shell"
718 optionals cfg.git.enable [
719 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
720 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
722 optionals cfg.hg.enable [
723 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
724 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
726 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
727 # Replace values begining with a '<' by the content of the file whose name is after.
728 ${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
729 install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
730 /dev/stdin ${users."sshsrht".home}/../config.ini
737 # srht-dispatch, *srht-keys, and *srht-shell
738 # look up in ../config.ini from this directory;
739 # that config.ini being set in *srht.service's ExecStartPre=
740 home = "/run/sourcehut/sshsrht/subdir";
742 # Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
743 # hence the main group is used.
744 if cfg.postgresql.enable
745 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
746 then groups.postgres.name
747 else groups.nogroup.name;
748 description = "sourcehut user for AuthorizedKeysCommand";
750 groups."sshsrht" = {};
756 (import ./service.nix "builds" {
757 inherit configIniOfService;
760 extraServices.buildsrht-worker = {
761 enable = cfg.builds.enableWorker;
762 partOf = [ "buildsrht.service" ];
763 path = [ pkgs.openssh pkgs.docker ];
765 qemuPackage = pkgs.qemu_kvm;
766 statePath = "/var/lib/sourcehut/buildsrht";
768 if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
769 # Create and import qemu:latest image for docker
770 ${pkgs.dockerTools.streamLayeredImage {
773 contents = [ qemuPackage ];
775 # Mark down current package version
776 printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
780 ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
781 Group = mkIf cfg.nginx.enable "nginx";
785 image_dirs = flatten (
786 mapAttrsToList (distro: revs:
787 mapAttrsToList (rev: archs:
788 mapAttrsToList (arch: image:
789 pkgs.runCommandNoCC "buildsrht-images" { } ''
790 mkdir -p $out/${distro}/${rev}/${arch}
791 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
797 image_dir_pre = pkgs.symlinkJoin {
798 name = "builds.sr.ht-worker-images-pre";
799 paths = image_dirs ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
801 image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } ''
803 cp -Lr ${image_dir_pre}/* $out/images
806 users.users.${cfg.builds.user} = {
808 extraGroups = [ groups."sshsrht".name ];
810 users.groups.docker.members = mkIf cfg.builds.enableWorker [ cfg.builds.user ];
812 virtualisation.docker.enable = true;
814 # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
815 # FIXME: see if there is a better way than disabling preStart.
816 systemd.services.buildsrht.preStart = mkForce "";
818 services.sourcehut.settings = mkMerge [
819 { # Register the builds.sr.ht dispatcher
820 "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
821 mkDefault "${cfg.builds.user}:${cfg.builds.user}";
823 (mkIf cfg.builds.enableWorker {
824 # Default worker stores logs that are accessible via this address:port
825 "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
826 "builds.sr.ht::worker".buildlogs = mkDefault "/var/log/sourcehut/buildsrht";
827 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
828 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
829 "builds.sr.ht::worker".timeout = mkDefault "3m";
833 services.nginx.virtualHosts."logs.${domain}" = mkIf (cfg.nginx.enable && cfg.builds.enableWorker) {
834 listen = with builtins;
835 let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
836 [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
837 locations."/logs".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
841 (import ./service.nix "dispatch" {
842 inherit configIniOfService;
845 (import ./service.nix "git" (let
847 path = [ cfg.git.package ];
848 serviceConfig.BindPaths = [
849 "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos"
851 serviceConfig.BindReadOnlyPaths = [
852 "${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script"
855 inherit configIniOfService;
856 commonService = mkMerge [ commonService {
857 serviceConfig.StateDirectory = [ "sourcehut/gitsrht/repos" ];
860 webhooks.redisDatabase = 1;
861 extraTimers.gitsrht-periodic = {
862 OnCalendar = ["20min"];
865 # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
866 # Probably could use gitsrht-shell if output is restricted to just parameters...
867 users.users.${cfg.git.user} = {
869 # Allow reading of ${users."sshsrht".home}/../config.ini
870 extraGroups = [ groups."sshsrht".name ];
871 home = users.sshsrht.home;
874 services.sourcehut.settings = {
875 # Register the git.sr.ht dispatcher
876 "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
877 mkDefault "${cfg.git.user}:${cfg.git.user}";
880 services.fcgiwrap.enable = mkIf cfg.nginx.enable true;
881 services.nginx.virtualHosts."git.${domain}" = mkIf cfg.nginx.enable {
883 location = /authorize {
884 proxy_pass http://${cfg.listenAddress}:${toString cfg.git.port};
885 proxy_pass_request_body off;
886 proxy_set_header Content-Length "";
887 proxy_set_header X-Original-URI $request_uri;
889 location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
890 auth_request /authorize;
891 root ${cfg.settings."git.sr.ht".repos};
892 fastcgi_pass unix:/run/fcgiwrap.sock;
893 fastcgi_param SCRIPT_FILENAME ${cfg.git.package}/bin/git-http-backend;
894 fastcgi_param PATH_INFO $uri;
895 fastcgi_param GIT_PROJECT_ROOT $document_root;
896 fastcgi_param GIT_HTTP_EXPORT_ALL "";
897 fastcgi_read_timeout 500s;
898 include ${config.services.nginx.package}/conf/fastcgi_params;
903 systemd.services.nginx = mkIf cfg.nginx.enable {
904 serviceConfig.BindReadOnlyPaths = [ cfg.settings."git.sr.ht".repos ];
907 systemd.services.sshd = commonService;
910 (import ./service.nix "hg" (let
912 path = [ cfg.hg.package ];
913 serviceConfig.BindPaths = [
914 "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos"
916 serviceConfig.BindReadOnlyPaths = [
917 "${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script"
920 inherit configIniOfService;
921 commonService = mkMerge [ commonService {
922 serviceConfig.StateDirectory = [ "sourcehut/hgsrht/repos" ];
925 webhooks.redisDatabase = 8;
926 extraTimers.hgsrht-periodic = {
927 OnCalendar = ["20min"];
929 extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
930 OnCalendar = ["daily"];
934 users.users.${cfg.hg.user} = {
936 extraGroups = [ groups."sshsrht".name ];
939 services.sourcehut.settings = {
940 # Register the hg.sr.ht dispatcher
941 "hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
942 mkDefault "${cfg.hg.user}:${cfg.hg.user}";
945 systemd.services.sshd = commonService;
948 (import ./service.nix "hub" {
949 inherit configIniOfService;
952 services.nginx.virtualHosts."hub.${domain}" = mkIf cfg.nginx.enable {
953 serverAliases = [ domain ];
957 (import ./service.nix "lists" {
958 inherit configIniOfService;
961 webhooks.redisDatabase = 2;
962 extraServices.listssrht-lmtp = {
963 requires = [ "postfix.service" ];
964 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
965 serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
966 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
967 serviceConfig.PrivateUsers = mkForce false;
969 extraServices.listssrht-process = {
970 serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
971 # Avoid crashing: os.getloadavg()
972 serviceConfig.ProcSubset = mkForce "all";
974 extraConfig = mkIf cfg.postfix.enable {
975 users.groups.${postfix.group}.members = [ cfg.lists.user ];
976 services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
977 services.postfix.transport = ''
978 lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
982 (import ./service.nix "man" {
983 inherit configIniOfService;
986 (import ./service.nix "meta" {
987 inherit configIniOfService;
989 webhooks.redisDatabase = 6;
990 extraServices.metasrht-api = {
991 serviceConfig.Restart = "always";
992 serviceConfig.RestartSec = "2s";
993 preStart = concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
994 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
996 oauthPath = "/var/lib/sourcehut/metasrht/${srv}.oauth";
998 # Configure client(s) as "preauthorized"
999 optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
1000 if test ! -e "${oauthPath}" || [ "$(cat ${oauthPath})" != "${s.oauth-client-id}" ]; then
1001 # Configure ${srv}'s OAuth client as "preauthorized"
1002 psql '${cfg.meta.database}' \
1003 -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
1005 printf "%s" "${s.oauth-client-id}" > "${oauthPath}"
1009 path = [ config.services.postgresql.package ];
1010 serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
1012 extraTimers.metasrht-daily = {
1013 OnCalendar = ["daily"];
1018 { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
1019 s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
1020 message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
1023 environment.systemPackages = [
1024 (pkgs.writeShellScriptBin "metasrht-manageuser" ''
1026 test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
1027 sudo -u '${cfg.meta.user}' "$0" "$@"
1028 # In order to load config.ini
1029 cd /run/sourcehut/metasrht ||
1031 Please run: sudo systemctl start metasrht
1033 ${cfg.python}/bin/metasrht-manageuser "$@"
1038 (import ./service.nix "pages" {
1039 inherit configIniOfService;
1041 #webhooks.redisDatabase = 9;
1043 srvsrht = "pagessrht";
1044 version = pkgs.sourcehut.${srvsrht}.version;
1045 stateDir = "/var/lib/sourcehut/${srvsrht}";
1046 iniKey = "pages.sr.ht";
1048 preStart = mkBefore ''
1050 # Use the /run/sourcehut/${srvsrht}/config.ini
1051 # installed by a previous ExecStartPre= in baseService
1052 cd /run/sourcehut/${srvsrht}
1054 if test ! -e ${stateDir}/db; then
1055 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
1056 echo ${version} > ${stateDir}/db
1059 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
1060 # Just try all the migrations because they're not linked to the version
1061 for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
1062 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
1067 touch ${stateDir}/webhook
1070 ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
1074 (import ./service.nix "paste" {
1075 inherit configIniOfService;
1077 webhooks.redisDatabase = 5;
1079 (import ./service.nix "todo" {
1080 inherit configIniOfService;
1082 webhooks.redisDatabase = 7;
1083 extraServices.todosrht-lmtp = {
1084 requires = [ "postfix.service" ];
1085 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1086 serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
1087 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1088 serviceConfig.PrivateUsers = mkForce false;
1090 extraConfig = mkIf cfg.postfix.enable {
1091 users.groups.${postfix.group}.members = [ cfg.todo.user ];
1092 services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
1093 services.postfix.transport = ''
1094 todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
1098 (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
1099 [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
1100 (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
1101 [ "services" "sourcehut" "listenAddress" ])
1104 meta.doc = ./sourcehut.xml;
1105 meta.maintainers = with maintainers; [ julm tomberek ];