refactor: hive-forge module dir owns its theme css
This commit is contained in:
parent
7c9d72b9ca
commit
cb755b677c
4 changed files with 8 additions and 8 deletions
516
nix/modules/hive-forge/default.nix
Normal file
516
nix/modules/hive-forge/default.nix
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.forge;
|
||||
gatewayCfg = config.services.hyperhive.gateway;
|
||||
hyperhiveDomain = config.services.hyperhive.domain;
|
||||
|
||||
# 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`.
|
||||
|
||||
# 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.
|
||||
'';
|
||||
};
|
||||
|
||||
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").
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
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;
|
||||
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";
|
||||
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;
|
||||
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 - -"
|
||||
];
|
||||
|
||||
# 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;
|
||||
};
|
||||
}
|
||||
304
nix/modules/hive-forge/theme-catppuccin-vibec0re.css
Normal file
304
nix/modules/hive-forge/theme-catppuccin-vibec0re.css
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
/* Catppuccin Mocha × vibec0re — hyperhive forge theme
|
||||
Palette: https://catppuccin.com/palette (Mocha)
|
||||
Primary accent: Mauve #cba6f7 (mirrors hive-c0re dashboard --purple)
|
||||
Syntax highlighting: Catppuccin Mocha chroma palette
|
||||
*/
|
||||
|
||||
/* ── chroma syntax highlighting ──────────────────────────────────────── */
|
||||
.chroma .bp{color:#89b4fa}.chroma .c,.chroma .c1,.chroma .ch,.chroma .cm{color:#6c7086;font-style:italic}.chroma .cp{color:#a6e3a1}.chroma .cpf{color:#89b4fa}.chroma .cs{color:#cba6f7}.chroma .dl{color:#89b4fa}.chroma .gd{color:#f38ba8;background-color:#3b2335}.chroma .ge{color:#cdd6f4;font-style:italic}.chroma .gh{color:#fab387;font-weight:bold}.chroma .gi{color:#a6e3a1;background-color:#1e3320}.chroma .go{color:#6c7086}.chroma .gp{color:#cdd6f4}.chroma .gr{color:#f38ba8}.chroma .gs{color:#cdd6f4;font-weight:bold}.chroma .gt{color:#fab387}.chroma .gu{color:#a6e3a1;font-weight:bold}.chroma .il{color:#fab387}.chroma .k{color:#cba6f7}.chroma .kc{color:#89b4fa}.chroma .kd{color:#cba6f7}.chroma .kn{color:#94e2d5}.chroma .kp{color:#a6e3a1}.chroma .kr{color:#cba6f7}.chroma .kt{color:#89dceb}.chroma .m,.chroma .mb,.chroma .mf,.chroma .mh,.chroma .mi,.chroma .mo{color:#fab387}.chroma .n{color:#cdd6f4}.chroma .na,.chroma .nb{color:#89b4fa}.chroma .nc{color:#f9e2af}.chroma .nd{color:#a6e3a1}.chroma .ne{color:#fab387}.chroma .nf,.chroma .ni{color:#89b4fa}.chroma .nl{color:#cba6f7}.chroma .nn{color:#cdd6f4}.chroma .no{color:#fab387}.chroma .nt{color:#f38ba8}.chroma .nv{color:#cdd6f4}.chroma .nx{color:#cdd6f4}.chroma .o{color:#89dceb}.chroma .ow{color:#a6e3a1}.chroma .p{color:#bac2de}.chroma .s,.chroma .s1,.chroma .s2{color:#a6e3a1}.chroma .sa{color:#fab387}.chroma .sb{color:#a6e3a1}.chroma .sc{color:#a6e3a1}.chroma .sd{color:#6c7086;font-style:italic}.chroma .se{color:#f38ba8}.chroma .sh{color:#a6e3a1}.chroma .si{color:#94e2d5}.chroma .sr{color:#cba6f7}.chroma .ss{color:#f38ba8}.chroma .sx{color:#fab387}.chroma .vc,.chroma .vg,.chroma .vi{color:#89b4fa}.chroma .w{color:#585b70}
|
||||
|
||||
/* ── dark-mode image visibility (same as gitea-dark) ─────────────────── */
|
||||
.markup [src$="#gh-light-mode-only"],.markup [src$="#light-mode-only"],.markup [href$="#gh-light-mode-only"],.markup [href$="#light-mode-only"]{display:none}
|
||||
.markup [src$="#gh-dark-mode-only"],.markup [src$="#dark-mode-only"],.markup [href$="#gh-dark-mode-only"],.markup [href$="#dark-mode-only"]{display:unset}
|
||||
|
||||
/* ── Catppuccin Mocha palette → Forgejo CSS vars ─────────────────────── */
|
||||
:root {
|
||||
--is-dark-theme: true;
|
||||
color-scheme: dark;
|
||||
|
||||
/* Primary: Mauve #cba6f7 — matches hive-c0re dashboard --purple */
|
||||
--color-primary: #cba6f7;
|
||||
--color-primary-contrast: #1e1e2e;
|
||||
--color-primary-dark-1: #d0aff8;
|
||||
--color-primary-dark-2: #d5b8f9;
|
||||
--color-primary-dark-3: #dac2fa;
|
||||
--color-primary-dark-4: #dfcbfb;
|
||||
--color-primary-dark-5: #ead9fc;
|
||||
--color-primary-dark-6: #f4effe;
|
||||
--color-primary-dark-7: #faf7ff;
|
||||
--color-primary-light-1: #b895e0;
|
||||
--color-primary-light-2: #a580c7;
|
||||
--color-primary-light-3: #9470b0;
|
||||
--color-primary-light-4: #7d5b9a;
|
||||
--color-primary-light-5: #4d3866;
|
||||
--color-primary-light-6: #2a1e42;
|
||||
--color-primary-light-7: #110d1e;
|
||||
--color-primary-alpha-10: #cba6f719;
|
||||
--color-primary-alpha-20: #cba6f733;
|
||||
--color-primary-alpha-30: #cba6f74b;
|
||||
--color-primary-alpha-40: #cba6f766;
|
||||
--color-primary-alpha-50: #cba6f780;
|
||||
--color-primary-alpha-60: #cba6f799;
|
||||
--color-primary-alpha-70: #cba6f7b3;
|
||||
--color-primary-alpha-80: #cba6f7cc;
|
||||
--color-primary-alpha-90: #cba6f7e1;
|
||||
--color-primary-hover: var(--color-primary-dark-1);
|
||||
--color-primary-active: var(--color-primary-dark-2);
|
||||
|
||||
/* Secondary: Surface1 #45475a */
|
||||
--color-secondary: #45475a;
|
||||
--color-secondary-dark-1: #4e5069;
|
||||
--color-secondary-dark-2: #585b70;
|
||||
--color-secondary-dark-3: #6c7086;
|
||||
--color-secondary-dark-4: #7f849c;
|
||||
--color-secondary-dark-5: #9399b2;
|
||||
--color-secondary-dark-6: #a6adc8;
|
||||
--color-secondary-dark-7: #bac2de;
|
||||
--color-secondary-dark-8: #cdd6f4;
|
||||
--color-secondary-dark-9: #d3dcf6;
|
||||
--color-secondary-dark-10: #d8e1f8;
|
||||
--color-secondary-dark-11: #dde5f9;
|
||||
--color-secondary-dark-12: #e2eafa;
|
||||
--color-secondary-dark-13: #e7eefb;
|
||||
--color-secondary-light-1: #313244;
|
||||
--color-secondary-light-2: #292a3a;
|
||||
--color-secondary-light-3: #1e1e2e;
|
||||
--color-secondary-light-4: #181825;
|
||||
--color-secondary-alpha-10: #45475a19;
|
||||
--color-secondary-alpha-20: #45475a33;
|
||||
--color-secondary-alpha-30: #45475a4b;
|
||||
--color-secondary-alpha-40: #45475a66;
|
||||
--color-secondary-alpha-50: #45475a80;
|
||||
--color-secondary-alpha-60: #45475a99;
|
||||
--color-secondary-alpha-70: #45475ab3;
|
||||
--color-secondary-alpha-80: #45475acc;
|
||||
--color-secondary-alpha-90: #45475ae1;
|
||||
--color-secondary-hover: var(--color-secondary-dark-3);
|
||||
--color-secondary-active: var(--color-secondary-dark-2);
|
||||
|
||||
/* Terminal / console: Crust/Mantle tones */
|
||||
--color-console-fg: #cdd6f4;
|
||||
--color-console-fg-subtle: #a6adc8;
|
||||
--color-console-bg: #11111b;
|
||||
--color-console-border: #313244;
|
||||
--color-console-hover-bg: #1e1e2e;
|
||||
--color-console-active-bg: #313244;
|
||||
--color-console-menu-bg: #181825;
|
||||
--color-console-menu-border: #45475a;
|
||||
|
||||
/* Named accent colours → Catppuccin equivalents */
|
||||
--color-red: #f38ba8;
|
||||
--color-orange: #fab387;
|
||||
--color-yellow: #f9e2af;
|
||||
--color-olive: #a6e3a1;
|
||||
--color-green: #a6e3a1;
|
||||
--color-teal: #94e2d5;
|
||||
--color-blue: #89b4fa;
|
||||
--color-violet: #b4befe;
|
||||
--color-purple: #cba6f7;
|
||||
--color-pink: #f5c2e7;
|
||||
--color-brown: #fab387;
|
||||
--color-black: #11111b;
|
||||
--color-red-light: #eba0ac;
|
||||
--color-orange-light: #fab387;
|
||||
--color-yellow-light: #f9e2af;
|
||||
--color-olive-light: #a6e3a1;
|
||||
--color-green-light: #a6e3a1;
|
||||
--color-teal-light: #94e2d5;
|
||||
--color-blue-light: #89b4fa;
|
||||
--color-violet-light: #b4befe;
|
||||
--color-purple-light: #cba6f7;
|
||||
--color-pink-light: #f5c2e7;
|
||||
--color-brown-light: #fab387;
|
||||
--color-black-light: #313244;
|
||||
--color-red-dark-1: #f38ba8;
|
||||
--color-orange-dark-1:#fab387;
|
||||
--color-yellow-dark-1:#f9e2af;
|
||||
--color-olive-dark-1: #a6e3a1;
|
||||
--color-green-dark-1: #a6e3a1;
|
||||
--color-teal-dark-1: #94e2d5;
|
||||
--color-blue-dark-1: #74c7ec;
|
||||
--color-violet-dark-1:#b4befe;
|
||||
--color-purple-dark-1:#cba6f7;
|
||||
--color-pink-dark-1: #f5c2e7;
|
||||
--color-brown-dark-1: #fab387;
|
||||
--color-black-dark-1: #1e1e2e;
|
||||
--color-red-dark-2: #eb8da4;
|
||||
--color-orange-dark-2:#f5aa80;
|
||||
--color-yellow-dark-2:#f4daa8;
|
||||
--color-olive-dark-2: #9fd99b;
|
||||
--color-green-dark-2: #9fd99b;
|
||||
--color-teal-dark-2: #8dd9cd;
|
||||
--color-blue-dark-2: #6cbfe6;
|
||||
--color-violet-dark-2:#aab4f8;
|
||||
--color-purple-dark-2:#c29ef2;
|
||||
--color-pink-dark-2: #f0b9e2;
|
||||
--color-brown-dark-2: #f0a378;
|
||||
--color-black-dark-2: #181825;
|
||||
|
||||
/* ANSI terminal colours */
|
||||
--color-ansi-black: #11111b;
|
||||
--color-ansi-red: #f38ba8;
|
||||
--color-ansi-green: #a6e3a1;
|
||||
--color-ansi-yellow: #f9e2af;
|
||||
--color-ansi-blue: #89b4fa;
|
||||
--color-ansi-magenta: #f5c2e7;
|
||||
--color-ansi-cyan: #94e2d5;
|
||||
--color-ansi-white: var(--color-console-fg-subtle);
|
||||
--color-ansi-bright-black: #45475a;
|
||||
--color-ansi-bright-red: #f38ba8;
|
||||
--color-ansi-bright-green: #a6e3a1;
|
||||
--color-ansi-bright-yellow: #f9e2af;
|
||||
--color-ansi-bright-blue: #89b4fa;
|
||||
--color-ansi-bright-magenta: #f5c2e7;
|
||||
--color-ansi-bright-cyan: #94e2d5;
|
||||
--color-ansi-bright-white: var(--color-console-fg);
|
||||
|
||||
--color-grey: #45475a;
|
||||
--color-grey-light: #7f849c;
|
||||
--color-gold: #f9e2af;
|
||||
--color-white: #cdd6f4;
|
||||
|
||||
/* Diff colours */
|
||||
--color-diff-removed-word-bg: #4b1c2c;
|
||||
--color-diff-added-word-bg: #1c3a2a;
|
||||
--color-diff-removed-row-bg: #3b1525;
|
||||
--color-diff-moved-row-bg: #3a3520;
|
||||
--color-diff-added-row-bg: #1a2e24;
|
||||
--color-diff-removed-row-border:#6b3044;
|
||||
--color-diff-moved-row-border: #b0a850;
|
||||
--color-diff-added-row-border: #2d5040;
|
||||
--color-diff-inactive: #181825;
|
||||
|
||||
/* Feedback colours */
|
||||
--color-error-border: #f38ba8;
|
||||
--color-error-bg: #3b1525;
|
||||
--color-error-bg-active: #4d1e30;
|
||||
--color-error-bg-hover: #44192b;
|
||||
--color-error-text: #f38ba8;
|
||||
--color-success-border: #a6e3a1;
|
||||
--color-success-bg: #1a2e24;
|
||||
--color-success-text: #a6e3a1;
|
||||
--color-warning-border: #f9e2af;
|
||||
--color-warning-bg: #2e2a1a;
|
||||
--color-warning-text: #f9e2af;
|
||||
--color-info-border: #89b4fa;
|
||||
--color-info-bg: #1a2040;
|
||||
--color-info-text: #89b4fa;
|
||||
|
||||
/* Badge colours */
|
||||
--color-red-badge: #f38ba8;
|
||||
--color-red-badge-bg: #f38ba81a;
|
||||
--color-red-badge-hover-bg: #f38ba84d;
|
||||
--color-green-badge: #a6e3a1;
|
||||
--color-green-badge-bg: #a6e3a11a;
|
||||
--color-green-badge-hover-bg: #a6e3a14d;
|
||||
--color-yellow-badge: #f9e2af;
|
||||
--color-yellow-badge-bg: #f9e2af1a;
|
||||
--color-yellow-badge-hover-bg:#f9e2af4d;
|
||||
--color-orange-badge: #fab387;
|
||||
--color-orange-badge-bg: #fab3871a;
|
||||
--color-orange-badge-hover-bg:#fab3874d;
|
||||
|
||||
/* Layout */
|
||||
--color-body: #1e1e2e;
|
||||
--color-box-header: #181825;
|
||||
--color-box-body: #11111b;
|
||||
--color-box-body-highlight: #1e1e2e;
|
||||
--color-text-dark: #cdd6f4;
|
||||
--color-text: #cdd6f4;
|
||||
--color-text-light: #bac2de;
|
||||
--color-text-light-1: #a6adc8;
|
||||
--color-text-light-2: #9399b2;
|
||||
--color-text-light-3: #7f849c;
|
||||
--color-footer: var(--color-nav-bg);
|
||||
--color-timeline: #45475a;
|
||||
--color-input-text: var(--color-text-dark);
|
||||
--color-input-background: #11111b;
|
||||
--color-input-toggle-background: #313244;
|
||||
--color-input-border: var(--color-secondary);
|
||||
--color-input-border-hover: var(--color-secondary-dark-1);
|
||||
--color-light: #cba6f71a;
|
||||
--color-light-mimic-enabled: rgba(0,0,0,calc(40/255*222/255/var(--opacity-disabled)));
|
||||
--color-light-border: #cba6f726;
|
||||
--color-hover: #cba6f714;
|
||||
--color-active: #313244;
|
||||
--color-menu: #181825;
|
||||
--color-card: #181825;
|
||||
--fancy-card-bg: #11111b;
|
||||
--fancy-card-border: #45475a;
|
||||
--color-markup-table-row: #cba6f70d;
|
||||
--color-markup-code-block: #cba6f710;
|
||||
--color-markup-code-inline:#cba6f724;
|
||||
--color-button: #181825;
|
||||
--color-code-bg: #11111b;
|
||||
--color-shadow: #11111b80;
|
||||
--color-secondary-bg: #313244;
|
||||
--color-expand-button: #292a3a;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--color-editor-line-highlight: var(--color-primary-light-5);
|
||||
--color-project-column-bg: var(--color-secondary-light-2);
|
||||
--color-caret: var(--color-text);
|
||||
--color-reaction-bg: #cba6f710;
|
||||
--color-reaction-hover-bg: var(--color-primary-light-4);
|
||||
--color-reaction-active-bg: var(--color-primary-light-5);
|
||||
--color-tooltip-text: #cdd6f4;
|
||||
--color-tooltip-bg: #11111bee;
|
||||
|
||||
/* Navigation */
|
||||
--color-nav-bg: #181825;
|
||||
--color-nav-hover-bg: var(--color-secondary-light-1);
|
||||
--color-secondary-nav-bg: #1e1e2e;
|
||||
|
||||
/* Labels */
|
||||
--color-label-text: var(--color-text);
|
||||
--color-label-bg: #7f849c40;
|
||||
--color-label-hover-bg: #7f849c99;
|
||||
--color-label-active-bg: #7f849cff;
|
||||
|
||||
/* Accent */
|
||||
--color-accent: var(--color-primary-light-1);
|
||||
--color-small-accent: var(--color-primary-light-5);
|
||||
|
||||
/* Highlight / selection */
|
||||
--color-highlight-fg: #cba6f7;
|
||||
--color-highlight-bg: #4d38663a;
|
||||
--color-overlay-backdrop: #11111bc0;
|
||||
--color-selection-bg: var(--color-primary-light-1);
|
||||
--color-selection-fg: #1e1e2e;
|
||||
|
||||
/* Misc */
|
||||
--checkerboard-color-1: #313244;
|
||||
--checkerboard-color-2: #1e1e2e;
|
||||
--color-indicator-offline: #6c7086;
|
||||
--color-indicator-offline-20: #6c70861a;
|
||||
--color-indicator-idle: #a6e3a1;
|
||||
--color-indicator-idle-20: #a6e3a11a;
|
||||
--color-indicator-active: #89b4fa;
|
||||
--color-indicator-active-20: #89b4fa33;
|
||||
|
||||
accent-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ── vibec0re: glow accents on interactive elements ──────────────────── */
|
||||
.ui.primary.button,
|
||||
a.ui.primary.button,
|
||||
.ui.primary.buttons .button {
|
||||
text-shadow: 0 0 8px rgba(203, 166, 247, 0.6);
|
||||
box-shadow: 0 0 12px -2px rgba(203, 166, 247, 0.35);
|
||||
}
|
||||
.ui.primary.button:hover,
|
||||
.ui.primary.buttons .button:hover {
|
||||
box-shadow: 0 0 18px -2px rgba(203, 166, 247, 0.55);
|
||||
}
|
||||
#navbar .item.active,
|
||||
#navbar .item:hover {
|
||||
text-shadow: 0 0 6px rgba(203, 166, 247, 0.45);
|
||||
}
|
||||
.repository .file-view .lines-num {
|
||||
background: #181825;
|
||||
border-color: #313244;
|
||||
}
|
||||
a:not(.ui.button):not(.item) {
|
||||
text-shadow: 0 0 3px rgba(137, 180, 250, 0.25);
|
||||
}
|
||||
Loading…
Reference in a new issue