From 9dabb9dc07b75378b751f24856af1ee3350c3dac Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 11:46:18 -0400 Subject: [PATCH 01/15] updated actions --- .github/workflows/fix_eval_warnings.yml | 30 -- .github/workflows/merge_flake_lock_update.yml | 20 +- .github/workflows/pytest.yml | 1 - .github/workflows/update-flake-lock.yml | 24 +- python/gitea.py | 335 ++++++++++++++++++ python/gitea_flake_lock.py | 138 ++++++++ 6 files changed, 493 insertions(+), 55 deletions(-) delete mode 100644 .github/workflows/fix_eval_warnings.yml create mode 100644 python/gitea.py create mode 100644 python/gitea_flake_lock.py diff --git a/.github/workflows/fix_eval_warnings.yml b/.github/workflows/fix_eval_warnings.yml deleted file mode 100644 index 79eeb4a..0000000 --- a/.github/workflows/fix_eval_warnings.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: fix_eval_warnings -on: - workflow_run: - workflows: ["build_systems"] - types: [completed] - -jobs: - check-warnings: - if: >- - github.event.workflow_run.conclusion != 'cancelled' && - github.event.workflow_run.head_branch == 'main' && - (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'schedule') - runs-on: self-hosted - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@v4 - - - name: Fix eval warnings - env: - GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }} - run: >- - nix develop .#devShells.x86_64-linux.default -c - python -m python.eval_warnings.main - --run-id "${{ github.event.workflow_run.id }}" - --repo "${{ github.repository }}" - --ollama-url "${{ secrets.OLLAMA_URL }}" - --run-url "${{ github.event.workflow_run.html_url }}" diff --git a/.github/workflows/merge_flake_lock_update.yml b/.github/workflows/merge_flake_lock_update.yml index 83c5d57..e4d34b6 100644 --- a/.github/workflows/merge_flake_lock_update.yml +++ b/.github/workflows/merge_flake_lock_update.yml @@ -6,24 +6,18 @@ on: jobs: merge: - runs-on: ubuntu-latest + runs-on: self-hosted permissions: contents: write pull-requests: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: merge_flake_lock_update - run: | - pr_number=$(gh pr list --state open --author RichieCahill --label flake_lock_update --json number --jq '.[0].number') - echo "pr_number=$pr_number" >> $GITHUB_ENV - if [ -n "$pr_number" ]; then - gh pr merge "$pr_number" --rebase - else - echo "No open PR found with label flake_lock_update" - fi + run: >- + nix develop .#devShells.x86_64-linux.default -c + python -m python.gitea_flake_lock merge + --repo "${{ github.repository }}" env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_URL: https://gitea.tmmworkshop.com diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 063a3f5..3e2fe6d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -7,7 +7,6 @@ on: pull_request: branches: - main - merge_group: jobs: pytest: diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml index 6b7ce74..d13563d 100644 --- a/.github/workflows/update-flake-lock.yml +++ b/.github/workflows/update-flake-lock.yml @@ -6,18 +6,20 @@ on: jobs: lockfile: - runs-on: ubuntu-latest + runs-on: self-hosted + permissions: + contents: write + pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - name: Update flake.lock - uses: DeterminateSystems/update-flake-lock@main - with: - token: ${{ secrets.GH_TOKEN_FOR_UPDATES }} - pr-title: "Update flake.lock" - pr-labels: | - dependencies - automated - flake_lock_update + run: nix flake update + - name: Create or update flake.lock PR + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_URL: https://gitea.tmmworkshop.com + run: >- + nix develop .#devShells.x86_64-linux.default -c + python -m python.gitea_flake_lock update + --repo "${{ github.repository }}" diff --git a/python/gitea.py b/python/gitea.py new file mode 100644 index 0000000..8b57fdf --- /dev/null +++ b/python/gitea.py @@ -0,0 +1,335 @@ +"""Small Gitea API client for repository automation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Self + +import httpx + +DEFAULT_PAGE_SIZE = 100 +EXPECTED_CREATED = 201 +EXPECTED_OK = 200 + + +@dataclass(frozen=True) +class CreatedIssue: + """Issue data returned by Gitea.""" + + number: int | None + html_url: str | None + title: str + + +@dataclass(frozen=True) +class PullRequest: + """Pull request data returned by Gitea.""" + + number: int + title: str + html_url: str | None + labels: tuple[str, ...] + head_branch: str | None + base_branch: str | None + + +@dataclass(frozen=True) +class WorkflowJob: + """Workflow job data returned by Gitea Actions.""" + + id: int + name: str + run_id: int | None + status: str | None + conclusion: str | None + + +class GiteaError(RuntimeError): + """Raised when Gitea rejects an API request.""" + + +def split_repo_name(repo: str) -> tuple[str, str]: + """Split an owner/repo string into its parts.""" + owner, separator, repo_name = repo.partition("/") + if not separator or not owner or not repo_name: + msg = f"Invalid repository name: {repo}" + raise ValueError(msg) + return owner, repo_name + + +class GiteaClient: + """HTTP client for the subset of Gitea APIs used in this repository.""" + + def __init__( + self, + *, + base_url: str, + token: str, + timeout: int = 30, + transport: httpx.BaseTransport | None = None, + ) -> None: + """Initialize the Gitea client.""" + self._client = httpx.Client( + base_url=base_url.rstrip("/"), + timeout=timeout, + headers={"Authorization": f"token {token}"}, + transport=transport, + ) + + def create_issue( + self, + *, + owner: str, + repo: str, + title: str, + body: str, + labels: list[int] | None = None, + ) -> CreatedIssue: + """Create a Gitea issue.""" + payload: dict[str, object] = {"title": title, "body": body, "labels": labels or []} + response = self._request( + "POST", + f"/api/v1/repos/{owner}/{repo}/issues", + expected_statuses={EXPECTED_CREATED}, + json=payload, + ) + data = response.json() + return CreatedIssue( + number=_optional_int(data.get("number")), + html_url=_optional_str(data.get("html_url")), + title=str(data.get("title", title)), + ) + + def resolve_label_ids(self, *, owner: str, repo: str, labels: list[str]) -> list[int]: + """Resolve label names to Gitea label IDs.""" + if not labels: + return [] + + available_labels: dict[str, int] = {} + page = 1 + while True: + response = self._request( + "GET", + f"/api/v1/repos/{owner}/{repo}/labels", + params={"page": page, "limit": DEFAULT_PAGE_SIZE}, + ) + batch = response.json() + if not batch: + break + for label in batch: + label_name = str(label.get("name", "")) + label_id = _optional_int(label.get("id")) + if label_name and label_id is not None: + available_labels[label_name] = label_id + if len(batch) < DEFAULT_PAGE_SIZE: + break + page += 1 + + missing = [label for label in labels if label not in available_labels] + if missing: + missing_names = ", ".join(sorted(missing)) + msg = f"Missing Gitea labels: {missing_names}" + raise GiteaError(msg) + + return [available_labels[label] for label in labels] + + def list_open_pull_requests( + self, + *, + owner: str, + repo: str, + labels: list[str] | None = None, + head: str | None = None, + ) -> list[PullRequest]: + """List open pull requests for a repository.""" + expected_labels = set(labels or []) + pull_requests: list[PullRequest] = [] + page = 1 + while True: + response = self._request( + "GET", + f"/api/v1/repos/{owner}/{repo}/pulls", + params={"state": "open", "page": page, "limit": DEFAULT_PAGE_SIZE}, + ) + batch = response.json() + if not batch: + break + + for item in batch: + pull_request = _pull_request_from_api(item) + if head and pull_request.head_branch != head: + continue + if expected_labels and not expected_labels.issubset(set(pull_request.labels)): + continue + pull_requests.append(pull_request) + + if len(batch) < DEFAULT_PAGE_SIZE: + break + page += 1 + + return pull_requests + + def create_pull_request( + self, + *, + owner: str, + repo: str, + title: str, + body: str, + head: str, + base: str, + labels: list[str] | None = None, + ) -> PullRequest: + """Create a pull request.""" + payload: dict[str, object] = { + "title": title, + "body": body, + "head": head, + "base": base, + } + if labels: + payload["labels"] = self.resolve_label_ids(owner=owner, repo=repo, labels=labels) + + response = self._request( + "POST", + f"/api/v1/repos/{owner}/{repo}/pulls", + expected_statuses={EXPECTED_CREATED}, + json=payload, + ) + return _pull_request_from_api(response.json()) + + def merge_pull_request( + self, + *, + owner: str, + repo: str, + number: int, + merge_method: str = "rebase", + head_commit_id: str | None = None, + delete_branch_after_merge: bool = False, + ) -> None: + """Merge a pull request.""" + payload: dict[str, object] = { + "Do": merge_method, + "delete_branch_after_merge": delete_branch_after_merge, + } + if head_commit_id: + payload["head_commit_id"] = head_commit_id + + self._request( + "POST", + f"/api/v1/repos/{owner}/{repo}/pulls/{number}/merge", + json=payload, + ) + + def list_run_jobs(self, *, owner: str, repo: str, run_id: str | int) -> list[WorkflowJob]: + """List workflow jobs for a specific run.""" + jobs: list[WorkflowJob] = [] + page = 1 + while True: + response = self._request( + "GET", + f"/api/v1/repos/{owner}/{repo}/actions/jobs", + params={"page": page, "limit": DEFAULT_PAGE_SIZE}, + ) + payload = response.json() + batch = payload.get("jobs", []) + if not batch: + break + + for item in batch: + if str(item.get("run_id")) != str(run_id): + continue + jobs.append(_workflow_job_from_api(item)) + + if len(batch) < DEFAULT_PAGE_SIZE: + break + page += 1 + + return jobs + + def download_job_logs(self, *, owner: str, repo: str, job_id: int) -> str: + """Download logs for a workflow job.""" + response = self._request( + "GET", + f"/api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs", + ) + return response.text + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._client.close() + + def __enter__(self) -> Self: + """Enter the context manager.""" + return self + + def __exit__(self, *args: object) -> None: + """Close the HTTP client.""" + self.close() + + def _request( + self, + method: str, + path: str, + *, + expected_statuses: set[int] | None = None, + **kwargs: object, + ) -> httpx.Response: + """Send an HTTP request and validate the response status.""" + response = self._client.request(method, path, **kwargs) + statuses = expected_statuses or {EXPECTED_OK} + if response.status_code not in statuses: + msg = f"Gitea request failed ({response.status_code}): {response.text}" + raise GiteaError(msg) + return response + + +def _pull_request_from_api(data: dict[str, object]) -> PullRequest: + """Convert Gitea API pull-request data into a dataclass.""" + number = _optional_int(data.get("number")) or _optional_int(data.get("index")) + if number is None: + msg = "Gitea pull request payload is missing a number" + raise GiteaError(msg) + + labels = tuple(str(label.get("name", "")) for label in data.get("labels", [])) + head = data.get("head", {}) + base = data.get("base", {}) + return PullRequest( + number=number, + title=str(data.get("title", "")), + html_url=_optional_str(data.get("html_url")), + labels=tuple(label for label in labels if label), + head_branch=_optional_str(head.get("ref")) or _optional_str(data.get("head_branch")), + base_branch=_optional_str(base.get("ref")) or _optional_str(data.get("base_branch")), + ) + + +def _workflow_job_from_api(data: dict[str, object]) -> WorkflowJob: + """Convert Gitea API workflow-job data into a dataclass.""" + job_id = _optional_int(data.get("id")) + if job_id is None: + msg = "Gitea workflow job payload is missing an ID" + raise GiteaError(msg) + + return WorkflowJob( + id=job_id, + name=str(data.get("name", "")), + run_id=_optional_int(data.get("run_id")), + status=_optional_str(data.get("status")), + conclusion=_optional_str(data.get("conclusion")), + ) + + +def _optional_int(value: object) -> int | None: + """Convert an API value to an integer when present.""" + if value is None: + return None + return int(value) + + +def _optional_str(value: object) -> str | None: + """Convert an API value to a string when present.""" + if value is None: + return None + return str(value) diff --git a/python/gitea_flake_lock.py b/python/gitea_flake_lock.py new file mode 100644 index 0000000..581438a --- /dev/null +++ b/python/gitea_flake_lock.py @@ -0,0 +1,138 @@ +"""Automation helpers for flake.lock pull requests on Gitea.""" + +from __future__ import annotations + +import subprocess +from os import getenv +from typing import Annotated + +import typer + +from python.gitea import GiteaClient, PullRequest, split_repo_name + +DEFAULT_BASE_BRANCH = "main" +DEFAULT_BRANCH = "automation/update-flake-lock" +DEFAULT_GITEA_URL = "https://gitea.tmmworkshop.com" +PR_LABELS = ["dependencies", "automated", "flake_lock_update"] +PR_TITLE = "Update flake.lock" +PR_BODY = "Automated flake.lock update." + +app = typer.Typer(add_completion=False) + + +def run_cmd(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]: + """Run a subprocess command.""" + return subprocess.run(cmd, capture_output=True, text=True, check=check) + + +def ensure_flake_lock_pull_request( + client: GiteaClient, + *, + owner: str, + repo: str, + branch: str, + base: str, +) -> PullRequest: + """Return an existing flake.lock PR for the branch or create one.""" + pull_requests = client.list_open_pull_requests(owner=owner, repo=repo, head=branch) + if pull_requests: + return pull_requests[0] + + return client.create_pull_request( + owner=owner, + repo=repo, + title=PR_TITLE, + body=PR_BODY, + head=branch, + base=base, + labels=PR_LABELS, + ) + + +def find_flake_lock_pull_request(client: GiteaClient, *, owner: str, repo: str) -> PullRequest | None: + """Find the first open flake.lock pull request.""" + pull_requests = client.list_open_pull_requests(owner=owner, repo=repo, labels=["flake_lock_update"]) + if not pull_requests: + return None + return pull_requests[0] + + +def has_worktree_changes() -> bool: + """Return whether `flake.lock` has worktree changes.""" + result = run_cmd(["git", "diff", "--quiet", "--", "flake.lock"], check=False) + return result.returncode != 0 + + +def commit_flake_lock_update(*, branch: str) -> None: + """Commit the updated lock file to the automation branch.""" + run_cmd(["git", "config", "user.name", "gitea-actions[bot]"]) + run_cmd(["git", "config", "user.email", "gitea-actions@tmmworkshop.com"]) + run_cmd(["git", "checkout", "-B", branch]) + run_cmd(["git", "add", "flake.lock"]) + run_cmd(["git", "commit", "-m", "chore: update flake.lock"]) + + +def push_branch(*, branch: str) -> None: + """Push the automation branch to origin.""" + run_cmd(["git", "push", "origin", f"HEAD:{branch}", "--force"]) + + +def _required_gitea_token() -> str: + """Read the required Gitea token from the environment.""" + token = getenv("GITEA_TOKEN") + if token: + return token + + msg = "GITEA_TOKEN environment variable is required" + raise RuntimeError(msg) + + +@app.command() +def update( + repo: Annotated[str, typer.Option("--repo", help="Gitea repository in owner/repo form")], + base: Annotated[str, typer.Option("--base", help="Base branch")] = DEFAULT_BASE_BRANCH, + branch: Annotated[str, typer.Option("--branch", help="Automation branch")] = DEFAULT_BRANCH, +) -> None: + """Commit flake.lock changes and ensure a pull request exists.""" + if not has_worktree_changes(): + typer.echo("No flake.lock changes detected") + return + + commit_flake_lock_update(branch=branch) + push_branch(branch=branch) + + owner, repo_name = split_repo_name(repo) + with GiteaClient( + base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL), + token=_required_gitea_token(), + ) as client: + pull_request = ensure_flake_lock_pull_request( + client, + owner=owner, + repo=repo_name, + branch=branch, + base=base, + ) + typer.echo(pull_request.html_url or f"Pull request #{pull_request.number}") + + +@app.command() +def merge( + repo: Annotated[str, typer.Option("--repo", help="Gitea repository in owner/repo form")], +) -> None: + """Merge the first open flake.lock pull request.""" + owner, repo_name = split_repo_name(repo) + with GiteaClient( + base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL), + token=_required_gitea_token(), + ) as client: + pull_request = find_flake_lock_pull_request(client, owner=owner, repo=repo_name) + if not pull_request: + typer.echo("No open PR found with label flake_lock_update") + return + client.merge_pull_request(owner=owner, repo=repo_name, number=pull_request.number, merge_method="rebase") + typer.echo(f"Merged PR #{pull_request.number}") + + +if __name__ == "__main__": + app() -- 2.54.0 From 1b53167b53fbbd5acbf034418cb927809bea0934 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 11:47:46 -0400 Subject: [PATCH 02/15] updated nix builders --- systems/jeeves/runners/default.nix | 15 +-------- systems/jeeves/runners/nix_builder.nix | 46 ++++++++++---------------- systems/jeeves/services/gitea.nix | 4 +++ 3 files changed, 22 insertions(+), 43 deletions(-) diff --git a/systems/jeeves/runners/default.nix b/systems/jeeves/runners/default.nix index 849006e..e24101d 100644 --- a/systems/jeeves/runners/default.nix +++ b/systems/jeeves/runners/default.nix @@ -1,20 +1,7 @@ -{ pkgs, ... }: +{ ... }: { imports = [ ./nix_builder.nix ]; - users = { - users.github-runners = { - shell = pkgs.bash; - isSystemUser = true; - group = "github-runners"; - uid = 601; - openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA/S8i+BNX/12JNKg+5EKGX7Aqimt5KM+ve3wt/SyWuO github-runners" # cspell:disable-line - ]; - }; - groups.github-runners.gid = 601; - }; - services.nix_builder.containers = { nix-builder-00.enable = true; nix-builder-01.enable = true; diff --git a/systems/jeeves/runners/nix_builder.nix b/systems/jeeves/runners/nix_builder.nix index bd686c6..e38ee04 100644 --- a/systems/jeeves/runners/nix_builder.nix +++ b/systems/jeeves/runners/nix_builder.nix @@ -23,12 +23,12 @@ in types.submodule ( { name, ... }: { - options.enable = mkEnableOption "GitHub runner container"; + options.enable = mkEnableOption "Gitea runner container"; } ) ); default = { }; - description = "GitHub runner container configurations"; + description = "Gitea runner container configurations"; }; }; @@ -43,7 +43,7 @@ in bindMounts = { storage = { hostPath = "/zfs/media/github-runners/${name}"; - mountPoint = "/zfs/media/github-runners/${name}"; + mountPoint = "/var/lib/gitea-runner/${name}"; isReadOnly = false; }; host-nix = { @@ -51,9 +51,9 @@ in hostPath = "/nix/var/nix/daemon-socket"; isReadOnly = false; }; - pat = { - hostPath = "${vars.secrets}/services/github-runners/runner_pat"; - mountPoint = "${vars.secrets}/services/github-runners/runner_pat"; + token = { + hostPath = "${vars.secrets}/services/gitea-runners/registration-token"; + mountPoint = "${vars.secrets}/services/gitea-runners/registration-token"; isReadOnly = true; }; }; @@ -92,43 +92,31 @@ in "nix-command" ]; sandbox = true; - allowed-users = [ "github-runners" ]; + allowed-users = [ "gitea-runner" ]; trusted-users = [ "root" - "github-runners" + "gitea-runner" ]; }; nixpkgs = { overlays = builtins.attrValues outputs.overlays; config.allowUnfree = true; }; - services.github-runners.${name} = { + services.gitea-actions-runner.instances.${name} = { enable = true; - replace = true; - workDir = "/zfs/media/github-runners/${name}"; - url = "https://github.com/RichieCahill/dotfiles"; - extraLabels = [ "nixos" ]; - tokenFile = "${vars.secrets}/services/github-runners/runner_pat"; - user = "github-runners"; - group = "github-runners"; - extraPackages = with pkgs; [ - gitMinimal - gh - nixfmt + name = "jeeves-${name}"; + url = "https://gitea.tmmworkshop.com"; + labels = [ + "self-hosted:host" + "nixos:host" + ]; + tokenFile = "${vars.secrets}/services/gitea-runners/registration-token"; + hostPackages = with pkgs; [ nixos-rebuild treefmt my_python ]; }; - users = { - users.github-runners = { - shell = pkgs.bash; - isSystemUser = true; - group = "github-runners"; - uid = 601; - }; - groups.github-runners.gid = 601; - }; system.stateVersion = "24.05"; }; } diff --git a/systems/jeeves/services/gitea.nix b/systems/jeeves/services/gitea.nix index 5f511e8..2ea98c4 100644 --- a/systems/jeeves/services/gitea.nix +++ b/systems/jeeves/services/gitea.nix @@ -21,6 +21,10 @@ in createDatabase = false; }; settings = { + actions = { + ENABLED = true; + DEFAULT_ACTIONS_URL = "github"; + }; service.DISABLE_REGISTRATION = true; server = { DOMAIN = "tmmworkshop.com"; -- 2.54.0 From d920b77babf683136c958381ff079f8dc2d50ab1 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 12:27:03 -0400 Subject: [PATCH 03/15] removed verilux --- systems/jeeves/web_services/acme.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/systems/jeeves/web_services/acme.nix b/systems/jeeves/web_services/acme.nix index a5caace..90cf112 100644 --- a/systems/jeeves/web_services/acme.nix +++ b/systems/jeeves/web_services/acme.nix @@ -5,7 +5,6 @@ let "gitea" "jellyfin" "share" - "verilux" ]; extraDomains = [ "www.norn-sight.com" ]; -- 2.54.0 From 2865dcbe9c52b899382ad23652faa18586681d69 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 12:35:47 -0400 Subject: [PATCH 04/15] set dbus.implementation = "dbus"; --- common/global/default.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/global/default.nix b/common/global/default.nix index 1b22efc..935bfcd 100644 --- a/common/global/default.nix +++ b/common/global/default.nix @@ -41,6 +41,8 @@ }; services = { + dbus.implementation = "dbus"; + # firmware update fwupd.enable = true; -- 2.54.0 From d7651bf588dfff5510371ab8907ed632e128cdbd Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 12:36:04 -0400 Subject: [PATCH 05/15] set update.nix to gitea --- common/optional/update.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/optional/update.nix b/common/optional/update.nix index dc5e43c..1fdb7f7 100644 --- a/common/optional/update.nix +++ b/common/optional/update.nix @@ -4,7 +4,7 @@ flags = [ "--accept-flake-config" ]; randomizedDelaySec = "1h"; persistent = true; - flake = "github:RichieCahill/dotfiles"; + flake = "git+https://gitea.tmmworkshop.com/richie/dotfiles?ref=main"; allowReboot = true; dates = "Sat *-*-* 06:00:00"; }; -- 2.54.0 From 29a99fc2101044444a8aedede5371cd27f28f061 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 12:46:15 -0400 Subject: [PATCH 06/15] flake lock update --- flake.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index c71ae1f..793eb3a 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ }, "locked": { "dir": "pkgs/firefox-addons", - "lastModified": 1776398575, - "narHash": "sha256-WArU6WOdWxzbzGqYk4w1Mucg+bw/SCl6MoSp+/cZMio=", + "lastModified": 1777521781, + "narHash": "sha256-bQ9oIcNyHsiagt7yptfe7OmfUDEyuXFUb7ajkrWNzSo=", "owner": "rycee", "repo": "nur-expressions", - "rev": "05815686caf4e3678f5aeb5fd36e567886ab0d30", + "rev": "8a444a5c02840666c9c2f92042bfbb7a10c68200", "type": "gitlab" }, "original": { @@ -29,11 +29,11 @@ ] }, "locked": { - "lastModified": 1776454077, - "narHash": "sha256-7zSUFWsU0+jlD7WB3YAxQ84Z/iJurA5hKPm8EfEyGJk=", + "lastModified": 1777518431, + "narHash": "sha256-SwgiG2T5pbyo33Vz7/vUCAhEMgwCK8Pa2nDSx5a6/WE=", "owner": "nix-community", "repo": "home-manager", - "rev": "565e5349208fe7d0831ef959103c9bafbeac0681", + "rev": "2e54a938cdd4c8e414b2518edc3d82308027c670", "type": "github" }, "original": { @@ -44,11 +44,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1775490113, - "narHash": "sha256-2ZBhDNZZwYkRmefK5XLOusCJHnoeKkoN95hoSGgMxWM=", + "lastModified": 1776983936, + "narHash": "sha256-ZOQyNqSvJ8UdrrqU1p7vaFcdL53idK+LOM8oRWEWh6o=", "owner": "nixos", "repo": "nixos-hardware", - "rev": "c775c2772ba56e906cbeb4e0b2db19079ef11ff7", + "rev": "2096f3f411ce46e88a79ae4eafcfc9df8ed41c61", "type": "github" }, "original": { @@ -60,11 +60,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776169885, - "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", "type": "github" }, "original": { @@ -76,11 +76,11 @@ }, "nixpkgs-master": { "locked": { - "lastModified": 1776469842, - "narHash": "sha256-sqzM6PKMQoGk8Sl+uv2sbP1qiS2SPQhA2yn5zgZINMc=", + "lastModified": 1777553282, + "narHash": "sha256-GCJkEogieqOYJ1BBhG0w9fqezul1cGdEcmBbJ+34F4U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "025c852a89be820b3117f604c8ace42e9b4caa08", + "rev": "0d93cb69a4fd4449088c69859e1836fda6eb9f6a", "type": "github" }, "original": { @@ -125,11 +125,11 @@ ] }, "locked": { - "lastModified": 1776119890, - "narHash": "sha256-Zm6bxLNnEOYuS/SzrAGsYuXSwk3cbkRQZY0fJnk8a5M=", + "lastModified": 1777338324, + "narHash": "sha256-bc+ZZCmOTNq86/svGnw0tVpH7vJaLYvGLLKFYP08Q8E=", "owner": "Mic92", "repo": "sops-nix", - "rev": "d4971dd58c6627bfee52a1ad4237637c0a2fb0cd", + "rev": "8eaee5c45428b28b8c47a83e4c09dccec5f279b5", "type": "github" }, "original": { -- 2.54.0 From fe9a2912e13c63e899afcffa265175894e149c7c Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Thu, 30 Apr 2026 12:46:38 -0400 Subject: [PATCH 07/15] added words to spell check --- users/richie/home/gui/vscode/settings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/users/richie/home/gui/vscode/settings.json b/users/richie/home/gui/vscode/settings.json index 43df851..a63f8af 100644 --- a/users/richie/home/gui/vscode/settings.json +++ b/users/richie/home/gui/vscode/settings.json @@ -78,6 +78,8 @@ "Corvidae", "drivername", "fastapi", + "Michal", + "Nornsight", "sandboxing", "syncthing", ], -- 2.54.0 From 3a86148352806eb4bf887343c521854f5897c0d5 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sat, 2 May 2026 17:10:02 -0400 Subject: [PATCH 08/15] working nix builder --- systems/jeeves/runners/nix_builder.nix | 40 ++++++++++++++++++++------ 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/systems/jeeves/runners/nix_builder.nix b/systems/jeeves/runners/nix_builder.nix index e38ee04..bcff4cd 100644 --- a/systems/jeeves/runners/nix_builder.nix +++ b/systems/jeeves/runners/nix_builder.nix @@ -2,6 +2,7 @@ config, lib, outputs, + utils, ... }: @@ -9,6 +10,8 @@ with lib; let vars = import ../vars.nix; cfg = config.services.nix_builder; + runnerUsername = "gitea-runner"; + runnerUserid = 601; in { options.services.nix_builder = { @@ -33,6 +36,15 @@ in }; config = { + users = { + users.${runnerUsername} = { + isSystemUser = true; + group = runnerUsername; + uid = runnerUserid; + }; + groups.${runnerUsername}.gid = runnerUserid; + }; + containers = mapAttrs ( name: containerCfg: mkIf containerCfg.enable { @@ -41,19 +53,14 @@ in hostBridge = cfg.bridgeName; ephemeral = true; bindMounts = { - storage = { - hostPath = "/zfs/media/github-runners/${name}"; - mountPoint = "/var/lib/gitea-runner/${name}"; - isReadOnly = false; - }; host-nix = { mountPoint = "/host-nix/var/nix/daemon-socket"; hostPath = "/nix/var/nix/daemon-socket"; isReadOnly = false; }; token = { - hostPath = "${vars.secrets}/services/gitea-runners/registration-token"; - mountPoint = "${vars.secrets}/services/gitea-runners/registration-token"; + hostPath = "${vars.secrets}/services/gitea-runners"; + mountPoint = "/run/secrets/gitea-runners"; isReadOnly = true; }; }; @@ -102,21 +109,36 @@ in overlays = builtins.attrValues outputs.overlays; config.allowUnfree = true; }; + users = { + users.${runnerUsername} = { + isSystemUser = true; + group = runnerUsername; + uid = runnerUserid; + }; + groups.${runnerUsername}.gid = runnerUserid; + }; services.gitea-actions-runner.instances.${name} = { enable = true; name = "jeeves-${name}"; - url = "https://gitea.tmmworkshop.com"; + url = "http://192.168.99.14:6443/"; labels = [ "self-hosted:host" "nixos:host" ]; - tokenFile = "${vars.secrets}/services/gitea-runners/registration-token"; + tokenFile = "/run/secrets/gitea-runners/registration-token"; hostPackages = with pkgs; [ nixos-rebuild treefmt my_python ]; }; + systemd.services."gitea-runner-${utils.escapeSystemdPath name}" = { + serviceConfig = { + DynamicUser = mkForce false; + User = mkForce runnerUsername; + Group = mkForce runnerUsername; + }; + }; system.stateVersion = "24.05"; }; } -- 2.54.0 From 687ef0c167ed445bd867adbdc766aabda1527958 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 00:39:19 -0400 Subject: [PATCH 09/15] moved acme_challenge backend --- systems/jeeves/web_services/haproxy.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systems/jeeves/web_services/haproxy.cfg b/systems/jeeves/web_services/haproxy.cfg index 899ef71..d2eaaf9 100644 --- a/systems/jeeves/web_services/haproxy.cfg +++ b/systems/jeeves/web_services/haproxy.cfg @@ -28,7 +28,6 @@ frontend ContentSwitching # ACME challenge routing (must be first) acl is_acme path_beg /.well-known/acme-challenge/ - use_backend acme_challenge if is_acme # tmmworkshop.com acl host_audiobookshelf hdr(host) -i audiobookshelf.tmmworkshop.com @@ -45,6 +44,7 @@ frontend ContentSwitching # Redirect all HTTP to HTTPS unless on the allow list or ACME challenge http-request redirect scheme https code 301 if !{ ssl_fc } !allow_http !is_acme + use_backend acme_challenge if is_acme use_backend audiobookshelf_nodes if host_audiobookshelf use_backend cache_nodes if host_cache use_backend jellyfin if host_jellyfin -- 2.54.0 From d1a3e7338a434f2386f590936fa41274df375606 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 00:39:23 -0400 Subject: [PATCH 10/15] added permittedInsecurePackages for discord-canary --- common/global/default.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/global/default.nix b/common/global/default.nix index 935bfcd..bd48fa2 100644 --- a/common/global/default.nix +++ b/common/global/default.nix @@ -37,7 +37,12 @@ nixpkgs = { overlays = builtins.attrValues outputs.overlays; - config.allowUnfree = true; + config = { + allowUnfree = true; + permittedInsecurePackages = [ + "openssl-1.1.1w" # This is for discord-canary + ]; + }; }; services = { -- 2.54.0 From 1abd53987ce5998d38aa6d5a6f089366203e7dd5 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 16:29:56 -0400 Subject: [PATCH 11/15] made nix_builders not ephemeral and depended on gitea --- systems/jeeves/runners/nix_builder.nix | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/systems/jeeves/runners/nix_builder.nix b/systems/jeeves/runners/nix_builder.nix index bcff4cd..23032a0 100644 --- a/systems/jeeves/runners/nix_builder.nix +++ b/systems/jeeves/runners/nix_builder.nix @@ -51,7 +51,6 @@ in autoStart = true; privateNetwork = true; hostBridge = cfg.bridgeName; - ephemeral = true; bindMounts = { host-nix = { mountPoint = "/host-nix/var/nix/daemon-socket"; @@ -143,5 +142,15 @@ in }; } ) cfg.containers; + + systemd.services = builtins.listToAttrs ( + map (name: { + name = "container@${name}"; + value = { + requires = [ "gitea.service" ]; + after = [ "gitea.service" ]; + }; + }) (builtins.attrNames (filterAttrs (_: c: c.enable) cfg.containers)) + ); }; } -- 2.54.0 From e95eedffe4a0c336b67772e8077e8a874a150252 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 16:30:51 -0400 Subject: [PATCH 12/15] updated br-nix-builder --- systems/jeeves/networking.nix | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/systems/jeeves/networking.nix b/systems/jeeves/networking.nix index eedac6a..8e5dd84 100644 --- a/systems/jeeves/networking.nix +++ b/systems/jeeves/networking.nix @@ -1,4 +1,13 @@ { + # Docker loads br_netfilter on jeeves. Disable bridge netfilter so + # br-nix-builder behaves like a pure L2 bridge and bridged traffic + # does not hit the host firewall/rpfilter path. + boot.kernel.sysctl = { + "net.bridge.bridge-nf-call-arptables" = 0; + "net.bridge.bridge-nf-call-ip6tables" = 0; + "net.bridge.bridge-nf-call-iptables" = 0; + }; + networking = { hostName = "jeeves"; hostId = "0e15ce35"; @@ -49,23 +58,10 @@ "60-br-nix-builder" = { matchConfig.Name = "br-nix-builder"; bridgeConfig = { }; - address = [ "192.168.3.10/24" ]; - routingPolicyRules = [ - { - From = "192.168.3.0/24"; - Table = 100; - Priority = 100; - } - ]; - routes = [ - { - Gateway = "192.168.3.1"; - Table = 100; - GatewayOnLink = false; - Metric = 2048; - PreferredSource = "192.168.3.10"; - } - ]; + networkConfig = { + IPv6AcceptRA = false; + LinkLocalAddressing = "no"; + }; linkConfig.RequiredForOnline = "no"; }; }; -- 2.54.0 From b58ea6055793951473d64cb892945d31427c7209 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 19:16:37 -0400 Subject: [PATCH 13/15] adding hostPackages --- systems/jeeves/runners/nix_builder.nix | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/systems/jeeves/runners/nix_builder.nix b/systems/jeeves/runners/nix_builder.nix index 23032a0..49bfa96 100644 --- a/systems/jeeves/runners/nix_builder.nix +++ b/systems/jeeves/runners/nix_builder.nix @@ -126,9 +126,17 @@ in ]; tokenFile = "/run/secrets/gitea-runners/registration-token"; hostPackages = with pkgs; [ - nixos-rebuild - treefmt + bash + coreutils + curl + gawk + gitMinimal + gnused my_python + nixos-rebuild + nodejs + treefmt + wget ]; }; systemd.services."gitea-runner-${utils.escapeSystemdPath name}" = { -- 2.54.0 From 4f28050eff2bba38ff565e777d0213cecf3b5a6f Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 20:47:03 -0400 Subject: [PATCH 14/15] added nixfmt and nix --- systems/jeeves/runners/nix_builder.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/systems/jeeves/runners/nix_builder.nix b/systems/jeeves/runners/nix_builder.nix index 49bfa96..0d1785a 100644 --- a/systems/jeeves/runners/nix_builder.nix +++ b/systems/jeeves/runners/nix_builder.nix @@ -133,6 +133,8 @@ in gitMinimal gnused my_python + nix + nixfmt nixos-rebuild nodejs treefmt -- 2.54.0 From 27e487e3226b0f93aef093131107331aaa684a8d Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 3 May 2026 21:23:20 -0400 Subject: [PATCH 15/15] removing signal_bot --- systems/jeeves/services/signal_bot.nix | 57 -------------------------- 1 file changed, 57 deletions(-) delete mode 100644 systems/jeeves/services/signal_bot.nix diff --git a/systems/jeeves/services/signal_bot.nix b/systems/jeeves/services/signal_bot.nix deleted file mode 100644 index a74ca6f..0000000 --- a/systems/jeeves/services/signal_bot.nix +++ /dev/null @@ -1,57 +0,0 @@ -{ - pkgs, - inputs, - ... -}: -let - vars = import ../vars.nix; -in -{ - users = { - users.signalbot = { - isSystemUser = true; - group = "signalbot"; - }; - groups.signalbot = { }; - }; - - systemd.services.signal-bot = { - description = "Signal command and control bot"; - after = [ - "network.target" - "podman-signal_cli_rest_api.service" - ]; - wants = [ "podman-signal_cli_rest_api.service" ]; - wantedBy = [ "multi-user.target" ]; - - environment = { - PYTHONPATH = "${inputs.self}"; - SIGNALBOT_DB = "signalbot"; - SIGNALBOT_USER = "signalbot"; - SIGNALBOT_HOST = "/run/postgresql"; - SIGNALBOT_PORT = "5432"; - }; - - serviceConfig = { - Type = "simple"; - WorkingDirectory = "${inputs.self}"; - User = "signalbot"; - Group = "signalbot"; - EnvironmentFile = "${vars.secrets}/services/signal-bot"; - ExecStart = "${pkgs.my_python}/bin/python -m python.signal_bot.main"; - StateDirectory = "signal-bot"; - Restart = "on-failure"; - RestartSec = "10s"; - StandardOutput = "journal"; - StandardError = "journal"; - NoNewPrivileges = true; - ProtectSystem = "strict"; - ProtectHome = "read-only"; - PrivateTmp = true; - ReadWritePaths = [ "/var/lib/signal-bot" ]; - ReadOnlyPaths = [ - "${inputs.self}" - ]; - }; - }; -} -- 2.54.0