1 { config, pkgs, lib, ... }:
4 inherit (config.services) nginx 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-wrapper" ''
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";
423 # Mercurial's changegroup hooks are run relative to their repository's directory,
424 # but hgsrht-hook-changegroup looks up ./config.ini
425 apply = p: pkgs.writeShellScript "hook-changegroup-wrapper" ''
426 test -e "''$PWD"/config.ini ||
427 ln -s ${users."sshsrht".home}/../config.ini "''$PWD"/config.ini
428 exec -a "$0" '${p}' "$@"
433 Path to mercurial repositories on disk.
434 If changing the default, you must ensure that
435 the hgsrht's user as read and write access to it.
438 default = "/var/lib/sourcehut/hgsrht/repos";
440 srhtext = mkOptionNullOrStr ''
441 Path to the srht mercurial extension
442 (defaults to where the hgsrht code is)
444 clone_bundle_threshold = mkOption {
445 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
446 type = types.ints.unsigned;
450 description = "Path to hg-ssh (if not in $PATH).";
452 default = "${pkgs.mercurial}/bin/hg-ssh";
453 defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
455 webhooks = mkOption {
456 description = "The redis connection used for the webhooks worker.";
458 default = "redis://localhost:6379/8";
462 options."hub.sr.ht" = commonServiceSettings "hub" // {
465 options."lists.sr.ht" = commonServiceSettings "lists" // {
466 allow-new-lists = mkEnableOption "Allow creation of new lists.";
467 notify-from = mkOption {
468 description = "Outgoing email for notifications generated by users.";
470 default = "lists-notify@localhost.localdomain";
472 posting-domain = mkOption {
473 description = "Posting domain.";
475 default = "lists.localhost.localdomain";
478 description = "The redis connection used for the celery worker.";
480 default = "redis://localhost:6379/4";
482 webhooks = mkOption {
483 description = "The redis connection used for the webhooks worker.";
485 default = "redis://localhost:6379/2";
488 options."lists.sr.ht::worker" = {
489 reject-mimetypes = mkOption {
491 Comma-delimited list of Content-Types to reject. Messages with Content-Types
492 included in this list are rejected. Multipart messages are always supported,
493 and each part is checked against this list.
495 Uses fnmatch for wildcard expansion.
497 type = with types; listOf str;
498 default = ["text/html"];
500 reject-url = mkOption {
501 description = "Reject URL.";
503 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
507 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
508 Alternatively, specify IP:PORT and an SMTP server will be run instead.
511 default = "/tmp/lists.sr.ht-lmtp.sock";
513 sock-group = mkOption {
515 The lmtp daemon will make the unix socket group-read/write
516 for users in this group.
523 options."man.sr.ht" = commonServiceSettings "man" // {
526 options."meta.sr.ht" =
527 removeAttrs (commonServiceSettings "meta")
528 ["oauth-client-id" "oauth-client-secret"] // {
529 api-origin = mkOption {
530 description = "Origin URL for API, 100 more than web.";
532 default = "http://localhost:5100";
534 webhooks = mkOption {
535 description = "The redis connection used for the webhooks worker.";
537 default = "redis://localhost:6379/6";
539 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
541 options."meta.sr.ht::settings" = {
542 registration = mkEnableOption "public registration";
543 onboarding-redirect = mkOption {
544 description = "Where to redirect new users upon registration.";
546 default = "https://meta.localhost.localdomain";
548 user-invites = mkOption {
550 How many invites each user is issued upon registration
551 (only applicable if open registration is disabled).
553 type = types.ints.unsigned;
557 options."meta.sr.ht::aliases" = mkOption {
558 description = "Aliases for the client IDs of commonly used OAuth clients.";
559 type = with types; attrsOf int;
561 example = { "git.sr.ht" = 12345; };
563 options."meta.sr.ht::billing" = {
564 enabled = mkEnableOption "the billing system";
565 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
566 stripe-secret-key = mkOptionNullOrStr ''
567 An absolute file path (which should be outside the Nix-store)
568 to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
570 apply = mapNullable (s: "<" + toString s);
574 options."pages.sr.ht" = commonServiceSettings "pages" // {
575 gemini-certs = mkOption {
577 An absolute file path (which should be outside the Nix-store)
578 to Gemini certificates.
580 type = with types; nullOr path;
583 max-site-size = mkOption {
584 description = "Maximum size of any given site (post-gunzip), in MiB.";
588 user-domain = mkOption {
590 Configures the user domain, if enabled.
591 All users are given <username>.this.domain.
593 type = with types; nullOr str;
597 options."pages.sr.ht::api" = {
600 options."paste.sr.ht" = commonServiceSettings "paste" // {
601 webhooks = mkOption {
602 description = "The redis connection used for the webhooks worker.";
604 default = "redis://localhost:6379/5";
608 options."todo.sr.ht" = commonServiceSettings "todo" // {
609 notify-from = mkOption {
610 description = "Outgoing email for notifications generated by users.";
612 default = "todo-notify@localhost.localdomain";
614 webhooks = mkOption {
615 description = "The redis connection used for the webhooks worker.";
617 default = "redis://localhost:6379/7";
620 options."todo.sr.ht::mail" = {
621 posting-domain = mkOption {
622 description = "Posting domain.";
624 default = "todo.localhost.localdomain";
628 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
629 Alternatively, specify IP:PORT and an SMTP server will be run instead.
632 default = "/tmp/todo.sr.ht-lmtp.sock";
634 sock-group = mkOption {
636 The lmtp daemon will make the unix socket group-read/write
637 for users in this group.
646 The configuration for the sourcehut network.
651 enableWorker = mkEnableOption "worker for builds.sr.ht";
654 type = with types; attrsOf (attrsOf (attrsOf package));
656 example = lib.literalExample ''(let
657 # Pinning unstable to allow usage with flakes and limit rebuilds.
658 pkgs_unstable = builtins.fetchGit {
659 url = "https://github.com/NixOS/nixpkgs";
660 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
661 ref = "nixos-unstable";
663 image_from_nixpkgs = (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
664 pkgs = (import pkgs_unstable {});
668 nixos.unstable.x86_64 = image_from_nixpkgs;
672 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
679 type = types.package;
681 example = literalExample "pkgs.gitFull";
683 Git package for git.sr.ht. This can help silence collisions.
686 fcgiwrap.preforkProcess = mkOption {
687 description = "Number of processes to prefork.";
695 type = types.package;
696 default = pkgs.mercurial;
698 Mercurial package for hg.sr.ht. This can help silence collisions.
701 cloneBundles = mkOption {
705 Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
711 config = mkIf cfg.enable (mkMerge [
713 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
715 services.sourcehut.settings = {
716 "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
717 "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
718 "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
719 "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
720 "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
721 "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
724 (mkIf cfg.postgresql.enable {
725 services.postgresql.enable = true;
727 (mkIf cfg.postfix.enable {
728 services.postfix.enable = true;
729 # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
730 systemd.services.postfix.serviceConfig.PrivateTmp = true;
732 (mkIf cfg.redis.enable {
733 services.redis.enable = true;
734 services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
736 (mkIf cfg.nginx.enable {
737 services.nginx.enable = true;
738 # For proxyPass= in virtual-hosts for Sourcehut services.
739 services.nginx.recommendedProxySettings = mkDefault true;
741 (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
743 # Note that sshd will continue to honor AuthorizedKeysFile
744 authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
745 # The sshsrht-dispatch user needs:
746 # 1. to read ${users."sshsrht".home}/../config.ini,
747 # 2. to access the redis server in redis-host,
748 # 3. to access the postgresql server in the service's connection-string,
749 # 4. to query metasrht-api (through the HTTP API).
750 # Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
751 # to write their log on /var/log with that user, and will fallback to stderr,
752 # making their log visible in sshd's log when sshd is in debug mode (-d).
753 # Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
755 authorizedKeysCommandUser = users."sshsrht".name;
757 PermitUserEnvironment SRHT_*
760 environment.etc."ssh/srht-dispatch" = {
761 # sshd_config(5): The program must be owned by root, not writable by group or others
763 source = pkgs.writeShellScript "srht-dispatch" ''
765 cd ${users."sshsrht".home}
766 exec ${cfg.python}/bin/gitsrht-dispatch "$@"
769 systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
770 #path = optional cfg.git.enable [ cfg.git.package ];
771 restartTriggers = [ configIni ];
773 RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
775 # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
776 # for instance to get the user from the [*.sr.ht::dispatch] settings.
777 optionals cfg.builds.enable [
778 "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
779 "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
780 "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
782 optionals cfg.git.enable [
783 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
784 "${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
786 optionals cfg.hg.enable [
787 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
788 "${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
790 ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
791 # Replace values begining with a '<' by the content of the file whose name is after.
792 ${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
793 install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
794 /dev/stdin ${users."sshsrht".home}/../config.ini
801 # srht-dispatch, *srht-keys, and *srht-shell
802 # look up in ../config.ini from this directory;
803 # that config.ini being set in *srht.service's ExecStartPre=
804 home = "/run/sourcehut/sshsrht/subdir";
806 # Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
807 # hence the main group is used.
808 if cfg.postgresql.enable
809 && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
810 then groups.postgres.name
811 else groups.nogroup.name;
812 description = "sourcehut user for sshd's AuthorizedKeysCommandUser";
814 groups."sshsrht" = {};
820 (import ./service.nix "builds" {
821 inherit configIniOfService;
822 srvsrht = "buildsrht";
825 extraServices.buildsrht-worker = let
826 qemuPackage = pkgs.qemu_kvm;
827 serviceName = "buildsrht-worker";
828 statePath = "/var/lib/sourcehut/${serviceName}";
829 in mkIf cfg.builds.enableWorker {
830 path = [ pkgs.openssh pkgs.docker ];
833 if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
834 || test "$(cat ${statePath}/docker-image-qemu)" != "${qemuPackage.version}"
836 # Create and import qemu:latest image for docker
837 ${pkgs.dockerTools.streamLayeredImage {
840 contents = [ qemuPackage ];
842 # Mark down current package version
843 echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
847 ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
848 RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
849 # builds.sr.ht-worker looks up ../config.ini
850 LogsDirectory = [ "sourcehut/${serviceName}" ];
851 StateDirectory = [ "sourcehut/${serviceName}" ];
852 WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
856 image_dirs = flatten (
857 mapAttrsToList (distro: revs:
858 mapAttrsToList (rev: archs:
859 mapAttrsToList (arch: image:
860 pkgs.runCommand "buildsrht-images" { } ''
861 mkdir -p $out/${distro}/${rev}/${arch}
862 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
868 image_dir_pre = pkgs.symlinkJoin {
869 name = "builds.sr.ht-worker-images-pre";
871 # FIXME: not working, apparently because ubuntu/latest is a broken link
872 # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
874 image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
876 cp -Lr ${image_dir_pre}/* $out/images
880 users.users.${cfg.builds.user} = {
882 # Allow reading of ${users."sshsrht".home}/../config.ini
883 extraGroups = [ groups."sshsrht".name ];
886 virtualisation.docker.enable = true;
888 services.sourcehut.settings = mkMerge [
889 { # Register the builds.sr.ht dispatcher
890 "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
891 mkDefault "${cfg.builds.user}:${cfg.builds.group}";
893 (mkIf cfg.builds.enableWorker {
894 "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
895 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
896 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
900 (mkIf cfg.builds.enableWorker {
902 docker.members = [ cfg.builds.user ];
905 (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
906 # Allow nginx access to buildlogs
907 users.users.${nginx.user}.extraGroups = [ cfg.builds.group ];
908 systemd.services.nginx = {
909 serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."builds.sr.ht::worker".buildlogs}:/var/log/nginx/buildsrht/logs" ];
911 services.nginx.virtualHosts."logs.${domain}" = {
912 /* FIXME: is a listen needed?
913 listen = with builtins;
914 # FIXME: not compatible with IPv6
915 let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
916 [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
918 locations."/logs/".alias = "/var/log/nginx/buildsrht/logs/";
923 (import ./service.nix "dispatch" {
924 inherit configIniOfService;
927 (import ./service.nix "git" (let
929 path = [ cfg.git.package ];
930 serviceConfig.BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
931 serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script" ];
933 mainService = mkMerge [ baseService {
934 serviceConfig.StateDirectory = [ "sourcehut/gitsrht" ];
937 inherit configIniOfService;
940 webhooks.redisDatabase = 1;
941 extraTimers.gitsrht-periodic = {
942 service = mainService;
944 OnCalendar = ["20min"];
947 extraConfig = mkMerge [
949 users.users.${cfg.git.user} = {
950 # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
951 # Probably could use gitsrht-shell if output is restricted to just parameters...
953 # Allow reading of ${users."sshsrht".home}/../config.ini
954 extraGroups = [ groups."sshsrht".name ];
955 home = users.sshsrht.home;
957 services.sourcehut.settings = {
958 # Register the git.sr.ht dispatcher
959 "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
960 mkDefault "${cfg.git.user}:${cfg.git.group}";
962 systemd.services.sshd = baseService;
964 (mkIf cfg.nginx.enable {
965 services.nginx.virtualHosts."git.${domain}" = {
966 locations."/authorize" = {
967 proxyPass = "http://${cfg.listenAddress}:${toString cfg.git.port}";
969 proxy_pass_request_body off;
970 proxy_set_header Content-Length "";
971 proxy_set_header X-Original-URI $request_uri;
974 locations."~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$" = {
975 root = "/var/lib/sourcehut/gitsrht/repos";
977 GIT_HTTP_EXPORT_ALL = "";
978 GIT_PROJECT_ROOT = "$document_root";
980 SCRIPT_FILENAME = "${cfg.git.package}/bin/git-http-backend";
983 auth_request /authorize;
984 fastcgi_read_timeout 500s;
985 fastcgi_pass unix:/run/gitsrht-fcgiwrap.sock;
990 systemd.sockets.gitsrht-fcgiwrap = {
991 before = [ "nginx.service" ];
992 wantedBy = [ "sockets.target" "gitsrht.service" ];
993 # This path remains accessible to nginx.service, which has no RootDirectory=
994 socketConfig.ListenStream = "/run/gitsrht-fcgiwrap.sock";
995 socketConfig.SocketUser = nginx.user;
996 socketConfig.SocketMode = "600";
1000 extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
1002 # Socket is passed by systemd.sockets.gitsrht-fcgiwrap
1003 ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
1004 # No need for config.ini
1005 ExecStartPre = mkForce [];
1008 BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
1009 IPAddressDeny = "any";
1010 InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis" ];
1011 PrivateNetwork = true;
1012 RestrictAddressFamilies = mkForce [ "none" ];
1013 SystemCallFilter = mkForce [
1015 "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid"
1016 # @timer is needed for alarm()
1021 (import ./service.nix "hg" (let
1023 path = [ cfg.hg.package ];
1024 serviceConfig.BindPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos" ];
1025 serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script" ];
1027 mainService = mkMerge [ baseService {
1028 serviceConfig.StateDirectory = [ "sourcehut/hgsrht" ];
1031 inherit configIniOfService;
1032 inherit mainService;
1034 webhooks.redisDatabase = 8;
1035 extraTimers.hgsrht-periodic = {
1036 service = mainService;
1038 OnCalendar = ["20min"];
1041 extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
1042 service = mainService;
1044 OnCalendar = ["daily"];
1048 extraConfig = mkMerge [
1050 users.users.${cfg.hg.user} = {
1052 # Allow reading of ${users."sshsrht".home}/../config.ini
1053 extraGroups = [ groups."sshsrht".name ];
1055 services.sourcehut.settings = {
1056 # Register the hg.sr.ht dispatcher
1057 "hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
1058 mkDefault "${cfg.hg.user}:${cfg.hg.group}";
1060 systemd.services.sshd = baseService;
1062 (mkIf cfg.nginx.enable {
1063 # Allow nginx access to repositories
1064 users.users.${nginx.user}.extraGroups = [ cfg.hg.group ];
1065 services.nginx.virtualHosts."hg.${domain}" = {
1066 locations."/authorize" = {
1067 proxyPass = "http://${cfg.listenAddress}:${toString cfg.hg.port}";
1069 proxy_pass_request_body off;
1070 proxy_set_header Content-Length "";
1071 proxy_set_header X-Original-URI $request_uri;
1074 # Let clients reach pull bundles. We don't really need to lock this down even for
1075 # private repos because the bundles are named after the revision hashes...
1076 # so someone would need to know or guess a SHA value to download anything.
1077 # TODO: proxyPass to an hg serve service?
1078 locations."~ ^/[~^][a-z0-9_]+/[a-zA-Z0-9_.-]+/\\.hg/bundles/.*$" = {
1079 root = "/var/lib/nginx/hgsrht/repos";
1081 auth_request /authorize;
1086 systemd.services.nginx = {
1087 serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/nginx/hgsrht/repos" ];
1092 (import ./service.nix "hub" {
1093 inherit configIniOfService;
1096 services.nginx = mkIf cfg.nginx.enable {
1097 virtualHosts."hub.${domain}" = {
1098 serverAliases = [ domain ];
1103 (import ./service.nix "lists" {
1104 inherit configIniOfService;
1107 webhooks.redisDatabase = 2;
1108 extraServices.listssrht-lmtp = {
1109 requires = [ "postfix.service" ];
1110 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1111 serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
1112 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1113 serviceConfig.PrivateUsers = mkForce false;
1115 extraServices.listssrht-process = {
1116 serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
1117 # Avoid crashing: os.getloadavg()
1118 serviceConfig.ProcSubset = mkForce "all";
1120 extraConfig = mkIf cfg.postfix.enable {
1121 users.groups.${postfix.group}.members = [ cfg.lists.user ];
1122 services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
1123 services.postfix.transport = ''
1124 lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
1128 (import ./service.nix "man" {
1129 inherit configIniOfService;
1132 (import ./service.nix "meta" {
1133 inherit configIniOfService;
1135 webhooks.redisDatabase = 6;
1136 extraServices.metasrht-api = {
1137 serviceConfig.Restart = "always";
1138 serviceConfig.RestartSec = "2s";
1139 preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
1140 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
1141 srv = head srvMatch;
1143 # Configure client(s) as "preauthorized"
1144 optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
1145 # Configure ${srv}'s OAuth client as "preauthorized"
1146 ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
1147 -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
1150 serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
1152 extraTimers.metasrht-daily.timerConfig = {
1153 OnCalendar = ["daily"];
1158 { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
1159 s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
1160 message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
1163 environment.systemPackages = [
1164 (pkgs.writeShellScriptBin "metasrht-manageuser" ''
1166 test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
1167 sudo -u '${cfg.meta.user}' "$0" "$@"
1168 # In order to load config.ini
1169 cd /run/sourcehut/metasrht ||
1171 Please run: sudo systemctl start metasrht
1173 ${cfg.python}/bin/metasrht-manageuser "$@"
1178 (import ./service.nix "pages" {
1179 inherit configIniOfService;
1181 #webhooks.redisDatabase = 9;
1183 srvsrht = "pagessrht";
1184 version = pkgs.sourcehut.${srvsrht}.version;
1185 stateDir = "/var/lib/sourcehut/${srvsrht}";
1186 iniKey = "pages.sr.ht";
1188 preStart = mkBefore ''
1190 # Use the /run/sourcehut/${srvsrht}/config.ini
1191 # installed by a previous ExecStartPre= in baseService
1192 cd /run/sourcehut/${srvsrht}
1194 if test ! -e ${stateDir}/db; then
1195 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
1196 echo ${version} >${stateDir}/db
1199 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
1200 # Just try all the migrations because they're not linked to the version
1201 for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
1202 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
1207 touch ${stateDir}/webhook
1210 ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
1214 (import ./service.nix "paste" {
1215 inherit configIniOfService;
1217 webhooks.redisDatabase = 5;
1219 (import ./service.nix "todo" {
1220 inherit configIniOfService;
1222 webhooks.redisDatabase = 7;
1223 extraServices.todosrht-lmtp = {
1224 requires = [ "postfix.service" ];
1225 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1226 serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
1227 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1228 serviceConfig.PrivateUsers = mkForce false;
1230 extraConfig = mkIf cfg.postfix.enable {
1231 users.groups.${postfix.group}.members = [ cfg.todo.user ];
1232 services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
1233 services.postfix.transport = ''
1234 todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
1238 (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
1239 [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
1240 (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
1241 [ "services" "sourcehut" "listenAddress" ])
1244 meta.doc = ./sourcehut.xml;
1245 meta.maintainers = with maintainers; [ julm tomberek ];