633 lines
29 KiB
Nix
633 lines
29 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.forge;
|
|
gatewayCfg = config.services.hyperhive.gateway;
|
|
hyperhiveDomain = config.services.hyperhive.domain;
|
|
tlsCfg = config.services.hyperhive.tls;
|
|
|
|
# Self-signed gateway TLS: forgejo (Go) validates outbound webhook
|
|
# deliveries (e.g. the config-PR webhook to https://<domain>/webhook/...)
|
|
# against its system cert store, which lacks the runtime-generated hive
|
|
# CA — so delivery fails with an x509 "unknown authority". Go has no
|
|
# additive trust env var (SSL_CERT_FILE *replaces* the default bundle),
|
|
# so bind the public CA in and hand forgejo a combined bundle (system
|
|
# CAs + hive CA) via SSL_CERT_FILE. Only active in self-signed mode;
|
|
# with an operator cert / ACME the public chain already validates and
|
|
# this whole block drops out. `gateway.useSelfSigned` is the single
|
|
# source of truth for the self-signed condition (no duplicated logic).
|
|
useSelfSigned = gatewayCfg.useSelfSigned;
|
|
caHostPath = "${tlsCfg.stateDir}/ca.pem";
|
|
caContainerPath = "/run/hive-ca/ca.pem";
|
|
forgeCaBundle = "/run/hive-forge-ca/ca-bundle.crt";
|
|
|
|
# ROOT_URL forgejo advertises in clone links + outbound URLs. When
|
|
# served behind the gateway, `cfg.domain` doubles as both the
|
|
# forgejo `DOMAIN` setting AND the gateway vhost server-name, so
|
|
# ROOT_URL just uses it directly. The gateway always terminates TLS
|
|
# (self-signed is the implicit floor when neither `tls.certDir` nor
|
|
# ACME is configured), so behind the gateway the forge is always
|
|
# advertised over `https` on `httpsPort` — the canonical 443 elides
|
|
# the port suffix. When direct (`behindGateway = false`), keep the
|
|
# host:httpPort shape so direct browser access still produces correct
|
|
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
|
# shapes.
|
|
defaultRootUrl =
|
|
if cfg.behindGateway then
|
|
let
|
|
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
|
in
|
|
"https://${cfg.domain}${portSuffix}/"
|
|
else
|
|
"http://${cfg.domain}:${toString cfg.httpPort}/";
|
|
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
|
|
|
# When CI is enabled, the runner needs `actions/checkout` resolvable
|
|
# without external DNS (hive-ci shares the host netns, so a host-resolver
|
|
# blip otherwise reds every `actions/checkout@vN` fetch from
|
|
# data.forgejo.org). Auto-append a pull-mirror of it and point
|
|
# forgejo's DEFAULT_ACTIONS_URL at this instance so `uses:` resolves local.
|
|
ciEnabled = config.services.hyperhive.forge.ci.enable;
|
|
actionCheckoutMirror = {
|
|
upstream = "https://github.com/actions/checkout";
|
|
dest = "actions/checkout";
|
|
};
|
|
# Auto-append the actions/checkout mirror only when CI is on AND the
|
|
# operator hasn't already declared that dest themselves (else CI-on +
|
|
# an explicit `actions/checkout` entry would duplicate it).
|
|
effectiveMirrors =
|
|
cfg.mirrors
|
|
++ lib.optional (
|
|
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
|
|
) actionCheckoutMirror;
|
|
in
|
|
{
|
|
# Private Forgejo in a `hive-forge` nixos-container, shared host
|
|
# netns. Agents reach it at `forge.<domain>` via the gateway. State
|
|
# at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` survives
|
|
# restart. See `docs/gateway.md::hive-forge container shape`.
|
|
|
|
# External Forgejo/Gitea/Codeberg-compatible forges (beyond the mandatory
|
|
# internal one) are entirely dashboard-provisioned — no nix config here.
|
|
# An operator manually creates a token on the external forge (however
|
|
# that forge lets them) and pastes name + base URL + token into the
|
|
# dashboard's FORGES tab; hive-c0re just persists it to
|
|
# `<state>/forge-<label>-token` + a `<state>/forge-<label>.json` sidecar
|
|
# (base URL), the same shape as the GitHub PAT / matrix extra-account
|
|
# flows. See `hive-c0re/src/dashboard/extra_forges.rs`.
|
|
|
|
# The internal forge is mandatory — it's the canonical store for the
|
|
# meta flake + every agent's config repo (and the `internal/*` repos),
|
|
# so there is no enable/disable toggle. It deploys whenever hyperhive
|
|
# itself is enabled (`services.hyperhive.enable`).
|
|
options.services.hyperhive.forge = {
|
|
httpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 3000;
|
|
description = ''
|
|
TCP port the forge serves HTTP on. Default 3000 sits outside
|
|
hyperhive's claimed ranges (dashboard 7000, every agent in
|
|
8100..8999 via FNV-1a hash). Change this if you already have
|
|
another forgejo bound to 3000.
|
|
'';
|
|
};
|
|
|
|
sshPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 2222;
|
|
description = ''
|
|
TCP port the forge's built-in SSH server listens on. Kept off
|
|
22 so it doesn't clash with the host's openssh. Agents push
|
|
with `ssh -p <sshPort> git@<domain>:<owner>/<repo>.git`.
|
|
'';
|
|
};
|
|
|
|
domain = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "forge.${hyperhiveDomain}";
|
|
defaultText = lib.literalExpression ''"forge.''${services.hyperhive.domain}"'';
|
|
example = "git.example.com";
|
|
description = ''
|
|
Public hostname for the forge. Doubles as both the forgejo
|
|
`DOMAIN` setting (clone URLs forgejo advertises) AND the
|
|
gateway vhost server-name when `behindGateway = true`
|
|
(sub-domain routing — see `docs/gateway.md`).
|
|
|
|
Defaults to `forge.''${services.hyperhive.domain}` (idiomatic
|
|
sub-domain shape — `forge` labelled under the hive's bare
|
|
domain). `services.hyperhive.domain` is required, so there's
|
|
always a domain to derive from.
|
|
|
|
Set to a full hostname (`git.example.com`,
|
|
`forge.internal.lan`, etc.) for a bespoke vhost shape — the
|
|
full domain goes here, no separate sub-domain-label option.
|
|
'';
|
|
};
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.forgejo;
|
|
defaultText = lib.literalExpression "pkgs.forgejo";
|
|
description = ''
|
|
Forgejo package to run inside the container. Defaults to
|
|
`pkgs.forgejo` (the latest release line) rather than the
|
|
nixpkgs-module default of `pkgs.forgejo-lts`, because LTS
|
|
lags far behind on schema and the DB easily ends up "newer
|
|
than the binary" if the operator ever ran a non-LTS forgejo
|
|
against the same state dir. Override to `pkgs.forgejo-lts`
|
|
if you actively want the slower release train.
|
|
'';
|
|
};
|
|
|
|
behindGateway = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = config.services.hyperhive.enable;
|
|
defaultText = lib.literalExpression "config.services.hyperhive.enable";
|
|
description = ''
|
|
Serve forgejo through the hive-gateway nginx as a sub-domain
|
|
vhost (`server_name = cfg.domain`) instead of directly on
|
|
`httpPort` (sub-domain routing — see `docs/gateway.md`).
|
|
|
|
When `true`:
|
|
- The gateway adds a `server { server_name = ''${cfg.domain}; }`
|
|
block that proxies all `/` → `http://127.0.0.1:''${httpPort}/`.
|
|
- Forgejo's `ROOT_URL` flips to `http(s)://''${cfg.domain}/`
|
|
(sub-domain root, no port suffix when gateway is on 80).
|
|
- `gateway.localHostsEntry = true` extends `/etc/hosts` to
|
|
include `cfg.domain → 127.0.0.1` for local dev.
|
|
|
|
Defaults to `services.hyperhive.enable` (the gateway always runs
|
|
alongside hyperhive, so forge auto-routes through it). Set `false`
|
|
explicitly to keep forge on the direct port even though the
|
|
gateway is running (e.g. an external git client that doesn't
|
|
traverse the gateway).
|
|
|
|
Sub-domain routing is the preferred shape for forge + matrix
|
|
(both are external standard apps with sub-domain-native config
|
|
defaults). Per-agent UIs stay on sub-path (`/agent/<name>/`)
|
|
because they're hyperhive-internal + already base-path-aware.
|
|
'';
|
|
};
|
|
|
|
rootUrl = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "https://forge.example.com/";
|
|
description = ''
|
|
Override the auto-derived forgejo `ROOT_URL`. When `null`
|
|
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
|
|
state, including the scheme:
|
|
|
|
- `behindGateway = true` → `https://''${cfg.domain}/`. The gateway
|
|
always terminates TLS (self-signed is the implicit floor when no
|
|
`gateway.tls.certDir` / ACME is set), so the forge is always
|
|
advertised over https. A non-canonical `gateway.httpsPort` is
|
|
appended as `:<port>`.
|
|
- `behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
|
|
|
|
The TLS scheme is derived automatically now, so you only need to
|
|
set this for a genuinely bespoke shape (e.g. an external reverse
|
|
proxy on a different host/path). Must end with `/` per forgejo's
|
|
`ROOT_URL` contract.
|
|
'';
|
|
};
|
|
|
|
webhookAllowedHosts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [
|
|
"ci.example.com"
|
|
"hooks.example.org"
|
|
];
|
|
description = ''
|
|
Extra hosts to add to forgejo's webhook SSRF allow-list
|
|
(`[webhook] ALLOWED_HOST_LIST`). The hive gateway host
|
|
(`services.hyperhive.domain`) is ALWAYS included — the config-PR
|
|
and knowledge webhooks target it, and it resolves to a private
|
|
gateway IP that forgejo's default guard would otherwise deny.
|
|
Add entries here to additionally allow webhooks from project
|
|
repos to other hosts (a user's own CI, external services, etc.).
|
|
|
|
Entries are forgejo hostmatcher patterns: hostnames, globs,
|
|
IPs/CIDRs, or the builtins `loopback` / `private` / `external`
|
|
/ `*`.
|
|
'';
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Open `httpPort` + `sshPort` in the host firewall. Off by
|
|
default (secure-by-default): agent containers reach the forge
|
|
at `forge.<domain>` via the gateway (not directly), and the
|
|
host reaches it on loopback — so the firewall opens only
|
|
matter for access from outside the host. Flip to `true` when
|
|
you want the operator's browser or external git clients to
|
|
hit the forge directly.
|
|
|
|
**Breaking change**: this used to default to `true`. If you
|
|
relied on the old default for external reach, add
|
|
`services.hyperhive.forge.openFirewall = true;` to your host
|
|
config before rebuilding.
|
|
'';
|
|
};
|
|
|
|
mirrors = lib.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
upstream = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "https://github.com/actions/checkout";
|
|
description = "Upstream clone URL to mirror from.";
|
|
};
|
|
dest = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "actions/checkout";
|
|
description = ''
|
|
Local `<owner>/<repo>` the pull-mirror is created at. The
|
|
`<owner>` org is auto-created if missing. Keep mirror dests
|
|
in their own orgs (e.g. `actions/*`) — separate from the
|
|
hive-c0re-managed namespaces (config/shared/agents/core) so
|
|
the seed never collides with core's own provisioning.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [ ];
|
|
example = lib.literalExpression ''
|
|
[ { upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; } ]
|
|
'';
|
|
description = ''
|
|
General-purpose Forgejo **pull-mirrors** to auto-seed on the local
|
|
forge. Each entry is created as a real Forgejo pull-mirror (it
|
|
re-syncs from `upstream` out-of-band), not a one-off pushed clone —
|
|
so a host-resolver blip leaves a *stale* mirror, never a hard
|
|
failure on whatever reads it.
|
|
|
|
When `services.hyperhive.forge.ci.enable` is set, an
|
|
`actions/checkout` mirror is auto-appended to this list and
|
|
forgejo's `DEFAULT_ACTIONS_URL` is pointed at this instance, so CI
|
|
`uses: actions/checkout@vN` steps resolve entirely on loopback with
|
|
no external DNS on the critical path (the seed/re-sync needs
|
|
external DNS, but that's off the CI path).
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf config.services.hyperhive.enable {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl;
|
|
message = ''
|
|
services.hyperhive.forge.rootUrl must end with "/". forgejo's
|
|
ROOT_URL contract requires a trailing slash for correct
|
|
relative-link generation; without it forgejo emits URLs like
|
|
`https://forge.example.com.user.id` instead of
|
|
`https://forge.example.com/user.id`. Got: ${toString cfg.rootUrl}
|
|
'';
|
|
}
|
|
{
|
|
# `cfg.domain` can't be empty — would render `.<hive>` shaped
|
|
# garbage as both server_name (nginx wildcard catch-all) and
|
|
# /etc/hosts entry (invalid). The default derives a non-empty
|
|
# `forge.<domain>`, but an operator-set empty string should fail
|
|
# loud.
|
|
assertion = cfg.domain != "";
|
|
message = ''
|
|
services.hyperhive.forge.domain = "" is rejected. The
|
|
rendered URLs would be invalid (nginx wildcard catch-all
|
|
for an empty server_name, /etc/hosts rejects empty entries).
|
|
Either leave at default (auto-derives to
|
|
"forge.<services.hyperhive.domain>"), or set a non-empty
|
|
hostname like "forge.example.com" or "git.internal".
|
|
'';
|
|
}
|
|
{
|
|
# Each mirror dest must be exactly `<owner>/<repo>` — the seed
|
|
# splits on the single slash to create the org + repo.
|
|
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
|
|
message = ''
|
|
Every services.hyperhive.forge.mirrors[].dest must be exactly
|
|
"<owner>/<repo>" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors}
|
|
'';
|
|
}
|
|
{
|
|
# Keep mirror orgs out of the hive-c0re-managed namespaces
|
|
# (config/shared/agents/core) so the seed never races / collides
|
|
# with hive-c0re's own startup provisioning of those orgs.
|
|
assertion = lib.all (
|
|
m:
|
|
!(lib.elem (builtins.elemAt (lib.splitString "/" m.dest) 0) [
|
|
"config"
|
|
"shared"
|
|
"agents"
|
|
"core"
|
|
])
|
|
) effectiveMirrors;
|
|
message = ''
|
|
services.hyperhive.forge.mirrors[].dest must not place a mirror
|
|
in a hive-c0re-managed org (config / shared / agents / core) —
|
|
those are provisioned by hive-c0re and a mirror there would
|
|
collide. Use a dedicated org (e.g. "actions/checkout").
|
|
'';
|
|
}
|
|
];
|
|
|
|
# The hive CA cert is generated at runtime by the host `hive-tls-ca`
|
|
# service. Order the container after it (self-signed mode only) so the
|
|
# bind source exists before nspawn sets the CA mount up — a
|
|
# condition-skipped/late CA would otherwise fail the container start.
|
|
systemd.services."container@hive-forge" = lib.mkIf useSelfSigned {
|
|
after = [ "hive-tls-ca.service" ];
|
|
requires = [ "hive-tls-ca.service" ];
|
|
};
|
|
|
|
containers.hive-forge = {
|
|
autoStart = true;
|
|
ephemeral = false;
|
|
# Share host netns — forgejo's HTTP / SSH listeners then look
|
|
# exactly like a host-side service, no port forwarding dance,
|
|
# and agent containers (which also share host netns) reach it
|
|
# via plain `localhost`.
|
|
privateNetwork = false;
|
|
# Self-signed mode: bind ONLY the public hive CA cert (never the
|
|
# `hive-tls` state dir — it holds the CA + leaf private keys) so
|
|
# forgejo can trust the gateway's self-signed leaf for outbound
|
|
# webhook delivery. The combined bundle is assembled at container
|
|
# start by hive-forge-ca-bundle below.
|
|
bindMounts = lib.optionalAttrs useSelfSigned {
|
|
${caContainerPath} = {
|
|
hostPath = caHostPath;
|
|
isReadOnly = true;
|
|
};
|
|
};
|
|
config =
|
|
{ pkgs, ... }:
|
|
let
|
|
# Build a custom static-root that is the standard forgejo data
|
|
# output with our theme CSS added. Using STATIC_ROOT_PATH instead
|
|
# of tmpfiles / bind-mounts means the theme is always present in
|
|
# the nix store — no separate hive-forge container rebuild needed,
|
|
# and no persistent-state directory involved.
|
|
staticRootWithTheme = pkgs.runCommand "forgejo-static-with-theme" { } ''
|
|
cp -r --no-preserve=mode,ownership ${cfg.package.data}/. $out/
|
|
mkdir -p $out/public/assets/css
|
|
cp ${./theme-catppuccin-vibec0re.css} \
|
|
$out/public/assets/css/theme-catppuccin-vibec0re.css
|
|
# Replace the default Forgejo logo + favicon with the hyperhive
|
|
# mark. Files in public/assets/img/ are served before built-ins.
|
|
mkdir -p $out/public/assets/img
|
|
cp ${../../../branding/hyperhive.svg} $out/public/assets/img/logo.svg
|
|
cp ${../../../branding/hyperhive.svg} $out/public/assets/img/favicon.svg
|
|
cp ${../../../branding/hyperhive.png} $out/public/assets/img/logo.png
|
|
cp ${../../../branding/hyperhive.png} $out/public/assets/img/favicon.png
|
|
cp ${../../../branding/hyperhive.png} $out/public/assets/img/avatar_default.png
|
|
'';
|
|
in
|
|
{
|
|
system.stateVersion = "25.11";
|
|
|
|
# Shared host netns: this container's own firewall.service
|
|
# would rewrite the HOST ruleset (flush nixos-fw, drop the
|
|
# host's nixos-nat-* chains) at every boot — killing the
|
|
# bridge DHCP/DNS holes and agent NAT. The host firewall owns
|
|
# all filtering; never run one in here.
|
|
networking.firewall.enable = false;
|
|
|
|
services.forgejo = {
|
|
enable = true;
|
|
package = cfg.package;
|
|
database.type = "sqlite3";
|
|
lfs.enable = true;
|
|
settings = {
|
|
DEFAULT.APP_NAME = "HyperHive";
|
|
server = {
|
|
DOMAIN = cfg.domain;
|
|
ROOT_URL = effectiveRootUrl;
|
|
HTTP_PORT = cfg.httpPort;
|
|
START_SSH_SERVER = true;
|
|
SSH_PORT = cfg.sshPort;
|
|
SSH_LISTEN_PORT = cfg.sshPort;
|
|
BUILTIN_SSH_SERVER_USER = "git";
|
|
DISABLE_SSH = false;
|
|
# Point forgejo at our extended static root that includes
|
|
# the custom theme CSS baked straight into the nix store.
|
|
STATIC_ROOT_PATH = staticRootWithTheme;
|
|
};
|
|
# Registration off — operator seeds agent users via
|
|
# `nixos-container run hive-forge -- forgejo admin
|
|
# user create …`.
|
|
service = {
|
|
DISABLE_REGISTRATION = true;
|
|
REQUIRE_SIGNIN_VIEW = false;
|
|
};
|
|
repository = {
|
|
DEFAULT_BRANCH = "main";
|
|
DEFAULT_PRIVATE = "private";
|
|
};
|
|
# Repo migrations / pull-mirrors fetch from the source
|
|
# URL *inside* Forgejo. hyperhive code is synced from
|
|
# `localhost` (and the host LAN), which Forgejo's
|
|
# migration guard blocks by default ("cannot import from
|
|
# disallowed hosts"). Allow loopback + RFC-1918 sources
|
|
# so an in-hive mirror of the hyperhive repo works.
|
|
migrations.ALLOW_LOCALNETWORKS = true;
|
|
# Webhook deliveries target the gateway
|
|
# (`https://<hyperhive domain>/webhook/*`), which resolves to a
|
|
# private (RFC-1918) gateway IP. Forgejo's webhook SSRF guard
|
|
# denies private hosts by default, so the config-PR + knowledge
|
|
# webhooks never actually deliver — only the 5-min poll fallback
|
|
# catches config PRs. The gateway host is always allowed (scoping
|
|
# to that one hostname keeps the SSRF surface tighter than the
|
|
# broad `private` builtin); operators extend the list via
|
|
# `forge.webhookAllowedHosts` for webhooks on their own repos.
|
|
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
|
|
[ hyperhiveDomain ] ++ cfg.webhookAllowedHosts
|
|
);
|
|
log.LEVEL = "Warn";
|
|
ui = {
|
|
DEFAULT_THEME = "catppuccin-vibec0re";
|
|
THEMES = "catppuccin-vibec0re,forgejo-auto,forgejo-light,forgejo-dark,gitea-auto,gitea-light,gitea-dark";
|
|
};
|
|
# Point forgejo at the GPG key generated by the
|
|
# forgejo-gpg-init service below. SIGNING_KEY = "default"
|
|
# resolves via the forgejo process's git config
|
|
# (`user.signingkey`) — which forgejo-gpg-init sets to the
|
|
# generated key — not by scanning GNUPGHOME. GNUPGHOME is
|
|
# the keyring forgejo signs from; must be absolute +
|
|
# writeable by the forgejo user.
|
|
"repository.signing" = {
|
|
SIGNING_KEY = "default";
|
|
GNUPGHOME = "/var/lib/forgejo/.gnupg";
|
|
};
|
|
# Enable Forgejo Actions so the runner registration token
|
|
# API endpoint is available. Without this the endpoint
|
|
# returns "runner registration token not found" regardless
|
|
# of token scopes. Required by `hive-ci-register.service`
|
|
# in the hive-ci container.
|
|
actions.ENABLED = true;
|
|
# When CI is enabled, resolve `uses: <org>/<action>@vN` from
|
|
# THIS instance (the seeded `actions/checkout` pull-mirror)
|
|
# instead of the upstream default `data.forgejo.org` — keeps
|
|
# the checkout step on loopback, immune to a host-resolver
|
|
# blip. `self` = forgejo expands actions against its
|
|
# own ROOT_URL.
|
|
actions.DEFAULT_ACTIONS_URL = lib.mkIf ciEnabled "self";
|
|
# F3 (federation) computes its data dir relative to the
|
|
# forgejo binary, which lands in the read-only nix
|
|
# store and crashes anything that touches the F3
|
|
# subsystem — including `forgejo admin user create`,
|
|
# which init-ses F3 even when ENABLED=false. Pin the
|
|
# path absolute alongside the disable so the init
|
|
# resolution succeeds before the flag is checked.
|
|
"F3" = {
|
|
ENABLED = false;
|
|
PATH = "/var/lib/forgejo/data/f3";
|
|
};
|
|
};
|
|
};
|
|
environment.systemPackages = [
|
|
pkgs.forgejo
|
|
pkgs.gnupg
|
|
];
|
|
|
|
# Forgejo's local Actions-artifact storage defaults to
|
|
# `{APP_DATA_PATH}/actions_artifacts` (=
|
|
# `/var/lib/forgejo/data/actions_artifacts`), but Forgejo does not
|
|
# pre-create that directory. The artifact endpoint ingests the
|
|
# chunked upload, then the merge-chunks step does an `lstat` on a
|
|
# tmp dir under it and fails:
|
|
# Error merge chunks: lstat
|
|
# /var/lib/forgejo/data/actions_artifacts/tmpNNN: no such file or
|
|
# directory
|
|
# so every `upload-artifact` step dies after the build succeeds.
|
|
# Pre-create the dir (forgejo-owned) so uploads actually persist.
|
|
# `actions.ENABLED = true` registers the endpoints; this gives them
|
|
# somewhere to write.
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/forgejo/data 0750 forgejo forgejo - -"
|
|
"d /var/lib/forgejo/data/actions_artifacts 0750 forgejo forgejo - -"
|
|
];
|
|
|
|
# Self-signed mode: assemble the combined TLS trust bundle
|
|
# (system CAs + the bind-mounted hive CA) forgejo's Go HTTP
|
|
# client validates outbound webhook deliveries against. Go's
|
|
# SSL_CERT_FILE *replaces* the default bundle, so we concatenate
|
|
# rather than point at the CA alone — otherwise mirror fetches
|
|
# from public hosts would lose their trust anchors. Runs before
|
|
# forgejo each boot; /run is tmpfs so the bundle is rebuilt from
|
|
# the current CA every start.
|
|
systemd.services.hive-forge-ca-bundle = lib.mkIf useSelfSigned {
|
|
description = "assemble forgejo TLS trust bundle (system CAs + hive CA)";
|
|
wantedBy = [ "forgejo.service" ];
|
|
before = [ "forgejo.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
SyslogIdentifier = "hive-forge-ca-bundle";
|
|
};
|
|
path = [ pkgs.coreutils ];
|
|
script = ''
|
|
set -euo pipefail
|
|
install -d -m 0755 /run/hive-forge-ca
|
|
cat /etc/ssl/certs/ca-certificates.crt ${caContainerPath} \
|
|
> ${forgeCaBundle}
|
|
chmod 0644 ${forgeCaBundle}
|
|
'';
|
|
};
|
|
# Point forgejo's Go TLS stack at the combined bundle so webhook
|
|
# delivery to the self-signed gateway validates.
|
|
systemd.services.forgejo.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned forgeCaBundle;
|
|
|
|
# Ensure Forgejo has a usable GPG signing key so UI merges / CRUD
|
|
# commits are signed instead of erroring "does not have a signing
|
|
# key". This service (a) generates a key in forgejo's persistent
|
|
# keyring iff one isn't already present — keyed on the actual
|
|
# secret key, NOT a stamp file, so a partial state wipe that loses
|
|
# the key still regenerates it — and (b) points the forgejo user's
|
|
# git config at it (`user.signingkey` + commit/tag gpgsign), which
|
|
# is how `SIGNING_KEY = "default"` actually resolves. Runs as the
|
|
# forgejo user before forgejo on each start; idempotent (the keygen
|
|
# is guarded, the git-config is a cheap re-set).
|
|
systemd.services.forgejo-gpg-init = {
|
|
description = "ensure Forgejo's GPG signing key + git signing config";
|
|
# Start before forgejo so the key + signing config are ready when
|
|
# forgejo reads repository.signing on startup.
|
|
wantedBy = [ "forgejo.service" ];
|
|
before = [ "forgejo.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "forgejo";
|
|
Group = "forgejo";
|
|
# Pin the journal identity (else it's the `script` store-path wrapper).
|
|
SyslogIdentifier = "forgejo-gpg-init";
|
|
};
|
|
# GNUPGHOME = the keyring forgejo signs from; HOME so
|
|
# `git config --global` lands where the forgejo process reads it.
|
|
environment = {
|
|
GNUPGHOME = "/var/lib/forgejo/.gnupg";
|
|
HOME = "/var/lib/forgejo";
|
|
};
|
|
path = [
|
|
pkgs.gnupg
|
|
pkgs.git
|
|
pkgs.gnugrep
|
|
pkgs.gawk
|
|
pkgs.coreutils
|
|
];
|
|
script = ''
|
|
set -euo pipefail
|
|
mkdir -p "$GNUPGHOME"
|
|
chmod 700 "$GNUPGHOME"
|
|
|
|
# Generate only if no secret key is present (key-based guard,
|
|
# not a stamp — a stamp can outlive the key after a state wipe
|
|
# and wrongly suppress regeneration).
|
|
if ! gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec:'; then
|
|
printf '%s\n' \
|
|
'%no-protection' \
|
|
'Key-Type: RSA' \
|
|
'Key-Length: 4096' \
|
|
'Name-Real: HyperHive Forge' \
|
|
'Name-Email: forgejo@hive' \
|
|
'Expire-Date: 0' \
|
|
| gpg --batch --gen-key
|
|
fi
|
|
|
|
# Point git (hence Forgejo's SIGNING_KEY="default") at the key.
|
|
KEYID=$(gpg --list-secret-keys --keyid-format long --with-colons \
|
|
| awk -F: '/^sec:/ { print $5; exit }')
|
|
if [ -n "$KEYID" ]; then
|
|
git config --global user.signingkey "$KEYID"
|
|
git config --global commit.gpgsign true
|
|
git config --global tag.gpgsign true
|
|
fi
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
allowedTCPPorts = [
|
|
cfg.httpPort
|
|
cfg.sshPort
|
|
];
|
|
};
|
|
|
|
# Forward the declared pull-mirrors to hive-c0re, which seeds them in
|
|
# its forge provisioning sweep (`forge.rs::ensure_mirrors`, alongside
|
|
# the SEEDED_ORGS ensure). c0re already holds the core admin token and
|
|
# ensures the orgs there, so the seeding lives in one place rather than
|
|
# a parallel host-side unit. JSON-encoded list of { upstream, dest };
|
|
# `[]` when nothing to seed (c0re no-ops).
|
|
systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS = builtins.toJSON effectiveMirrors;
|
|
};
|
|
}
|