diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6f6bfa6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769461804, + "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ef20b2b --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + description = "Troutbot - GitHub webhook bot"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forEachSystem = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + in { + nixosModules = { + troutbot = import ./nix/modules/nixos.nix self; + default = self.nixosModules.troutbot; + }; + + packages = forEachSystem (pkgs: { + troutbot = pkgs.callPackage ./nix/package.nix {}; + default = self.packages.${pkgs.hostPlatform.system}.troutbot; + }); + + devShells = forEachSystem (pkgs: { + default = pkgs.mkShell { + name = "troutbot-dev"; + packages = [pkgs.nodejs-slim pkgs.pnpm]; + }; + }); + }; +} diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix new file mode 100644 index 0000000..b9502c7 --- /dev/null +++ b/nix/modules/nixos.nix @@ -0,0 +1,83 @@ +self: { + config, + pkgs, + lib, + ... +}: let + inherit (lib.modules) mkIf; + inherit (lib.options) mkOption mkEnableOption literalExpression; + inherit (lib.types) nullOr str port package; + + defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.troutbot; + cfg = config.services.troutbot; +in { + options.services.troutbot = { + enable = mkEnableOption "troutbot"; + + package = mkOption { + type = nullOr package; + default = defaultPackage; + defaultText = literalExpression "inputs.troutbot.packages.${pkgs.stdenv.hostPlatform.system}.troutbot"; + description = '' + The Troutbot package to use. + + By default, this option will use the `packages.default` as exposed by this flake. + ''; + }; + + user = mkOption { + type = str; + default = "troutbot"; + }; + + group = mkOption { + type = str; + default = "troutbot"; + }; + + port = mkOption { + type = port; + default = 3000; + }; + + environmentFile = mkOption { + type = nullOr str; + default = null; + }; + + configPath = mkOption { + type = nullOr str; + default = null; + }; + }; + + config = mkIf cfg.enable { + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + }; + + users.groups.${cfg.group} = {}; + + systemd.services.troutbot = { + description = "Troutbot"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + ExecStart = "${lib.getExe cfg.package}"; + Restart = "on-failure"; + EnvironmentFile = cfg.environmentFile; + NODE_ENV = "production"; + CONFIG_PATH = cfg.configPath; + PORT = toString cfg.port; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + }; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..5557d37 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,71 @@ +{ + lib, + stdenv, + nodejs, + pnpmConfigHook, + fetchPnpmDeps, + pnpm, + makeBinaryWrapper, +}: +stdenv.mkDerivation (finalAttrs: { + pname = "troutbot"; + version = "0-unstable-2026-01-30"; + + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.unions [ + ../src + ../config.example.ts + ../package.json + ../pnpm-lock.yaml + ../tsconfig.json + ]; + }; + + strictDeps = true; + nativeBuildInputs = [ + nodejs # in case scripts are run outside of a pnpm call + pnpmConfigHook + pnpm # at least required by pnpmConfigHook, if not other (custom) phases + + makeBinaryWrapper + ]; + + pnpmDeps = fetchPnpmDeps { + inherit (finalAttrs) pname version src; + fetcherVersion = 3; + hash = "sha256-y8LV1D+EgGcZ79lmxS20dqYBPEfk4atma+RWf7pJI30="; + }; + + buildPhase = '' + runHook preBuild + + pnpm run build --outDir dist + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/{bin,share} + + # Copy transpiled result + cp -rv dist/* $out/share + + # Copy the example config + install -Dm755 config.example.ts $out/share + + makeWrapper ${lib.getExe nodejs} $out/bin/troutbot \ + --set-default NODE_ENV production \ + --add-flags "$out/share/index.js" + + runHook postInstall + ''; + + meta = { + description = "The ultimate trout population helper"; + license = lib.licenses.eupl12; + maintainers = with lib.maintainers; [NotAShelf]; + }; +})