From 7ff64a5c16029ac0b0d8e21b27c78f684359ef63 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Thu, 12 Mar 2026 15:21:54 +0100 Subject: [PATCH 1/5] Add hydra spec files --- .hydra/declarative-jobsets.nix | 53 ++++++++++++++++++++++++++++++++++ .hydra/spec.json | 30 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 .hydra/declarative-jobsets.nix create mode 100644 .hydra/spec.json diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix new file mode 100644 index 00000000..e3570625 --- /dev/null +++ b/.hydra/declarative-jobsets.nix @@ -0,0 +1,53 @@ +{ nixpkgs, pulls, ... }: + +let + pkgs = import nixpkgs { }; + + prs = builtins.fromJSON (builtins.readFile pulls); + prJobsets = pkgs.lib.mapAttrs (num: info: { + enabled = 1; + hidden = false; + description = "PR ${num}: ${info.title}"; + checkinterval = 300; + schedulingshares = 20; + enableemail = false; + emailoverride = ""; + keepnr = 1; + type = 1; + flake = "${info.head.repo.html_url}/archive/${info.head.ref}.tar.gz"; + }) prs; + mkFlakeJobset = branch: { + description = "Build ${branch} branch of the Malobeo Infrastructure repo"; + checkinterval = 300; + enabled = "1"; + schedulingshares = 100; + enableemail = false; + emailoverride = ""; + keepnr = 3; + hidden = false; + type = 1; + flake = "git+https://git.dynamicdiscord.de/malobeo/infrastructure/archive/${branch}.tar.gz"; + }; + + desc = prJobsets // { + "master" = mkFlakeJobset "master"; + }; + + log = { + pulls = prs; + jobsets = desc; + }; + +in +{ + jobsets = pkgs.runCommand "spec-jobsets.json" { } '' + cat >$out <<'EOF' + ${builtins.toJSON desc} + EOF + # This is to get nice .jobsets build logs on Hydra + cat >tmp <<'EOF' + ${builtins.toJSON log} + EOF + ${pkgs.jq}/bin/jq . tmp + ''; +} diff --git a/.hydra/spec.json b/.hydra/spec.json new file mode 100644 index 00000000..eaa882c6 --- /dev/null +++ b/.hydra/spec.json @@ -0,0 +1,30 @@ +{ + "enabled": 1, + "hidden": false, + "description": "Malobeo infrastructure repo", + "nixexprinput": "nixexpr", + "nixexprpath": ".hydra/declarative-jobsets.nix", + "checkinterval": 60, + "schedulingshares": 100, + "enableemail": false, + "emailoverride": "", + "keepnr": 5, + "type": 0, + "inputs": { + "nixexpr": { + "value": "https://git.dynamicdiscord.de/ahtlon/infrastructure master", + "type": "git", + "emailresponsible": false + }, + "nixpkgs": { + "value": "https://github.com/NixOS/nixpkgs nixos-25.11", + "type": "git", + "emailresponsible": false + }, + "pulls": { + "type": "path", + "value": "http://127.0.0.1:27364/gitea-pulls-sorted.json", + "emailresponsible": false + } + } +} From ed19426eb7df50387106796e73a3428185742f84 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Fri, 13 Mar 2026 15:21:14 +0100 Subject: [PATCH 2/5] Add status callback --- .hydra/declarative-jobsets.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index e3570625..33d5b581 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -15,6 +15,28 @@ let keepnr = 1; type = 1; flake = "${info.head.repo.html_url}/archive/${info.head.ref}.tar.gz"; + inputs = { + gitea_repo_name = { + type = "string"; + value = "${info.head.repo.name}"; + emailresponsible = false; + }; + gitea_repo_owner = { + type = "string"; + value = "${info.head.repo.owner.username}"; + emailresponsible = false; + }; + gitea_http_url = { + type = "string"; + value = "https://git.dynamicdiscord.de"; + emailresponsible = false; + }; + gitea_status_repo = { + type = "string"; + value = "${info.head.ref}"; + emailresponsible = false; + }; + }; }) prs; mkFlakeJobset = branch: { description = "Build ${branch} branch of the Malobeo Infrastructure repo"; From 8cd2eafaa520ab10074ed61e8b2f90f71cacaca8 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Fri, 13 Mar 2026 15:50:32 +0100 Subject: [PATCH 3/5] Fix master build --- .hydra/declarative-jobsets.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 33d5b581..ef421959 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -48,7 +48,7 @@ let keepnr = 3; hidden = false; type = 1; - flake = "git+https://git.dynamicdiscord.de/malobeo/infrastructure/archive/${branch}.tar.gz"; + flake = "https://git.dynamicdiscord.de/malobeo/infrastructure/archive/${branch}.tar.gz"; }; desc = prJobsets // { From c80628a1a9a6b886dd439982c700a9050b5f3e11 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Fri, 13 Mar 2026 16:09:12 +0100 Subject: [PATCH 4/5] Add gitea-translator server and module --- machines/modules/malobeo/gitea_translator.nix | 78 +++++++++++++ scripts/gitea_hydra_server.py | 107 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 machines/modules/malobeo/gitea_translator.nix create mode 100644 scripts/gitea_hydra_server.py diff --git a/machines/modules/malobeo/gitea_translator.nix b/machines/modules/malobeo/gitea_translator.nix new file mode 100644 index 00000000..d87b7739 --- /dev/null +++ b/machines/modules/malobeo/gitea_translator.nix @@ -0,0 +1,78 @@ +{ config, self, lib, inputs, pkgs, ... }: + +with lib; + +let + cfg = config.services.malobeo.gitea-translator; +in +{ + options = { + services.malobeo.gitea-translator = { + enable = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc "Start a webserver for hydra to use the gitea pull request api."; + }; + + baseurl = mkOption { + type = types.str; + default = "git.dynamicdiscord.de"; + description = lib.mdDoc "Base URL of the Gitea instance."; + }; + + owner = mkOption { + type = types.str; + default = "malobeo"; + description = lib.mdDoc "Repository owner on the Gitea instance."; + }; + + repo = mkOption { + type = types.str; + default = "infrastructure"; + description = lib.mdDoc "Repository name on the Gitea instance."; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = lib.mdDoc "Address the server binds to."; + }; + + port = mkOption { + type = types.port; + default = 27364; + description = lib.mdDoc "Port the server listens on."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.gitea-translator = { + description = "Gitea Pull Request Translator for Hydra"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = '' + ${pkgs.python3}/bin/python3 ${inputs.self + /scripts/gitea_hydra_server.py} \ + --baseurl ${cfg.baseurl} \ + --owner ${cfg.owner} \ + --repo ${cfg.repo} \ + --host ${cfg.host} \ + --port ${toString cfg.port} + ''; + Restart = "on-failure"; + RestartSec = 5; + + # Hardening because why not + DynamicUser = true; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + }; + }; + }; +} \ No newline at end of file diff --git a/scripts/gitea_hydra_server.py b/scripts/gitea_hydra_server.py new file mode 100644 index 00000000..eafa8d08 --- /dev/null +++ b/scripts/gitea_hydra_server.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +#imports +import os +import json +import argparse +from http.server import BaseHTTPRequestHandler, HTTPServer +import urllib.request + +def _get_api_response(baseurl, owner, repo): + ###https://docs.gitea.com/api/1.21/#tag/repository/operation/repoListPullRequests + ###GET /api/v1/repos/{owner}/{repo}/pulls + url=(f"https://{baseurl}/api/v1/repos/{owner}/{repo}/pulls?state=open") + headers={"Accept": "application/json"} + req=urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode("utf-8")) + +def _parse_response(baseurl, owner, repo): + target_repo_url=f"https://{baseurl}/{owner}/{repo}.git" + pulls: dict={} + response=_get_api_response(baseurl, owner, repo) + for pr in response: + pr["target_repo_url"]=target_repo_url + pulls[str(pr["number"])]=pr + return pulls + +class PullsHandler(BaseHTTPRequestHandler): + _VALID_PATHS={"/", "/gitea-pulls-sorted.json"} + + # Class variables to store configuration + baseurl = None + owner = None + repo = None + + def do_GET(self): + if self.path not in self._VALID_PATHS: + self.send_error(404, "Not Found") + return + + answer=dict(_parse_response(self.baseurl, self.owner, self.repo)) + + body=json.dumps(answer, indent=2, sort_keys=True).encode("utf-8") + + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, fmt, *args): + print( + f"[gitea-translator] {self.address_string()} - {fmt % args}", + flush=True, + ) + +def main(): + parser = argparse.ArgumentParser( + description="Hydra Server to Gitea-pull-request translator" + ) + parser.add_argument( + "--baseurl", + default="git.dynamicdiscord.de", + help="Base URL of Gitea instance (default: git.dynamicdiscord.de)" + ) + parser.add_argument( + "--owner", + default="malobeo", + help="Repository owner (default: malobeo)" + ) + parser.add_argument( + "--repo", + default="infrastructure", + help="Repository name (default: infrastructure)" + ) + parser.add_argument( + "--host", + default="127.0.0.1", + help="Host to bind to (default: 127.0.0.1)" + ) + parser.add_argument( + "--port", + type=int, + default=27364, + help="Port to bind to (default: 27364)" + ) + + args = parser.parse_args() + + # Set class variables so they're accessible in request handlers + PullsHandler.baseurl = args.baseurl + PullsHandler.owner = args.owner + PullsHandler.repo = args.repo + + print( + f"[gitea-translator] Starting, pulling from {args.baseurl}/{args.owner}/{args.repo}", + flush=True, + ) + server=HTTPServer((args.host, args.port), PullsHandler) + print( + f"[gitea-translator] online @ {args.host}:{args.port}", + flush=True, + ) + server.serve_forever() + +if __name__ == "__main__": + main() From b6cd2b57f835fb1fda0901d066bb97dd639afe8c Mon Sep 17 00:00:00 2001 From: ahtlon Date: Fri, 13 Mar 2026 16:29:49 +0100 Subject: [PATCH 5/5] Document the gitea-translator module --- doc/src/SUMMARY.md | 1 + doc/src/module/gitea-translator.md | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 doc/src/module/gitea-translator.md diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 1a32f92e..a9121caf 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -12,6 +12,7 @@ - [musik](./projekte/musik.md) - [TODO](./todo.md) - [Modules]() + - [Gitea-translator](./module/gitea-translator.md) - [Initrd-ssh](./module/initssh.md) - [Disks](./module/disks.md) - [How-to]() diff --git a/doc/src/module/gitea-translator.md b/doc/src/module/gitea-translator.md new file mode 100644 index 00000000..389950d8 --- /dev/null +++ b/doc/src/module/gitea-translator.md @@ -0,0 +1,21 @@ +# Gitea-tanslator +The module can be used by importing `inputs.self.nixosModules.malobeo.gitea-translator` + +This module starts a python server that fetches the gitea pull request api and translates it to a file that hydra understands. +To use, just set the parameters of the gitea server, then send a GET request to either `http://${host}:${port}/` or `http://${host}:${port}/gitea-pulls-sorted.json` + +## Module config +##### enable (default = false) - enables the module +##### baseurl (default = "git.dynamicdiscord.de") - Base URL of the Gitea instance +##### owner (default = "malobeo") - Repository owner +##### repo (default = "infrastructure") - Repository name +##### host (default = "127.0.0.1") - Address the server binds to +##### port (default = 27364) - Port the server listens on + +## Hydra config +If you change the default port or host, the file `.hydra/spec.json` has to be modified accordingly. +With the module running on the hydra host, create a new hydra project, then: + + - Set `Declarative spec file` to `.hydra/spec.json` + - Change declaritive input type to `Git checkout` + - Set your git repo location in the field below that