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 "builds.sr.ht::worker".buildlogs = "/var/log/sourcehut/buildsrht/logs";
42 "git.sr.ht".post-update-script = "/var/lib/sourcehut/gitsrht/bin/post-update-script";
43 "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
44 "hg.sr.ht".changegroup-script = "/var/lib/sourcehut/hgsrht/bin/changegroup-script";
45 "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
47 commonServiceSettings = srv: {
49 description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
51 default = "https://${srv}.localhost.localdomain";
53 debug-host = mkOption {
54 description = "Address to bind the debug server to.";
55 type = with types; nullOr str;
58 debug-port = mkOption {
59 description = "Port to bind the debug server to.";
60 type = with types; nullOr str;
63 connection-string = mkOption {
64 description = "SQLAlchemy connection string for the database.";
66 default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
68 migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
69 oauth-client-id = mkOption {
70 description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
73 oauth-client-secret = mkOption {
74 description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
76 apply = s: "<" + toString s;
80 # Specialized python containing all the modules
81 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
94 # Not a python package
99 mkOptionNullOrStr = description: mkOption {
101 type = with types; nullOr str;
106 options.services.sourcehut = {
107 enable = mkEnableOption ''
108 sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
109 task dispatching, wiki and account management services
112 services = mkOption {
113 type = with types; listOf (enum
114 [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
115 defaultText = "locally enabled services";
117 Services that may be displayed as links in the title bar of the Web interface.
121 listenAddress = mkOption {
123 default = "localhost";
124 description = "Address to bind to.";
129 type = types.package;
132 The python package to use. It should contain references to the *srht modules and also
138 enable = mkEnableOption ''local minio integration'';
142 enable = mkEnableOption ''local nginx integration'';
146 enable = mkEnableOption ''local postfix integration'';
150 enable = mkEnableOption ''local postgresql integration'';
154 enable = mkEnableOption ''local redis integration'';
155 firstDatabase = mkOption {
159 Number of the first Redis database to use.
160 At most 9 consecutive databases are currently used.
165 settings = mkOption {
166 type = lib.types.submodule {
167 freeformType = settingsFormat.type;
169 global-domain = mkOption {
170 description = "Global domain name.";
172 example = "example.com";
174 environment = mkOption {
175 description = "Values other than \"production\" adds a banner to each page.";
176 type = types.enum [ "development" "production" ];
177 default = "development";
179 network-key = mkOption {
181 An absolute file path (which should be outside the Nix-store)
182 to a secret key to encrypt internal messages with. Use <code>srht-keygen network</code> to
183 generate this key. It must be consistent between all services and nodes.
186 apply = s: "<" + toString s;
188 owner-email = mkOption {
189 description = "Owner's email.";
191 default = "contact@example.com";
193 owner-name = mkOption {
194 description = "Owner's name.";
196 default = "John Doe";
198 redis-host = mkOption {
199 type = with types; nullOr str;
201 The redis host URL. This is used for caching and temporary storage, and must
202 be shared between nodes (e.g. g - 1it1.sr.ht and git2.sr.ht), but need not be
203 shared between services. It may be shared between services, however, with no
204 ill effect, if this better suits your infrastructure.
207 site-blurb = mkOption {
208 description = "Blurb for your site.";
210 default = "the hacker's forge";
212 site-info = mkOption {
213 description = "The top-level info page for your site.";
215 default = "https://sourcehut.org";
217 service-key = mkOption {
219 An absolute file path (which should be outside the Nix-store)
220 to a key used for encrypting session cookies. Use <code>srht-keygen service</code> to
221 generate the service key. This must be shared between each node of the same
222 service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
223 different keys. If you configure all of your services with the same
224 config.ini, you may use the same service-key for all of them.
227 apply = s: "<" + toString s;
229 site-name = mkOption {
230 description = "The name of your network of sr.ht-based sites.";
232 default = "sourcehut";
234 source-url = mkOption {
235 description = "The source code for your fork of sr.ht.";
237 default = "https://git.sr.ht/~sircmpwn/srht";
241 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
242 smtp-port = mkOption {
243 description = "Outgoing SMTP port.";
244 type = with types; nullOr port;
247 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
248 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
249 smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
250 error-to = mkOptionNullOrStr "Address receiving application exceptions";
251 error-from = mkOptionNullOrStr "Address sending application exceptions";
252 pgp-privkey = mkOptionNullOrStr ''
253 An absolute file path (which should be outside the Nix-store)
254 to an OpenPGP private key.
256 Your PGP key information (DO NOT mix up pub and priv here)
257 You must remove the password from your secret key, if present.
258 You can do this with <code>gpg --edit-key [key-id]</code>,
259 then use the <code>passwd</code> command and do not enter a new password.
261 pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
262 pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
265 s3-upstream = mkOption {
266 description = "Configure the S3-compatible object storage service.";
267 type = with types; nullOr str;
270 s3-access-key = mkOption {
271 description = "Access key to the S3-compatible object storage service";
272 type = with types; nullOr str;
275 s3-secret-key = mkOption {
277 An absolute file path (which should be outside the Nix-store)
278 to the secret key of the S3-compatible object storage service.
280 type = with types; nullOr path;
282 apply = mapNullable (s: "<" + toString s);
286 private-key = mkOption {
288 An absolute file path (which should be outside the Nix-store)
289 to a base64-encoded Ed25519 key for signing webhook payloads.
290 This should be consistent for all *.sr.ht sites,
291 as this key will be used to verify signatures
292 from other sites in your network.
293 Use the <code>srht-keygen webhook</code> command to generate a key.
296 apply = s: "<" + toString s;
300 options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
302 options."dispatch.sr.ht::github" = {
303 oauth-client-id = mkOptionNullOrStr "OAuth client id.";
304 oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
306 options."dispatch.sr.ht::gitlab" = {
307 enabled = mkEnableOption "GitLab integration";
308 canonical-upstream = mkOption {
310 description = "Canonical upstream.";
311 default = "gitlab.com";
313 repo-cache = mkOption {
315 description = "Repository cache directory.";
316 default = "./repo-cache";
318 "gitlab.com" = mkOption {
319 type = with types; nullOr str;
320 description = "GitLab id and secret.";
322 example = "GitLab:application id:secret";
326 options."builds.sr.ht" = commonServiceSettings "builds" // {
327 allow-free = mkEnableOption "nonpaying users to submit builds";
329 description = "The redis connection used for the celery worker.";
331 default = "redis://localhost:6379/3";
335 Scripts used to launch on SSH connection.
336 <literal>/usr/bin/master-shell</literal> on master,
337 <literal>/usr/bin/runner-shell</literal> on runner.
338 If master and worker are on the same system
339 set to <literal>/usr/bin/runner-shell</literal>.
341 type = types.enum ["/usr/bin/master-shell" "/usr/bin/runner-shell"];
342 default = "/usr/bin/master-shell";
345 options."builds.sr.ht::worker" = {
346 bind-address = mkOption {
348 HTTP bind address for serving local build information/monitoring.
351 default = "localhost:8080";
353 buildlogs = mkOption {
354 description = "Path to write build logs.";
356 default = "/var/log/sourcehut/buildsrht";
360 Listening address and listening port
361 of the build runner (with HTTP port if not 80).
364 default = "localhost:5020";
369 See <link xlink:href="https://golang.org/pkg/time/#ParseDuration"/>.
376 options."git.sr.ht" = commonServiceSettings "git" // {
377 outgoing-domain = mkOption {
378 description = "Outgoing domain.";
380 default = "https://git.localhost.localdomain";
382 post-update-script = mkOption {
384 A post-update script which is installed in every git repo.
385 This setting is propagated to newer and existing repositories.
388 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
389 defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
390 # Git hooks are run relative to their repository's directory,
391 # but gitsrht-update-hook looks up ../config.ini
392 apply = p: pkgs.writeShellScript "update-hook" ''
393 test -e "''${PWD%/*}"/config.ini ||
394 ln -s ${users."sshsrht".home}/../config.ini "''${PWD%/*}"/config.ini
395 exec -a "$0" '${p}' "$@"
400 Path to git repositories on disk.
401 If changing the default, you must ensure that
402 the gitsrht's user as read and write access to it.
405 default = "/var/lib/sourcehut/gitsrht/repos";
407 webhooks = mkOption {
408 description = "The redis connection used for the webhooks worker.";
410 default = "redis://localhost:6379/1";
414 options."hg.sr.ht" = commonServiceSettings "hg" // {
415 changegroup-script = mkOption {
417 A changegroup script which is installed in every mercurial repo.
418 This setting is propagated to newer and existing repositories.
421 default = "${cfg.python}/bin/hgsrht-hook-changegroup";
422 defaultText = "\${cfg.python}/bin/hgsrht-hook-changegroup";
426 Path to mercurial repositories on disk.
427 If changing the default, you must ensure that
428 the hgsrht's user as read and write access to it.
431 default = "/var/lib/sourcehut/hgsrht/repos";
433 srhtext = mkOptionNullOrStr ''
434 Path to the srht mercurial extension
435 (defaults to where the hgsrht code is)
437 clone_bundle_threshold = mkOption {
438 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
439 type = types.ints.unsigned;
443 description = "Path to hg-ssh (if not in $PATH).";
445 default = "${pkgs.mercurial}/bin/hg-ssh";
446 defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
448 webhooks = mkOption {
449 description = "The redis connection used for the webhooks worker.";
451 default = "redis://localhost:6379/8";
455 options."hub.sr.ht" = commonServiceSettings "hub" // {
458 options."lists.sr.ht" = commonServiceSettings "lists" // {
459 allow-new-lists = mkEnableOption "Allow creation of new lists.";
460 notify-from = mkOption {
461 description = "Outgoing email for notifications generated by users.";
463 default = "lists-notify@localhost.localdomain";
465 posting-domain = mkOption {
466 description = "Posting domain.";
468 default = "lists.localhost.localdomain";
471 description = "The redis connection used for the celery worker.";
473 default = "redis://localhost:6379/4";
475 webhooks = mkOption {
476 description = "The redis connection used for the webhooks worker.";
478 default = "redis://localhost:6379/2";
481 options."lists.sr.ht::worker" = {
482 reject-mimetypes = mkOption {
484 Comma-delimited list of Content-Types to reject. Messages with Content-Types
485 included in this list are rejected. Multipart messages are always supported,
486 and each part is checked against this list.
488 Uses fnmatch for wildcard expansion.
490 type = with types; listOf str;
491 default = ["text/html"];
493 reject-url = mkOption {
494 description = "Reject URL.";
496 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
500 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
501 Alternatively, specify IP:PORT and an SMTP server will be run instead.
504 default = "/tmp/lists.sr.ht-lmtp.sock";
506 sock-group = mkOption {
508 The lmtp daemon will make the unix socket group-read/write
509 for users in this group.
516 options."man.sr.ht" = commonServiceSettings "man" // {
519 options."meta.sr.ht" =
520 removeAttrs (commonServiceSettings "meta")
521 ["oauth-client-id" "oauth-client-secret"] // {
522 api-origin = mkOption {
523 description = "Origin URL for API, 100 more than web.";
525 default = "http://localhost:5100";
527 webhooks = mkOption {
528 description = "The redis connection used for the webhooks worker.";
530 default = "redis://localhost:6379/6";
532 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
534 options."meta.sr.ht::settings" = {
535 registration = mkEnableOption "public registration";
536 onboarding-redirect = mkOption {
537 description = "Where to redirect new users upon registration.";
539 default = "https://meta.localhost.localdomain";
541 user-invites = mkOption {
543 How many invites each user is issued upon registration
544 (only applicable if open registration is disabled).
546 type = types.ints.unsigned;
550 options."meta.sr.ht::aliases" = mkOption {
551 description = "Aliases for the client IDs of commonly used OAuth clients.";
552 type = with types; attrsOf int;
554 example = { "git.sr.ht" = 12345; };
556 options."meta.sr.ht::billing" = {
557 enabled = mkEnableOption "the billing system";
558 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
559 stripe-secret-key = mkOptionNullOrStr ''
560 An absolute file path (which should be outside the Nix-store)
561 to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
563 apply = mapNullable (s: "<" + toString s);
567 options."pages.sr.ht" = commonServiceSettings "pages" // {
568 gemini-certs = mkOption {
570 An absolute file path (which should be outside the Nix-store)
571 to Gemini certificates.
573 type = with types; nullOr path;
576 max-site-size = mkOption {
577 description = "Maximum size of any given site (post-gunzip), in MiB.";
581 user-domain = mkOption {
583 Configures the user domain, if enabled.
584 All users are given <username>.this.domain.
586 type = with types; nullOr str;
590 options."pages.sr.ht::api" = {
593 options."paste.sr.ht" = commonServiceSettings "paste" // {
594 webhooks = mkOption {
595 description = "The redis connection used for the webhooks worker.";
597 default = "redis://localhost:6379/5";
601 options."todo.sr.ht" = commonServiceSettings "todo" // {
602 notify-from = mkOption {
603 description = "Outgoing email for notifications generated by users.";
605 default = "todo-notify@localhost.localdomain";
607 webhooks = mkOption {
608 description = "The redis connection used for the webhooks worker.";
610 default = "redis://localhost:6379/7";
613 options."todo.sr.ht::mail" = {
614 posting-domain = mkOption {
615 description = "Posting domain.";
617 default = "todo.localhost.localdomain";
621 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
622 Alternatively, specify IP:PORT and an SMTP server will be run instead.
625 default = "/tmp/todo.sr.ht-lmtp.sock";
627 sock-group = mkOption {
629 The lmtp daemon will make the unix socket group-read/write
630 for users in this group.
639 The configuration for the sourcehut network.
644 enableWorker = mkEnableOption "worker for builds.sr.ht";
647 type = with types; attrsOf (attrsOf (attrsOf package));
649 example = lib.literalExample ''(let
650 # Pinning unstable to allow usage with flakes and limit rebuilds.
651 pkgs_unstable = builtins.fetchGit {
652 url = "https://github.com/NixOS/nixpkgs";
653 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
654 ref = "nixos-unstable";
656 image_from_nixpkgs = (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
657 pkgs = (import pkgs_unstable {});
661 nixos.unstable.x86_64 = image_from_nixpkgs;
665 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
672 type = types.package;
674 example = literalExample "pkgs.gitFull";
676 Git package for git.sr.ht. This can help silence collisions.
683 type = types.package;
684 default = pkgs.mercurial;
686 Mercurial package for hg.sr.ht. This can help silence collisions.
689 cloneBundles = mkOption {
693 Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
699 config = mkIf cfg.enable (mkMerge [
701 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
703 services.sourcehut.settings = {
704 "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
705 "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
706 "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
707 "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
708 "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
709 "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
712 (mkIf cfg.postgresql.enable {
713 services.postgresql.enable = true;
715 (mkIf cfg.postfix.enable {
716 services.postfix.enable = true;
717 # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
718 systemd.services.postfix.serviceConfig.PrivateTmp = true;
720 (mkIf cfg.redis.enable {
721 services.redis.enable = true;
722 services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
724 (mkIf cfg.nginx.enable {
725 services.nginx.enable = true;
727 (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
729 # Note that sshd will continue to honor AuthorizedKeysFile
730 authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
731 # The sshsrht-dispatch user needs:
732 # 1. to read ${users."sshsrht".home}/../config.ini,
733 # 2. to access the redis server in redis-host,
734 # 3. to access the postgresql server in the service's connection-string,
735 # 4. to query metasrht-api (through the HTTP API).
736 # Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
737 # to write their log on /var/log with that user, and will fallback to stderr,
738 # making their log visible in sshd's log when sshd is in debug mode (-d).
739 # Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
741 authorizedKeysCommandUser = users."sshsrht".name;
743 PermitUserEnvironment SRHT_*
746 environment.etc."ssh/srht-dispatch" = {
747 # sshd_config(5): The program must be owned by root, not writable by group or others
749 source = pkgs.writeShellScript "srht-dispatch" ''
751 cd ${users."sshsrht".home}
752 exec ${cfg.python}/bin/gitsrht-dispatch "$@"
755 systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
756 #path = optional cfg.git.enable [ cfg.git.package ];
757 restartTriggers = [ configIni ];
759 RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
761 # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
762 # for instance to get the user from the [*.sr.ht::dispatch] settings.
763 optionals cfg.builds.enable [
764 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
765 "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
766 "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
768 optionals cfg.git.enable [
769 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
770 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
772 optionals cfg.hg.enable [
773 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
774 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
776 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
777 # Replace values begining with a '<' by the content of the file whose name is after.
778 ${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
779 install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
780 /dev/stdin ${users."sshsrht".home}/../config.ini
787 # srht-dispatch, *srht-keys, and *srht-shell
788 # look up in ../config.ini from this directory;
789 # that config.ini being set in *srht.service's ExecStartPre=
790 home = "/run/sourcehut/sshsrht/subdir";
792 # Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
793 # hence the main group is used.
794 if cfg.postgresql.enable
795 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
796 then groups.postgres.name
797 else groups.nogroup.name;
798 description = "sourcehut user for sshd's AuthorizedKeysCommandUser";
800 groups."sshsrht" = {};
806 (import ./service.nix "builds" {
807 inherit configIniOfService;
808 srvsrht = "buildsrht";
811 extraServices.buildsrht-worker = let
812 qemuPackage = pkgs.qemu_kvm;
813 serviceName = "buildsrht-worker";
814 statePath = "/var/lib/sourcehut/${serviceName}";
815 in mkIf cfg.builds.enableWorker {
816 partOf = [ "buildsrht.service" ];
817 path = [ pkgs.openssh pkgs.docker ];
820 if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
821 || test "$(cat ${statePath}/docker-image-qemu)" != "${qemuPackage.version}"
823 # Create and import qemu:latest image for docker
824 ${pkgs.dockerTools.streamLayeredImage {
827 contents = [ qemuPackage ];
829 # Mark down current package version
830 echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
834 ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
835 RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
836 # builds.sr.ht-worker looks up ../config.ini
837 WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
838 StateDirectory = [ "sourcehut/${serviceName}" ];
839 Group = mkIf cfg.nginx.enable "nginx";
843 image_dirs = flatten (
844 mapAttrsToList (distro: revs:
845 mapAttrsToList (rev: archs:
846 mapAttrsToList (arch: image:
847 pkgs.runCommand "buildsrht-images" { } ''
848 mkdir -p $out/${distro}/${rev}/${arch}
849 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
855 image_dir_pre = pkgs.symlinkJoin {
856 name = "builds.sr.ht-worker-images-pre";
858 # FIXME: not working, apparently because ubuntu/latest is a broken link
859 # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
861 image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
863 cp -Lr ${image_dir_pre}/* $out/images
867 users.users.${cfg.builds.user} = {
869 extraGroups = [ groups."sshsrht".name ];
872 virtualisation.docker.enable = true;
874 services.sourcehut.settings = mkMerge [
875 { # Register the builds.sr.ht dispatcher
876 "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
877 mkDefault "${cfg.builds.user}:${cfg.builds.user}";
879 (mkIf cfg.builds.enableWorker {
880 "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
881 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
882 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
886 (mkIf cfg.builds.enableWorker {
888 docker.members = [ cfg.builds.user ];
891 (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
892 systemd.services.nginx = {
893 serviceConfig.BindReadOnlyPaths = [
894 "${cfg.settings."builds.sr.ht::worker".buildlogs}:/var/log/sourcehut/buildsrht/logs"
897 services.nginx.virtualHosts."logs.${domain}" = {
899 listen = with builtins;
900 # FIXME: not compatible with IPv6
901 let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
902 [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
904 locations."/logs/".alias = "/var/log/sourcehut/buildsrht/logs/";
909 (import ./service.nix "dispatch" {
910 inherit configIniOfService;
913 (import ./service.nix "git" (let
915 path = [ cfg.git.package ];
916 serviceConfig.BindPaths = [
917 "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos"
919 serviceConfig.BindReadOnlyPaths = [
920 "${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script"
923 inherit configIniOfService;
924 commonService = mkMerge [ commonService {
925 serviceConfig.StateDirectory = [ "sourcehut/gitsrht/repos" ];
928 webhooks.redisDatabase = 1;
929 extraTimers.gitsrht-periodic = {
930 OnCalendar = ["20min"];
933 # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
934 # Probably could use gitsrht-shell if output is restricted to just parameters...
935 users.users.${cfg.git.user} = {
937 # Allow reading of ${users."sshsrht".home}/../config.ini
938 extraGroups = [ groups."sshsrht".name ];
939 home = users.sshsrht.home;
942 services.sourcehut.settings = {
943 # Register the git.sr.ht dispatcher
944 "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
945 mkDefault "${cfg.git.user}:${cfg.git.user}";
948 services.fcgiwrap.enable = mkIf cfg.nginx.enable true;
949 services.nginx.virtualHosts."git.${domain}" = mkIf cfg.nginx.enable {
951 location = /authorize {
952 proxy_pass http://${cfg.listenAddress}:${toString cfg.git.port};
953 proxy_pass_request_body off;
954 proxy_set_header Content-Length "";
955 proxy_set_header X-Original-URI $request_uri;
957 location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
958 auth_request /authorize;
959 root ${cfg.settings."git.sr.ht".repos};
960 fastcgi_pass unix:/run/fcgiwrap.sock;
961 fastcgi_param SCRIPT_FILENAME ${cfg.git.package}/bin/git-http-backend;
962 fastcgi_param PATH_INFO $uri;
963 fastcgi_param GIT_PROJECT_ROOT $document_root;
964 fastcgi_param GIT_HTTP_EXPORT_ALL "";
965 fastcgi_read_timeout 500s;
966 include ${config.services.nginx.package}/conf/fastcgi_params;
971 systemd.services.nginx = mkIf cfg.nginx.enable {
972 serviceConfig.BindReadOnlyPaths = [ cfg.settings."git.sr.ht".repos ];
975 systemd.services.sshd = commonService;
978 (import ./service.nix "hg" (let
980 path = [ cfg.hg.package ];
981 serviceConfig.BindPaths = [
982 "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos"
984 serviceConfig.BindReadOnlyPaths = [
985 "${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script"
988 inherit configIniOfService;
989 commonService = mkMerge [ commonService {
990 serviceConfig.StateDirectory = [ "sourcehut/hgsrht/repos" ];
993 webhooks.redisDatabase = 8;
994 extraTimers.hgsrht-periodic = {
995 OnCalendar = ["20min"];
997 extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
998 OnCalendar = ["daily"];
1002 users.users.${cfg.hg.user} = {
1004 extraGroups = [ groups."sshsrht".name ];
1007 services.sourcehut.settings = {
1008 # Register the hg.sr.ht dispatcher
1009 "hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
1010 mkDefault "${cfg.hg.user}:${cfg.hg.user}";
1013 systemd.services.sshd = commonService;
1016 (import ./service.nix "hub" {
1017 inherit configIniOfService;
1020 services.nginx = mkIf cfg.nginx.enable {
1021 virtualHosts."hub.${domain}" = {
1022 serverAliases = [ domain ];
1027 (import ./service.nix "lists" {
1028 inherit configIniOfService;
1031 webhooks.redisDatabase = 2;
1032 extraServices.listssrht-lmtp = {
1033 requires = [ "postfix.service" ];
1034 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1035 serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
1036 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1037 serviceConfig.PrivateUsers = mkForce false;
1039 extraServices.listssrht-process = {
1040 serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
1041 # Avoid crashing: os.getloadavg()
1042 serviceConfig.ProcSubset = mkForce "all";
1044 extraConfig = mkIf cfg.postfix.enable {
1045 users.groups.${postfix.group}.members = [ cfg.lists.user ];
1046 services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
1047 services.postfix.transport = ''
1048 lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
1052 (import ./service.nix "man" {
1053 inherit configIniOfService;
1056 (import ./service.nix "meta" {
1057 inherit configIniOfService;
1059 webhooks.redisDatabase = 6;
1060 extraServices.metasrht-api = {
1061 serviceConfig.Restart = "always";
1062 serviceConfig.RestartSec = "2s";
1063 preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
1064 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
1065 srv = head srvMatch;
1067 # Configure client(s) as "preauthorized"
1068 optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
1069 # Configure ${srv}'s OAuth client as "preauthorized"
1070 ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
1071 -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
1074 serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
1076 extraTimers.metasrht-daily = {
1077 OnCalendar = ["daily"];
1082 { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
1083 s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
1084 message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
1087 environment.systemPackages = [
1088 (pkgs.writeShellScriptBin "metasrht-manageuser" ''
1090 test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
1091 sudo -u '${cfg.meta.user}' "$0" "$@"
1092 # In order to load config.ini
1093 cd /run/sourcehut/metasrht ||
1095 Please run: sudo systemctl start metasrht
1097 ${cfg.python}/bin/metasrht-manageuser "$@"
1102 (import ./service.nix "pages" {
1103 inherit configIniOfService;
1105 #webhooks.redisDatabase = 9;
1107 srvsrht = "pagessrht";
1108 version = pkgs.sourcehut.${srvsrht}.version;
1109 stateDir = "/var/lib/sourcehut/${srvsrht}";
1110 iniKey = "pages.sr.ht";
1112 preStart = mkBefore ''
1114 # Use the /run/sourcehut/${srvsrht}/config.ini
1115 # installed by a previous ExecStartPre= in baseService
1116 cd /run/sourcehut/${srvsrht}
1118 if test ! -e ${stateDir}/db; then
1119 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
1120 echo ${version} >${stateDir}/db
1123 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
1124 # Just try all the migrations because they're not linked to the version
1125 for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
1126 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
1131 touch ${stateDir}/webhook
1134 ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
1138 (import ./service.nix "paste" {
1139 inherit configIniOfService;
1141 webhooks.redisDatabase = 5;
1143 (import ./service.nix "todo" {
1144 inherit configIniOfService;
1146 webhooks.redisDatabase = 7;
1147 extraServices.todosrht-lmtp = {
1148 requires = [ "postfix.service" ];
1149 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1150 serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
1151 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1152 serviceConfig.PrivateUsers = mkForce false;
1154 extraConfig = mkIf cfg.postfix.enable {
1155 users.groups.${postfix.group}.members = [ cfg.todo.user ];
1156 services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
1157 services.postfix.transport = ''
1158 todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
1162 (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
1163 [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
1164 (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
1165 [ "services" "sourcehut" "listenAddress" ])
1168 meta.doc = ./sourcehut.xml;
1169 meta.maintainers = with maintainers; [ julm tomberek ];