Add webhook-triggered rebuilds for Pelican sites

Adds a webhook-deploy service that listens for Codeberg push events and
triggers per-site nixos-rebuild runs. Skips rebuilds if the flake lock
revision is unchanged. Fixes root SSH client config so nix flake update
can reach Codeberg using the existing deploy key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-05-14 13:36:10 +03:00
parent cd4d53386c
commit bb419a13af
4 changed files with 94 additions and 2 deletions

View file

@ -48,6 +48,7 @@
./modules/services/nostr-rs-relay.nix
./modules/services/podman.nix
./modules/services/static-sites
./modules/services/webhook-deploy.nix
./modules/users/tobias.nix
];
};

View file

@ -30,6 +30,9 @@ randogoth.com, www.randogoth.com {
}
sublunar.space, www.sublunar.space {
handle /hooks/* {
reverse_proxy localhost:9000
}
root * /var/www/sublunar.space/public
encode zstd gzip
file_server

View file

@ -0,0 +1,87 @@
{ config, pkgs, lib, ... }:
let
sites = [
{ name = "sublunar"; input = "sublunar"; }
{ name = "tobias-raayoni-last"; input = "flux_vision"; }
{ name = "nmns"; input = "nfc_web"; }
];
# Template hooks.json with a placeholder secret — real secret injected at
# service start time from the sops-managed file.
hooksTemplate = pkgs.writeText "webhook-hooks-template.json" (builtins.toJSON
(map (site: {
id = "rebuild-${site.name}";
execute-command = "/run/current-system/sw/bin/systemctl";
pass-arguments-to-command = [
{ source = "string"; name = "start"; }
{ source = "string"; name = "--no-block"; }
{ source = "string"; name = "update-${site.name}"; }
];
response-message = "Queued rebuild of ${site.name}";
trigger-rule.match = {
type = "payload-hmac-sha256";
secret = "placeholder";
parameter = { source = "header"; name = "X-Gitea-Signature"; };
};
}) sites)
);
# Writes /run/webhook-deploy/hooks.json with the real secret at startup.
makeHooks = pkgs.writeShellScript "make-webhook-hooks" ''
set -euo pipefail
${pkgs.jq}/bin/jq \
--arg s "$(cat ${config.sops.secrets.webhook_secret.path})" \
'map(.["trigger-rule"].match.secret = $s)' \
${hooksTemplate} > /run/webhook-deploy/hooks.json
'';
updateScript = site: pkgs.writeShellScript "do-update-${site.name}" ''
set -euo pipefail
cd /etc/nixos
OLD=$(${pkgs.jq}/bin/jq -r '.nodes.${site.input}.locked.rev' flake.lock)
nix flake update ${site.input}
NEW=$(${pkgs.jq}/bin/jq -r '.nodes.${site.input}.locked.rev' flake.lock)
if [ "$OLD" = "$NEW" ]; then
echo "${site.name}: no change, skipping rebuild"
exit 0
fi
echo "${site.name}: updated $OLD -> $NEW, rebuilding"
nixos-rebuild switch --flake /etc/nixos#bucur
'';
in
{
sops.secrets.webhook_secret = {
owner = "root";
mode = "0400";
};
# System-wide SSH client config so root can reach Codeberg via the deploy key.
programs.ssh.extraConfig = ''
Host codeberg.org
IdentityFile /etc/ssh/codeberg_id_ed25519
StrictHostKeyChecking accept-new
'';
systemd.services = lib.listToAttrs
(map (site: lib.nameValuePair "update-${site.name}" {
description = "Pull and rebuild ${site.name}";
serviceConfig = {
Type = "oneshot";
ExecStart = updateScript site;
};
}) sites)
// {
webhook-deploy = {
description = "Webhook server for Pelican site rebuilds";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "sops-nix.service" ];
serviceConfig = {
Type = "simple";
RuntimeDirectory = "webhook-deploy";
ExecStartPre = "+${makeHooks}";
ExecStart = "${pkgs.webhook}/bin/webhook -hooks /run/webhook-deploy/hooks.json -port 9000";
Restart = "on-failure";
};
};
};
}

View file

@ -1,5 +1,6 @@
mtproto_secret: ENC[AES256_GCM,data:MyzyUCH8cLOG+GMtmV03daJ9sqwOw/ozN2yi/14SMlY=,iv:LLf2LIvX+jNjRdMHclHpsmTZa7wmVM3gD6RbqeCGmik=,tag:i36lVESyKJafsn14kSzh3g==,type:str]
codeberg_id_ed25519: ENC[AES256_GCM,data:AZbFWfc80HVskenEVLYL9MOYrGhW7Cwzdqv8gS4+1JGfk+nC801SmMgElUdISo+3m/O54b8RbJtdVz2u99gL8WG6wgbK7xqf40q4HNvV5cFXp7L/ozUkT41uXjWsDIdnMwumOqL/vjXowmAay30kMOiD5M4DL0t6EmuOo1Rcdffe/HwCVd2Pz7ZpU+BSTQV/pDTK1X3taUCjCY3Ui0XrcSCY8G2tuX5pFQ6t+vLrdqy3UGhofYHQ+nZuoyEKQHCcvEhYvEgDPJtyy+gk96vZCDYyXb9am/5k0I+MoIsArpavp4P0r5dNDcTnuCHH+fVPTG4tOsG/Ztf1rFBCsqPiM0azNElI8FERSKLYipuDDD8GI46p43OalHw4pxmAHXNOs70PfIH25iFJ5AUb32v2A71LScJMNP/TFSSezuq6g5eTd/JrtpQnWwToH1eVrwcN/8YX3xflLZ2JO1q4fjCXrkYHyw004dV/HAq3u1XQDmfMviZiH80+hiVIgZLfMWv5IyzfuomqSnfOj6oJ6D+j,iv:37HiX60OnO/zZFs1v4VWQLxcaoC1byZ3N2ueYFh4BVQ=,tag:5+z2YlnA8+MEcQTTrYhPJA==,type:str]
webhook_secret: ENC[AES256_GCM,data:iLDVsnRCifIXdZzuTTDRT/XHQoW4O/C2Wc33cxn3QoiZNgVXJef50In8+mKPy6SXV05iJqe/0YdOXF3IsHbelA==,iv:Ve/jQx6xPeS3O5h67cideBpGEhJSCVgtDfIypQSmIMc=,tag:Lcp+buNI23SSXVW9Vsn1RQ==,type:str]
sops:
age:
- recipient: age1647c5f46njylggj33h26t6twhq57n5qeaza04l845uy0fksv0ucsq82c3d
@ -11,7 +12,7 @@ sops:
U0x1ZmtzLytSL1Y3cVo1ZVJ2Ymtob1EKTpgjSymQx14wLSuUhh5Txq++3DrLS4Bs
x8vVtQgcXZgv1q0OMGup6DT4bkY6UiNbn9bAt4rHCaRPeb15x4ZKNw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-05T14:18:42Z"
mac: ENC[AES256_GCM,data:sZTFVbWYhtYML6tZrivXYGm8ug+isUf7wi0VWwQ0Wx1Swany1RSkyQI7N0eFs58tVsY2KoizOShvLkC8UcpRvtog1M/OyZi0LypYkfMUD7Rth11AF2n3Tg4Q3VNH5DGcLWkzYleA4mj1mJnPyCQzKOykXSPocODAcKUyFYcP8uQ=,iv:ZxuNWYQesR6mtezi5yBRacPF4GC5NDsevaSlRJ+A6X4=,tag:htOCSco/V2/twoRRWeoBbA==,type:str]
lastmodified: "2026-05-14T10:35:18Z"
mac: ENC[AES256_GCM,data:aTYOA9JxUYYhlqN9Cgocxl50qiSvZ5wCVCgNf15yUNmxXhuaQW8FV1TNXBrJ++k6AQ7+Yerh6cDEu3wjXg38oNdVF60pU4xlMFsQZy337yUXIi0IwStuy7hwshDj6prnROg2a0s77MpSJQLPHF2yQpFv1pHW3aTC3HAiCXcKrL0=,iv:PYOYeHCa4IHE9Kx87ECyMNRtfgmaZ4QUg4rsvjiSgSI=,tag:dGBhf8RW/PRGbA6XziaSAQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0