Compare commits

..

372 Commits

Author SHA1 Message Date
yukkop 8625000952 feat: matrix-cluster: shared users 2026-05-27 12:54:15 +00:00
yukkop a8dd82d05f feat: matrix-cluster: shared users 2026-05-27 12:41:51 +00:00
yukkop 92f55320b5 fix: matrix 2026-05-25 23:12:05 +00:00
yukkop 4ce1945abe feat: matrix prepare move to poland 2026-05-25 07:48:16 +00:00
yukkop e29f09a694 feat: matrix: diferent servers one home server 2026-05-25 07:11:43 +00:00
yukkop ecabc0b6c2 fix: some recomendations 2026-05-24 15:08:40 +00:00
yukkop bcf5ae5da7 fix: element 2026-05-24 14:54:01 +00:00
yukkop 0eecea6324 fix: element 2026-05-23 23:11:58 +00:00
yukkop 4c0a178646 some 2026-05-23 22:57:09 +00:00
yukkop 9c6de9d067 fix: hectic-lab: mechabellum 2026-05-23 20:53:30 +00:00
yukkop 934c02609a feat: hectic-lab: mechabellum for lismy 2026-05-23 19:40:36 +00:00
yukkop fb6ee3cb54 chore: devide xrays systems 2026-05-23 18:27:02 +00:00
yukkop bce720fcff chore: hectic-lab: +vismajor in matrix 2026-05-23 18:17:27 +00:00
yukkop 7f39fc2dc3 fix 2026-05-23 17:13:49 +00:00
yukkop 819f1691b4 chore: hecitc-lab: +lvgkcfjl email to hectic-lab 2026-05-23 14:11:27 +00:00
yukkop 0a6e97f456 fix: media-browser: view button 2026-05-23 12:18:05 +00:00
yukkop d220a4588f fix: media-browser: synced files 2026-05-23 12:03:56 +00:00
yukkop 7dc66abc21 feat: media-browser: preview for local files 2026-05-23 11:56:04 +00:00
yukkop e38bf7582d feat: media-browser for matrix 2026-05-23 11:46:58 +00:00
yukkop afd4970cf4 fix: matrix: s3 sync 2026-05-23 11:22:49 +00:00
yukkop ef59f14372 feat: matrix: s3 object storage 2026-05-23 09:59:34 +00:00
yukkop d86eeae4a5 fix?: support bot 2026-05-23 08:50:10 +00:00
yukkop 4d7908de4b chore: hecitc-lab: add lvgkcfjl to matrix 2026-05-23 08:49:51 +00:00
yukkop 20b12d42a8 feat: hectic-lab: matrix 100mb max file 2026-05-23 06:47:34 +00:00
yukkop 51d40841ce feat: hectic-lab: users for matrix 2026-05-22 21:19:52 +00:00
yukkop 09ed045da9 feat: module: ~matrix 2026-05-22 20:13:47 +00:00
yukkop 56881b766a feat: +package merge-archive 2026-05-22 07:45:47 +00:00
yukkop 19b95e707d feat: deploy: 1 no tty option 2026-05-05 20:19:52 +00:00
yukkop e71886c34e feat: lab: +models 2026-05-04 10:19:38 +00:00
yukkop da999fc37b feat: lab: configure olama models 2026-05-04 05:41:25 +00:00
yukkop f9c292643d Merge branch 'master' of github.com:hectic-lab/util.nix 2026-05-03 22:07:13 +00:00
yukkop bcdc8f0836 feat: lab: configure olama 2026-05-03 22:06:57 +00:00
yukkop 66722bd8a4 fix: db-tool: NO_TTY 2026-05-03 15:35:22 +00:00
yukkop 1aaf21e9c0 fix: posix-helpers 2026-05-03 05:23:23 +00:00
yukkop 4204d68eed fix(db-tool): prevent fd leak from logger to long-running daemons
The hectic logger opens fd 3 as a dup of stderr. Child processes inherit
this fd, and daemonized PostgreSQL/PostgREST keeping it open prevents the
terminal from returning to the prompt after the spawning script exits.

- Add with_closed_fds helper that runs commands in a subshell with fds
  3-9 redirected to /dev/null
- Inline the helper into both database and postgres-init builds
- Wrap pg_ctl start and postgrest with the helper
2026-05-03 03:14:15 +00:00
yukkop 4378b13877 feat: windows-devshell 2026-05-02 15:31:26 +00:00
yukkop c041f2e88d fix: linux-devshell for ubuntu 2026-05-01 23:11:23 +00:00
yukkop 61861ff170 fix: linux-devshell from root 2026-05-01 22:56:03 +00:00
yukkop d64314d68b chore: fix nixpkgs 25.05 deprecation warnings 2026-05-01 22:26:29 +00:00
yukkop 35e35980b4 test: fix: . 2026-05-01 22:19:43 +00:00
yukkop 0a54a41670 fix: wsl system 2026-05-01 21:57:41 +00:00
yukkop d5d4d5a5c3 test: arch 2026-05-01 21:40:54 +00:00
yukkop ff0766519c test: linux-devshell 2026-05-01 21:13:15 +00:00
yukkop 87b57d5011 feat: linux-devshell: init 2026-05-01 20:39:38 +00:00
yukkop e46a4bf8a1 fix: db-tool: envcontent 2026-05-01 12:31:44 +00:00
yukkop 8b7a9b5141 fix(db-tool): typo hydate.stdout.log -> hydrate.stdout.log 2026-04-30 23:10:46 +00:00
yukkop d3cdbdf3e2 docs(postgres-hooks): document hectic bundle + responsibility split
Add lib/hook/sql/README.md describing bundle layout, apply order, Nix API
(self.lib.hectic.*), shell helper contract, and the steps for adding a new
SQL file. Rewrite db-tool README's hectic section: drop stale
PG_HECTIC_INHERITANCE / HECTIC_INHERITANCE_SQL env vars, add
HECTIC_DOTENV_FILE, document the postgres-init / migrator init / database
hydrate responsibility split.
2026-04-30 22:12:18 +00:00
yukkop 1a209f6960 test(postgres-hooks): retarget hectic bundle tests to migrator init + db-tool hydrate
Move postgres-init-hectic-inheritance test (13 cases) to
migrator/init-hectic-bundle since the bundle is now applied by `migrator init`
instead of `postgres-init`. Drop init-migrator-with-inherits since
`--inherits` is now a deprecation warning, not an error. Add db-tool
hydrate-hook test (5 cases) covering --no-hook skip, default apply,
idempotency, and HECTIC_DOTENV_FILE. Augment init-migrator with
hectic.version and hectic.secret table assertions.
2026-04-30 22:12:09 +00:00
yukkop 3d5e3fdb36 feat: postgres hooks 2026-04-30 21:59:53 +00:00
yukkop bf7ee34716 feat: start impliment postgres hooks 2026-04-30 21:36:22 +00:00
yukkop 70c44f1fa7 feat: include hectic-landing to hectic 2026-04-30 19:33:21 +00:00
yukkop 2eaa568f5b feat(db-tool): hectic-inheritance: add hectic.immutable + diff coverage
Add a hectic.immutable parent table. Tables inheriting it get auto-attached
BEFORE INSERT/UPDATE/DELETE/TRUNCATE row+statement triggers that block DML
unless the session sets hectic.migration_mode='on' (intended use: SET LOCAL
inside a migration transaction). Same exemptions as the rest of the bundle
apply (hectic schema, partitions, temp tables, GUC-excluded schemas).

database diff now appends an --- IMMUTABLE TABLE DATA --- section to its
output, with per-table unified row diffs of every table inheriting
hectic.immutable, surfacing drift in 'frozen' reference data alongside schema
drift. Subcommand exits non-zero when either schema or data differs.

Test postgres-init-hectic-inheritance extended to 10 cases covering
immutable triggers, DML blocked outside migration_mode, SET LOCAL allowing
DML inside a transaction, GUC not leaking past COMMIT, and TRUNCATE under
migration_mode.
2026-04-30 16:10:38 +00:00
yukkop 31d2994997 feat(db-tool): postgres-init: apply hectic-inheritance by default
Flip PG_HECTIC_INHERITANCE default 0 -> 1. Set PG_HECTIC_INHERITANCE=0 to opt out.
2026-04-30 15:48:33 +00:00
yukkop 58e9aa8fad feat(db-tool): hectic-inheritance: bootstrap hectic.created_at / hectic.updated_at inheritance hook
Adds a SQL bundle plus event triggers that enforce `INHERITS (hectic.created_at)`
on every user CREATE TABLE and auto-attach a BEFORE UPDATE row trigger when a
table inherits `hectic.updated_at`. Always-exempt: `hectic`, `information_schema`,
`pg_*`, declarative partitions, temp tables. Per-DB opt-out via the GUC
`hectic.inheritance_extra_excluded_schemas`.

Exposed three ways:
  * `pkgs.hectic.hectic-inheritance` — derivation with the SQL at
    $out/share/hectic/hectic-inheritance.sql
  * `self.lib.hecticInheritance.{sql,path}` — pkgs-free Nix surface
  * `postgres-init` opt-in via `PG_HECTIC_INHERITANCE=1` (HECTIC_INHERITANCE_SQL
    overrides the default)

Test postgres-init-hectic-inheritance covers all six branches: bootstrap,
non-inheriting reject, accepting inheritance, auto updated_at trigger fires,
GUC exclusion, declarative partition exemption.
2026-04-30 15:16:57 +00:00
yukkop 7d5300853b fix(db-tool): postgres-init: createdb on reuse when target DB missing
Previously when PG_REUSE=1 and PG_VERSION existed but the target database had
never been successfully created (e.g. devshell exited mid-init in a prior run),
postgres-init skipped createdb and the subsequent psql connection failed with
'database "<db>" does not exist'.

Now on reuse path we probe pg_database and create the target DB if missing,
making postgres-init fully idempotent across stale-state recovery.

Adds postgres-init-reuse-missing-db test.
2026-04-30 12:16:09 +00:00
yukkop e732ecb878 feat(db-tool): expose overridable postgresql arg and PG_CONF_FILE env
Wrap db-tool, postgres-init, postgres-cleanup with lib.makeOverridable so
consumers can inject extension-enabled PostgreSQL via .override { postgresql = ...; }.
Add PG_CONF_FILE: when set, replaces script-generated postgresql.conf entirely
(runtime port and unix_socket_directories still appended/overridden).
2026-04-30 12:00:08 +00:00
yukkop 705f5bbd01 fix(db-tool): postgres-init: always invoke main so sourcing exports vars
Previous basename guard only ran the function when invoked as the
postgres-init binary, but consumer shellHooks source the script to
inherit POSTGRESQL_HOST/PGURL/etc. Sourcing left the function defined
but never called, leaving WIPE_PGURL/PGURL unset and breaking devshell
entry in proxydoe and ruststats.
2026-04-30 11:37:19 +00:00
yukkop 06a5d16ee3 fix(package): db-tool: call postgres-init/cleanup binaries instead of local devshell scripts
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-30 10:10:11 +00:00
yukkop 5713eaad14 fix(package): db-tool: default PGURL to empty in dispatch to fix pull_staging exit=3 contract 2026-04-30 09:24:58 +00:00
yukkop b5dcbf08a1 feat(\db-tool\): introduce unified db-tool package with postgres harness and tests (T0-T8) 2026-04-30 09:06:44 +00:00
yukkop 395bddee94 Merge branch 'master' of github.com:hectic-lab/util.nix 2026-04-29 09:57:40 +00:00
snuff d8312b69b8 feat?(neuro): create aeronautics minecraft server 2026-04-28 14:04:14 +03:00
yukkop 62757df384 fix(package): sentinèlla: use dig(1) instead of host(1) which is not in bind.dnsutils 2026-04-27 17:30:01 +00:00
yukkop f2ea372742 debug(package): sentinèlla: surface host(1) and SRV-parse failures in watcher 2026-04-27 17:28:01 +00:00
yukkop f158a80492 fix(package): sentinèlla: use ip(8) for local-IP detection (inetutils hostname lacks -I); harden numeric parsing 2026-04-27 17:21:00 +00:00
yukkop 0c81c0454d feat(package): sentinèlla: switch peer discovery from A records to SRV records 2026-04-27 14:08:23 +00:00
yukkop 7268ec3704 fix(package): sentinèlla: probe — use socat EXEC pipes to avoid pty CR/LF mangling 2026-04-27 13:34:52 +00:00
yukkop 0dfbee6472 feat: sentinella: some 2026-04-27 12:58:36 +00:00
yukkop 50d682d6f7 fix: hectic-lab: email name 2026-04-26 22:22:31 +00:00
yukkop 3ca4d4fb86 feat: sentinella: update 2026-04-26 22:22:15 +00:00
yukkop 4fae68764c feat(hectic-lab): enable sentinèlla watcher service
Enable the p2p watcher alongside the existing probe. Peers are discovered
via DNS name peers.hectic-lab.com. TG credentials are loaded from a SOPS
secret sentinèlla/watcher/environment (to be populated with TG_TOKEN and
TG_CHAT_ID).
2026-04-26 22:04:35 +00:00
yukkop 6035397e9b feat(sentinèlla): p2p topology with DNS peer discovery
- Replace central sentinel with watcher: each node polls peers discovered
  via a single DNS name with multiple A records (e.g. peers.sentinella.com)
- Auto-detect own IPs via hostname -I; SELF env var available as optional
  override for NAT/floating-IP setups
- Fix Basic Auth bug in router.sh: compare tok against AUTH_TOKENS instead
  of unset $USER/$PASS
- Rename sentinel binary to watcher; drop unused shellplot dep
- Add inetutils to watcher runtime deps for hostname -I
- Update NixOS module: replace sentinel options with watcher p2p options
  (peersDns, self, peersPort, peersScheme, pollingIntervalSec)
- Add sentinèlla test suite: probe-status-empty, probe-disk, watcher-state-file
2026-04-26 21:54:07 +00:00
yukkop 249d027f6b docs: +plan 2026-04-26 14:11:37 +00:00
yukkop 3343a8be6e Merge branch 'merge-hectic-lab' 2026-04-26 13:52:17 +00:00
yukkop b8fc0971c6 fix: hectic-lab: something 2026-04-26 13:48:36 +00:00
yukkop 406acf662e feat: hectic-lab: mail server 2026-04-26 10:33:28 +00:00
yukkop 970c1214af chore: config 2026-04-25 15:18:01 +00:00
yukkop 0316d0b535 zalupa 2026-04-18 20:53:44 +00:00
yukkop 305efdbbae feat: work on profinity 2026-04-14 08:43:56 +00:00
yukkop 6e72841337 docs: neuro: router forwarding 2026-03-31 20:09:27 +00:00
yukkop 29acdc9ad6 feat: bfs: add xray package 2026-03-31 19:59:40 +00:00
yukkop 04bc83e004 chore: bfs: add 3 cert 2026-03-23 19:47:54 +00:00
yukkop 2513b76658 some jitsi shit 2026-03-23 19:22:40 +00:00
yukkop 62b7742ef0 some 2026-03-22 06:55:10 +00:00
yukkop f23720e151 feat: wsl: some things 2026-03-15 10:59:19 +00:00
yukkop b9730784ba fix: deploy: logs 2026-03-01 09:36:22 +00:00
yukkop 577c167d5a feat: migrator: +multifiles migrations 2026-02-28 21:19:29 +00:00
yukkop 525c6a220b chore: 2026-02-28 20:25:13 +00:00
yukkop bf41ee6e0a feat: merge own wsl config 2026-02-28 01:57:42 +00:00
yukkop cfcd6647ae feat: init hectic-lab merge 2026-02-27 19:57:44 +00:00
yukkop 4e2264dc03 feat: neuro: some 2026-02-27 12:53:51 +00:00
yukkop 3414edf2a8 fix: some warnings 2026-02-23 18:27:36 +00:00
yukkop 0d7b84e3bc refactor: derustification 2026-02-20 17:36:20 +00:00
yukkop bea9ca602e fix: +fixed nixpkgs 2026-02-20 17:30:36 +00:00
yukkop d8a51cd389 refactor: update to 25.11 2026-02-20 17:18:38 +00:00
yukkop f93e12accd feat: nixvim + zsh, fix: warnings 2026-02-20 16:41:18 +00:00
yukkop 63a7fd716f apply patch from neuro 2026-02-20 12:51:32 +00:00
yukkop 799ad9a15d feat: +neuro devshell 2026-02-20 12:06:55 +00:00
yukkop 9010b2678e refactor: deploy: update logs 2026-02-20 12:04:15 +00:00
yukkop 26bdecb8e0 fix: migrator: more accurate check for hectic.migrator isntalled 2026-02-17 16:50:08 +00:00
yukkop ff87e8c411 chore: something about hemar tree sitter 2026-02-17 16:17:47 +00:00
yukkop d7ada535d4 feat: +pol bfs server 2026-02-17 16:13:58 +00:00
yukkop 7490ef2fe9 fix: nvim-pager close all pages on q 2026-02-17 08:55:41 +00:00
yukkop a7cb54d04c hz 2026-02-14 18:35:04 +00:00
yukkop 0f4f6a6339 hz 2026-02-14 15:59:13 +00:00
yukkop 043b333654 feat: migrator: colorize help in migrator.sh 2026-02-06 08:38:36 +00:00
yukkop 683370a71c feat: stong &2 in log.sh 2026-02-05 06:50:54 +00:00
yukkop 2724295bcf feat(nixos): postgresql: +settings script 2026-01-29 18:56:22 +00:00
yukkop 78a3329271 Merge branch 'master' of github.com:hectic-lab/util.nix 2026-01-16 16:16:43 +00:00
yukkop a61f2cfe5e feat(nixos): +zombro hardware 2026-01-16 16:16:13 +00:00
ISNUFFI ed5c1cc1af chore: removed excess variables 2026-01-13 18:13:52 +03:00
yukkop 8ffe9ad244 iso 2026-01-13 15:04:33 +00:00
yukkop bf72ac7e97 feat(template): +nodejs 2026-01-04 20:40:43 +00:00
yukkop 4a9bb42015 feat(nixos): some moves to nixos server 2025-12-28 19:18:15 +00:00
yukkop 96782b7b51 feat(nixos): +neuro system 2025-12-26 20:04:42 +00:00
yukkop 3d84864382 fix(package): migrator: sqlite transactions 2025-12-23 19:31:44 +00:00
yukkop cb96de6125 fix(package): migrator: handle sqlite posible settings 2025-12-23 18:30:26 +00:00
yukkop 686a794a69 fix(pakcage): migrator: -z -> +x 2025-12-23 18:18:51 +00:00
yukkop 2ffe187b12 fix(package): migrator: awk dependency 2025-12-21 13:19:46 +00:00
yukkop 89ab7d56af feat(pakcage): migrator: add dependency 2025-12-20 01:19:40 +00:00
yukkop 01f13723a8 feat(package): migrator: up to latest 2025-12-18 00:45:09 +00:00
yukkop 9c25e82c79 fix(package): migrator: uncomplite RAISE on sqlite 2025-12-17 03:28:03 +00:00
yukkop 956239ab79 feat(package): migrator: ! sqlite support 2025-12-17 03:24:59 +00:00
yukkop bb2ae34758 feat(package): migrator: mvp 2025-12-16 17:28:36 +00:00
yukkop 13fdfac2ef test(hemar): update parser test to match better paths 2025-12-09 14:21:03 +00:00
yukkop 93a7636c61 fix(package): hemar: paths better handling 2025-12-09 14:13:34 +00:00
yukkop 6fb3ee1af8 test(hemar): many tests but not pass 2025-12-09 13:49:43 +00:00
yukkop 73dc9aee9e feat(package): hemar: json_escape() 2025-12-09 03:47:27 +00:00
yukkop 71082b6414 feat(package): hemar: interpolation 2025-12-08 23:47:33 +00:00
yukkop 65a96c860b feat(package): accord: some cli things 2025-12-08 01:55:33 +00:00
yukkop 9b9254bb93 feat(nixos): bgs: vless 2025-12-07 03:40:07 +00:00
yukkop 1beb980b95 refactor(nixos): bfs: some secrets 2025-12-07 02:25:29 +00:00
yukkop 082b80f252 feate(package): accord: init 2025-12-06 23:49:14 +00:00
yukkop f4a65afb01 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-12-06 04:47:46 +00:00
yukkop 373a3fe3f0 fix(package): migrator: index_of subshell issue 2025-12-06 04:47:31 +00:00
snuff 8bbc0eda8f fix: matrix works with self-hosted element 2025-12-06 00:36:01 +03:00
yukkop 3b506a7888 fix: bfs nixosConfiguration check 2025-12-05 21:01:53 +00:00
yukkop 9a5bfa3f80 feate(nixos) bfs: matrix, element, bla bla bla 2025-12-05 19:36:53 +00:00
yukkop 5177f9a1f6 feat(nixos): bfs: fix: element-rtc 2025-12-05 18:17:41 +00:00
yukkop 49bf4c6d91 feat(nixos): bfs: +matrix, very unsave 2025-12-05 16:30:34 +00:00
yukkop 9d914ed863 feat(nixos): netherlands xray 2025-12-05 00:56:08 +00:00
yukkop 3e6bf6ce7f feat(nixos): xray 2025-12-04 21:03:01 +00:00
yukkop b786098867 feat(package): migrator: something 2025-12-04 21:02:38 +00:00
yukkop dfa76b2a19 feat(package): hemar: more grammar parsing 2025-11-30 13:31:40 +00:00
yukkop 6dad0d95fb fix(nixos): generic postgresql 2025-11-29 13:48:57 +00:00
yukkop 78f4777141 feat(package): hemar: some parser work 2025-11-29 00:30:27 +00:00
yukkop 48077aaccc feat(package): hemar: add indexes to tree-sitter 2025-11-28 10:29:59 +00:00
yukkop 4b4ea7a1d2 feat(package): hemar: add tree sitter hightlights 2025-11-27 19:45:12 +00:00
yukkop d1353d3a36 fix(package): hemar: treesitter language name 2025-11-27 19:15:35 +00:00
yukkop 8048c4adff fix(package): hemar: treesitter language name 2025-11-27 19:13:07 +00:00
yukkop 345f94742e feat(package): hemar: tree sitter grammar 2025-11-27 17:53:49 +00:00
yukkop 777d48bf3d feat(package): hemar: antlr grammar, but still does not work 2025-11-26 14:48:51 +00:00
yukkop 8a08272f68 feat(package): hemar: I think I was wrong 2025-11-25 15:45:02 +00:00
yukkop 4729770b59 feat(package): hemar: some stages of parsing 2025-11-25 13:19:45 +00:00
yukkop f5d412997e feat(package): hemar: conected to nix config 2025-11-24 14:33:45 +00:00
yukkop 783cbe20e8 fix(package): migrator: init migration 2025-11-22 11:50:42 +00:00
yukkop 9a7e7d677a feat(package): migrator: some migrate up works and init 2025-11-17 16:26:25 +00:00
yukkop 5b1f05589d test: fix: migrator: multifile test 2025-11-17 01:57:32 +00:00
yukkop 00f05cc3db feat(legacy): helpers: allow HECTIC_NAMESPACE be empty 2025-11-16 16:56:26 +00:00
yukkop bb99ef9a8a feat(legacy): helpers: update log, unefficient but works 2025-11-16 16:38:38 +00:00
yukkop 33cdebb64a refactor: remove nix warnings 2025-11-14 21:13:32 +00:00
yukkop 88573ea4aa feat(package): nbt2json: pack into nix 2025-11-14 21:12:41 +00:00
yukkop 4ae460f6e3 fix(template): correct hectic utils input 2025-11-14 15:53:12 +00:00
yukkop 5a8fd0a275 feat(package): voronoi: simple voronoi algoritm realization 2025-11-14 15:51:28 +00:00
yukkop 6204ba5abb feat(package): migrator: ! migration up 2025-11-14 15:50:55 +00:00
yukkop 00bbd42cab feat(package): migrator: init migration table 2025-11-04 13:21:15 +00:00
yukkop 111db5a1ca feat(package): migrator: create new migration 2025-11-04 12:15:29 +00:00
yukkop 9d97fd8d87 refactor(package): pull out all realization from default.nix 2025-11-03 20:08:20 +00:00
yukkop 94fbbdf0d9 fix(package): in default.nix migratior -> migrator 2025-11-03 19:15:57 +00:00
yukkop 43dc59a04f feat: +checks flake output 2025-11-03 19:02:50 +00:00
yukkop e3167ce6e5 feat(package): +migrator 2025-11-03 18:55:35 +00:00
yukkop 532f4b0901 feat(legacy): +writeDash +helpers 2025-11-03 18:51:38 +00:00
yukkop ad00891e6b fix(nixos): hardwares 2025-10-30 09:38:58 +00:00
yukkop 60bd185fa4 feat(nixos): cloudzy hardware 2025-10-28 16:16:01 +00:00
yukkop f08bd1e702 feat(template): +avarage 2025-10-28 11:41:26 +00:00
yukkop d01674e6a4 feat(package): sentinella: sqlschema 2025-10-28 11:37:22 +00:00
yukkop 39abe553f5 refactor(package): sentinella: +logging 2025-10-25 09:54:57 +00:00
yukkop af5d46b3f0 fix(nixos): module: wrong separator 2025-10-25 07:21:07 +00:00
yukkop 450ba9229b feat(package): sentinel: some changes 2025-10-25 06:48:48 +00:00
yukkop 9903e83f68 feat(package): deploy: rollback 2025-10-23 14:48:10 +00:00
yukkop 3ee41979bd refactor(nixos): modules: hetzner network config 2025-10-23 11:49:34 +00:00
yukkop b400acd18e some fixes 2025-10-23 11:28:56 +00:00
yukkop dd0d823e36 feat(package): +deploy 2025-10-23 10:22:48 +00:00
yukkop fafc1c4664 refactor(nixos): modules: hetzner hardware 2025-10-22 09:32:41 +00:00
yukkop 3bfc5799c7 feat(nixos): modules: +network configuration to hetzner hardware 2025-10-22 08:20:27 +00:00
yukkop e7423780e8 ruined every thing again 2025-10-21 17:27:09 +00:00
yukkop b3041edbdc ruined every thing 2025-10-20 17:17:08 +00:00
yukkop dbe4dfc2bc feat(package): sentinèlla: sentinel: loging 2025-10-19 15:16:38 +00:00
yukkop abdc808693 feat(nixos): lenovo hardware 2025-10-16 12:22:19 +00:00
yukkop 3139fd1d9f fix: lib systems 2025-10-16 09:52:49 +00:00
yukkop dc6ca7a0a9 fix(nixos): senttinèlla-probe: module args 2025-10-12 08:59:03 +00:00
yukkop 6dd7d112c5 fix: writers & overlays 2025-10-11 14:56:25 +00:00
yukkop 65fa944df8 refactor: legacyPackages 2025-10-10 13:13:03 +00:00
yukkop 7d55064a44 feat(package): onlinepubs: init 2025-10-08 15:58:35 +00:00
yukkop a1b31b1684 fix(package): `sentinèlla one more unset error 2025-10-07 10:55:37 +00:00
yukkop d446b24c47 fix(package): `sentinèlla unset error 2025-10-07 04:02:49 +00:00
yukkop 76e09ce5c3 fix(package): sentinèlla: base64 logic 2025-10-03 19:02:33 +00:00
yukkop 7fd3ba46c9 feat(package): sentinèlla: add auth 2025-10-03 02:30:24 +00:00
yukkop 0eb95197bc feat(package): hemar: continios work 2025-10-01 11:09:09 +00:00
yukkop 38bf9151eb refactor: hemar reboot 2025-09-28 04:44:46 +00:00
yukkop 5dd03a947f feat: appropriate sops dream wrapper 2025-09-12 17:17:29 +00:00
yukkop adbd946f50 fix 2025-09-02 10:33:04 +00:00
yukkop 78dcb215cc fix: stupid error 2025-08-31 16:01:00 +00:00
yukkop 744a8c9cf4 refactor(nixos): module: hectic services config path 2025-08-28 18:17:18 +00:00
yukkop 00e02f328a feat(nixos): module: +support-bot 2025-08-28 15:02:58 +00:00
yukkop 7dadd2290f feat(package): shellplot: created 2025-08-23 16:37:08 +00:00
yukkop 25575a69f3 feat(package): server-health: created 2025-08-23 15:16:15 +00:00
yukkop bb16555d4f feat: add templates 2025-08-05 04:41:27 +00:00
yukkop 492597b7ab fix(package): nix-derivation-hash: purify 2025-07-24 19:33:51 +00:00
yukkop 52cb6b2410 feat(package): nix-derivation-hash 2025-07-23 20:15:53 +00:00
yukkop 6e77853453 feat(lib): shell: local-dir 2025-07-22 21:15:42 +00:00
yukkop 8f4ff74546 feat: service.postgresql module extension 2025-07-22 02:11:32 +00:00
yukkop e74ef30dc6 fix(lib): shell: remove unneccessary ''$ 2025-07-18 03:23:25 +00:00
yukkop 3516d24868 fix(nixos): module: user 2025-07-17 16:53:48 +00:00
yukkop 92def64fea feat(lib): shell: +check-tool 2025-07-17 16:22:31 +00:00
yukkop be3aab9b48 fix(lib): logs: mkdir error work around 2025-07-17 16:17:43 +00:00
yukkop 5cac466f90 feat(nixos): module: +user.yukkop 2025-07-17 16:10:04 +00:00
yukkop 8cbfdcc0ea refactor: consistence 2025-07-17 16:00:59 +00:00
yukkop 3d0fd7a030 refactor: pull out flake devshells 2025-07-17 15:48:20 +00:00
yukkop 33422320a5 refactor: pull out flake packages 2025-07-17 15:28:29 +00:00
yukkop 23d9db27e6 refactor: pull out systems 2025-07-17 15:21:06 +00:00
yukkop 20dcbdfdc4 feat: modules 2025-07-17 13:37:33 +00:00
yukkop 8fb996501c feat: shell modules 2025-07-16 06:36:27 +00:00
yukkop b433fd7ea0 feat: +some python packages 2025-07-06 22:12:14 +00:00
yukkop f0596784a1 feat: remove vmw_pvscsi on aarch64-linux 2025-07-05 23:24:58 +00:00
yukkop 5670f11510 feat(package): slpt: jq dependency 2025-07-03 01:21:44 +00:00
yukkop 0db6ce3dd4 feat(package): update slpt 2025-07-02 16:08:30 +00:00
yukkop 2c16cebf9a feat(package) add slpt 2025-07-01 02:26:34 +00:00
yukkop 497213a2ea fix(package): bolt-unpack update nodejs_22 2025-06-30 23:48:14 +00:00
yukkop c73c887a9c Merge branch 'master' of github.com:hectic-lab/util.nix 2025-06-30 21:00:04 +00:00
yukkop f7fda629f5 refactor: move to nixpkgs 25.05 2025-06-30 20:43:38 +00:00
yukkop 5e112f5df8 fix: plsh 2025-06-30 16:31:04 +00:00
yukkop 510c350b7a fix: remove broken haskell 2025-06-30 16:24:56 +00:00
yukkop 8a13b8c8fd feat: plsh 2025-06-30 16:06:43 +00:00
yukkop 0ab2061776 feat: aliases to system preset 2025-06-11 22:38:54 +00:00
yukkop 8631eac0d4 chore: pager AnsiEsc 2025-06-11 22:25:34 +00:00
yukkop 9c8a3790c5 feat: find a way separate pg extension from overlay 2025-05-22 11:57:09 +00:00
yukkop 70253c9a3a done 2025-05-21 20:00:02 +00:00
yukkop 3b7c4d71e7 checkpoint 2025-05-21 19:50:47 +00:00
yukkop d1b0d7154d test: hemar: fix parse tests according code cahnges 2025-05-19 14:36:37 +00:00
yukkop 7ce48d75f8 fix: hemar: EBUCHIE WHITESPACЫ 2025-05-19 13:59:26 +00:00
yukkop 6c953563d0 fix: hemar: section whitespaces checkpoint 2025-05-19 11:07:10 +00:00
yukkop ef314495ba fix: hemar: include object does not exists 2025-05-18 20:18:54 +00:00
yukkop 1b1deaf721 test: hemar: test all at once 2025-05-18 19:58:04 +00:00
yukkop 746779e952 feat: hemar: included tag 2025-05-18 18:51:21 +00:00
yukkop 5cabf6c04d fix: hemar: section implimentation 2025-05-18 13:20:57 +00:00
yukkop 3493c14686 feat: hemar: section checkpoint 2025-05-18 10:27:38 +00:00
yukkop 19ea8992a0 fix: hemar: jsonb_value_by_path string copy 2025-05-17 17:59:10 +00:00
yukkop a17a20b7d6 chore: hemar: more logs for god of logs 2025-05-17 15:56:46 +00:00
yukkop eabfbbdc95 feat: hemar: separate jsonb_get_by_path form postgresql 2025-05-17 14:30:18 +00:00
yukkop 86d30ea494 chore: forgoten changes 2025-05-17 14:13:11 +00:00
yukkop bcc755d325 test: exec 2025-05-17 13:39:40 +00:00
yukkop 6a2f7cd1fa test: hemar: more tests for god of tests 2025-05-16 01:18:01 +00:00
yukkop 39495115a2 feat: hemar: exec handle the {{ }} inside 2025-05-16 00:37:30 +00:00
yukkop f0d7ca60fe fix: hemar: parser 2025-05-16 00:30:11 +00:00
yukkop 725f6eea30 test: hemar: puh 2025-05-15 23:57:18 +00:00
yukkop 421cc6e870 test: hemar: +20 tests for parsing 2025-05-15 18:54:56 +00:00
yukkop 653330dd48 test: hemar: +10 complex tests 2025-05-15 17:53:45 +00:00
yukkop 54684c0f78 feat: find element in jsonb by path is works!!! 2025-05-15 17:48:28 +00:00
yukkop c28e7fcfd5 feat!: vpizdu 2025-05-15 15:33:53 +00:00
yukkop a770215f0c feat: hemar: handling [] in path 2025-05-14 21:52:01 +00:00
yukkop b3172edbd7 feat: something 2025-05-14 21:30:16 +00:00
yukkop 83029b52ea feat: array with objects 2025-05-14 18:56:42 +00:00
yukkop 56fa799d53 fix: more parsing errors handling, section body fix 2025-05-14 14:09:48 +00:00
yukkop e9ba38f4eb feat!: to the moon 2025-05-14 12:15:45 +00:00
yukkop bcb8afbbf4 feat!: one more checkpoint 2025-05-13 21:56:27 +00:00
yukkop 598eae7e71 feat!: better? 2025-05-13 21:41:37 +00:00
yukkop e260e8f7cc feat!: some section parse issue 2025-05-13 15:44:55 +00:00
yukkop 7f58985c41 refactor: gemar grand rework 2025-05-13 12:12:57 +00:00
yukkop 88327be038 fix: hemar & hectic: nothing 2025-05-13 02:30:15 +00:00
yukkop 609bae352f fix: hemar: unbeliveble, without memmory error? not true 2025-05-13 01:15:43 +00:00
yukkop 5f52fc42af feat!: many work useles with hemar 2025-05-10 11:00:36 +00:00
yukkop 4db5cc171b fix: hectic C: template parse sections 2025-05-04 19:33:52 +00:00
yukkop 9b045b71f9 fix: hectic C: template parse 2025-05-04 13:49:29 +00:00
yukkop e800f11103 refactor: hemar: logs 2025-04-24 00:37:03 +00:00
yukkop e55437b856 feat: hel -> hemar--amend 2025-04-23 12:47:28 +00:00
yukkop cda46a7e13 feat: static page for fun 2025-04-23 12:37:32 +00:00
yukkop cc7de6c0dd feat hectic C: logger log in file 2025-04-23 01:04:49 +00:00
yukkop e25190422b feat hectic C: no asan option 2025-04-22 15:09:47 +00:00
zerosummed 016db3d06a feat(package): +support-bot 2025-04-22 10:00:33 +03:00
yukkop 286555e026 feat: wrapper for pg_dump 2025-04-22 01:13:23 +00:00
yukkop dc93bada89 feat: wrapper for pg_dumpall 2025-04-22 00:44:52 +00:00
yukkop 8857835409 refactor: hectic C: static-asan 2025-04-20 13:38:16 +00:00
yukkop 30778b1955 refactor: hectic C: perfavor 2025-04-20 13:32:22 +00:00
yukkop 791ff77e14 refactor: hectic C: libhectic.so -> hectic.so 2025-04-20 09:57:32 +00:00
yukkop 17acafd50a fix: hel: anus linking 2025-04-20 00:33:34 +00:00
yukkop 7ddceab11c build: hectic C: shared library in build 2025-04-19 23:44:39 +00:00
yukkop 0abd4fc70e fix: hectic: lib building 2025-04-19 23:23:09 +00:00
yukkop f97e451af1 fix: hel: Makefile LIBS 2025-04-19 23:10:13 +00:00
yukkop a9d64fe059 feat: hel: c89... 2025-04-19 23:04:29 +00:00
yukkop 62abe7b846 feat: hel: "" -> <> 2025-04-19 22:38:58 +00:00
yukkop 0c58befc43 feat: hecitc C: add hectic-config 2025-04-19 22:05:56 +00:00
yukkop 2229cf072b feat: hel: render without execute 2025-04-19 20:30:00 +00:00
yukkop 10a0a08dcd fix: overlay postgres extensions 2025-04-19 02:39:10 +00:00
zerosummed 60d977110c fix?(package): postgreact: ~stir the stupid shit till it works [3] 2025-04-18 21:37:23 +03:00
zerosummed aec4876832 fix?(package): postgreact: ~stir the stupid shit till it works [2] 2025-04-17 07:06:34 +03:00
zerosummed 90f588843a fix?(package): postgreact: ~stir the stupid shit till it works [1] 2025-04-17 07:06:31 +03:00
zerosummed fb8fa34ff2 fix?(package): postgreact 2025-04-17 06:22:21 +03:00
zerosummed 29ff6989e5 style: ~apply alejandra 2025-04-17 03:57:54 +03:00
zerosummed 3dfe3d61f6 refactor: postgreact: ~file modes, control file 2025-04-17 03:46:53 +03:00
yukkop e607bf09fa feat: postgreact: hello world 2025-04-15 20:05:53 +00:00
yukkop 1612799fb7 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-04-15 17:04:12 +00:00
yukkop 5055983881 test: hectic C!: templater sectin 2025-04-15 17:03:15 +00:00
yukkop 1da02a7ad6 test: hectic C: templater interpolation 2025-04-15 16:33:49 +00:00
yukkop 6b8e825a4b feat: hectic C: prettify debug strings 2025-04-15 15:53:39 +00:00
yukkop ba6ec7a113 feat: +plhaskell package 2025-04-15 05:51:37 +00:00
yukkop 8697b00cfc feat: hectic C: some sigfault 2025-04-15 04:54:31 +00:00
yukkop d45307336a feat: hectic C: debug to json parser 2025-04-14 19:43:57 +00:00
yukkop 1dcb165abf fix: hectic C: union debug string constructor 2025-04-14 17:27:32 +00:00
yukkop abf1e706b7 feat: hectic C: union in debug string constructor 2025-04-14 16:28:16 +00:00
yukkop 807ea83892 fix: hectic C: impruve cycle detection for debug strig constructor 2025-04-13 23:13:28 +00:00
yukkop 267d0ec464 refactor: hectic C: logger rules and results 2025-04-13 19:38:11 +00:00
yukkop 2f6806185d feat: hectic C: template node debug, but union issue in debug string constructor 2025-04-12 18:25:25 +00:00
yukkop 8981af315c test: hectic C: fix logger test 2025-04-12 15:38:06 +00:00
yukkop 1f25c4fcad feat: hectic C: generic result 2025-04-12 15:21:33 +00:00
yukkop db5b88388e test: hectic C: debug constructor 2025-04-12 01:38:40 +00:00
yukkop 2cb814f054 feat: hectic C: debug constructor impruve 2025-04-12 01:07:20 +00:00
yukkop d7bdc0722f feat: hectic C: debug constructor wroks!!! 2025-04-11 22:34:43 +00:00
yukkop 80eac39b90 feat: hectic C: debug string constructor init 2025-04-11 10:33:20 +00:00
yukkop de08900297 fix: hectic C: template parser config 2025-04-10 14:26:21 +00:00
yukkop b3d040d6fd feat: hectic C: template parser, template to json 2025-04-09 19:52:43 +00:00
yukkop 7c82a5a866 docs: hectic C: macro for reduse boilerplane with code position 2025-04-07 22:55:37 +00:00
yukkop cda65a56eb docs: hectic C: types for template parser 2025-04-07 22:44:05 +00:00
yukkop d04a78de23 feat: hectic C: logging settings 2025-04-07 16:54:26 +00:00
yukkop 98a26f1f61 fix: hectic C: impruve logging guidelines 2025-04-07 05:26:30 +00:00
yukkop 11150151f9 fix: hectic C: impruve logging 2025-04-06 19:18:14 +00:00
yukkop bfb69b06ea fix: hmpl: section blocks 2025-04-06 15:51:21 +00:00
yukkop f151438969 feat: hectic C: debug print 2025-04-06 15:17:26 +00:00
yukkop 14c246de3e feat: hmpl: section test passeeed 2025-04-05 21:31:35 +00:00
yukkop 42f8e839d1 refactor: change packages names 2025-04-05 19:09:13 +00:00
yukkop 686344e356 feat: some python packages 2025-04-05 17:04:29 +00:00
yukkop 34c798b222 fix: c: log colorized with terminal mode 2025-04-04 14:13:54 +00:00
yukkop d5ef2bf551 feat: c: log colorized 2025-04-04 14:07:17 +00:00
yukkop 7258adbcc2 fix: hectic C: func definitions 2025-04-04 02:26:55 +00:00
yukkop c5e9d9c8cb feat: watch: try pager mode.. 2025-04-04 02:09:43 +00:00
yukkop d800266fe0 feat: some nix shit 2025-04-04 01:59:57 +00:00
yukkop 5ab0833d78 feat: watch: paterns 2025-04-03 22:57:55 +00:00
yukkop d680e080ad feat: watch: init 2025-04-03 22:34:50 +00:00
yukkop 35db5e4bf6 test(hectic C): slice 2025-03-27 21:07:00 +00:00
yukkop 3bb2b23be5 refactor(hmpl): fix warnings 2025-03-24 19:27:11 +00:00
yukkop 4280f0df95 refactor(hectic C): fix warnings 2025-03-24 14:21:00 +00:00
yukkop d39ed7f9fe refactor(hmpl,hectic): warnings 2025-03-24 13:36:31 +00:00
yukkop 972e2d2968 refactor(hmpl): eval 2025-03-22 19:45:17 +00:00
yukkop f64636fe9e test(hmpl): render interpolation tags with prefix 2025-03-22 15:08:48 +00:00
yukkop 546a35f1d2 test(hmpl): render interpolation tags 2025-03-22 14:56:03 +00:00
yukkop aeea41b0e1 fix: json eval 2025-03-22 04:15:58 +00:00
yukkop 27e42e9010 fix: segfalt in json_get_object_item 2025-03-22 03:18:34 +00:00
yukkop bcaca5038d feat(hectic C): json api 2025-03-22 01:22:36 +00:00
yukkop aff923d470 refactor(hmpl): rewrite via arenas 2025-03-21 15:29:00 +00:00
yukkop 06e5fdf1ab fix(hmpl): memory leak 2025-03-21 13:33:01 +00:00
yukkop c4d14e5aa2 feat(hmpl): render simple placeholder with memory leak of course 2025-03-21 04:00:23 +00:00
yukkop 9438280750 fix(libhectic): arena allocators 2025-03-21 02:54:47 +00:00
yukkop 4f2391e9b5 feat(libhectic): arenas 2025-03-21 01:53:02 +00:00
yukkop 647155f859 test: fix: test raise generic 2025-03-20 18:47:44 +00:00
yukkop c91b52b448 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-03-20 18:16:04 +00:00
yukkop 18b36e43b7 test: libhectic 2025-03-20 18:15:52 +00:00
zerosummed c22a769e50 feat!: makeEnvironment: +strip prefix, ~rename to readEnvironment 2025-03-20 14:00:10 +03:00
yukkop a7e754f48d feat: min C writer 2025-03-19 01:44:19 +00:00
yukkop 1f1f3c7211 Merge branch 'feat/pg-neo-migration' of github.com:hectic-lab/util.nix into feat/pg-neo-migration 2025-03-19 01:43:46 +00:00
yukkop 3f9d8902d5 feat: C writer 2025-03-19 01:09:47 +00:00
yukkop 58fc204f46 feat(c): extrude lib c 2025-03-12 23:19:45 +01:00
yukkop cbba9016cd refactor: stir some shit around again 2025-03-07 00:40:00 +00:00
yukkop 0bd5bb65f6 refector(pg-neo-migration): stir some shit around trust me it's fine 2025-03-06 16:07:53 +01:00
yukkop cc7bd6b1a2 feat(pg-neo-migration): init 2025-03-05 19:13:00 +00:00
yukkop e8e2890171 feat(pg-schema): init 2025-03-02 10:48:52 +00:00
yukkop 4d1c210986 fix(pg-migration): db_url fetch 2025-02-16 17:21:02 +00:00
yukkop 3e790cd18a fix(pg-migration): postgres build inputs 2025-02-16 17:03:59 +00:00
yukkop d02f559635 feat(pg-migration): variable providing for migrations 2025-02-16 15:58:31 +00:00
yukkop f3bbbeb12d feat: inheritance for migration table 2025-02-16 14:21:20 +00:00
yukkop c3cdf9858a feat: postgres extension pg_smtp_client 2025-02-16 10:32:37 +00:00
zerosummed 7cc4c6292b fix: nix check errors 2025-02-14 21:27:48 +03:00
zerosummed e1c484b1c0 style: format (alejandra) 2025-02-14 21:06:51 +03:00
5994 changed files with 743 additions and 739666 deletions
-29
View File
@@ -1,29 +0,0 @@
name: runner nix smoke
on:
workflow_dispatch:
push:
branches:
- master
paths:
- .gitea/workflows/runner-nix-smoke.yaml
- flake.lock
- flake.nix
- infra/gitea-runners/**
jobs:
smoke:
name: nix label smoke
runs-on: nix
steps:
- name: Nix version and cache configuration
run: |
set -eu
nix --version
nix config show substituters
nix config show trusted-public-keys
- name: Repository flake evaluation
run: |
set -eu
nix flake check --no-build
-31
View File
@@ -1,31 +0,0 @@
name: runner ubuntu smoke
on:
workflow_dispatch:
push:
branches:
- master
paths:
- .gitea/workflows/runner-ubuntu-smoke.yaml
- infra/gitea-runners/**
jobs:
smoke:
name: ubuntu-latest label smoke
runs-on: ubuntu-latest
steps:
- name: Basic runner information
run: |
set -eu
echo "hello from gitea runner"
uname -a
- name: Docker smoke when available
run: |
set -eu
if command -v docker >/dev/null 2>&1; then
docker version --format 'docker client={{.Client.Version}} server={{.Server.Version}}'
docker run --rm hello-world
else
echo "docker command not available; skipping Docker smoke"
fi
-9
View File
@@ -47,15 +47,6 @@ creation_rules:
- *hectic-lab-server - *hectic-lab-server
- *umbriel-bfs - *umbriel-bfs
- path_regex: sus/gitea-runners.yaml$
key_groups:
- age:
- *nrv
- *yukkop
- *yukkop-alt
- *hectic-lab-server
- *umbriel-bfs
- path_regex: sus/matrix-cluster.yaml$ - path_regex: sus/matrix-cluster.yaml$
key_groups: key_groups:
- age: - age:
-21
View File
@@ -1,21 +0,0 @@
{
flake,
self,
inputs,
system ? "aarch64-darwin",
...
}: let
name = builtins.baseNameOf ./.;
in inputs.nix-darwin.lib.darwinSystem {
inherit system;
specialArgs = { inherit flake self inputs; };
modules = [
inputs.home-manager.darwinModules.home-manager
{
networking.hostName = name;
nixpkgs.hostPlatform = system;
nixpkgs.overlays = [ self.overlays.default ];
}
./${name}.nix
];
}
-177
View File
@@ -1,177 +0,0 @@
{
flake,
pkgs,
lib,
...
}: let
name = "yukkop";
in {
system.primaryUser = name;
nix.settings.experimental-features = "nix-command flakes";
programs.zsh.enable = true;
services.openssh.enable = true;
users.users.${name} = {
home = "/Users/${name}";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJf9ljuqny71bZJokebK4Ybfml0MFMCkApS+tbMdBudp u0_a472@localhost"
];
};
environment.systemPackages = with pkgs; [
aerospace
git
moreutils
neovim
tmux
];
launchd.user.agents.aerospace = {
serviceConfig = {
ProgramArguments = [
"${pkgs.aerospace}/Applications/AeroSpace.app/Contents/MacOS/AeroSpace"
];
RunAtLoad = true;
KeepAlive = true;
StandardOutPath = "/tmp/aerospace.out.log";
StandardErrorPath = "/tmp/aerospace.err.log";
};
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.sharedModules = [
(flake + "/home/module/program/tmux.nix")
];
home-manager.users.${name} = {
home.stateVersion = "25.11";
home.packages = with pkgs; [
iproute2mac
jujutsu
ripgrep
];
programs.git = {
enable = true;
lfs.enable = true;
settings = {
user.name = name;
user.email = "hectic.yukkop@gmail.com";
push.autoSetupRemote = true;
init.defaultBranch = "master";
};
};
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
history = {
size = 10000;
path = "$HOME/.zsh/.zsh_history";
};
shellAliases = {
drs = "darwin-rebuild switch --flake ~/pj/hearth#'yukkop|aarch64-darwin'";
nv = "nvim";
tmux = "tmux a";
};
initContent = ''
export PATH=/Users/yukkop/.opencode/bin:$PATH
'';
};
xdg.configFile."aerospace/aerospace.toml".text = ''
start-at-login = false
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true
default-root-container-layout = 'tiles'
default-root-container-orientation = 'auto'
accordion-padding = 30
on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
automatically-unhide-macos-hidden-apps = false
[exec]
inherit-env-vars = true
[exec.env-vars]
PATH = '/run/current-system/sw/bin:/etc/profiles/per-user/yukkop/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:''${PATH}'
[gaps]
inner.horizontal = 8
inner.vertical = 8
outer.left = 8
outer.bottom = 8
outer.top = 8
outer.right = 8
[mode.main.binding]
alt-enter = 'exec-and-forget open -n /System/Applications/Utilities/Terminal.app'
alt-slash = 'layout tiles horizontal vertical'
alt-comma = 'layout accordion horizontal vertical'
alt-f = 'fullscreen'
alt-h = 'focus left'
alt-j = 'focus down'
alt-k = 'focus up'
alt-l = 'focus right'
alt-shift-h = 'move left'
alt-shift-j = 'move down'
alt-shift-k = 'move up'
alt-shift-l = 'move right'
alt-minus = 'resize smart -50'
alt-equal = 'resize smart +50'
alt-1 = 'workspace 1'
alt-2 = 'workspace 2'
alt-3 = 'workspace 3'
alt-4 = 'workspace 4'
alt-5 = 'workspace 5'
alt-6 = 'workspace 6'
alt-7 = 'workspace 7'
alt-8 = 'workspace 8'
alt-9 = 'workspace 9'
alt-shift-1 = 'move-node-to-workspace 1'
alt-shift-2 = 'move-node-to-workspace 2'
alt-shift-3 = 'move-node-to-workspace 3'
alt-shift-4 = 'move-node-to-workspace 4'
alt-shift-5 = 'move-node-to-workspace 5'
alt-shift-6 = 'move-node-to-workspace 6'
alt-shift-7 = 'move-node-to-workspace 7'
alt-shift-8 = 'move-node-to-workspace 8'
alt-shift-9 = 'move-node-to-workspace 9'
alt-tab = 'workspace-back-and-forth'
alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'
alt-shift-semicolon = 'mode service'
[mode.service.binding]
esc = ['reload-config', 'mode main']
r = ['flatten-workspace-tree', 'mode main']
f = ['layout floating tiling', 'mode main']
b = ['balance-sizes', 'mode main']
backspace = ['close-all-windows-but-current', 'mode main']
alt-shift-h = ['join-with left', 'mode main']
alt-shift-j = ['join-with down', 'mode main']
alt-shift-k = ['join-with up', 'mode main']
alt-shift-l = ['join-with right', 'mode main']
'';
};
system.stateVersion = 6;
}
-1
View File
@@ -8,7 +8,6 @@
haskell = import ./haskell.nix { inherit self system pkgs; }; haskell = import ./haskell.nix { inherit self system pkgs; };
neuro = import ./neuro.nix { inherit self system pkgs; }; neuro = import ./neuro.nix { inherit self system pkgs; };
xmpp = import ./xmpp.nix { inherit self system pkgs; }; xmpp = import ./xmpp.nix { inherit self system pkgs; };
gitea-runners = import ./gitea-runners.nix { inherit pkgs; };
default = pkgs.mkShell { default = pkgs.mkShell {
buildInputs = buildInputs =
(with self.packages.${system}; [ (with self.packages.${system}; [
-122
View File
@@ -1,122 +0,0 @@
{ pkgs, ... }: let
opentofuUnstable = "github:NixOS/nixpkgs/nixos-unstable#opentofu";
tofu = pkgs.writeShellScriptBin "tofu" ''
exec ${pkgs.nix}/bin/nix run ${opentofuUnstable} -- "$@"
'';
giteaRunnersSetup = pkgs.writeShellScriptBin "gitea-runners-setup" /* sh */ ''
cat <<'EOF'
Gitea runners setup checklist
Tools available in this shell:
tofu, kubectl, kustomize, kubeconform, sops, age, awscli2, hcloud, tea,
docker, skopeo, go-containerregistry, jq, yq-go, curl, git, openssh, nix
Environment expected before real deploy/apply:
TF_VAR_hcloud_token
TF_VAR_ssh_public_key
TF_VAR_ssh_private_key
S3 backend credentials and endpoint access
a matching SOPS age identity for sus/gitea-runners.yaml
kubectl access to the target cluster
a concrete registry digest for the pushed Nix-capable runner image if enabling
the nix label
OpenTofu validation gate:
tofu version
tofu -chdir=infra/gitea-runners/opentofu validate
Nix image build/publish/digest gate:
nix build .#gitea-runner-nix-image
publish the archive, then pin the registry-reported digest in the runner label
mapping
nix:docker://gitea.hectic-lab.com/hectic-lab/gitea-runner-nix-image@sha256:<registry-digest>
SOPS token Secret creation gate:
kubectl apply -f infra/gitea-runners/k8s/namespace.yaml
umask 077
token_file=$(mktemp /tmp/gitea-runner-token.XXXXXX)
trap 'rm -f "$token_file"' EXIT
sops -d --extract '["gitea"]["hectic-lab"]["org-runner-registration-token"]' sus/gitea-runners.yaml > "$token_file"
kubectl -n gitea-runners create secret generic gitea-runner-token \
--from-file=token="$token_file" \
--dry-run=client \
-o yaml | kubectl -n gitea-runners apply -f -
Cluster provision gate:
tofu -chdir=infra/gitea-runners/opentofu init
tofu -chdir=infra/gitea-runners/opentofu validate
tofu -chdir=infra/gitea-runners/opentofu plan -out=.sisyphus/evidence/task-12-deploy.plan
tofu -chdir=infra/gitea-runners/opentofu apply .sisyphus/evidence/task-12-deploy.plan
export KUBECONFIG="$(tofu -chdir=infra/gitea-runners/opentofu output -raw kubeconfig_path)"
Kubernetes apply gate:
kubectl config current-context
kubectl get nodes -o wide
kubectl get sc
kubectl apply -k infra/gitea-runners/k8s
Verification commands:
kubectl -n gitea-runners get statefulset gitea-runner
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get events --sort-by=.lastTimestamp | tail -n 50
kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200
Main blockers and gates:
do not run tofu apply without all external inputs
do not apply the k8s overlay until the gitea-runner-token Secret exists
do not enable the nix label until the image has been published with a concrete digest
do not print, load, or require secrets on shell entry
EOF
'';
in pkgs.mkShell {
name = "gitea-runners";
buildInputs = [
tofu
giteaRunnersSetup
pkgs.nix
pkgs.kubectl
pkgs.kustomize
pkgs.kubeconform
pkgs.sops
pkgs.age
pkgs.awscli2
pkgs.hcloud
pkgs.tea
pkgs.docker
pkgs.skopeo
pkgs.go-containerregistry
pkgs.jq
pkgs.yq-go
pkgs.curl
pkgs.git
pkgs.openssh
];
shellHook = ''
export GITEA_RUNNERS_ROOT="$PWD/infra/gitea-runners"
export GITEA_RUNNERS_TOFU_DIR="$GITEA_RUNNERS_ROOT/opentofu"
export GITEA_RUNNERS_K8S_DIR="$GITEA_RUNNERS_ROOT/k8s"
export GITEA_RUNNERS_IMAGE_DIR="$GITEA_RUNNERS_ROOT/image"
export GITEA_RUNNERS_NAMESPACE="gitea-runners"
alias cd-gitea-runners='cd "$GITEA_RUNNERS_ROOT"'
alias cd-gitea-runners-tofu='cd "$GITEA_RUNNERS_TOFU_DIR"'
alias cd-gitea-runners-k8s='cd "$GITEA_RUNNERS_K8S_DIR"'
echo ""
echo "=== Gitea runner setup DevShell ==="
echo ""
echo "Run gitea-runners-setup for the full setup checklist."
echo "Paths: "
echo " root=$GITEA_RUNNERS_ROOT"
echo " tofu=$GITEA_RUNNERS_TOFU_DIR"
echo " k8s=$GITEA_RUNNERS_K8S_DIR"
echo " image=$GITEA_RUNNERS_IMAGE_DIR"
echo ""
'';
}
-3
View File
@@ -1,3 +0,0 @@
# Documentation
- [Using the `hectic` Attic Cache](./attic-cache.md)
-237
View File
@@ -1,237 +0,0 @@
# Using the `hectic` Attic Cache
This document explains how to:
1. pull build artifacts from the cache
2. push new artifacts to the cache
3. configure this flake to use the cache
## Cache endpoints
- API endpoint: `https://cache.hectic-lab.com`
- Binary cache endpoint: `https://cache.hectic-lab.com/hectic`
The `hectic` cache is:
- public for reads
- private for pushes
## Requirements
Use the Attic client package:
```sh
nix shell nixpkgs#attic-client
```
Or run commands directly with:
```sh
nix shell nixpkgs#attic-client -c <command>
```
## Read from the cache
### Get the cache public key
```sh
nix shell nixpkgs#attic-client -c attic cache info hectic
```
Copy the `Public Key` value, which looks like:
```text
hectic:...
```
### Configure Nix to trust the cache
Per-user: `~/.config/nix/nix.conf`
```ini
substituters = https://cache.nixos.org https://cache.hectic-lab.com/hectic
trusted-public-keys = hectic:PASTE_PUBLIC_KEY_HERE
```
System-wide: `/etc/nix/nix.conf`
```ini
substituters = https://cache.nixos.org https://cache.hectic-lab.com/hectic
trusted-public-keys = hectic:PASTE_PUBLIC_KEY_HERE
```
After that, normal Nix commands can download from the cache automatically:
```sh
nix build .#migrator
nix develop
nix flake check
```
## Use the cache from this flake
You can also advertise the cache from `flake.nix`:
```nix
nixConfig = {
extra-substituters = [
"https://cache.nixos.org"
"https://cache.hectic-lab.com/hectic"
];
extra-trusted-public-keys = [
"hectic:PASTE_PUBLIC_KEY_HERE"
];
};
```
Then users can run:
```sh
nix build --accept-flake-config .#migrator
```
## Log in for pushing
Pushing requires an Attic token.
```sh
nix shell nixpkgs#attic-client -c attic login local https://cache.hectic-lab.com "<TOKEN>"
```
Example with `pass`:
```sh
nix shell nixpkgs#attic-client -c attic login local https://cache.hectic-lab.com "$(pass show atticd/hectic-lab/token)"
```
## Push build results
### Push a package
```sh
nix build .#migrator
nix shell nixpkgs#attic-client -c attic push local:hectic ./result
```
### Push a check
```sh
nix build .#checks.x86_64-linux.arguments
nix shell nixpkgs#attic-client -c attic push local:hectic ./result
```
### Push a NixOS system build
```sh
nix build '.#nixosConfigurations."hectic-lab|x86_64-linux".config.system.build.toplevel'
nix shell nixpkgs#attic-client -c attic push local:hectic ./result
```
## Recommended workflow
### Local development
Use the cache for reads only:
```sh
nix build .#migrator
nix develop
nix flake check
```
### CI / builder
1. Build
2. Push to Attic
Example:
```sh
nix build .#migrator
nix shell nixpkgs#attic-client -c attic push local:hectic ./result
```
## Useful commands
### Show cache info
```sh
nix shell nixpkgs#attic-client -c attic cache info hectic
```
### Check login config
```sh
nix shell nixpkgs#attic-client -c attic cache info local:hectic
```
### Re-login with a new token
```sh
nix shell nixpkgs#attic-client -c attic login local https://cache.hectic-lab.com "<NEW_TOKEN>"
```
## Common issues
### `flake 'nixpkgs' does not provide attribute 'attic'`
Use:
```sh
nix shell nixpkgs#attic-client
```
Not:
```sh
nix shell nixpkgs#attic
```
### `HTTP 413 Payload Too Large`
This means nginx rejected the upload body size. The server must allow large uploads on the Attic vhost.
### Push succeeds for some paths but fails for others
Usually means:
- nginx body size limit
- timeout/reverse proxy issue
- bad token permissions
### Cache pulls do not work
Check:
- `substituters`
- `trusted-public-keys`
- the exact public key from `attic cache info hectic`
## Notes about retention and storage
- The cache currently uses Hetzner Object Storage
- If no `retention-period` is configured, cached objects do not expire automatically
- This is good for long-lived reuse, but storage usage can grow over time
## Summary
### Read access
```sh
nix build .#migrator
```
after configuring:
```ini
substituters = https://cache.nixos.org https://cache.hectic-lab.com/hectic
trusted-public-keys = hectic:PASTE_PUBLIC_KEY_HERE
```
### Push access
```sh
nix shell nixpkgs#attic-client -c attic login local https://cache.hectic-lab.com "<TOKEN>"
nix build .#migrator
nix shell nixpkgs#attic-client -c attic push local:hectic ./result
```
Generated
+32 -37
View File
@@ -326,7 +326,7 @@
"hectic-landing": { "hectic-landing": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs-fixed"
] ]
}, },
"locked": { "locked": {
@@ -346,7 +346,7 @@
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs-fixed"
] ]
}, },
"locked": { "locked": {
@@ -671,15 +671,15 @@
"mechabellum-replay-analysis": { "mechabellum-replay-analysis": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs-fixed"
] ]
}, },
"locked": { "locked": {
"lastModified": 1780905541, "lastModified": 1779576166,
"narHash": "sha256-hxaKZTcowCDF5RfcCZIWpRY9/ZMm2zJyInNnovBayRg=", "narHash": "sha256-5bSuXkQs7KdbaYwDTdwUFlqOccVjPI2y42TZVq8lsNg=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "6f1f292db325145bbdf4d0452ce16963c07ecdb1", "rev": "f00295225c0dade61fe18b32262970c2665fb5fe",
"revCount": 123, "revCount": 110,
"type": "git", "type": "git",
"url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git" "url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"
}, },
@@ -688,27 +688,6 @@
"url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git" "url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"
} }
}, },
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772129556,
"narHash": "sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "ebec37af18215214173c98cf6356d0aca24a2585",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-25.11",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-minecraft": { "nix-minecraft": {
"inputs": { "inputs": {
"flake-compat": "flake-compat_3", "flake-compat": "flake-compat_3",
@@ -893,13 +872,29 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_2": { "nixpkgs-fixed": {
"locked": { "locked": {
"lastModified": 1779796641, "lastModified": 1771419570,
"narHash": "sha256-ZsIrKmhp4vbBXoXXmR/tBXA/UCsAQiJL9vsgZEduhVY=", "narHash": "sha256-bxAlQgre3pcQcaRUm/8A0v/X8d2nhfraWSFqVmMcBcU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "25f538306313eae3927264466c70d7001dcea1df", "rev": "6d41bc27aaf7b6a3ba6b169db3bd5d6159cfaa47",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1771419570,
"narHash": "sha256-bxAlQgre3pcQcaRUm/8A0v/X8d2nhfraWSFqVmMcBcU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6d41bc27aaf7b6a3ba6b169db3bd5d6159cfaa47",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -913,7 +908,7 @@
"inputs": { "inputs": {
"flake-parts": "flake-parts_2", "flake-parts": "flake-parts_2",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs-fixed"
], ],
"nuschtosSearch": "nuschtosSearch", "nuschtosSearch": "nuschtosSearch",
"systems": "systems_5" "systems": "systems_5"
@@ -988,13 +983,13 @@
"hyprland": "hyprland", "hyprland": "hyprland",
"impermanence": "impermanence", "impermanence": "impermanence",
"mechabellum-replay-analysis": "mechabellum-replay-analysis", "mechabellum-replay-analysis": "mechabellum-replay-analysis",
"nix-darwin": "nix-darwin",
"nix-minecraft": "nix-minecraft", "nix-minecraft": "nix-minecraft",
"nixos-anywhere": "nixos-anywhere", "nixos-anywhere": "nixos-anywhere",
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixos-mailserver": "nixos-mailserver", "nixos-mailserver": "nixos-mailserver",
"nixos-wsl": "nixos-wsl", "nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs_2",
"nixpkgs-fixed": "nixpkgs-fixed",
"nixvim": "nixvim", "nixvim": "nixvim",
"rust-overlay": "rust-overlay", "rust-overlay": "rust-overlay",
"sops-nix": "sops-nix" "sops-nix": "sops-nix"
@@ -1007,11 +1002,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780629589, "lastModified": 1738290352,
"narHash": "sha256-oHysjxZdaEqkmyDpN8G1bl3V+r9uyRD1O66bH0bq0Cs=", "narHash": "sha256-YKOHUmc0Clm4tMV8grnxYL4IIwtjTayoq/3nqk0QM7k=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "7a5a1c0a5cb86a28224304309b68f050835fd1f6", "rev": "b031b584125d33d23a0182f91ddbaf3ab4880236",
"type": "github" "type": "github"
}, },
"original": { "original": {
+5 -23
View File
@@ -1,18 +1,8 @@
{ {
description = "yukkop's nix utilities"; description = "yukkop's nix utilities";
nixConfig = {
extra-substituters = [
"https://cache.nixos.org"
"https://cache.hectic-lab.com/hectic"
];
extra-trusted-public-keys = [
"hectic:KMQsKow4SoA9K2vOJlOljmx7/Zpf91Yy+5qEtxDDCzA="
];
};
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-fixed.url = "github:NixOS/nixpkgs/nixos-25.11";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs = { inputs = {
@@ -29,7 +19,7 @@
}; };
nixvim = { nixvim = {
url = "github:nix-community/nixvim/nixos-25.11"; url = "github:nix-community/nixvim/nixos-25.11";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs-fixed";
}; };
disko = { disko = {
url = "github:nix-community/disko"; url = "github:nix-community/disko";
@@ -40,11 +30,7 @@
}; };
home-manager = { home-manager = {
url = "github:nix-community/home-manager/release-25.11"; url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs-fixed";
};
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
inputs.nixpkgs.follows = "nixpkgs";
}; };
nixos-wsl = { nixos-wsl = {
url = "github:nix-community/NixOS-WSL"; url = "github:nix-community/NixOS-WSL";
@@ -70,13 +56,13 @@
# NOTE(yukkop): private repo - SSH access required. # NOTE(yukkop): private repo - SSH access required.
# Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built. # Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built.
url = "git+ssh://git@github.com/liquizz/hectic-landing.git"; url = "git+ssh://git@github.com/liquizz/hectic-landing.git";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs-fixed";
}; };
mechabellum-replay-analysis = { mechabellum-replay-analysis = {
# NOTE(yukkop): private repo - SSH access required. # NOTE(yukkop): private repo - SSH access required.
# Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built. # Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built.
url = "git+ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"; url = "git+ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs-fixed";
}; };
}; };
@@ -124,11 +110,7 @@
"neuro|x86_64-linux" = import ./nixos/system/neuro { inherit flake self inputs; system = "x86_64-linux"; }; "neuro|x86_64-linux" = import ./nixos/system/neuro { inherit flake self inputs; system = "x86_64-linux"; };
"games|x86_64-linux" = import ./nixos/system/games { inherit flake self inputs; system = "x86_64-linux"; }; "games|x86_64-linux" = import ./nixos/system/games { inherit flake self inputs; system = "x86_64-linux"; };
"wsl|x86_64-linux" = import ./nixos/system/wsl { inherit flake self inputs; system = "x86_64-linux"; }; "wsl|x86_64-linux" = import ./nixos/system/wsl { inherit flake self inputs; system = "x86_64-linux"; };
"tenix|x86_64-linux" = import ./nixos/system/tenix { inherit flake self inputs; system = "x86_64-linux"; };
"hectic-lab|x86_64-linux" = import ./nixos/system/hectic-lab { inherit flake self inputs; system = "x86_64-linux"; }; "hectic-lab|x86_64-linux" = import ./nixos/system/hectic-lab { inherit flake self inputs; system = "x86_64-linux"; };
}; };
darwinConfigurations = {
"yukkop|aarch64-darwin" = import ./darwin/system/yukkop { inherit flake self inputs; system = "aarch64-darwin"; };
};
}; };
} }
-30
View File
@@ -1,30 +0,0 @@
{ pkgs, ... }: {
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
-80
View File
@@ -1,80 +0,0 @@
# Gitea runner Nix image
The repo-owned Nix-capable job image is built by the flake package
`gitea-runner-nix-image`.
```sh
nix build .#gitea-runner-nix-image
```
The package emits a Docker archive with the local build tag:
```text
gitea-runner-nix-image:2026-06-07
```
That tag is build metadata only. Do not use it as the final Gitea runner label
mapping because runner job images must be immutable.
## Publication target
Preferred registry:
```text
gitea.hectic-lab.com/hectic-lab/gitea-runner-nix-image
```
Publish the archive without adding secrets to the image layers, then use the
registry-reported digest as the only final `nix` label image reference:
```text
nix:docker://gitea.hectic-lab.com/hectic-lab/gitea-runner-nix-image@sha256:<registry-digest>
```
The `2026-06-07` tag may be pushed as a human-readable companion tag, but the
runner label mapping must use the `@sha256:` reference above. Keep
`ubuntu-latest` on the `gitea/runner` default image unless a later runner
configuration task explicitly changes it. Only the `nix` label should select
this custom image.
If the Gitea container registry is unavailable, select a private registry that
is reachable from the runner Kubernetes cluster and requires authentication that
can be provided through Kubernetes image-pull secrets. Record the selected
registry and replace the host in the same digest-pinned form:
```text
nix:docker://<private-registry>/<namespace>/gitea-runner-nix-image@sha256:<registry-digest>
```
Do not fall back to `latest` or a tag-only mapping.
## Task 7 publication status
Local build evidence is recorded in
`.sisyphus/evidence/task-7-image-digest.txt`. In this environment, Docker could
load and tag the image, but pushing to the preferred registry failed with
`unauthorized: reqPackageAccess`, so no registry digest was available to pin as a
concrete final mapping. Kubernetes pull smoke is recorded in
`.sisyphus/evidence/task-7-image-pull.txt` and is blocked here because `kubectl`
is not installed or not on `PATH`.
Once registry credentials are available, rerun the push, capture the
registry-reported digest, and replace `<registry-digest>` in the mapping above
before Task 6/9 consumes the label configuration.
## Image contents
The image includes `nix`, `git`, `bash`, `coreutils`, and `cacert`. Its
`/etc/nix/nix.conf` enables flakes and configures the repo substituters from the
top-level `flake.nix`:
```text
experimental-features = nix-command flakes
substituters = https://cache.nixos.org https://cache.hectic-lab.com/hectic
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gW4x6l1xP+GxgH0r7u+f6p1VFlr0= hectic:KMQsKow4SoA9K2vOJlOljmx7/Zpf91Yy+5qEtxDDCzA=
sandbox = false
```
No Gitea runner token, SSH key, SOPS key, kubeconfig, Hetzner token, or S3
credential belongs in this image. Runtime secrets stay with the Kubernetes
runner configuration and token-file mount contract.
@@ -1,154 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-runner-lifecycle
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner-lifecycle
app.kubernetes.io/part-of: gitea-actions
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gitea-runner-lifecycle
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner-lifecycle
app.kubernetes.io/part-of: gitea-actions
rules:
- apiGroups:
- ""
resources:
- pods
- persistentvolumeclaims
verbs:
- get
- list
- apiGroups:
- apps
resources:
- statefulsets
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitea-runner-lifecycle
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner-lifecycle
app.kubernetes.io/part-of: gitea-actions
subjects:
- kind: ServiceAccount
name: gitea-runner-lifecycle
namespace: gitea-runners
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gitea-runner-lifecycle
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-runner-lifecycle
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner-lifecycle
app.kubernetes.io/part-of: gitea-actions
data:
cleanup-dry-run.sh: |
#!/bin/sh
set -eu
namespace="${RUNNER_NAMESPACE:-gitea-runners}"
mode="${CLEANUP_MODE:-dry-run}"
selector="app.kubernetes.io/name=gitea-runner"
if [ "$namespace" != "gitea-runners" ]; then
printf 'refusing to run outside namespace gitea-runners: %s\n' "$namespace" >&2
exit 13
fi
if [ "$mode" != "dry-run" ]; then
printf 'refusing destructive mode: set CLEANUP_MODE=dry-run for this CronJob\n' >&2
exit 13
fi
printf 'gitea runner lifecycle cleanup dry-run\n'
printf 'namespace: %s\n' "$namespace"
printf 'mode: %s\n\n' "$mode"
printf 'StatefulSet:\n'
kubectl -n "$namespace" get statefulset gitea-runner -o wide
printf '\nActive runner pods:\n'
kubectl -n "$namespace" get pods -l "$selector" -o wide
printf '\nRunner /data PVCs:\n'
kubectl -n "$namespace" get pvc -l "$selector" -o wide
printf '\nPVCs whose matching StatefulSet pod is absent (candidates only; no deletion):\n'
found_candidate=0
for pvc in $(kubectl -n "$namespace" get pvc -l "$selector" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
pod="${pvc#data-}"
if ! kubectl -n "$namespace" get pod "$pod" >/dev/null 2>&1; then
found_candidate=1
printf 'candidate pvc=%s expected_pod=%s action=investigate-before-delete\n' "$pvc" "$pod"
fi
done
if [ "$found_candidate" -eq 0 ]; then
printf 'none\n'
fi
printf '\nGitea registration reconciliation:\n'
printf 'dry-run only: compare the pod/PVC list above with Gitea org runner registrations.\n'
printf 'only deregister a runner after its pod/PVC was intentionally deleted or /data/.runner was intentionally reset.\n'
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: gitea-runner-cleanup-dry-run
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner-lifecycle
app.kubernetes.io/part-of: gitea-actions
spec:
schedule: "17 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: gitea-runner-lifecycle
app.kubernetes.io/part-of: gitea-actions
spec:
serviceAccountName: gitea-runner-lifecycle
restartPolicy: Never
containers:
- name: cleanup-dry-run
image: bitnami/kubectl:1.30
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- /scripts/cleanup-dry-run.sh
env:
- name: RUNNER_NAMESPACE
value: gitea-runners
- name: CLEANUP_MODE
value: dry-run
volumeMounts:
- name: lifecycle-scripts
mountPath: /scripts
readOnly: true
volumes:
- name: lifecycle-scripts
configMap:
name: gitea-runner-lifecycle
defaultMode: 0555
@@ -1,9 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- service.yaml
- service-account.yaml
- runner-config.yaml
- statefulset.yaml
- cleanup-lifecycle.yaml
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
@@ -1,36 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-runner-config
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
data:
config.yaml: |
log:
level: info
runner:
file: /data/.runner
capacity: 1
envs: {}
timeout: 3h
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
labels:
- ubuntu-latest
# The nix label is intentionally disabled until the runner image has a
# concrete registry-reported digest; see ../runbook.md before deploy.
cache:
enabled: true
dir: /data/cache
container:
network: bridge
privileged: false
force_pull: true
valid_volumes: []
docker_host: unix:///runner-docker/docker.sock
@@ -1,36 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-runner
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gitea-runner
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
rules: []
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitea-runner
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
subjects:
- kind: ServiceAccount
name: gitea-runner
namespace: gitea-runners
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gitea-runner
-16
View File
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: gitea-runner
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
spec:
clusterIP: None
selector:
app.kubernetes.io/name: gitea-runner
ports:
- name: cache
port: 8088
targetPort: cache
-156
View File
@@ -1,156 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: gitea-runner
namespace: gitea-runners
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
spec:
serviceName: gitea-runner
replicas: 5
podManagementPolicy: Parallel
selector:
matchLabels:
app.kubernetes.io/name: gitea-runner
template:
metadata:
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
annotations:
hectic-lab.com/security-note: "Privileged rootful DinD is limited to trusted internal Gitea workflows only. Do not enable untrusted fork or PR jobs for this pool."
spec:
serviceAccountName: gitea-runner
automountServiceAccountToken: false
terminationGracePeriodSeconds: 60
securityContext:
fsGroup: 1000
containers:
- name: runner
image: gitea/act_runner:0.2.11
imagePullPolicy: IfNotPresent
env:
- name: GITEA_INSTANCE_URL
value: https://gitea.hectic-lab.com
- name: GITEA_RUNNER_REGISTRATION_TOKEN_FILE
value: /runner-secrets/token
- name: CONFIG_FILE
value: /runner-config/config.yaml
- name: DOCKER_HOST
value: unix:///runner-docker/docker.sock
ports:
- name: cache
containerPort: 8088
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
livenessProbe:
exec:
command:
- /bin/sh
- -ec
- test -s /data/.runner && test -S /runner-docker/docker.sock
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
exec:
command:
- /bin/sh
- -ec
- test -S /runner-docker/docker.sock
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /runner-config
readOnly: true
- name: runner-token
mountPath: /runner-secrets
readOnly: true
- name: docker-socket
mountPath: /runner-docker
- name: docker
image: docker:27-dind
imagePullPolicy: IfNotPresent
args:
- --host=unix:///runner-docker/docker.sock
- --storage-driver=overlay2
- --tls=false
env:
- name: DOCKER_TLS_CERTDIR
value: ""
- name: DOCKER_HOST
value: unix:///runner-docker/docker.sock
securityContext:
# Privileged rootful DinD is intentionally scoped to this trusted
# internal runner pool; never expose it to untrusted fork/PR jobs.
privileged: true
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
livenessProbe:
exec:
command:
- docker
- info
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 6
readinessProbe:
exec:
command:
- docker
- info
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 6
volumeMounts:
- name: docker-socket
mountPath: /runner-docker
- name: docker-graph
mountPath: /var/lib/docker
volumes:
- name: config
configMap:
name: gitea-runner-config
- name: runner-token
secret:
secretName: gitea-runner-token
items:
- key: token
path: token
defaultMode: 0400
- name: docker-socket
emptyDir: {}
- name: docker-graph
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: gitea-runner
app.kubernetes.io/part-of: gitea-actions
spec:
accessModes:
- ReadWriteOnce
storageClassName: hcloud-volumes
resources:
requests:
storage: 20Gi
-29
View File
@@ -1,29 +0,0 @@
# OpenTofu working directory and downloaded modules/providers.
.terraform/
.terraform.lock.hcl
# State must live in the S3 backend for production. Local state is allowed only
# for throwaway syntax checks with `tofu init -backend=false` and must not be
# committed.
terraform.tfstate
terraform.tfstate.*
*.tfstate
*.tfstate.*
crash.log
crash.*.log
# Plans can contain secrets or derived infrastructure data.
*.tfplan
*.plan
kubeconfig
kubeconfig.yaml
*_kubeconfig.yaml
# Variable files commonly carry credentials. Keep production inputs in SOPS or
# external environment/configuration, not in checked-in files.
*.tfvars
*.tfvars.json
override.tf
override.tf.json
*_override.tf
*_override.tf.json
-90
View File
@@ -1,90 +0,0 @@
# Gitea runner OpenTofu backend contract
This directory defines the safe backend, provider contract, and kube-hetzner
cluster stack for the Gitea runner Kubernetes cluster.
## Required backend
Production state must use the OpenTofu S3 backend in `backend.tf`:
- bucket: `gitea-runner-hectic-lab`
- key: `gitea-runners/kube-hetzner/terraform.tfstate`
- region: `fsn1`, aligned with the target Hetzner location
- encryption: `encrypt = true`
- locking: `use_lockfile = true` where the selected S3-compatible endpoint
supports it
Before any production `tofu init`, verify the S3-compatible endpoint, credential
source, bucket versioning, encryption behavior, and lockfile support for the
chosen object-storage provider. Keep backend authentication externalized through
environment variables, AWS-compatible shared config, or the production secret
injection path from Task 3. Do not add `access_key`, `secret_key`, Hetzner
tokens, runner tokens, kubeconfig material, or decrypted SOPS data to checked-in
OpenTofu files.
## Local state safety
Production local state is forbidden. Only syntax-only validation/prototyping may
use local state, and it must use backend-disabled initialization:
```sh
tofu -chdir=infra/gitea-runners/opentofu init -backend=false
tofu -chdir=infra/gitea-runners/opentofu validate
```
Fail the run if production local state appears:
```sh
test ! -e infra/gitea-runners/opentofu/terraform.tfstate
test ! -e infra/gitea-runners/opentofu/terraform.tfstate.backup
grep -R 'backend "s3"' infra/gitea-runners/opentofu
```
The `.gitignore` in this directory blocks local state, plans, downloaded
providers/modules, and variable files from being committed. Treat any local
state file as disposable validation residue, never as production state.
## Provider and module pins
`versions.tf` pins the OpenTofu-compatible Hetzner Cloud provider to
`hetznercloud/hcloud` version `1.60.1`. kube-hetzner research for this plan
observed module version `2.19.3`, source `kube-hetzner/kube-hetzner/hcloud`, and
module minimum hcloud provider requirement `>= 1.59.0`; these values are recorded
as locals so Task 5 can wire the module without re-opening the version contract.
`providers.tf` leaves the `hcloud` provider empty so authentication comes from
the provider's external environment/config mechanisms such as `HCLOUD_TOKEN`.
Do not set token values in `.tf` or `.tfvars` files.
## Cluster shape
The default cluster is deliberately fixed-size:
- cluster name: `gitea-runners`
- Hetzner location: `fsn1`
- private network region: `eu-central`
- control plane: one `cpx21` node in pool `control-plane`
- workers: three `cpx31` nodes in pool `runner-workers`
- storage: Hetzner CSI enabled with expected StorageClass `hcloud-volumes`
- Longhorn: disabled
- autoscaling/KEDA: not enabled in this stack
The three default workers are sized for the initial five trusted privileged DinD
jobs. To scale toward ten jobs later, keep autoscaling disabled and either raise
`worker_count` to `5` or increase `worker_server_type`, then run a fresh
`tofu plan` and the Task 11 Kubernetes pressure checks before applying.
Required inputs must come from environment or secret injection, for example
`TF_VAR_hcloud_token`, `TF_VAR_ssh_public_key`, and `TF_VAR_ssh_private_key`.
Do not commit `.tfvars` files. kube-hetzner v2.19.3 writes the generated
kubeconfig to `./<cluster_name>_kubeconfig.yaml` when `create_kubeconfig` is
enabled; this path is ignored as operational secret material.
## Known state caveat
kube-hetzner may thread `hcloud_token` into Kubernetes secrets/state through its
internal `kube_system_secrets` handling. This task does not claim that risk is
solved. Task 5 must verify the generated plan and state before production apply
and prove that Hetzner tokens, S3 credentials, runner tokens, kubeconfig private
keys, and decrypted secrets are absent from committed files and unsafe state
evidence.
-16
View File
@@ -1,16 +0,0 @@
terraform {
backend "s3" {
bucket = "gitea-runner-hectic-lab"
key = "gitea-runners/kube-hetzner/terraform.tfstate"
region = "fsn1"
encrypt = true
use_lockfile = true
}
}
check "remote_state_contract" {
assert {
condition = local.production_remote_state
error_message = "Production OpenTofu state must use the configured S3 backend; local production state is forbidden."
}
}
-60
View File
@@ -1,60 +0,0 @@
locals {
default_storage_class = "hcloud-volumes"
control_plane_nodepools = [
{
name = "control-plane"
server_type = var.control_plane_server_type
location = var.hetzner_location
labels = []
taints = []
count = 1
},
]
agent_nodepools = [
{
name = "runner-workers"
server_type = var.worker_server_type
location = var.hetzner_location
labels = ["node-role.hectic-lab/gitea-runner=true"]
taints = []
count = var.worker_count
},
]
}
module "kube_hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "2.19.3"
providers = {
hcloud = hcloud
}
hcloud_token = var.hcloud_token
ssh_public_key = var.ssh_public_key
ssh_private_key = var.ssh_private_key
cluster_name = var.cluster_name
base_domain = var.base_domain
# kube-hetzner v2.19.3 writes <cluster_name>_kubeconfig.yaml; outputs below
# expose that expected path without outputting kubeconfig private key material.
create_kubeconfig = true
network_region = var.network_region
load_balancer_location = var.hetzner_location
control_plane_nodepools = local.control_plane_nodepools
agent_nodepools = local.agent_nodepools
# Hetzner CSI is the required StorageClass provider for runner PVCs.
disable_hetzner_csi = false
# Longhorn is intentionally off; the initial runner PVCs use Hetzner CSI only.
enable_longhorn = false
# Scaling note: for 10 trusted DinD jobs later, keep autoscaling disabled and
# raise worker_count to 5 or increase worker_server_type after validating pod
# CPU, memory, and ephemeral-storage pressure in Task 11.
}
-22
View File
@@ -1,22 +0,0 @@
output "kubeconfig_path" {
description = "Path where kube-hetzner writes kubeconfig after apply. The file is operational secret material and must not be committed."
value = coalesce(var.kubeconfig_path, "./${var.cluster_name}_kubeconfig.yaml")
}
output "cluster_name" {
description = "kube-hetzner cluster name."
value = var.cluster_name
}
output "node_pool_names" {
description = "Control-plane and worker node pool names used by this stack."
value = {
control_plane = [for pool in local.control_plane_nodepools : pool.name]
workers = [for pool in local.agent_nodepools : pool.name]
}
}
output "default_storage_class" {
description = "Default Hetzner CSI StorageClass expected for runner PVCs."
value = local.default_storage_class
}
@@ -1 +0,0 @@
provider "hcloud" {}
-74
View File
@@ -1,74 +0,0 @@
variable "hcloud_token" {
description = "Hetzner Cloud API token for kube-hetzner. Set with TF_VAR_hcloud_token or secret injection only; never commit it. kube-hetzner may place this value into Kubernetes secret resources/state, so scan plans before apply."
type = string
sensitive = true
}
variable "ssh_public_key" {
description = "SSH public key installed on cluster nodes. Supply from an external file or secret injection path."
type = string
}
variable "ssh_private_key" {
description = "SSH private key used by kube-hetzner during bootstrap. Supply from an external file or secret injection path; never commit it."
type = string
sensitive = true
}
variable "cluster_name" {
description = "Name for the kube-hetzner runner cluster."
type = string
default = "gitea-runners"
validation {
condition = can(regex("^[a-z0-9-]+$", var.cluster_name))
error_message = "cluster_name must contain only lowercase letters, numbers, and dashes."
}
}
variable "hetzner_location" {
description = "Hetzner Cloud location for all node pools. fsn1 keeps the first runner cluster in Falkenstein."
type = string
default = "fsn1"
}
variable "network_region" {
description = "Hetzner private network region. eu-central covers fsn1."
type = string
default = "eu-central"
}
variable "control_plane_server_type" {
description = "Default control-plane server type. cpx21 is small but leaves headroom for kube-system workloads."
type = string
default = "cpx21"
}
variable "worker_server_type" {
description = "Default worker server type for the initial trusted DinD runner pool. Three cpx31 workers provide enough headroom for five privileged jobs before Task 11 scaling validation."
type = string
default = "cpx31"
}
variable "worker_count" {
description = "Fixed worker count. Increase to 5 or choose a larger worker_server_type later to target 10 concurrent DinD jobs; do not enable autoscaling in this stack."
type = number
default = 3
validation {
condition = var.worker_count >= 1
error_message = "worker_count must be at least 1."
}
}
variable "kubeconfig_path" {
description = "Expected kubeconfig path. kube-hetzner v2.19.3 writes this as <cluster_name>_kubeconfig.yaml when create_kubeconfig is true."
type = string
default = null
}
variable "base_domain" {
description = "Optional base domain for node reverse DNS. Empty keeps kube-hetzner defaults."
type = string
default = ""
}
-17
View File
@@ -1,17 +0,0 @@
terraform {
required_version = ">= 1.10.1"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "1.60.1"
}
}
}
locals {
kube_hetzner_module_source = "kube-hetzner/kube-hetzner/hcloud"
kube_hetzner_module_version = "2.19.3"
hcloud_provider_minimum = ">= 1.59.0"
production_remote_state = true
}
-503
View File
@@ -1,503 +0,0 @@
# Gitea Runner Infrastructure Runbook
## Scope
This directory is the repo-owned boundary for the first Gitea Actions runner
pool. Task 1 only establishes the scaffold and immutable decision contract;
downstream tasks will add OpenTofu backend/provider files, Kubernetes manifests,
and a Nix-capable runner image under the existing subdirectories.
The target service is `https://gitea.hectic-lab.com` for the Gitea organization
`hectic-lab`. The first pool is fixed-size and trusted-only. "Ephemeral" means
workflow job containers are ephemeral, while each runner pod keeps its runner
identity in per-pod `/data/.runner` storage backed by a StatefulSet PVC.
## Immutable decisions
- Infrastructure is managed with OpenTofu command examples only, using the
`tofu` CLI.
- Cloud provider is Hetzner; cluster bootstrap uses kube-hetzner.
- Remote state uses the S3 backend bucket `gitea-runner-hectic-lab`.
- Runner implementation is the non-Enterprise `gitea/runner`.
- Runner registration uses a Gitea organization-scoped token for `hectic-lab`.
- Runtime token delivery is SOPS-backed and mounted into the runner pod as a
file read through `GITEA_RUNNER_REGISTRATION_TOKEN_FILE`; plaintext token
environment variables are not the contract.
- Kubernetes runner lifecycle uses a StatefulSet with one PVC per pod for
`/data`, including `/data/.runner`.
- Container builds run through privileged rootful DinD inside trusted runner
pods; host Docker socket mounting is not an implementation path.
- The active runner label is `ubuntu-latest`. The `nix` label is not live until
the Nix-capable image has been pushed and a concrete registry-reported digest
is added to the runner ConfigMap.
- First scope is trusted internal workflows only, with no untrusted fork or PR
workflow support.
- First scope has no autoscaling, no KEDA, and no dynamic runner controller.
## Lifecycle boundaries
- `infra/gitea-runners/opentofu/`: downstream OpenTofu stack for the S3 backend
contract, Hetzner provider configuration, and kube-hetzner module wiring.
- `infra/gitea-runners/k8s/`: downstream namespace, ConfigMap, Secret mount,
StatefulSet, PVC, DinD sidecar, cleanup, and operational manifest work.
- `infra/gitea-runners/image/`: downstream notes or sources for the runner image
handoff; package or flake output changes are outside Task 1.
- `infra/gitea-runners/runbook.md`: this contract plus later operational
commands, rollback notes, and acceptance evidence references.
## Guardrails
- Enterprise ARC/actions-runner-controller are rejected alternatives and must
not be implemented here. Do not add ARC custom resources, controller install
instructions, or GitHub Actions ARC assumptions.
- Untrusted fork/PR workflows are out of first scope; privileged DinD is only
acceptable for trusted internal jobs.
- Autoscaling/KEDA is out of first scope; start with a fixed-size StatefulSet
runner pool.
- No actual secrets are committed: no kubeconfig, runner token, Hetzner token,
S3 credentials, decrypted SOPS files, or SOPS age keys.
- OpenTofu must not manage plaintext Kubernetes Secrets containing the Gitea
runner token; Kubernetes receives the token as a mounted file secret instead.
- Do not use `localhost` or `127.0.0.1` as the Gitea URL inside job containers;
jobs must reach the public HTTPS service.
## Initial acceptance commands
Run from the repository root:
```sh
test -d infra/gitea-runners/opentofu && test -d infra/gitea-runners/k8s && test -d infra/gitea-runners/image
test -f infra/gitea-runners/runbook.md
grep -n "OpenTofu\|kube-hetzner\|StatefulSet\|DinD\|SOPS\|trusted" infra/gitea-runners/runbook.md
grep -R "[E]nterprise ARC\|[a]ctions-runner-controller" infra/gitea-runners
grep -R "[t]erraform " infra/gitea-runners || true
grep -R "[D]ECISION NEEDED" infra/gitea-runners || true
```
Expected outcomes: the directory and file checks exit 0; the architecture-term
grep shows this contract; ARC references appear only in the rejected-alternative
guardrail above; there are no forbidden CLI command examples and no unresolved
decision placeholders.
## Downstream placeholders
- Task 2: add OpenTofu backend/provider files and verify S3 state safety.
- Task 3: add SOPS secret contract and runtime token delivery details.
- Task 4: define or package the Nix-capable runner image for the `nix` label.
- Task 5+: provision kube-hetzner, add Kubernetes resources, verify workflows,
and document cleanup, rollback, and scaling operations.
## Runner lifecycle cleanup
All lifecycle commands are scoped to the runner namespace:
```sh
kubectl -n gitea-runners get statefulset gitea-runner
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
```
The scheduled cleanup manifest is dry-run only. It lists the StatefulSet, active
runner pods, runner PVCs, and PVCs whose expected StatefulSet pod is absent. It
does not delete pods, PVCs, Docker data, or Gitea runner registrations.
Run the same inventory on demand without waiting for the schedule:
```sh
kubectl -n gitea-runners create job gitea-runner-cleanup-dry-run-manual --from=cronjob/gitea-runner-cleanup-dry-run
kubectl -n gitea-runners wait --for=condition=complete job/gitea-runner-cleanup-dry-run-manual --timeout=2m
kubectl -n gitea-runners logs job/gitea-runner-cleanup-dry-run-manual -c cleanup-dry-run
```
The cleanup job template has `ttlSecondsAfterFinished: 3600`, so completed
manual dry-run jobs are garbage-collected by Kubernetes instead of requiring an
operator to remove finished jobs manually.
If a PVC such as `data-gitea-runner-3` is intentionally deleted, the matching
pod loses `/data/.runner`. That runner identity must then be deregistered from
Gitea or the replacement pod must be allowed to re-register intentionally with
the current organization runner token. Do not delete an active runner PVC as a
normal cleanup step.
Non-UI Gitea registration reconciliation uses the Gitea API with a separate
admin token. Store that token outside this repository and pass it as a file; do
not print it:
```sh
kubectl -n gitea-runners create secret generic gitea-runner-admin-token --from-file=token=/secure/path/gitea-admin-token
kubectl -n gitea-runners run gitea-runner-registration-dry-run \
--restart=Never \
--image=curlimages/curl:8.10.1 \
--overrides='{"spec":{"containers":[{"name":"gitea-runner-registration-dry-run","image":"curlimages/curl:8.10.1","command":["/bin/sh","-ec","umask 077; cfg=$(mktemp); trap '\''rm -f \"$cfg\"'\'' EXIT; { printf '\''header = \"Authorization: token '\''; cat /admin-token/token; printf '\''\"\\n'\''; printf '\''url = \"https://gitea.hectic-lab.com/api/v1/orgs/hectic-lab/actions/runners\"\\n'\''; } > \"$cfg\"; curl -fsS --config \"$cfg\""],"volumeMounts":[{"name":"admin-token","mountPath":"/admin-token","readOnly":true}]}],"volumes":[{"name":"admin-token","secret":{"secretName":"gitea-runner-admin-token","defaultMode":256}}]}}'
kubectl -n gitea-runners logs pod/gitea-runner-registration-dry-run
```
Delete the temporary `gitea-runner-admin-token` Secret only after the dry-run pod
has completed and its logs have been collected. Do not keep this admin token in
the runner namespace longer than the reconciliation window.
Only remove a stale Gitea runner registration after the corresponding pod/PVC
was intentionally deleted or `/data/.runner` was intentionally reset. Prefer a
Gitea CLI/API deletion from the Gitea server or an admin workstation; manual UI
cleanup is a fallback, not the only path. Record the removed runner name and the
Kubernetes PVC/pod deletion that made it stale.
After the dry-run list identifies a stale registration and the PVC/pod deletion
has been recorded, remove that exact Gitea runner by id through the API:
```sh
runner_id='REPLACE_WITH_STALE_RUNNER_ID'
umask 077
curl_config=$(mktemp /tmp/gitea-runner-admin-curl.XXXXXX)
trap 'rm -f "$curl_config"' EXIT
{
printf 'request = "DELETE"\n'
printf 'header = "Authorization: token '
cat /secure/path/gitea-admin-token
printf '"\n'
printf 'url = "https://gitea.hectic-lab.com/api/v1/orgs/hectic-lab/actions/runners/%s"\n' "$runner_id"
} > "$curl_config"
curl -fsS --config "$curl_config"
```
Do not run the delete command for a runner that still has an active
`gitea-runner-*` pod or a retained `data-gitea-runner-*` PVC unless that PVC is
being intentionally reset for re-registration.
## Docker-in-Docker storage cleanup
Docker layers live inside each DinD sidecar at `/var/lib/docker`, backed by the
pod-local `docker-graph` `emptyDir`; the host Docker socket is not used. Always
list disk usage before pruning, and run the command only against the `docker`
container in runner pods in `gitea-runners`. Because this storage is pod-local,
loop over pods for pool-wide cleanup:
```sh
for pod in $(kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
kubectl -n gitea-runners exec "pod/${pod}" -c docker -- docker system df
done
for pod in $(kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
kubectl -n gitea-runners exec "pod/${pod}" -c docker -- docker system prune --all --force --filter until=24h
done
for pod in $(kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
kubectl -n gitea-runners exec "pod/${pod}" -c docker -- docker system df
done
```
For one pod, replace the StatefulSet target with the pod name:
```sh
kubectl -n gitea-runners exec pod/gitea-runner-0 -c docker -- docker system df
kubectl -n gitea-runners exec pod/gitea-runner-0 -c docker -- docker system prune --all --force --filter until=24h
```
Do not run host-level Docker cleanup commands and do not mount or prune a host
Docker socket. If a pod is deleted, its `emptyDir` Docker graph is removed by
Kubernetes; the `/data` PVC remains and still controls runner identity.
## Token rotation
Rotate the Gitea organization runner token without printing decrypted values:
```sh
sops sus/gitea-runners.yaml
umask 077
token_file=$(mktemp /tmp/gitea-runner-token.XXXXXX)
trap 'rm -f "$token_file"' EXIT
sops -d --extract '["gitea"]["hectic-lab"]["org-runner-registration-token"]' sus/gitea-runners.yaml > "$token_file"
kubectl -n gitea-runners create secret generic gitea-runner-token \
--from-file=token="$token_file" \
--dry-run=client \
-o yaml | kubectl -n gitea-runners apply -f -
kubectl -n gitea-runners rollout restart statefulset/gitea-runner
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200 | grep -Eq 'token|GITEA_RUNNER_REGISTRATION_TOKEN' && exit 1 || true
```
The `rollout restart` command above is the controlled restart path for this
StatefulSet. Observe the rollout and each ordinal until all replacement pods are
Ready; do not delete runner pods directly as part of normal token rotation:
```sh
kubectl -n gitea-runners wait --for=condition=Ready pod/gitea-runner-0 --timeout=5m
kubectl -n gitea-runners wait --for=condition=Ready pod/gitea-runner-1 --timeout=5m
kubectl -n gitea-runners wait --for=condition=Ready pod/gitea-runner-2 --timeout=5m
kubectl -n gitea-runners wait --for=condition=Ready pod/gitea-runner-3 --timeout=5m
kubectl -n gitea-runners wait --for=condition=Ready pod/gitea-runner-4 --timeout=5m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
```
Verification must confirm the token file mount remains present while the token
value never appears in logs or evidence:
```sh
kubectl -n gitea-runners describe pod gitea-runner-0 | grep -n '/runner-secrets\|gitea-runner-token'
kubectl -n gitea-runners logs pod/gitea-runner-0 -c runner --tail=200 | grep -Eq 'token|GITEA_RUNNER_REGISTRATION_TOKEN' && exit 1 || true
```
## Deploy and status
These commands are executable only when the external inputs are available:
- `TF_VAR_hcloud_token`
- `TF_VAR_ssh_public_key`
- `TF_VAR_ssh_private_key`
- S3 backend credentials and endpoint access
- a matching SOPS age identity for `sus/gitea-runners.yaml`
- `kubectl` access to the target cluster
- a concrete digest for the pushed Nix-capable runner image, if enabling the
`nix` label
If any input is missing, stop before `tofu apply`. Do not guess values or reuse
stale kubeconfig files.
Before production Kubernetes apply or rollout, satisfy both manifest gates:
1. Create or update the `gitea-runner-token` Secret from SOPS. The active
Kustomize overlay intentionally does not include a placeholder Secret, but
the StatefulSet still mounts `secretName: gitea-runner-token` as
`/runner-secrets/token` for `GITEA_RUNNER_REGISTRATION_TOKEN_FILE`.
2. Keep the active ConfigMap on `ubuntu-latest` only unless the Nix-capable
image has been pushed successfully. Enable the `nix` label only by adding a
digest-pinned `docker://` mapping with the exact registry-reported sha256
digest from that push.
Use the same SOPS materialization pattern as token rotation before applying the
Kubernetes overlay. Applying the namespace alone is allowed so the Secret has a
target namespace; the full overlay remains gated on the Secret and digest
decisions:
```sh
kubectl apply -f infra/gitea-runners/k8s/namespace.yaml
umask 077
token_file=$(mktemp /tmp/gitea-runner-token.XXXXXX)
trap 'rm -f "$token_file"' EXIT
sops -d --extract '["gitea"]["hectic-lab"]["org-runner-registration-token"]' sus/gitea-runners.yaml > "$token_file"
kubectl -n gitea-runners create secret generic gitea-runner-token \
--from-file=token="$token_file" \
--dry-run=client \
-o yaml | kubectl -n gitea-runners apply -f -
```
Do not run `kubectl apply -k infra/gitea-runners/k8s` until the Secret command
above succeeds. Do not claim or enable the `nix` runner label until the image
publication step has produced the concrete digest.
```sh
tofu -chdir=infra/gitea-runners/opentofu init
tofu -chdir=infra/gitea-runners/opentofu validate
tofu -chdir=infra/gitea-runners/opentofu plan -out=.sisyphus/evidence/task-12-deploy.plan
tofu -chdir=infra/gitea-runners/opentofu apply .sisyphus/evidence/task-12-deploy.plan
export KUBECONFIG="$(tofu -chdir=infra/gitea-runners/opentofu output -raw kubeconfig_path)"
kubectl config current-context
kubectl get nodes -o wide
kubectl get sc
kubectl apply -k infra/gitea-runners/k8s
kubectl -n gitea-runners get statefulset gitea-runner
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get events --sort-by=.lastTimestamp | tail -n 50
kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200
```
Expected status after deploy:
- `kubectl config current-context` names the runner cluster context.
- `kubectl get nodes -o wide` shows all expected Hetzner nodes Ready.
- `kubectl get sc` shows the Hetzner CSI storage class used by runner PVCs.
- `kubectl -n gitea-runners get statefulset gitea-runner` shows 5 desired and 5 ready replicas.
- `kubectl -n gitea-runners get pvc` shows 5 Bound PVCs.
- `kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200` shows the runner daemon started and no token value.
## Scale 5 to 10 to 5
Scaling is a temporary capacity exercise, not the steady-state setting. Scale up,
wait for readiness, run the concurrent smoke jobs, then scale back down to 5.
```sh
kubectl -n gitea-runners scale statefulset/gitea-runner --replicas=10
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
# Run the concurrent smoke workflows now.
kubectl -n gitea-runners scale statefulset/gitea-runner --replicas=5
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
```
After scaling back down, inspect the cleanup dry-run and deregister any stale
runner registrations only for pods or PVCs that were intentionally removed.
## Cleanup and stale runner deregistration
Use the dry-run cleanup job to list the StatefulSet, active pods, PVCs, and any
PVC candidates whose pod is gone. It must not delete active resources.
```sh
kubectl -n gitea-runners create job gitea-runner-cleanup-dry-run-manual --from=cronjob/gitea-runner-cleanup-dry-run
kubectl -n gitea-runners wait --for=condition=complete job/gitea-runner-cleanup-dry-run-manual --timeout=2m
kubectl -n gitea-runners logs job/gitea-runner-cleanup-dry-run-manual -c cleanup-dry-run
```
Pool-wide DinD storage checks and cleanup:
```sh
for pod in $(kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
kubectl -n gitea-runners exec "pod/${pod}" -c docker -- docker system df
done
for pod in $(kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
kubectl -n gitea-runners exec "pod/${pod}" -c docker -- docker system prune --all --force --filter until=24h
done
for pod in $(kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
kubectl -n gitea-runners exec "pod/${pod}" -c docker -- docker system df
done
```
If a PVC such as `data-gitea-runner-3` is intentionally deleted, the matching
pod loses `/data/.runner`. Deregister that runner from Gitea, or let the
replacement pod re-register intentionally with the current organization token.
Never delete an active runner PVC as routine cleanup.
Non-UI Gitea registration reconciliation uses an admin token stored outside this
repository:
```sh
kubectl -n gitea-runners create secret generic gitea-runner-admin-token --from-file=token=/secure/path/gitea-admin-token
kubectl -n gitea-runners run gitea-runner-registration-dry-run \
--restart=Never \
--image=curlimages/curl:8.10.1 \
--overrides='{"spec":{"containers":[{"name":"gitea-runner-registration-dry-run","image":"curlimages/curl:8.10.1","command":["/bin/sh","-ec","umask 077; cfg=$(mktemp); trap '\''rm -f \"$cfg\"'\'' EXIT; { printf '\''header = \"Authorization: token '\''; cat /admin-token/token; printf '\''\"\\n'\''; printf '\''url = \"https://gitea.hectic-lab.com/api/v1/orgs/hectic-lab/actions/runners\"\\n'\''; } > \"$cfg\"; curl -fsS --config \"$cfg\""],"volumeMounts":[{"name":"admin-token","mountPath":"/admin-token","readOnly":true}]}],"volumes":[{"name":"admin-token","secret":{"secretName":"gitea-runner-admin-token","defaultMode":256}}]}}'
kubectl -n gitea-runners logs pod/gitea-runner-registration-dry-run
```
Delete the temporary `gitea-runner-admin-token` Secret only after the dry-run
pod has completed and its logs have been collected.
After the dry-run list identifies a stale registration and the PVC or pod
deletion has been recorded, remove that exact Gitea runner by id through the
API:
```sh
runner_id='REPLACE_WITH_STALE_RUNNER_ID'
umask 077
curl_config=$(mktemp /tmp/gitea-runner-admin-curl.XXXXXX)
trap 'rm -f "$curl_config"' EXIT
{
printf 'request = "DELETE"\n'
printf 'header = "Authorization: token '
cat /secure/path/gitea-admin-token
printf '"\n'
printf 'url = "https://gitea.hectic-lab.com/api/v1/orgs/hectic-lab/actions/runners/%s"\n' "$runner_id"
} > "$curl_config"
curl -fsS --config "$curl_config"
```
Do not run the delete command for a runner that still has an active
`gitea-runner-*` pod or retained `data-gitea-runner-*` PVC unless that PVC is
being intentionally reset for re-registration.
## Application rollback
Rollback the app layer only. Do not use this section to destroy the cluster.
```sh
kubectl -n gitea-runners rollout history statefulset/gitea-runner
kubectl -n gitea-runners rollout undo statefulset/gitea-runner --to-revision=<known-good-revision>
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200
```
If a manifest rollback is needed, reapply the repo overlay after checking out the
known-good revision, then re-run the rollout checks:
```sh
kubectl -n gitea-runners apply -k infra/gitea-runners/k8s
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
```
## Full cluster teardown
This destroys Hetzner resources owned by the kube-hetzner stack, including the
`gitea-runners` cluster nodes, the `control-plane` node pool, the
`runner-workers` node pool, the cluster network, load balancer resources,
firewall objects, and any attached Hetzner CSI volumes still managed by the
stack. Do not run teardown unless the destruction is intentional.
```sh
tofu -chdir=infra/gitea-runners/opentofu plan -destroy -out=.sisyphus/evidence/task-12-destroy.plan
tofu -chdir=infra/gitea-runners/opentofu show -no-color .sisyphus/evidence/task-12-destroy.plan
tofu -chdir=infra/gitea-runners/opentofu apply .sisyphus/evidence/task-12-destroy.plan
```
## Partial OpenTofu apply recovery
If `tofu apply` fails after creating some resources, do not destroy blindly.
First reconcile state and inspect what the stack thinks exists:
```sh
tofu -chdir=infra/gitea-runners/opentofu plan -refresh-only -out=.sisyphus/evidence/task-12-refresh.plan
tofu -chdir=infra/gitea-runners/opentofu show -no-color .sisyphus/evidence/task-12-refresh.plan
tofu -chdir=infra/gitea-runners/opentofu state list
```
Then rerun the normal plan path. Use `-target` only as a last resort when a
single resource is stuck and the drift is understood.
## S3 backend recovery
If backend init or state access fails, first verify the bucket and versioning
outside OpenTofu, then reconfigure the backend:
```sh
nix run nixpkgs#awscli2 -- s3api head-bucket --bucket gitea-runner-hectic-lab
nix run nixpkgs#awscli2 -- s3api get-bucket-versioning --bucket gitea-runner-hectic-lab
tofu -chdir=infra/gitea-runners/opentofu init -reconfigure
tofu -chdir=infra/gitea-runners/opentofu plan
```
If the backend reports a stale lock, confirm no `tofu` process is active, then
use `tofu force-unlock <LOCK_ID>` with the lock id from the error. Never force
unlock a live plan or apply.
## Gitea outage troubleshooting
Use the public HTTPS service, not `localhost` or `127.0.0.1` inside job
containers.
```sh
kubectl -n gitea-runners run gitea-outage-probe --rm --restart=Never --image=curlimages/curl:8.10.1 -- curl -fsS https://gitea.hectic-lab.com/api/healthz
kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200 | grep -E 'connection refused|timeout|tls|certificate|temporary failure' || true
kubectl -n gitea-runners get events --sort-by=.lastTimestamp | tail -n 50
```
If Gitea is down, keep the existing StatefulSet and PVCs intact. Do not delete
`/data/.runner` just because the service is unavailable. Once Gitea returns,
repeat the token rotation or re-registration path if a pod restarted while the
service was unavailable and lost its runner identity.
## Release checklist
Do not release unless the following evidence files exist and are readable:
- `.sisyphus/evidence/task-5-cluster-plan.txt`
- `.sisyphus/evidence/task-5-secret-plan-scan.txt`
- `.sisyphus/evidence/task-9-deploy.txt`
- `.sisyphus/evidence/task-9-secret-mount.txt`
- `.sisyphus/evidence/task-10-ubuntu-workflow.txt`
- `.sisyphus/evidence/task-10-nix-workflow.txt`
- `.sisyphus/evidence/task-11-scale.txt`
- `.sisyphus/evidence/task-11-restart-cleanup.txt`
If any evidence file is missing, stop and collect it before treating the runbook
as complete.
+12 -71
View File
@@ -26,39 +26,12 @@
"aarch64-darwin" "aarch64-darwin"
]; ];
cudaUnfreeNames = [
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
];
cudaUnfreePredicate = pkg:
builtins.elem (nixpkgs.lib.getName pkg) cudaUnfreeNames;
forSystemsWithPkgs = supportedSystems: pkgOverlays: f: forSystemsWithPkgs = supportedSystems: pkgOverlays: f:
builtins.foldl' ( builtins.foldl' (
acc: system: let acc: system: let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
overlays = pkgOverlays; overlays = pkgOverlays;
config.allowUnfreePredicate = cudaUnfreePredicate;
}; };
systemOutputs = f { systemOutputs = f {
system = system; system = system;
@@ -85,7 +58,7 @@
else {}; else {};
in { in {
# -- For all systems -- # -- For all systems --
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems cudaUnfreeNames cudaUnfreePredicate; inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems;
forSystems = systems: nixpkgs.lib.genAttrs systems; forSystems = systems: nixpkgs.lib.genAttrs systems;
forAllSystems = nixpkgs.lib.genAttrs AllSystems; forAllSystems = nixpkgs.lib.genAttrs AllSystems;
@@ -94,7 +67,6 @@ in {
logs = builtins.readFile ./shell/logs.sh; logs = builtins.readFile ./shell/logs.sh;
check-tool = builtins.readFile ./shell/check-tool.sh; check-tool = builtins.readFile ./shell/check-tool.sh;
local-dir = builtins.readFile ./shell/local-dir.sh; local-dir = builtins.readFile ./shell/local-dir.sh;
load-sops = builtins.readFile ./shell/load-sops.sh;
}; };
sharedShellAliases = { sharedShellAliases = {
@@ -130,7 +102,7 @@ in {
# Consolidated SQL bundles for the `hectic` schema. Single source of truth # Consolidated SQL bundles for the `hectic` schema. Single source of truth
# for everything that creates objects in the `hectic` namespace, used by # for everything that creates objects in the `hectic` namespace, used by
# migrator (init-time), db-dev/database hydrate, and db-ops secrets loading. Consumers apply # migrator (init-time) and db-tool (postgres-init + hydrate). Consumers apply
# the full bundle via lib/hook/apply-hectic-bundle.sh. # the full bundle via lib/hook/apply-hectic-bundle.sh.
# #
# The whole hectic system shares one `versionString`; `hectic-version.sql` # The whole hectic system shares one `versionString`; `hectic-version.sql`
@@ -143,42 +115,19 @@ in {
hectic = let hectic = let
versionString = lib.fileContents ./hook/sql/HECTIC_VERSION; versionString = lib.fileContents ./hook/sql/HECTIC_VERSION;
static = path: { inherit path; sql = builtins.readFile path; }; static = path: { inherit path; sql = builtins.readFile path; };
templated = path: let templated = path: {
sql = builtins.replaceStrings sql = builtins.replaceStrings
[ "@HECTIC_VERSION@" ] [ "@HECTIC_VERSION@" ]
[ versionString ] [ versionString ]
(builtins.readFile path); (builtins.readFile path);
in {
inherit sql;
path = builtins.toFile (builtins.baseNameOf (toString path)) sql;
}; };
in rec { in {
inherit versionString; inherit versionString;
version = templated ./hook/sql/hectic-version.sql; version = templated ./hook/sql/hectic-version.sql;
secret = static ./hook/sql/hectic-secret.sql; secret = static ./hook/sql/hectic-secret.sql;
migration = static ./hook/sql/hectic-migration.sql; migration = static ./hook/sql/hectic-migration.sql;
inheritance = static ./hook/sql/hectic-inheritance.sql; inheritance = static ./hook/sql/hectic-inheritance.sql;
bundleFiles = [ applyBundleScript = ./hook/apply-hectic-bundle.sh;
version.path
secret.path
migration.path
inheritance.path
];
applyBundleScript =
builtins.replaceStrings
[
"@HECTIC_VERSION_SQL@"
"@HECTIC_SECRET_SQL@"
"@HECTIC_MIGRATION_SQL@"
"@HECTIC_INHERITANCE_SQL@"
]
[
"${version.path}"
"${secret.path}"
"${migration.path}"
"${inheritance.path}"
]
(builtins.readFile ./hook/apply-hectic-bundle.sh);
}; };
# Back-compat alias. Prefer `self.lib.hectic.inheritance`. # Back-compat alias. Prefer `self.lib.hectic.inheritance`.
@@ -215,27 +164,19 @@ in {
readModulesRecursive' = path: extraArgs: readModulesRecursive' = path: extraArgs:
with lib; with lib;
with builtins; let with builtins; let
collectPaths = dir: prefix: paths = pipe "${path}" [
concatLists (mapAttrsToList (name: type: let (filesystem.listFilesRecursive)
path' = dir + "/${name}"; (filter (hasSuffix ".nix"))
name' = if prefix == "" then name else "${prefix}/${name}"; ];
in
if type == "directory"
then collectPaths path' name'
else [{
inherit path';
name = name';
}]
) (readDir dir));
paths = filter (path': hasSuffix ".nix" path'.name) (collectPaths path "");
pathToName = flip pipe [ pathToName = flip pipe [
(removePrefix "${path}/")
(replaceStrings ["/" ".nix"] ["." ""]) (replaceStrings ["/" ".nix"] ["." ""])
(removeSuffix ".nix") (removeSuffix ".nix")
]; ];
attrList = attrList =
map (path': { map (path': {
name = pathToName path'.name; name = pathToName (unsafeDiscardStringContext path');
value = import path'.path' extraArgs; value = import path' extraArgs;
}) })
paths; paths;
in in
+24 -17
View File
@@ -7,12 +7,17 @@
# #
# Idempotent: each SQL file uses IF NOT EXISTS / CREATE OR REPLACE. # Idempotent: each SQL file uses IF NOT EXISTS / CREATE OR REPLACE.
# #
# Required env (caller injects from Nix):
# HECTIC_VERSION_SQL - path to hectic-version.sql (substituted)
# HECTIC_SECRET_SQL - path to hectic-secret.sql
# HECTIC_MIGRATION_SQL - path to hectic-migration.sql
# HECTIC_INHERITANCE_SQL - path to hectic-inheritance.sql
#
# Usage: # Usage:
# apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>] # apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>]
# #
# If DOTENV_CONTENT is non-empty, it is base64-encoded and then loaded into # If DOTENV_CONTENT is non-empty, it is loaded into hectic.secret via
# hectic.secret via hectic.load_secrets_from_env() after the bundle is applied. # hectic.load_secrets_from_env() after the bundle is applied.
# SQL file paths are substituted by Nix evaluation time.
apply_hectic_bundle() { apply_hectic_bundle() {
pgurl="${1:-}" pgurl="${1:-}"
@@ -23,27 +28,29 @@ apply_hectic_bundle() {
return 3 return 3
fi fi
set -- \ for var in HECTIC_VERSION_SQL HECTIC_SECRET_SQL HECTIC_MIGRATION_SQL HECTIC_INHERITANCE_SQL; do
"@HECTIC_VERSION_SQL@" \ eval "val=\${$var:-}"
"@HECTIC_SECRET_SQL@" \ if [ -z "$val" ]; then
"@HECTIC_MIGRATION_SQL@" \ printf '%s\n' "apply-hectic-bundle: $var not set" >&2
"@HECTIC_INHERITANCE_SQL@" return 3
fi
for sql_path do if [ ! -r "$val" ]; then
if [ ! -r "$sql_path" ]; then printf '%s\n' "apply-hectic-bundle: $var not readable: $val" >&2
printf '%s\n' "apply-hectic-bundle: SQL file not readable: $sql_path" >&2
return 1 return 1
fi fi
done done
for sql_path do psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_VERSION_SQL" || return 1
psql "$pgurl" -v ON_ERROR_STOP=1 -f "$sql_path" || return 1 psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_SECRET_SQL" || return 1
done psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_MIGRATION_SQL" || return 1
psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_INHERITANCE_SQL" || return 1
if [ -n "$env_content" ]; then if [ -n "$env_content" ]; then
env_content_b64="$(printf '%s' "$env_content" | base64 | tr -d '\n')" || return 1 # Dollar-quote with $ps_env$ tag to preserve all content verbatim.
psql "$pgurl" -v ON_ERROR_STOP=1 <<SQL || return 1 psql "$pgurl" -v ON_ERROR_STOP=1 <<SQL || return 1
SELECT hectic.load_secrets_from_env(convert_from(decode('$env_content_b64', 'base64'), 'UTF8')); SELECT hectic.load_secrets_from_env(\$ps_env\$
$env_content
\$ps_env\$);
SQL SQL
fi fi
+25 -23
View File
@@ -4,8 +4,8 @@ Single source of truth for every object created in the `hectic` PostgreSQL
schema. Consumed by: schema. Consumed by:
- `package/migrator` — applies the bundle on `migrator init` (mandatory). - `package/migrator` — applies the bundle on `migrator init` (mandatory).
- `package/db-tool` — applies the bundle in `db-dev` / `database hydrate` - `package/db-tool` — applies the bundle in `database hydrate` (default; opt
(default; opt out with `--no-hook`) and in `db-ops secrets load`. out with `--no-hook`).
- External consumers (e.g. `proxydoe`) — invoke `psql -f` directly against the - External consumers (e.g. `proxydoe`) — invoke `psql -f` directly against the
paths exposed via `self.lib.hectic.*.path`. paths exposed via `self.lib.hectic.*.path`.
@@ -42,26 +42,23 @@ that already matches.
```nix ```nix
self.lib.hectic = { self.lib.hectic = {
versionString; # e.g. "0.1.0" versionString; # e.g. "0.1.0"
version = { sql; path; }; # templated version = { sql; }; # templated
secret = { sql; path; }; secret = { sql; path; };
migration = { sql; path; }; migration = { sql; path; };
inheritance = { sql; path; }; inheritance = { sql; path; };
bundleFiles; # ordered bundle file paths applyBundleScript; # ./hook/apply-hectic-bundle.sh
applyBundleScript; # generated helper shell source with paths embedded
}; };
``` ```
`.sql` is the file contents as a string. `.path` is the Nix store path of the `.sql` is the file contents as a string. `.path` is the Nix store path of the
materialized file to pass to `psql -f`. `version.path` is generated at Nix verbatim source (only available on non-templated entries; consumers needing a
evaluation time from the templated SQL; the other `*.path` entries point at the materialized version of `version.sql` must do
verbatim source files in the store. `pkgs.runCommand "hectic-version.sql" { text = self.lib.hectic.version.sql; passAsFile = ["text"]; } ''cp "$textPath" "$out"''`).
## Shell helper (`apply-hectic-bundle.sh`) ## Shell helper (`apply-hectic-bundle.sh`)
`lib/hook/apply-hectic-bundle.sh` is a dash-compatible helper template. `lib/hook/apply-hectic-bundle.sh` is a dash-compatible helper sourced by both
`self.lib.hectic.applyBundleScript` is the generated shell source with concrete `migrator` and `db-tool`. Public entry point:
SQL paths embedded at Nix evaluation time. `migrator`, `db-dev`, and `db-ops` splice that
shell source directly into their generated scripts. Public entry point:
```sh ```sh
apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>] apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>]
@@ -73,23 +70,28 @@ apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>]
dollar-quoted (`$ps_env$`) string so secret values cannot terminate the dollar-quoted (`$ps_env$`) string so secret values cannot terminate the
literal. literal.
The SQL file paths are embedded into the helper at Nix evaluation time, so Required environment (paths to the SQL files):
callers only need to source the generated script and call the function.
External consumers that do not want to source the helper can still invoke - `HECTIC_VERSION_SQL`
`psql -f` against `self.lib.hectic.bundleFiles` or the individual - `HECTIC_SECRET_SQL`
`self.lib.hectic.*.path` entries directly. - `HECTIC_MIGRATION_SQL`
- `HECTIC_INHERITANCE_SQL`
`migrator` and `db-tool` set these via Nix at build time. External consumers
typically invoke `psql -f` against the paths directly instead of sourcing the
helper.
## Adding a new SQL file ## Adding a new SQL file
1. Add `lib/hook/sql/hectic-<name>.sql`. 1. Add `lib/hook/sql/hectic-<name>.sql`.
2. Wire it into `lib/default.nix` under `lib.hectic.<name>`. 2. Wire it into `lib/default.nix` under `lib.hectic.<name>`.
3. Add its `.path` to `lib.hectic.bundleFiles` in the correct order. 3. Inject `HECTIC_<NAME>_SQL` in both `package/migrator/default.nix` and
4. Add a matching placeholder/replacement in `lib.hectic.applyBundleScript` and `package/db-tool/default.nix`.
update `lib/hook/apply-hectic-bundle.sh` to apply the file. 4. Append a `psql -f "$HECTIC_<NAME>_SQL"` step to
`lib/hook/apply-hectic-bundle.sh` in the correct order.
5. Bump `HECTIC_VERSION` if the new content changes existing semantics. 5. Bump `HECTIC_VERSION` if the new content changes existing semantics.
6. Update tests in `test/package/migrator/test/postgresql/init-hectic-bundle/`, 6. Update tests in `test/package/migrator/test/postgresql/init-hectic-bundle/`
`test/package/db-tool/test/postgresql/hydrate-hook/`, and any `db-ops` and `test/package/db-tool/test/postgresql/hydrate-hook/`.
bundle-loading coverage.
## Versioning ## Versioning
-204
View File
@@ -1,204 +0,0 @@
load_sops_shell_quote() {
printf "'"
printf '%s' "$1" | sed "s/'/'\"'\"'/g"
printf "'"
}
load_sops_normalize_key() {
load_sops_normalized_key=$(printf '%s' "$1" | tr '.-' '__' | tr '[:lower:]' '[:upper:]')
case "$load_sops_normalized_key" in
''|[!A-Z_]*|*[!A-Z0-9_]*)
printf 'load-sops: invalid environment name after key normalization\n' >&2
return 1
;;
esac
printf '%s\n' "$load_sops_normalized_key"
}
load_sops_env_from_sops_file() {
load_sops_file=$1
load_sops_extract=${2-}
if ! command -v sops >/dev/null 2>&1; then
printf 'load-sops: required tool `sops` not found\n' >&2
return 1
fi
if ! command -v yq >/dev/null 2>&1; then
printf 'load-sops: required tool `yq` not found\n' >&2
return 1
fi
load_sops_decrypted=''
load_sops_attempt=0
load_sops_max_retries=${LOAD_SOPS_MAX_RETRIES:-1}
load_sops_prompt=${LOAD_SOPS_PROMPT:-0}
while :; do
if [ -n "$load_sops_extract" ]; then
if load_sops_decrypted=$(sops -d --extract "$load_sops_extract" "$load_sops_file" 2>/dev/null); then
load_sops_status=0
else
load_sops_status=$?
fi
else
if load_sops_decrypted=$(sops -d "$load_sops_file" 2>/dev/null); then
load_sops_status=0
else
load_sops_status=$?
fi
fi
if [ "$load_sops_status" -eq 0 ]; then
break
fi
if [ "$load_sops_prompt" != 1 ]; then
printf 'load-sops: failed to decrypt file\n' >&2
return 1
fi
if ! [ -t 0 ] || ! [ -r /dev/tty ]; then
printf 'load-sops: decrypt failed and prompt requested, but no TTY is available\n' >&2
return 1
fi
load_sops_attempt=$((load_sops_attempt + 1))
if [ "$load_sops_max_retries" != 0 ] && [ "$load_sops_attempt" -gt "$load_sops_max_retries" ]; then
printf 'load-sops: decrypt failed after configured retries\n' >&2
return 1
fi
load_sops_use_script=${LOAD_SOPS_USE_SCRIPT:-auto}
load_sops_quoted_file=$(load_sops_shell_quote "$load_sops_file") || return 1
load_sops_quoted_tty=$(load_sops_shell_quote "$(tty)") || return 1
case "$load_sops_use_script" in
auto)
if command -v script >/dev/null 2>&1 && [ -t 0 ]; then
if script -qefc "env GPG_TTY=$load_sops_quoted_tty sops --output /dev/null -d $load_sops_quoted_file" /dev/null >/dev/null 2>&1; then
load_sops_script_status=0
else
load_sops_script_status=$?
fi
if [ "$load_sops_script_status" -eq 0 ]; then
continue
fi
fi
;;
1)
if ! command -v script >/dev/null 2>&1; then
printf 'load-sops: required tool `script` not found\n' >&2
return 1
fi
if script -qefc "env GPG_TTY=$load_sops_quoted_tty sops --output /dev/null -d $load_sops_quoted_file" /dev/null >/dev/null 2>&1; then
load_sops_script_status=0
else
load_sops_script_status=$?
fi
if [ "$load_sops_script_status" -eq 0 ]; then
continue
fi
;;
0)
;;
*)
printf 'load-sops: LOAD_SOPS_USE_SCRIPT must be auto, 0, or 1\n' >&2
return 1
;;
esac
printf 'load-sops: enter SOPS_AGE_KEY_CMD: ' >/dev/tty
if ! IFS= read -r SOPS_AGE_KEY_CMD </dev/tty; then
printf 'load-sops: failed to read prompt input\n' >&2
return 1
fi
export SOPS_AGE_KEY_CMD
done
load_sops_env_from_yaml_text "$load_sops_decrypted"
}
load_sops_env_from_yaml_file() {
load_sops_file=$1
if ! command -v yq >/dev/null 2>&1; then
printf 'load-sops: required tool `yq` not found\n' >&2
return 1
fi
load_sops_env_from_yaml_text "$(cat "$load_sops_file")"
}
load_sops_env_from_yaml_text() {
load_sops_yaml=$1
load_sops_seen=''
if load_sops_keys=$(printf '%s' "$load_sops_yaml" | yq -r 'keys | .[]' 2>/dev/null); then
load_sops_keys_status=0
else
load_sops_keys_status=$?
fi
if [ "$load_sops_keys_status" -ne 0 ]; then
printf 'load-sops: failed to inspect YAML top-level keys\n' >&2
return 1
fi
while IFS= read -r load_sops_key; do
[ -n "$load_sops_key" ] || continue
load_sops_name=$(load_sops_normalize_key "$load_sops_key") || return 1
case "
$load_sops_seen
" in
*"
$load_sops_name
"*)
if [ "${LOAD_SOPS_ALLOW_COLLISIONS:-0}" != 1 ]; then
printf 'load-sops: normalized environment name collision\n' >&2
return 1
fi
;;
esac
load_sops_seen=${load_sops_seen}${load_sops_seen:+"
"}$load_sops_name
load_sops_kind=$(printf '%s' "$load_sops_yaml" | yq -r '."'"$load_sops_key"'" | kind' 2>/dev/null) || {
printf 'load-sops: failed to inspect YAML value kind\n' >&2
return 1
}
load_sops_tag=$(printf '%s' "$load_sops_yaml" | yq -r '."'"$load_sops_key"'" | tag' 2>/dev/null) || {
printf 'load-sops: failed to inspect YAML value tag\n' >&2
return 1
}
if [ "$load_sops_kind" != scalar ]; then
printf 'load-sops: top-level YAML values must be scalars\n' >&2
return 1
fi
if [ "$load_sops_tag" = '!!null' ]; then
printf 'load-sops: top-level YAML null values are not supported\n' >&2
return 1
fi
if [ "${LOAD_SOPS_OVERWRITE:-1}" = 0 ]; then
eval 'load_sops_already_set=${'"$load_sops_name"'+x}'
if [ -n "$load_sops_already_set" ]; then
continue
fi
fi
load_sops_value=$(printf '%s' "$load_sops_yaml" | yq -r '."'"$load_sops_key"'"' 2>/dev/null) || {
printf 'load-sops: failed to read YAML scalar value\n' >&2
return 1
}
load_sops_quoted=$(load_sops_shell_quote "$load_sops_value") || return 1
eval "export $load_sops_name=$load_sops_quoted"
done <<EOF
$load_sops_keys
EOF
}
+2 -2
View File
@@ -9,11 +9,11 @@ with self.lib;
let let
# Combine hectic modules into one # Combine hectic modules into one
hectic.imports = attrValues ( hectic.imports = attrValues (
readModulesRecursive' (flake + "/nixos/module/hectic") { inherit flake self inputs; } readModulesRecursive' ./hectic { inherit flake self inputs; }
); );
# Read generic modules separately # Read generic modules separately
generic = readModulesRecursive' generic = readModulesRecursive'
(flake + "/nixos/module/generic") ./generic
{ inherit flake self inputs; }; { inherit flake self inputs; };
in generic // { in generic // {
inherit hectic; inherit hectic;
@@ -14,15 +14,10 @@
"lvgkcfjl" "lvgkcfjl"
"MrAlex0O" "MrAlex0O"
"Антоша" "Антоша"
"snuff"
]; ];
adminNames = [ "yukkop" ]; adminNames = [ "yukkop" ];
matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml";
cfg = config.hectic.generic.matrix-cluster;
in { in {
config = lib.mkIf cfg.enable {
hectic.generic.matrix-cluster.users = builtins.listToAttrs ( hectic.generic.matrix-cluster.users = builtins.listToAttrs (
map (name: { map (name: {
inherit name; inherit name;
@@ -40,9 +35,8 @@ in {
value = { value = {
key = "matrix/users/${name}/password"; key = "matrix/users/${name}/password";
owner = "matrix-synapse"; owner = "matrix-synapse";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
}) userNames }) userNames
); );
};
} }
-16
View File
@@ -231,15 +231,6 @@ in {
description = "File containing PORKBUN_SECRET_API_KEY value."; description = "File containing PORKBUN_SECRET_API_KEY value.";
}; };
}; };
jitsi.preferredDomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Optional self-hosted Jitsi Meet domain to advertise to Matrix/Element
clients alongside the cluster-managed homeserver.
'';
};
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [ config = lib.mkIf cfg.enable (lib.mkMerge [
@@ -250,7 +241,6 @@ in {
# failover flip does not need a separate provisioning step. # failover flip does not need a separate provisioning step.
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d /var/lib/matrix-synapse 0750 matrix-synapse matrix-synapse -" "d /var/lib/matrix-synapse 0750 matrix-synapse matrix-synapse -"
"Z ${s3Cfg.mediaStorePath} 0700 matrix-synapse matrix-synapse -"
]; ];
systemd.services.matrix-cluster-signing-key = { systemd.services.matrix-cluster-signing-key = {
@@ -320,12 +310,6 @@ in {
media_store_path = s3Cfg.mediaStorePath; media_store_path = s3Cfg.mediaStorePath;
signing_key_path = "/var/lib/matrix-synapse/homeserver.signing.key"; signing_key_path = "/var/lib/matrix-synapse/homeserver.signing.key";
# Tolerate bursty Element/iPhone presence syncs without disabling limits.
rc_presence.per_user = {
per_second = 0.5;
burst_count = 5;
};
experimental_features = { experimental_features = {
msc3266_enabled = true; msc3266_enabled = true;
msc4140_enabled = true; msc4140_enabled = true;
+2 -10
View File
@@ -1,7 +1,7 @@
{ {
inputs, inputs,
flake,
self, self,
...
}: { }: {
pkgs, pkgs,
lib, lib,
@@ -27,15 +27,7 @@ in {
users.defaultUserShell = pkgs.zsh; users.defaultUserShell = pkgs.zsh;
# Enable flakes and new 'nix' command # Enable flakes and new 'nix' command
nix.settings = { nix.settings.experimental-features = "nix-command flakes";
experimental-features = "nix-command flakes";
extra-substituters = [
"https://cache.hectic-lab.com/hectic"
];
extra-trusted-public-keys = [
"hectic:KMQsKow4SoA9K2vOJlOljmx7/Zpf91Yy+5qEtxDDCzA="
];
};
networking.firewall.enable = true; networking.firewall.enable = true;
+11 -66
View File
@@ -1,5 +1,7 @@
{ {
... inputs,
flake,
self,
}: }:
{ {
pkgs, pkgs,
@@ -8,28 +10,9 @@
... ...
}: let }: let
cfg = config.hectic.hardware.hetzner-cloud; cfg = config.hectic.hardware.hetzner-cloud;
isNewer = cfg.generation == "newer";
networkMatchConfig =
(lib.optionalAttrs (cfg.networkMatchConfigName != null) {
Name = cfg.networkMatchConfigName;
})
// (lib.optionalAttrs (cfg.networkMatchConfigMac != null) {
PermanentMACAddress = cfg.networkMatchConfigMac;
});
in { in {
options.hectic.hardware.hetzner-cloud = { options.hectic.hardware.hetzner-cloud = {
enable = lib.mkEnableOption "Enable hetzner-cloud hardware configurations"; enable = lib.mkEnableOption "Enable hetzner-cloud hardware configurations";
generation = lib.mkOption {
type = lib.types.enum [ "classic" "newer" ];
default = "classic";
description = ''
Hetzner server generation profile.
`classic` keeps the historical `/dev/sda` assumption.
`newer` is for ccx/NVMe-era servers and defaults the disk device to
`/dev/nvme0n1`.
'';
};
#bootParUuid = lib.mkOption { #bootParUuid = lib.mkOption {
# type = with lib.types; nullOr oneOf [ # type = with lib.types; nullOr oneOf [
# (lib.types.strMatching "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$") # (lib.types.strMatching "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$")
@@ -56,49 +39,27 @@ in {
''; '';
}; };
floatingIpv4 = lib.mkOption {
type = with lib.types; nullOr (strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$");
default = null;
example = "188.243.124.247";
description = ''
Optional Hetzner Floating IPv4 configured as `/32` on the primary interface.
'';
};
device = lib.mkOption { device = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = if isNewer then "/dev/nvme0n1" else "/dev/sda"; default = "/dev/sda";
example = "/dev/disk/by-uuid/f184a16b-6eca-41cb-b48a-ff37cdce1d79"; example = "/dev/disk/by-uuid/f184a16b-6eca-41cb-b48a-ff37cdce1d79";
description = '' description = ''
boot device uuid boot device uuid
if it is null then will use "/dev/sda" if it is null then will use "/dev/sda"
/dev/sda - default hetzner cloud device /dev/sda - default hetzner cloud device
/dev/nvme0n1 - default for newer Hetzner generations
!! But can changes on reboot if server have volumes !! But can changes on reboot if server have volumes
!! So use IDs !! So use IDs
''; '';
}; };
networkMatchConfigName = lib.mkOption { networkMatchConfigName = lib.mkOption {
type = with lib.types; nullOr str; type = lib.types.strMatching "^(enp1s0|ens3)$";
default = null;
example = "enp1s0"; example = "enp1s0";
description = '' description = ''
Optional interface name to match in systemd-networkd. type of network conection,
on older hetzner servers may be `ens3`
on newer probably `enp1s0`
Prefer `networkMatchConfigMac` for stable matching across rescue you can use `networkctl list` on server to know it
images and installed systems that may rename interfaces differently.
You can use `networkctl list` on server to know it.
'';
};
networkMatchConfigMac = lib.mkOption {
type = with lib.types; nullOr (strMatching "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$");
default = null;
example = "92:00:08:4a:b0:32";
description = ''
Optional permanent MAC address to match in systemd-networkd.
This is the preferred Hetzner Cloud matching method because interface
names can differ between rescue images and installed NixOS systems.
''; '';
}; };
}; };
@@ -106,15 +67,6 @@ in {
config = lib.mkIf cfg.enable (lib.mkMerge config = lib.mkIf cfg.enable (lib.mkMerge
[ [
{ {
boot.loader.systemd-boot.enable = false;
boot.loader.efi.canTouchEfiVariables = false;
boot.loader.grub = {
enable = true;
efiSupport = true;
efiInstallAsRemovable = true;
device = "nodev";
};
boot.initrd.availableKernelModules = [ boot.initrd.availableKernelModules = [
"ata_piix" "ata_piix"
"uhci_hcd" "uhci_hcd"
@@ -125,12 +77,12 @@ in {
networking.useNetworkd = true; networking.useNetworkd = true;
systemd.network.enable = true; systemd.network.enable = true;
systemd.network.networks."30-wan" = { systemd.network.networks."30-wan" = {
matchConfig = networkMatchConfig; matchConfig.Name = cfg.networkMatchConfigName;
networkConfig.DHCP = "no"; networkConfig.DHCP = "no";
address = [ address = [
"${cfg.ipv4}/32" "${cfg.ipv4}/32"
"${cfg.ipv6}::/64" "${cfg.ipv6}::/64"
] ++ lib.optional (cfg.floatingIpv4 != null) "${cfg.floatingIpv4}/32"; ];
routes = [ routes = [
{ Gateway = "172.31.1.1"; GatewayOnLink = true; } { Gateway = "172.31.1.1"; GatewayOnLink = true; }
{ Gateway = "fe80::1"; } { Gateway = "fe80::1"; }
@@ -172,13 +124,6 @@ in {
}; };
}; };
}; };
assertions = [
{
assertion = cfg.networkMatchConfigName != null || cfg.networkMatchConfigMac != null;
message = "hectic.hardware.hetzner-cloud requires networkMatchConfigName or networkMatchConfigMac";
}
];
} }
(lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") { (lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") {
boot.initrd.kernelModules = [ "virtio_gpu" ]; boot.initrd.kernelModules = [ "virtio_gpu" ];
-134
View File
@@ -1,134 +0,0 @@
{ ... }:
{
lib,
config,
...
}: let
cfg = config.hectic.hardware.njalla;
in {
options.hectic.hardware.njalla = {
enable = lib.mkEnableOption "Enable njalla hardware configurations";
ipv4 = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "185.193.126.103";
description = ''
Njalla IPv4 address assigned to the host.
'';
};
ipv4PrefixLength = lib.mkOption {
type = lib.types.int;
default = 24;
example = 24;
description = ''
Njalla IPv4 prefix length.
'';
};
ipv4Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
default = "185.193.126.1";
example = "185.193.126.1";
description = ''
Njalla IPv4 gateway.
'';
};
ipv6 = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
example = "2a0a:3840:1337:126:0:b9c1:7e67:1337";
description = ''
Njalla IPv6 address assigned to the host.
'';
};
ipv6PrefixLength = lib.mkOption {
type = lib.types.int;
default = 64;
example = 64;
description = ''
Njalla IPv6 prefix length.
'';
};
ipv6Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
default = "2a0a:3840:1337:126::1";
example = "2a0a:3840:1337:126::1";
description = ''
Njalla IPv6 gateway.
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.str;
default = "eth0";
example = "eth0";
description = ''
Njalla container network interface name.
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "/dev/vda";
example = "/dev/disk/by-id/virtio-root";
description = ''
Njalla installation disk for disko/nixos-anywhere.
`/dev/vda` is the default block device visible on the inspected Njalla
host. Prefer a stable `/dev/disk/by-id/...` path when available.
'';
};
enableDisko = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to provide a disko layout for nixos-anywhere installs.
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
boot.isContainer = true;
networking.useDHCP = false;
networking.useNetworkd = true;
systemd.network.enable = true;
systemd.network.networks."30-wan" = {
matchConfig.Name = cfg.networkMatchConfigName;
networkConfig.DHCP = "no";
address = [
"${cfg.ipv4}/${toString cfg.ipv4PrefixLength}"
"${cfg.ipv6}/${toString cfg.ipv6PrefixLength}"
];
routes = [
{ Gateway = cfg.ipv4Gateway; }
{ Gateway = cfg.ipv6Gateway; }
];
};
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
}
(lib.mkIf cfg.enableDisko {
boot.loader.grub.device = cfg.device;
disko.devices.disk.main = {
type = "disk";
device = cfg.device;
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
priority = 1;
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
})
]);
}
@@ -9,10 +9,10 @@
config, config,
... ...
}: let }: let
cfg = config.hectic.hardware.zomro; cfg = config.hectic.hardware.zombro;
in { in {
options.hectic.hardware.zomro = { options.hectic.hardware.zombro = {
enable = lib.mkEnableOption "Enable zomro hardware configurations"; enable = lib.mkEnableOption "Enable zombro hardware configurations";
device = lib.mkOption { device = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/dev/vda"; default = "/dev/vda";
@@ -20,7 +20,7 @@ in {
description = '' description = ''
boot device uuid boot device uuid
if it is null then will use "/dev/vda" if it is null then will use "/dev/vda"
/dev/vda - default zomro device /dev/vda - default zombro device
!! But can changes on reboot if server have volumes !! But can changes on reboot if server have volumes
!! So use IDs !! So use IDs
''; '';
+30 -1
View File
@@ -25,7 +25,36 @@ in {
programs.bash.shellAliases.tmux = "tmux a"; programs.bash.shellAliases.tmux = "tmux a";
home-manager.sharedModules = [ home-manager.sharedModules = [
(flake + "/home/module/program/tmux.nix") {
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
]; ];
home-manager.users.root.home.stateVersion = lib.mkDefault "25.05"; home-manager.users.root.home.stateVersion = lib.mkDefault "25.05";
-73
View File
@@ -1,73 +0,0 @@
{ ... }: {
lib,
config,
...
}: let
cfg = config.hectic.services.attic;
in {
options.hectic.services.attic = {
enable = lib.mkEnableOption "Attic binary cache server";
hostName = lib.mkOption {
type = lib.types.str;
description = "Public hostname used by clients to reach this Attic server.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Local address atticd binds to behind the reverse proxy.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Local port atticd binds to behind the reverse proxy.";
};
environmentFile = lib.mkOption {
type = lib.types.path;
description = ''
SOPS-backed environment file containing Attic JWT and object-storage
credentials.
'';
};
storage = {
bucket = lib.mkOption {
type = lib.types.str;
description = "Hetzner Object Storage bucket name used by Attic.";
};
endpoint = lib.mkOption {
type = lib.types.str;
description = "S3-compatible HTTPS endpoint for Hetzner Object Storage.";
};
region = lib.mkOption {
type = lib.types.str;
description = "Region name for Hetzner Object Storage.";
};
};
};
config = lib.mkIf cfg.enable {
services.atticd = {
enable = true;
environmentFile = cfg.environmentFile;
settings = {
listen = "${cfg.listenAddress}:${toString cfg.port}";
allowed-hosts = [ cfg.hostName ];
api-endpoint = "https://${cfg.hostName}/";
compression.type = "zstd";
storage = {
type = "s3";
bucket = cfg.storage.bucket;
endpoint = cfg.storage.endpoint;
region = cfg.storage.region;
};
};
};
};
}
+4 -7
View File
@@ -10,15 +10,12 @@
... ...
}: let }: let
legacyCfg = config.hectic.services.matrix; legacyCfg = config.hectic.services.matrix;
hasClusterCfg = config.hectic ? generic && config.hectic.generic ? matrix-cluster; clusterCfg = config.hectic.generic.matrix-cluster;
clusterCfg = if hasClusterCfg then config.hectic.generic.matrix-cluster else null;
clusterSynapseEnabled = clusterSynapseEnabled =
if hasClusterCfg clusterCfg.enable
then clusterCfg.enable && (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary");
&& (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary")
else false;
enabled = legacyCfg.enable || clusterSynapseEnabled; enabled = legacyCfg.enable || clusterSynapseEnabled;
matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else if hasClusterCfg then clusterCfg.matrixDomain else ""; matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else clusterCfg.matrixDomain;
in { in {
config = lib.mkIf enabled (let config = lib.mkIf enabled (let
keyFile = "/run/livekit.key"; keyFile = "/run/livekit.key";
+5 -18
View File
@@ -10,19 +10,12 @@
... ...
}: let }: let
legacyCfg = config.hectic.services.matrix; legacyCfg = config.hectic.services.matrix;
hasClusterCfg = config.hectic ? generic && config.hectic.generic ? matrix-cluster; clusterCfg = config.hectic.generic.matrix-cluster;
clusterCfg = if hasClusterCfg then config.hectic.generic.matrix-cluster else null;
clusterSynapseEnabled = clusterSynapseEnabled =
if hasClusterCfg clusterCfg.enable
then clusterCfg.enable && (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary");
&& (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary")
else false;
enabled = legacyCfg.enable || clusterSynapseEnabled; enabled = legacyCfg.enable || clusterSynapseEnabled;
matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else if hasClusterCfg then clusterCfg.matrixDomain else ""; matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else clusterCfg.matrixDomain;
jitsiPreferredDomain =
if legacyCfg.enable && config.hectic.services.jitsi.enable
then config.hectic.services.jitsi.hostName
else if hasClusterCfg then clusterCfg.jitsi.preferredDomain else null;
in { in {
config = lib.mkIf enabled { config = lib.mkIf enabled {
services.nginx.virtualHosts."element.${matrixDomain}" = { services.nginx.virtualHosts."element.${matrixDomain}" = {
@@ -31,7 +24,7 @@ in {
locations."= /config.element.${matrixDomain}.json".return = "302 /config.json"; locations."= /config.element.${matrixDomain}.json".return = "302 /config.json";
root = pkgs.hectic.element-web.override { root = pkgs.element-web.override {
conf = { conf = {
default_server_config = { default_server_config = {
"m.homeserver".base_url = "https://${matrixDomain}"; "m.homeserver".base_url = "https://${matrixDomain}";
@@ -43,12 +36,6 @@ in {
matrixDomain matrixDomain
]; ];
hectic.videoMessages.enabled = true;
jitsi = lib.optionalAttrs (jitsiPreferredDomain != null) {
preferred_domain = jitsiPreferredDomain;
};
default_theme = "dark"; default_theme = "dark";
show_labs_settings = true; show_labs_settings = true;
}; };
-224
View File
@@ -1,224 +0,0 @@
{ ... }: {
lib,
config,
...
}: let
cfg = config.hectic.services.ente;
webHostNames = [
cfg.domains.accounts
cfg.domains.cast
cfg.domains.photos
];
in {
options.hectic.services.ente = {
enable = lib.mkEnableOption "Ente Photos self-hosted service";
apiDomain = lib.mkOption {
type = lib.types.str;
description = "Public hostname for the Ente Museum API.";
};
domains = {
accounts = lib.mkOption {
type = lib.types.str;
description = "Public hostname for the Ente accounts web app.";
};
cast = lib.mkOption {
type = lib.types.str;
description = "Public hostname for the Ente cast web app.";
};
albums = lib.mkOption {
type = lib.types.str;
description = "Public hostname for public Ente album links.";
};
photos = lib.mkOption {
type = lib.types.str;
description = "Public hostname for the Ente Photos web app.";
};
};
maxUploadSize = lib.mkOption {
type = lib.types.str;
default = "10G";
description = "Maximum request body accepted by nginx in front of Museum.";
};
disableRegistration = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether Museum should reject new account registration.";
};
smtp = {
enable = lib.mkEnableOption "SMTP delivery for Ente verification emails";
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "SMTP host Museum uses to send verification emails.";
};
port = lib.mkOption {
type = lib.types.port;
default = 25;
description = "SMTP port Museum uses to send verification emails.";
};
email = lib.mkOption {
type = lib.types.str;
description = "From email address used by Museum.";
};
senderName = lib.mkOption {
type = lib.types.str;
default = "Ente Photos";
description = "Display name used for Ente verification emails.";
};
encryption = lib.mkOption {
type = lib.types.nullOr (lib.types.enum [ "tls" "ssl" ]);
default = null;
description = "Optional SMTP encryption mode. Leave null for local plaintext SMTP.";
};
};
storage = {
bucket = lib.mkOption {
type = lib.types.str;
description = "S3-compatible bucket used by Ente for photo object storage.";
};
endpoint = lib.mkOption {
type = lib.types.str;
description = "S3-compatible endpoint URL.";
};
region = lib.mkOption {
type = lib.types.str;
description = "S3-compatible region name.";
};
hotStorage = lib.mkOption {
type = lib.types.enum [
"b2-eu-cen"
"wasabi-eu-central-2-v3"
"scw-eu-fr-v3"
];
default = "b2-eu-cen";
description = ''
Museum's primary hot-storage key. Upstream requires one of its
historical S3 storage identifiers even when the backing provider is a
generic S3-compatible service.
'';
};
usePathStyleUrls = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether Museum should use path-style S3 URLs.";
};
};
secrets = {
encryptionKeyFile = lib.mkOption {
type = lib.types.path;
description = "File containing Museum key.encryption.";
};
hashKeyFile = lib.mkOption {
type = lib.types.path;
description = "File containing Museum key.hash.";
};
jwtSecretFile = lib.mkOption {
type = lib.types.path;
description = "File containing Museum jwt.secret.";
};
s3AccessKeyFile = lib.mkOption {
type = lib.types.path;
description = "File containing the S3 access key.";
};
s3SecretKeyFile = lib.mkOption {
type = lib.types.path;
description = "File containing the S3 secret key.";
};
};
};
config = lib.mkIf cfg.enable {
services.ente = {
api = {
enable = true;
enableLocalDB = true;
domain = cfg.apiDomain;
nginx.enable = true;
settings = {
key = {
encryption._secret = cfg.secrets.encryptionKeyFile;
hash._secret = cfg.secrets.hashKeyFile;
};
jwt.secret._secret = cfg.secrets.jwtSecretFile;
s3 = {
hot_storage.primary = cfg.storage.hotStorage;
derived-storage = cfg.storage.hotStorage;
are_local_buckets = false;
use_path_style_urls = cfg.storage.usePathStyleUrls;
${cfg.storage.hotStorage} = {
key._secret = cfg.secrets.s3AccessKeyFile;
secret._secret = cfg.secrets.s3SecretKeyFile;
endpoint = cfg.storage.endpoint;
region = cfg.storage.region;
bucket = cfg.storage.bucket;
};
};
internal.disable-registration = cfg.disableRegistration;
smtp = lib.mkIf cfg.smtp.enable ({
inherit (cfg.smtp) host port email;
sender-name = cfg.smtp.senderName;
} // lib.optionalAttrs (cfg.smtp.encryption != null) {
encryption = cfg.smtp.encryption;
});
};
};
web = {
enable = true;
domains = {
api = cfg.apiDomain;
inherit (cfg.domains) accounts cast albums photos;
};
};
};
services.nginx.virtualHosts =
(lib.genAttrs webHostNames (_: {
enableACME = true;
forceSSL = true;
})) // {
${cfg.apiDomain} = {
enableACME = true;
forceSSL = true;
extraConfig = lib.mkForce ''
client_max_body_size ${cfg.maxUploadSize};
'';
locations."/".extraConfig = ''
proxy_read_timeout 600s;
proxy_send_timeout 600s;
'';
};
};
};
}
+2 -2
View File
@@ -82,8 +82,8 @@ in {
security.acme = { security.acme = {
acceptTerms = true; acceptTerms = true;
defaults = { defaults = {
email = lib.mkDefault "hectic.yukkop.it@gmail.com"; email = "hectic.yukkop.it@gmail.com";
enableDebugLogs = lib.mkDefault true; enableDebugLogs = true;
}; };
}; };
+2 -12
View File
@@ -56,18 +56,8 @@ in {
certificateScheme = "acme-nginx"; certificateScheme = "acme-nginx";
}; };
services.postfix.settings.main = { # NOTE(yukkop): avoid Gmail rejection due to missing IPv6 PTR records
# NOTE(yukkop): avoid Gmail rejection due to missing IPv6 PTR records. services.postfix.settings.main.inet_protocols = lib.mkDefault "ipv4";
inet_protocols = lib.mkDefault "ipv4";
# NOTE(yukkop): nixos-mailserver enables DANE by default. Some large MXes
# currently fail certificate verification under this policy, which leaves
# otherwise valid transactional mail deferred in the queue. Keep STARTTLS
# opportunistic for outbound delivery rather than blocking mail entirely.
smtp_tls_security_level = lib.mkForce "may";
smtp_dns_support_level = lib.mkForce "enabled";
smtp_tls_policy_maps = lib.mkForce "";
};
security.acme.acceptTerms = true; security.acme.acceptTerms = true;
security.acme.defaults.email = "security@" + cfg.domain; security.acme.defaults.email = "security@" + cfg.domain;
+1 -1
View File
@@ -147,7 +147,7 @@ in {
(lib.mkIf cfg.watcher.enable { (lib.mkIf cfg.watcher.enable {
sops.secrets."sentinèlla/watcher/environment" = lib.mkDefault { sops.secrets."sentinèlla/watcher/environment" = lib.mkDefault {
sopsFile = flake + "/sus/sentinella-default.yaml"; sopsFile = "${flake}/sus/sentinella-default.yaml";
}; };
systemd.services."sentinella-watcher" = { systemd.services."sentinella-watcher" = {
@@ -1,153 +0,0 @@
{ ... }: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.stable-video-diffusion;
in {
options.hectic.services.stable-video-diffusion = {
enable = lib.mkEnableOption "local Stable Video Diffusion HTTP API";
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address the Stable Video Diffusion API binds to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 7861;
description = "Port the Stable Video Diffusion API binds to.";
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.hectic.stable-video-diffusion-api;
defaultText = lib.literalExpression "pkgs.hectic.stable-video-diffusion-api";
description = "Package providing the Stable Video Diffusion API executable.";
};
modelId = lib.mkOption {
type = lib.types.str;
default = "stabilityai/stable-video-diffusion-img2vid-xt";
description = "Model identifier loaded by the Stable Video Diffusion API.";
};
device = lib.mkOption {
type = with lib.types; nullOr (enum [ "cpu" "cuda" ]);
default = null;
description = ''
Torch device requested from the Stable Video Diffusion API. When null,
the package keeps its own auto-detection behavior.
'';
};
libraryPath = lib.mkOption {
type = with lib.types; listOf str;
default = [];
description = ''
Runtime library paths added to LD_LIBRARY_PATH. CUDA/NVIDIA deployments
should include /run/opengl-driver/lib so libcuda.so is visible to torch.
'';
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/stable-video-diffusion-api";
description = "Persistent state directory for the Stable Video Diffusion API.";
};
cacheDir = lib.mkOption {
type = lib.types.str;
default = "/var/cache/stable-video-diffusion-api";
description = "Cache directory for downloaded model and runtime artifacts.";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.stateDir}/outputs";
defaultText = lib.literalExpression ''"\${config.hectic.services.stable-video-diffusion.stateDir}/outputs"'';
description = "Directory where generated video outputs are written.";
};
environmentFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = ''
Optional environment file for secrets or runtime overrides. Values from
the unit environment define SVD_API_HOST, SVD_API_PORT, SVD_MODEL_ID,
SVD_STATE_DIR, SVD_CACHE_DIR, SVD_OUTPUT_DIR, and optionally SVD_DEVICE
and LD_LIBRARY_PATH by default.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the API port in the firewall.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.device != "cuda" || cfg.libraryPath != [];
message = "hectic.services.stable-video-diffusion.libraryPath must include the NVIDIA runtime library path when device is cuda.";
}
];
users.users.stable-video-diffusion = {
isSystemUser = true;
group = "stable-video-diffusion";
};
users.groups.stable-video-diffusion = {};
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0750 stable-video-diffusion stable-video-diffusion -"
"d ${cfg.cacheDir} 0750 stable-video-diffusion stable-video-diffusion -"
"d ${cfg.outputDir} 0750 stable-video-diffusion stable-video-diffusion -"
];
systemd.services.stable-video-diffusion-api = {
description = "Stable Video Diffusion HTTP API";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
SVD_API_HOST = cfg.host;
SVD_API_PORT = toString cfg.port;
SVD_MODEL_ID = cfg.modelId;
SVD_STATE_DIR = cfg.stateDir;
SVD_CACHE_DIR = cfg.cacheDir;
SVD_OUTPUT_DIR = cfg.outputDir;
}
// lib.optionalAttrs (cfg.device != null) {
SVD_DEVICE = cfg.device;
}
// lib.optionalAttrs (cfg.libraryPath != []) {
LD_LIBRARY_PATH = lib.concatStringsSep ":" cfg.libraryPath;
};
serviceConfig = lib.mkMerge [
{
Type = "simple";
User = "stable-video-diffusion";
Group = "stable-video-diffusion";
WorkingDirectory = cfg.stateDir;
ExecStart = lib.getExe' cfg.package "stable-video-diffusion-api";
Restart = "on-failure";
RestartSec = "5s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
StandardOutput = "journal";
StandardError = "journal";
}
(lib.mkIf (cfg.environmentFile != null) {
EnvironmentFile = cfg.environmentFile;
})
];
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}
@@ -0,0 +1,230 @@
# Matrix Cluster Failover Runbook (`accord.tube`)
Primary: `hectic-lab` (NL, `128.140.75.58`)
Standby: `bfs.poland.xray` (PL, `91.198.166.181`)
Module: `hectic.generic.matrix-cluster` (`nixos/module/generic/matrix-cluster.nix`).
Shared secrets: `sus/matrix-cluster.yaml`.
All `psql` and `pg_ctl` invocations use PostgreSQL **17** at data dir
`/var/lib/postgresql/17`.
## Initial setup
### 1. Provision shared SOPS file (`sus/matrix-cluster.yaml`)
On a workstation with both yukkop and yukkop-alt age keys available:
```sh
sudo cat /var/lib/matrix-synapse/homeserver.signing.key # on NL (hectic-lab)
# Copy the single line value into the buffer for the next step.
sops sus/matrix-cluster.yaml
```
Populate the editor with:
```yaml
matrix:
signing-key: <paste verbatim signing-key line from NL>
postgres-replication-password: <openssl rand -base64 32>
object-storage:
credentials: |
ACCESS_KEY_ID=<verbatim copy from sus/hectic-lab.yaml>
SECRET_ACCESS_KEY=<verbatim copy from sus/hectic-lab.yaml>
porkbun-api-key: <PORKBUN_API_KEY>
porkbun-secret-api-key: <PORKBUN_SECRET_API_KEY>
```
Verify recipients:
```sh
sops updatekeys sus/matrix-cluster.yaml
sops -d sus/matrix-cluster.yaml | grep -E 'signing-key|porkbun-api-key|object-storage'
```
Expected: all five keys present, exit 0.
### 2. Deploy NL primary first
```sh
nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux' --target-host root@128.140.75.58
```
Verify on NL:
```sh
sudo systemctl status matrix-synapse postgresql matrix-cluster-replication-password
sudo -u postgres psql -c "select rolname, rolreplication from pg_roles where rolname='replication';"
# Expected: replication | t
```
### 3. Seed PL replica with `pg_basebackup`
On PL:
```sh
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup \
-h 128.140.75.58 \
-p 5432 \
-U replication \
-D /var/lib/postgresql/17 \
-Fp -Xs -P -R \
--no-password
```
`-R` writes `standby.signal` and an initial `primary_conninfo`. The
matrix-cluster module's `matrix-cluster-standby-bootstrap` service will
overwrite `primary_conninfo` to use a libpq passfile on next boot.
### 4. Deploy PL standby
```sh
nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux' --target-host root@91.198.166.181
sudo systemctl start postgresql
```
Verify streaming on NL:
```sh
sudo -u postgres psql -c 'select client_addr, state, sync_state from pg_stat_replication;'
# Expected: 91.198.166.181 | streaming | async
```
Verify standby on PL:
```sh
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# Expected: t
sudo systemctl is-active matrix-synapse
# Expected: inactive (standby keeps Synapse off)
```
### 5. Remove duplicate S3 credentials from `sus/hectic-lab.yaml`
Only AFTER NL is confirmed healthy reading from the new shared file:
```sh
sops sus/hectic-lab.yaml
# Delete the matrix/object-storage/credentials block.
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
```
## Normal operations
```sh
# NL: replication health
sudo -u postgres psql -c 'select * from pg_stat_replication;'
# Expected: 1 row, state=streaming, sync_state=async
# PL: replay status
sudo -u postgres psql -c 'select now() - pg_last_xact_replay_timestamp() as lag;'
# Both: cert renewal
sudo systemctl status acme-accord.tube.timer
sudo journalctl -u acme-accord.tube.service --since '24 hours ago'
# Synapse health (NL primary)
curl -sf https://accord.tube/_matrix/client/versions | head
```
## Planned failover (NL -> PL)
```sh
# 1. Drain NL: stop accepting writes.
sudo systemctl stop matrix-synapse
sudo systemctl stop postgresql # ensure no new WAL after this point
# 2. Promote PL replica.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# Wait until pg_is_in_recovery() returns f:
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# 3. Make the role switch declarative before rebuilding.
# Edit the flake so rebuilds match the promoted database state:
# - nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = true;
# hectic.generic.matrix-cluster.secretsFile = config.sops.secrets."matrix/secrets".path;
# - nixos/system/hectic-lab/hectic-lab.nix:
# hectic.generic.matrix-cluster.role = "standby";
# hectic.generic.matrix-cluster.overrideEnableSynapse = false;
# hectic.generic.matrix-cluster.replication.peerHost = "91.198.166.181";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "128.140.75.58/32" ];
# (You will also need a matrix/secrets entry on PL - copy from NL via SOPS.)
sudo nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux'
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
sudo systemctl status matrix-synapse
# 4. Swap DNS A record at Porkbun:
# accord.tube A 91.198.166.181 (was 128.140.75.58)
# TTL: set to 300 in advance of any planned failover.
# Porkbun UI: https://porkbun.com/account/domainsSpeedy -> accord.tube -> DNS -> edit A record.
# Or via API:
sudo curl -sX POST https://api.porkbun.com/api/json/v3/dns/editByNameType/accord.tube/A \
-H 'content-type: application/json' \
-d "$(jq -n --arg k "$PORKBUN_API_KEY" --arg s "$PORKBUN_SECRET_API_KEY" \
'{secretapikey:$s,apikey:$k,content:"91.198.166.181",ttl:"300"}')"
# 5. Federation smoke test.
curl -s 'https://federationtester.matrix.org/api/report?server_name=accord.tube' | jq .FederationOK
# Expected: true
```
Expected after the rebuilds:
- `bfs.poland.xray` evaluates and runs as `role = "primary"`.
- `hectic-lab` evaluates as `role = "standby"` with Synapse forced off.
- Future `nixos-rebuild` runs preserve the promoted topology instead of reapplying standby settings to PL.
## Failback (PL -> NL)
```sh
# 1. Stop NL postgres if still up; clear its data dir.
sudo systemctl stop postgresql matrix-synapse
sudo rm -rf /var/lib/postgresql/17
# 2. Re-seed NL from PL (now the live primary).
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup -h 91.198.166.181 -p 5432 -U replication \
-D /var/lib/postgresql/17 -Fp -Xs -P -R --no-password
# 3. Temporarily flip roles in the flake:
# - hectic-lab.nix: role = "standby"; peerHost = "91.198.166.181";
# - bfs.poland.xray.nix: role = "primary"; peerHost = "128.140.75.58";
# Rebuild both.
# 4. Once NL is streaming green, do the reverse failover dance:
sudo systemctl stop matrix-synapse # on PL
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote # on NL
# Then revert the flake role assignments back to NL=primary / PL=standby and
# rebuild both hosts.
# 5. Swap DNS back at Porkbun (A -> 128.140.75.58).
```
## Disaster recovery (NL permanently lost)
```sh
# 1. Promote PL as the new permanent primary.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# 2. Edit nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = lib.mkForce null;
# hectic.generic.matrix-cluster.replication.peerHost = "<new-standby-ip>";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "<new-standby-ip>/32" ];
# 3. Provision a new host (any region with Porkbun-managed DNS) and import
# self.nixosModules.matrix-cluster with role = "standby" pointed at PL's IP.
# 4. Bootstrap the new standby via pg_basebackup from PL exactly as in
# "Initial setup" step 3, replacing 128.140.75.58 with PL's IP.
# 5. Update Porkbun A record to PL's IP permanently.
```
@@ -9,11 +9,15 @@
config, config,
... ...
}: let }: let
matrixBackend = "https://128.140.75.58";
matrixHost = "accord.tube"; matrixHost = "accord.tube";
jitsiHost = "meet.accord.tube";
elementEntryDomain = "element.bfs.band"; elementEntryDomain = "element.bfs.band";
polandEntryDomain = "bfs.band"; polandEntryDomain = "bfs.band";
matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml"; backendProxyConfig = ''
proxy_ssl_server_name on;
proxy_ssl_name ${matrixHost};
proxy_set_header Host ${matrixHost};
'';
in { in {
imports = [ imports = [
self.nixosModules.xray-system self.nixosModules.xray-system
@@ -41,6 +45,7 @@ in {
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path; credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
}; };
replication = { replication = {
peerHost = "128.140.75.58";
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path; passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
}; };
acme = { acme = {
@@ -48,29 +53,6 @@ in {
porkbunApiKeyFile = config.sops.secrets."matrix/porkbun-api-key".path; porkbunApiKeyFile = config.sops.secrets."matrix/porkbun-api-key".path;
porkbunSecretApiKeyFile = config.sops.secrets."matrix/porkbun-secret-api-key".path; porkbunSecretApiKeyFile = config.sops.secrets."matrix/porkbun-secret-api-key".path;
}; };
jitsi.preferredDomain = jitsiHost;
};
hectic.services.media-browser = {
enable = true;
port = 3001;
s3Bucket = "matrix-hectic-lab";
s3Endpoint = "https://hel1.your-objectstorage.com";
s3Region = "hel1";
s3CredentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
hectic.services.jitsi = {
enable = true;
hostName = jitsiHost;
};
# NOTE(yukkop): disk was provisioned outside disko, so the expected partition
# label does not exist. Pin root to the live filesystem UUID so stage 1 can
# mount `/` reliably.
fileSystems."/" = lib.mkForce {
device = "/dev/disk/by-uuid/06b48ef1-a1eb-428d-821c-90c96a624542";
fsType = "ext4";
}; };
security.acme = { security.acme = {
@@ -78,18 +60,6 @@ in {
defaults.email = "security@bfs.band"; defaults.email = "security@bfs.band";
}; };
# NOTE(yukkop): this host gets an IPv6 route via RA, but object storage
# fetches to hel1.your-objectstorage.com currently stall over IPv6 while
# IPv4 works. Synapse's S3 media backend uses getaddrinfo ordering, so
# prefer IPv4 here to keep Element media downloads responsive.
environment.etc."gai.conf".text = ''
precedence ::ffff:0:0/96 100
'';
systemd.services.matrix-synapse.restartTriggers = [
config.environment.etc."gai.conf".source
];
services.nginx = { services.nginx = {
enable = true; enable = true;
@@ -126,17 +96,19 @@ in {
}; };
locations."= /livekit/jwt" = { locations."= /livekit/jwt" = {
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/"; proxyPass = "${matrixBackend}/livekit/jwt";
extraConfig = backendProxyConfig;
}; };
locations."^~ /livekit/jwt/" = { locations."^~ /livekit/jwt/" = {
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/"; proxyPass = "${matrixBackend}/livekit/jwt/";
extraConfig = backendProxyConfig;
}; };
locations."= /livekit/sfu" = { locations."= /livekit/sfu" = {
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/"; proxyPass = "${matrixBackend}/livekit/sfu";
proxyWebsockets = true; proxyWebsockets = true;
extraConfig = '' extraConfig = backendProxyConfig + ''
proxy_send_timeout 120; proxy_send_timeout 120;
proxy_read_timeout 120; proxy_read_timeout 120;
proxy_buffering off; proxy_buffering off;
@@ -147,9 +119,9 @@ in {
}; };
locations."^~ /livekit/sfu/" = { locations."^~ /livekit/sfu/" = {
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/"; proxyPass = "${matrixBackend}/livekit/sfu/";
proxyWebsockets = true; proxyWebsockets = true;
extraConfig = '' extraConfig = backendProxyConfig + ''
proxy_send_timeout 120; proxy_send_timeout 120;
proxy_read_timeout 120; proxy_read_timeout 120;
proxy_buffering off; proxy_buffering off;
@@ -160,14 +132,13 @@ in {
}; };
locations."^~ /_matrix/" = { locations."^~ /_matrix/" = {
proxyPass = "http://127.0.0.1:8008"; proxyPass = "${matrixBackend}/_matrix/";
extraConfig = '' extraConfig = backendProxyConfig;
client_max_body_size ${config.hectic.generic.matrix-cluster.maxUploadSize};
'';
}; };
locations."^~ /_synapse/client/" = { locations."^~ /_synapse/client/" = {
proxyPass = "http://127.0.0.1:8008"; proxyPass = "${matrixBackend}/_synapse/client/";
extraConfig = backendProxyConfig;
}; };
}; };
@@ -177,7 +148,7 @@ in {
locations."= /config.${elementEntryDomain}.json".return = "302 /config.json"; locations."= /config.${elementEntryDomain}.json".return = "302 /config.json";
root = pkgs.hectic.element-web.override { root = pkgs.element-web.override {
conf = { conf = {
default_server_config = { default_server_config = {
"m.homeserver".base_url = "https://${polandEntryDomain}"; "m.homeserver".base_url = "https://${polandEntryDomain}";
@@ -185,12 +156,6 @@ in {
"m.identity_server".base_url = "https://vector.im"; "m.identity_server".base_url = "https://vector.im";
}; };
jitsi = {
preferred_domain = jitsiHost;
};
hectic.videoMessages.enabled = true;
room_directory.servers = [ matrixHost ]; room_directory.servers = [ matrixHost ];
default_theme = "dark"; default_theme = "dark";
@@ -204,41 +169,41 @@ in {
key = "matrix/signing-key"; key = "matrix/signing-key";
owner = "matrix-synapse"; owner = "matrix-synapse";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
sops.secrets."matrix/postgres-replication-password" = { sops.secrets."matrix/postgres-replication-password" = {
key = "matrix/postgres-replication-password"; key = "matrix/postgres-replication-password";
owner = "postgres"; owner = "postgres";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
sops.secrets."matrix/object-storage/credentials" = { sops.secrets."matrix/object-storage/credentials" = {
key = "matrix/object-storage/credentials"; key = "matrix/object-storage/credentials";
owner = "matrix-synapse"; owner = "matrix-synapse";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
sops.secrets."matrix/secrets" = { sops.secrets."matrix/secrets" = {
key = "matrix/secrets"; key = "matrix/secrets";
owner = "matrix-synapse"; owner = "matrix-synapse";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
sops.secrets."matrix/turn-secret" = { sops.secrets."matrix/turn-secret" = {
key = "matrix/turn-secret"; key = "matrix/turn-secret";
owner = "turnserver"; owner = "turnserver";
group = "turnserver"; group = "turnserver";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
sops.secrets."matrix/porkbun-api-key" = { sops.secrets."matrix/porkbun-api-key" = {
key = "matrix/porkbun-api-key"; key = "matrix/porkbun-api-key";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
sops.secrets."matrix/porkbun-secret-api-key" = { sops.secrets."matrix/porkbun-secret-api-key" = {
key = "matrix/porkbun-secret-api-key"; key = "matrix/porkbun-secret-api-key";
mode = "0400"; mode = "0400";
sopsFile = matrixClusterSopsFile; sopsFile = "${flake}/sus/matrix-cluster.yaml";
}; };
} }
-4
View File
@@ -13,10 +13,6 @@ in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs { pkgs = import inputs.nixpkgs {
inherit system; inherit system;
overlays = [ self.overlays.default ]; overlays = [ self.overlays.default ];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [
"jitsi-meet-1.0.8792"
];
}; };
modules = [ modules = [
{ networking.hostName = hostName; } { networking.hostName = hostName; }
-30
View File
@@ -1,30 +0,0 @@
{
domain,
...
}: {
config,
...
}: {
hectic.services.attic = {
enable = true;
hostName = "cache.${domain}";
port = 8081;
environmentFile = config.sops.secrets."atticd/environment".path;
storage = {
bucket = "cache-hectic-lab";
endpoint = "https://hel1.your-objectstorage.com";
region = "hel1";
};
};
services.nginx.virtualHosts."cache.${domain}" = {
enableACME = true;
forceSSL = true;
extraConfig = ''
client_max_body_size 0;
'';
locations."/" = {
proxyPass = "http://127.0.0.1:8081";
};
};
}
-42
View File
@@ -1,42 +0,0 @@
{
domain,
...
}: {
config,
...
}: let
enteDomain = "ente.${domain}";
in {
hectic.services.ente = {
enable = true;
apiDomain = "api.${enteDomain}";
disableRegistration = false;
domains = {
accounts = "accounts.${enteDomain}";
cast = "cast.${enteDomain}";
albums = "albums.${enteDomain}";
photos = "photos.${enteDomain}";
};
smtp = {
enable = true;
host = "mail.${domain}";
email = "security@${domain}";
};
storage = {
bucket = "ente-hectic-lab";
endpoint = "https://hel1.your-objectstorage.com";
region = "hel1";
};
secrets = {
encryptionKeyFile = config.sops.secrets."ente/key-encryption".path;
hashKeyFile = config.sops.secrets."ente/key-hash".path;
jwtSecretFile = config.sops.secrets."ente/jwt-secret".path;
s3AccessKeyFile = config.sops.secrets."ente/s3-access-key".path;
s3SecretKeyFile = config.sops.secrets."ente/s3-secret-key".path;
};
};
}
@@ -1,74 +0,0 @@
{ pkgs, ... }:
let
port = 22222;
stateDir = "/var/lib/experimental-sshd";
configFile = pkgs.writeText "experimental-sshd_config" ''
Port 22222
ListenAddress 0.0.0.0
ListenAddress ::
HostKey ${stateDir}/ssh_host_ed25519_key
HostKey ${stateDir}/ssh_host_rsa_key
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
UsePAM yes
AuthenticationMethods publickey
AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u
LogLevel DEBUG3
VersionAddendum none
HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
KexAlgorithms curve25519-sha256,diffie-hellman-group14-sha256
Ciphers aes256-ctr,aes128-ctr
MACs hmac-sha2-512,hmac-sha2-256
'';
keygenScript = pkgs.writeShellScript "experimental-sshd-keygen" ''
set -eu
${pkgs.coreutils}/bin/mkdir -p -m 0700 ${stateDir}
if [ ! -f ${stateDir}/ssh_host_ed25519_key ]; then
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f ${stateDir}/ssh_host_ed25519_key -N ""
fi
if [ ! -f ${stateDir}/ssh_host_rsa_key ]; then
${pkgs.openssh}/bin/ssh-keygen -t rsa -b 4096 -f ${stateDir}/ssh_host_rsa_key -N ""
fi
'';
in
{
environment.etc."ssh/experimental-sshd_config".source = configFile;
networking.firewall.allowedTCPPorts = [ port ];
systemd.services.experimental-sshd-keygen = {
description = "Generate experimental SSH host keys";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "experimental-sshd";
ExecStart = keygenScript;
};
};
systemd.services.experimental-sshd = {
description = "Experimental SSH daemon";
wantedBy = [ "multi-user.target" ];
wants = [ "experimental-sshd-keygen.service" ];
after = [ "network.target" "experimental-sshd-keygen.service" ];
serviceConfig = {
Type = "simple";
StateDirectory = "experimental-sshd";
RuntimeDirectory = "experimental-sshd";
ExecStart = "${pkgs.openssh}/bin/sshd -D -e -f /etc/ssh/experimental-sshd_config";
};
preStart = ''
${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config
'';
};
}
+161 -91
View File
@@ -15,52 +15,26 @@ with builtins;
with lib; with lib;
let let
domain = "hectic-lab.com"; domain = "hectic-lab.com";
sshPort = 22; matrixDomain = "accord.tube";
mailUserNames = [ sslOpts = {
"security" sslCertificate = config.sops.secrets."ssl/porkbun/${domain}/domain.cert.pem".path;
"founders" sslCertificateKey = config.sops.secrets."ssl/porkbun/${domain}/private.key.pem".path;
"lvgkcfjl"
"yukkop"
"daniil-perlyk"
"iana-perlyk"
"snuff"
"antoshka"
"evgenii-kazakov"
];
mkMailPasswordSecret = name: {
name = "mailserver/${name}/hashedPassword";
value = {};
};
mkMailLoginAccount = name: {
inherit name;
value = {
hashedPasswordFile = config.sops.secrets."mailserver/${name}/hashedPassword".path;
};
};
mkEnteSecret = name: {
name = "ente/${name}";
value = {
owner = "ente";
group = "ente";
};
}; };
in { in {
imports = [ imports = [
self.nixosModules.hectic self.nixosModules.hectic
self.nixosModules.matrix-cluster self.nixosModules.matrix-cluster
inputs.sops-nix.nixosModules.sops inputs.sops-nix.nixosModules.sops
self.nixosModules.matrix-cluster-users
self.nixosModules."shadowsocks-rust" # NOTE(nrv): impl self.nixosModules."shadowsocks-rust" # NOTE(nrv): impl
self.nixosModules."shadowsocks" # NOTE(nrv): usage/instance self.nixosModules."shadowsocks" # NOTE(nrv): usage/instance
inputs.hectic-landing.nixosModules.hectic-landing inputs.hectic-landing.nixosModules.hectic-landing
(import ./attic.nix { inherit flake self inputs domain; })
(import ./containers.nix { inherit flake self inputs; }) (import ./containers.nix { inherit flake self inputs; })
./experimental-sshd.nix (import ./mechabellum.nix { inherit flake self inputs domain sslOpts; })
(import ./ente.nix { inherit domain; }) (import (./. + "/sentinèlla.nix") { inherit flake self inputs domain sslOpts; })
(import ./mechabellum.nix { inherit flake self inputs domain; })
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain; })
]; ];
services.hectic-landing = { services.hectic-landing = {
@@ -81,12 +55,47 @@ in {
enable = true; enable = true;
networkMatchConfigName = "enp1s0"; networkMatchConfigName = "enp1s0";
ipv4 = "128.140.75.58"; ipv4 = "128.140.75.58";
floatingIpv4 = "78.47.243.0";
ipv6 = "2a01:4f8:c2c:d54a"; ipv6 = "2a01:4f8:c2c:d54a";
}; };
services.matrix = { services.matrix = {
enable = false; enable = false;
}; };
generic.matrix-cluster = {
enable = true;
overrideEnableSynapse = false;
role = "standby";
inherit matrixDomain;
signingKeyFile = config.sops.secrets."matrix/signing-key".path;
secretsFile = config.sops.secrets."matrix/secrets".path;
turnSecretFile = config.sops.secrets."matrix/turn-secret".path;
publicIp = "128.140.75.58";
objectStorage.s3 = {
bucket = "matrix-hectic-lab";
regionName = "hel1";
endpointUrl = "https://hel1.your-objectstorage.com";
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
replication = {
peerHost = "91.198.166.181";
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
allowedSourceIPs = [ "91.198.166.181/32" ];
};
acme = {
enable = false;
porkbunApiKeyFile = config.sops.secrets."matrix/porkbun-api-key".path;
porkbunSecretApiKeyFile = config.sops.secrets."matrix/porkbun-secret-api-key".path;
};
};
services.media-browser = {
enable = true;
port = 3001;
s3Bucket = "matrix-hectic-lab";
s3Endpoint = "https://hel1.your-objectstorage.com";
s3Region = "hel1";
s3CredentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
}; };
# NOTE(yukkop): disk was provisioned by Hetzner rescue image, disko was never # NOTE(yukkop): disk was provisioned by Hetzner rescue image, disko was never
@@ -108,7 +117,6 @@ in {
''; '';
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
tcpdump
git git
rsync rsync
python311 python311
@@ -119,25 +127,10 @@ in {
sops = { sops = {
gnupg.sshKeyPaths = [ ]; gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = flake + "/sus/hectic-lab.yaml"; defaultSopsFile = "${flake}/sus/hectic-lab.yaml";
secrets = builtins.listToAttrs (map mkMailPasswordSecret mailUserNames) // {
"init-postgresql" = {
key = "init-postgresql";
};
"atticd/environment" = {};
"wg-bfs/private-key" = {};
} // builtins.listToAttrs (map mkEnteSecret [
"key-encryption"
"key-hash"
"jwt-secret"
"s3-access-key"
"s3-secret-key"
]);
}; };
users.users.root.openssh.authorizedKeys.keys = [ users.users.root.openssh.authorizedKeys.keys = [
# neuro machine
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDfqSROY+rp7amPPiArY3sZM7jTjYBS02csWxF/NeIr/ root@neuro"
# yukkop # yukkop
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuP5NSfEQmO6m77xBWZvZ3hk7cw1q2k2vbsFd37rybU u0_a327@localhost" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuP5NSfEQmO6m77xBWZvZ3hk7cw1q2k2vbsFd37rybU u0_a327@localhost"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBLxMo5icX2Xyng7mcWGnIi+c4ZbVygjPhuU8noCkfZ" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBLxMo5icX2Xyng7mcWGnIi+c4ZbVygjPhuU8noCkfZ"
@@ -164,12 +157,85 @@ in {
]; ];
}; };
services.openssh.ports = [ sshPort ]; sops.secrets."mailserver/security/hashedPassword" = {};
sops.secrets."mailserver/yukkop/hashedPassword" = {};
sops.secrets."mailserver/daniil-perlyk/hashedPassword" = {};
sops.secrets."mailserver/snuff/hashedPassword" = {};
sops.secrets."mailserver/antoshka/hashedPassword" = {};
sops.secrets."mailserver/founders/hashedPassword" = {};
sops.secrets."mailserver/lvgkcfjl/hashedPassword" = {};
sops.secrets."init-postgresql" = {
key = "init-postgresql";
};
sops.secrets."matrix/secrets" = {
key = "matrix/secrets";
owner = "matrix-synapse";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/turn-secret" = {
key = "matrix/turn-secret";
owner = "root";
group = "root";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/object-storage/credentials" = {
key = "matrix/object-storage/credentials";
owner = "matrix-synapse";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
# Shared cluster secrets (PL standby also reads from this file).
sops.secrets."matrix/signing-key" = {
key = "matrix/signing-key";
owner = "matrix-synapse";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/postgres-replication-password" = {
key = "matrix/postgres-replication-password";
owner = "postgres";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/porkbun-api-key" = {
key = "matrix/porkbun-api-key";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/porkbun-secret-api-key" = {
key = "matrix/porkbun-secret-api-key";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
services.mailserver = { services.mailserver = {
enable = true; enable = true;
domain = domain; domain = domain;
loginAccounts = builtins.listToAttrs (map mkMailLoginAccount mailUserNames); loginAccounts = {
"security" = {
hashedPasswordFile = config.sops.secrets."mailserver/security/hashedPassword".path;
};
"founders" = {
hashedPasswordFile = config.sops.secrets."mailserver/founders/hashedPassword".path;
};
"lvgkcfjl" = {
hashedPasswordFile = config.sops.secrets."mailserver/lvgkcfjl/hashedPassword".path;
};
"yukkop" = {
hashedPasswordFile = config.sops.secrets."mailserver/yukkop/hashedPassword".path;
};
"daniil-perlyk" = {
hashedPasswordFile = config.sops.secrets."mailserver/daniil-perlyk/hashedPassword".path;
};
"snuff" = {
hashedPasswordFile = config.sops.secrets."mailserver/snuff/hashedPassword".path;
};
"antoshka" = {
hashedPasswordFile = config.sops.secrets."mailserver/antoshka/hashedPassword".path;
};
};
}; };
mailserver.stateVersion = 3; mailserver.stateVersion = 3;
@@ -186,7 +252,6 @@ in {
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ allowedTCPPorts = [
sshPort # ssh
80 80
443 443
3306 # mysql 3306 # mysql
@@ -209,11 +274,15 @@ in {
"d /var/www/store 0755 nginx nginx -" "d /var/www/store 0755 nginx nginx -"
]; ];
sops.secrets."ssl/porkbun/${domain}/domain.cert.pem" = { group = "nginx"; mode = "0440"; };
sops.secrets."ssl/porkbun/${domain}/private.key.pem" = { group = "nginx"; mode = "0440"; };
sops.secrets."ssl/porkbun/${domain}/public.key.pem" = { group = "nginx"; mode = "0440"; };
services.nginx = { services.nginx = {
enable = true; enable = true;
# NOTE(yukkop): virtualHosts.${domain} is owned by the hectic-landing module # NOTE(yukkop): virtualHosts.${domain} is owned by the hectic-landing module
virtualHosts."store.${domain}" = { # (ACME-managed). See services.hectic-landing above.
enableACME = true; virtualHosts."store.${domain}" = sslOpts // {
forceSSL = true; forceSSL = true;
root = "/var/www/store"; root = "/var/www/store";
locations."/" = { locations."/" = {
@@ -222,8 +291,7 @@ in {
''; '';
}; };
}; };
virtualHosts."snuff.${domain}" = { virtualHosts."snuff.${domain}" = sslOpts // {
enableACME = true;
forceSSL = true; forceSSL = true;
locations."/" = { locations."/" = {
extraConfig = '' extraConfig = ''
@@ -232,8 +300,7 @@ in {
''; '';
}; };
}; };
virtualHosts."nrv.${domain}" = { virtualHosts."nrv.${domain}" = sslOpts // {
enableACME = true;
forceSSL = true; forceSSL = true;
locations."/" = { locations."/" = {
extraConfig = '' extraConfig = ''
@@ -242,8 +309,7 @@ in {
''; '';
}; };
}; };
virtualHosts."yukkop.${domain}" = { virtualHosts."yukkop.${domain}" = sslOpts // {
enableACME = true;
forceSSL = true; forceSSL = true;
locations."/" = { locations."/" = {
extraConfig = '' extraConfig = ''
@@ -252,35 +318,39 @@ in {
''; '';
}; };
}; };
virtualHosts."gitea.${domain}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://127.0.0.1:11011/;
proxy_redirect off;
'';
};
};
}; };
services = { # === WireGuard (disabled) ===
gitea = {
enable = true; sops.secrets."wg-bfs/private-key" = {};
package = pkgs.hectic.gitea-heatmap;
settings.service.DISABLE_REGISTRATION = true; # networking.wireguard.interfaces = let
settings.server = { # subnet = "10.13.37";
HTTP_PORT = 11011; # externalInterface = "eth0";
SSH_PORT = sshPort; # in {
SSH_DOMAIN = "hectic-lab.com"; # wg-bfs = {
}; # ips = [ "${subnet}.1/24" ];
database = { # listenPort = 51820;
createDatabase = true; # postSetup = ''
type = "postgres"; # ${pkgs.iptables}/bin/iptables -t 'nat' -A 'POSTROUTING' -s '${subnet}.0/24' -o '${externalInterface}' -j 'MASQUERADE'
socket = "/run/postgresql"; # '';
user = "gitea"; # postShutdown = ''
name = "gitea"; # ${pkgs.iptables}/bin/iptables -t 'nat' -D 'POSTROUTING' -s '${subnet}.0/24' -o '${externalInterface}' -j 'MASQUERADE'
}; # '';
}; # privateKeyFile = config.sops.secrets."wg-bfs/private-key".path;
}; # generatePrivateKeyFile = false;
# peers = with lib; with builtins; let
# pubkeys = [
# "3dVzf1jxnVVTkLAyxedW+kRQBexZDzYDwpaLIcTrLjc=" # nrv (host: 2)
# "Kk2d0ncj24rO0qbuKh4V4t1OLnmVYbeaYvuEnL2OPFM=" # lysmi (host: 3)
# "BkM/NEDbR/XQ6WYQ0Yt+nJrc2HFCVsoW4QxBmkqxHn8=" # yukkop (host: 4)
# ];
# hosts = lists.range 2 254;
# zipped = zipLists pubkeys hosts;
# in flip map zipped ({ fst, snd }: {
# publicKey = "${fst}";
# allowedIPs = [ "${subnet}.${toString snd}/32" ];
# });
# };
# };
} }
+8 -5
View File
@@ -1,6 +1,7 @@
{ {
inputs, inputs,
domain, domain,
sslOpts,
... ...
}: { }: {
pkgs, pkgs,
@@ -28,16 +29,18 @@ in {
enable = true; enable = true;
}; };
services.nginx.virtualHosts."${mechDomain}" = { services.nginx.virtualHosts."${mechDomain}" = sslOpts // {
enableACME = true;
forceSSL = true; forceSSL = true;
root = inputs.mechabellum-replay-analysis.packages.${system}.frontend; root = inputs.mechabellum-replay-analysis.packages.${system}.frontend;
locations."/api" = { locations."/api/" = {
proxyPass = "http://${apiHost}:${builtins.toString apiPort}"; proxyPass = "http://${apiHost}:${builtins.toString apiPort}/api/";
extraConfig = '' extraConfig = ''
proxy_http_version 1.1; proxy_http_version 1.1;
client_max_body_size 500M; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
''; '';
}; };
+2 -2
View File
@@ -3,6 +3,7 @@
flake, flake,
self, self,
domain, domain,
sslOpts,
... ...
}: { ... }: { }: { ... }: {
hectic.services."sentinèlla" = { hectic.services."sentinèlla" = {
@@ -16,8 +17,7 @@
}; };
services.nginx = { services.nginx = {
virtualHosts."probe.${domain}" = { virtualHosts."probe.${domain}" = sslOpts // {
enableACME = true;
forceSSL = true; forceSSL = true;
locations."/" = { locations."/" = {
proxyPass = "http://127.0.0.1:5988"; proxyPass = "http://127.0.0.1:5988";
+22 -2
View File
@@ -15,12 +15,32 @@ in self.lib.nixpkgs-lib.nixosSystem {
self.overlays.default self.overlays.default
inputs.nix-minecraft.overlay inputs.nix-minecraft.overlay
]; ];
config.allowUnfreePredicate = pkg: config.allowUnfreePredicate = pkg: builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
"minecraft-server" "minecraft-server"
"neoforge" "neoforge"
"nvidia-x11" "nvidia-x11"
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
]; ];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x) # jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [ config.permittedInsecurePackages = [
-19
View File
@@ -17,11 +17,6 @@
ollamaServiceBundledLibraryPath = "${ollamaPrebuilt}/lib/ollama:${ollamaPrebuilt}/lib/ollama/cuda_v12:${ollamaPrebuilt}/lib/ollama/cuda_v13"; ollamaServiceBundledLibraryPath = "${ollamaPrebuilt}/lib/ollama:${ollamaPrebuilt}/lib/ollama/cuda_v12:${ollamaPrebuilt}/lib/ollama/cuda_v13";
stableVideoDiffusionLibraryPath = lib.makeLibraryPath [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
ollamaPrebuilt = pkgs.stdenvNoCC.mkDerivation { ollamaPrebuilt = pkgs.stdenvNoCC.mkDerivation {
pname = "ollama"; pname = "ollama";
version = "0.22.1"; version = "0.22.1";
@@ -181,19 +176,6 @@ in {
openFirewall = false; openFirewall = false;
}; };
hectic.services.stable-video-diffusion = {
enable = true;
host = "127.0.0.1";
port = 7861;
package = pkgs.hectic.stable-video-diffusion-api;
device = "cuda";
libraryPath = [
stableVideoDiffusionLibraryPath
"/run/opengl-driver/lib"
];
openFirewall = false;
};
networking = { networking = {
networkmanager.enable = true; networkmanager.enable = true;
useDHCP = lib.mkDefault true; useDHCP = lib.mkDefault true;
@@ -281,7 +263,6 @@ in {
in [ in [
#python-ai #python-ai
git git
hectic.hiddify-core
neovim neovim
wget wget
ethtool ethtool
-20
View File
@@ -1,20 +0,0 @@
{
flake,
self,
inputs,
system ? "x86_64-linux",
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
-35
View File
@@ -1,35 +0,0 @@
{
inputs,
self,
...
}: {
pkgs,
modulesPath,
...
}: {
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
inputs.sops-nix.nixosModules.sops
self.nixosModules.hectic
];
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud = {
enable = true;
device = "/dev/sda";
networkMatchConfigMac = "92:00:08:4a:b0:32";
ipv4 = "46.225.237.218";
ipv6 = "2a01:4f8:c2c:3b14";
};
};
users.users.root.openssh.authorizedKeys.keys = [
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKAaObjLBslsdTlqEcYaS1TqX4x9aVJu75y27/8MFevO''
];
environment.systemPackages = with pkgs; [
git
rsync
];
}
+11 -37
View File
@@ -1,21 +1,18 @@
# db-tool # db-tool
PostgreSQL utility package exposing separate development and operations binaries. PostgreSQL development database management tool. Drop-in replacement for per-project database.sh / postgres-init.sh / postgres-cleanup.sh scripts. Provides database, postgres-init, and postgres-cleanup binaries.
`db-dev` preserves the current development workflow via the `database` binary;
`db-ops` provides target-safe operational helpers such as secrets hydration.
## Provided Binaries ## Provided Binaries
| Binary | Description | | Binary | Description |
| --- | --- | | --- | --- |
| `database` | Main `db-dev` script for managing local development databases, migrations, deployments, and logs. | | `database` | Main script for managing migrations, deployments, and logs. |
| `db-ops` | Operational helper binary for target/runtime database actions. |
| `postgres-init` | Ephemeral PostgreSQL cluster initialization and startup. | | `postgres-init` | Ephemeral PostgreSQL cluster initialization and startup. |
| `postgres-cleanup` | Graceful shutdown and cleanup of the PostgreSQL cluster. | | `postgres-cleanup` | Graceful shutdown and cleanup of the PostgreSQL cluster. |
## Required Environment Variables ## Required Environment Variables
These variables must be set for `db-dev` / `database` to function. These variables must be set for `db-tool` to function.
| Variable | Description | | Variable | Description |
| --- | --- | | --- | --- |
@@ -42,7 +39,7 @@ These variables must be set for `db-dev` / `database` to function.
## Postgres Package Override ## Postgres Package Override
By default, `db-dev`/`db-ops`/`postgres-init`/`postgres-cleanup` use plain `postgresql_17` from nixpkgs. If you need extensions (e.g. `pg_cron`), override the postgres package per-output: By default, `db-tool`/`postgres-init`/`postgres-cleanup` use plain `postgresql_17` from nixpkgs. If you need extensions (e.g. `pg_cron`), override the postgres package per-output:
```nix ```nix
let let
@@ -51,8 +48,7 @@ let
]); ]);
in { in {
packages = [ packages = [
(pkgs.hectic."db-dev".override { postgresql = myPg; }) (pkgs.hectic."db-tool".override { postgresql = myPg; })
(pkgs.hectic."db-ops".override { postgresql = myPg; })
(pkgs.hectic."postgres-init".override { postgresql = myPg; }) (pkgs.hectic."postgres-init".override { postgresql = myPg; })
(pkgs.hectic."postgres-cleanup".override { postgresql = myPg; }) (pkgs.hectic."postgres-cleanup".override { postgresql = myPg; })
]; ];
@@ -70,28 +66,6 @@ The `pull_staging` subcommand allows importing data from a remote staging enviro
If any of these variables are missing when `pull_staging` is invoked, the tool will exit with code 3 and print the name of the missing variable to stderr. If any of these variables are missing when `pull_staging` is invoked, the tool will exit with code 3 and print the name of the missing variable to stderr.
## normalize-backup Contract
The `normalize-backup` subcommand converts a physical PostgreSQL backup into a
local-development restore artifact without modifying the input backup:
```sh
database normalize-backup [OPTIONS] [path]
```
The input `path` defaults to `${LOCAL_DIR}/focus/postgresql-backup` and must be a
backup directory compatible with `database restore`, containing `base.tar.gz`
and optionally `pg_wal.tar.gz`. The output defaults to a normalized backup path
and can be overridden with `--output <path>`. The output directory must not be
the same path as the input and must not be inside the input directory.
Normalization extracts the physical backup into temporary PGDATA, replaces
production PostgreSQL access/configuration with local restore-safe
`postgresql.conf` and `pg_hba.conf` files, removes standby/recovery/runtime
leftovers, starts the cluster locally, ensures the requested `--role` and
`--database` exist, stops cleanly, and repacks a backup directory that can be
used by `database restore`.
## Subcommands ## Subcommands
- `deploy`: Execute the full deployment flow (hydrate + patch). Supports `--cleanup` to teardown after success. - `deploy`: Execute the full deployment flow (hydrate + patch). Supports `--cleanup` to teardown after success.
@@ -99,21 +73,20 @@ used by `database restore`.
- `test`: Execute database tests located in `${DATABASE_DIR}/test/test.sql`. - `test`: Execute database tests located in `${DATABASE_DIR}/test/test.sql`.
- `check`: Run a deployment validation in an isolated, temporary PostgreSQL cluster. - `check`: Run a deployment validation in an isolated, temporary PostgreSQL cluster.
- `cleanup`: Stop the local database cluster and remove the `PG_WORKING_DIR`. - `cleanup`: Stop the local database cluster and remove the `PG_WORKING_DIR`.
- `normalize-backup`: Rewrite a physical backup artifact for local restore use.
- `pull_staging`: Import data from the staging environment based on the env contract. - `pull_staging`: Import data from the staging environment based on the env contract.
- `init`: Wrapper around `postgres-init` to start the cluster. - `init`: Wrapper around `postgres-init` to start the cluster.
- `migrator`: Directly invoke the migration tool with the correct environment context. - `migrator`: Directly invoke the migration tool with the correct environment context.
## shellHook Example ## shellHook Example
To use `db-dev` in a Nix development shell, add the following to your `flake.nix` or `shell.nix`: To use `db-tool` in a Nix development shell, add the following to your `flake.nix` or `shell.nix`:
```nix ```nix
{ {
# ... # ...
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
packages = [ packages = [
pkgs.hectic.db-dev pkgs.hectic.db-tool
pkgs.hectic.postgres-init pkgs.hectic.postgres-init
pkgs.hectic.postgres-cleanup pkgs.hectic.postgres-cleanup
]; ];
@@ -138,7 +111,7 @@ To use `db-dev` in a Nix development shell, add the following to your `flake.nix
## hectic Bundle ## hectic Bundle
`db-dev`, `db-ops`, and `migrator` apply a single bundle of SQL files that bootstrap the `db-tool` and `migrator` apply a single bundle of SQL files that bootstrap the
`hectic` schema. The bundle lives in `hectic` schema. The bundle lives in
[`lib/hook/sql/`](../../lib/hook/sql/README.md) — see that README for full [`lib/hook/sql/`](../../lib/hook/sql/README.md) — see that README for full
contract, file layout, and the `self.lib.hectic.*` Nix API. contract, file layout, and the `self.lib.hectic.*` Nix API.
@@ -194,7 +167,6 @@ ALTER DATABASE mydb SET hectic.inheritance_extra_excluded_schemas = 'legacy,etl'
| `postgres-init` | **No.** Pure PostgreSQL provisioner — starts a vanilla cluster, nothing more. | | `postgres-init` | **No.** Pure PostgreSQL provisioner — starts a vanilla cluster, nothing more. |
| `migrator init` | **Yes, mandatory.** The bundle is a hard prerequisite for `hectic.migration`. | | `migrator init` | **Yes, mandatory.** The bundle is a hard prerequisite for `hectic.migration`. |
| `database hydrate` | **Yes, by default.** Re-applied on every hydrate. Skip with `--no-hook`. After applying the bundle, hydrate also calls `hectic.load_secrets_from_env(<dotenv>)` if `HECTIC_DOTENV_FILE` (or `${LOCAL_DIR}/.env.${ENVIRONMENT}`) is readable. | | `database hydrate` | **Yes, by default.** Re-applied on every hydrate. Skip with `--no-hook`. After applying the bundle, hydrate also calls `hectic.load_secrets_from_env(<dotenv>)` if `HECTIC_DOTENV_FILE` (or `${LOCAL_DIR}/.env.${ENVIRONMENT}`) is readable. |
| `db-ops secrets load` | **Yes, mandatory.** Applies the bundle and requires a dotenv source before loading secrets into `hectic.secret`. |
The bundle is idempotent — repeated application is safe. The bundle is idempotent — repeated application is safe.
@@ -215,13 +187,15 @@ directly against the paths exposed by `self.lib.hectic.*.path`:
```nix ```nix
services.postgresql.initialScript = pkgs.writeText "hectic-init.sql" '' services.postgresql.initialScript = pkgs.writeText "hectic-init.sql" ''
\i ${self.lib.hectic.version.path}
\i ${self.lib.hectic.secret.path} \i ${self.lib.hectic.secret.path}
\i ${self.lib.hectic.migration.path} \i ${self.lib.hectic.migration.path}
\i ${self.lib.hectic.inheritance.path} \i ${self.lib.hectic.inheritance.path}
''; '';
``` ```
The version file (`self.lib.hectic.version`) is templated and only exposes
`.sql` (a string). Materialize it with `pkgs.writeText` if a path is needed.
## Exit Codes ## Exit Codes
| Code | Meaning | | Code | Meaning |
@@ -3,8 +3,7 @@
: "${REMAINING_ARGS:=}" : "${REMAINING_ARGS:=}"
: "${DEFAULT_BACKUP_PATH:=${LOCAL_DIR:-$PWD}/focus/postgresql-backup}" : "${DEFAULT_BACKUP_PATH:=${LOCAL_DIR:-$PWD}/focus/postgresql-backup/}"
: "${DEFAULT_NORMALIZED_BACKUP_PATH:=${LOCAL_DIR:-$PWD}/focus/postgresql-backup-normalized}"
: "${SCRIPT_NAME:=$(basename "$0")}" : "${SCRIPT_NAME:=$(basename "$0")}"
SCRIPT_NAME=${SCRIPT_NAME%%.sh} SCRIPT_NAME=${SCRIPT_NAME%%.sh}
@@ -187,9 +186,6 @@ ${BGREEN}Database Subcommands:${NC}
${BCYAN}restore${NC} ${CYAN}[PATH]$NC Restore database from backup ${BCYAN}restore${NC} ${CYAN}[PATH]$NC Restore database from backup
${BCYAN}normalize-backup${NC} ${CYAN}[OPTIONS] [PATH]$NC
Normalize a raw physical backup for local restore/diff
${BCYAN}backup${NC} Create database backup ${BCYAN}backup${NC} Create database backup
Creates compressed backup at $BBLACK$DEFAULT_BACKUP_PATH$NC Creates compressed backup at $BBLACK$DEFAULT_BACKUP_PATH$NC
@@ -250,7 +246,6 @@ ${BGREEN}Examples:${NC}
$SCRIPT_NAME init Initialize PostgreSQL only $SCRIPT_NAME init Initialize PostgreSQL only
$SCRIPT_NAME restore Restore from default backup $SCRIPT_NAME restore Restore from default backup
$SCRIPT_NAME restore /path/to/backup Restore from specific path $SCRIPT_NAME restore /path/to/backup Restore from specific path
$SCRIPT_NAME normalize-backup /path/to/raw-backup
$SCRIPT_NAME backup Create backup $SCRIPT_NAME backup Create backup
$SCRIPT_NAME log Show database logs $SCRIPT_NAME log Show database logs
$SCRIPT_NAME log list List available log files $SCRIPT_NAME log list List available log files
@@ -426,43 +421,6 @@ EOF
)" | "$PAGER_OR_CAT" )" | "$PAGER_OR_CAT"
} }
help_normalize_backup() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME normalize-backup [OPTIONS] [path]
Normalize a raw PostgreSQL physical backup for local development.
This command leaves the input backup untouched. It extracts $BBLACK\`base.tar.gz\`$NC
and optional $BBLACK\`pg_wal.tar.gz\`$NC into temporary PGDATA, writes local
$BBLACK\`postgresql.conf\`$NC and $BBLACK\`pg_hba.conf\`$NC files, removes standby,
recovery, and runtime leftovers, starts the cluster locally, ensures a login role
and database exist, stops cleanly, then repacks a normalized backup directory.
${BGREEN}Arguments:${NC}
${BCYAN}path$NC Input backup directory (optional)
Defaults to $BBLACK$DEFAULT_BACKUP_PATH$NC
${BGREEN}Options:${NC}
$BCYAN-o$NC, $BCYAN--output$NC ${CYAN}<path>$NC Output backup directory
Defaults to $BBLACK$DEFAULT_NORMALIZED_BACKUP_PATH$NC
$BCYAN--role$NC ${CYAN}<name>$NC Login role to ensure (default $BBLACK\$(id -un)$NC)
$BCYAN--database$NC ${CYAN}<name>$NC Database to ensure (default $BBLACK\${PG_DATABASE:-testdb}$NC)
$BCYAN--admin-role$NC ${CYAN}<name>$NC Role used for local maintenance
Defaults to $BBLACK\${URI_USER:-postgres}$NC
$BCYAN-h$NC, $BCYAN--help$NC Show this help message
${BGREEN}Files Created:${NC}
- ${BBLACK}\`base.tar.gz\`$NC: Normalized database cluster backup
${BGREEN}Examples:${NC}
$SCRIPT_NAME normalize-backup /path/to/raw-backup
$SCRIPT_NAME normalize-backup --output focus/postgresql-backup-normalized
EOF
)" | "$PAGER_OR_CAT"
}
help_diff() { help_diff() {
# shellcheck disable=SC2059 # shellcheck disable=SC2059
printf "$(cat <<EOF printf "$(cat <<EOF
@@ -483,8 +441,6 @@ ${BGREEN}Arguments:
${BGREEN}Options:${NC} ${BGREEN}Options:${NC}
$BCYAN--tables${NC} ${CYAN}<list>${NC} Comma-separated list of tables to diff $BCYAN--tables${NC} ${CYAN}<list>${NC} Comma-separated list of tables to diff
Example: --tables users,orders,products Example: --tables users,orders,products
$BCYAN--ignore-migration-fail${NC}
Continue diff even if DB1 migrations fail
$BCYAN-m${NC}, $BCYAN--mock${NC} Mock external API calls when hydrating DB2 $BCYAN-m${NC}, $BCYAN--mock${NC} Mock external API calls when hydrating DB2
Replaces HTTP-calling functions with stubs/test data Replaces HTTP-calling functions with stubs/test data
$BCYAN-h${NC}, $BCYAN--help${NC} Show this help message $BCYAN-h${NC}, $BCYAN--help${NC} Show this help message
@@ -508,7 +464,6 @@ ${BGREEN}Examples:${NC}
$SCRIPT_NAME diff Compare using default backup $SCRIPT_NAME diff Compare using default backup
$SCRIPT_NAME diff /path/to/backup Compare using specific backup $SCRIPT_NAME diff /path/to/backup Compare using specific backup
$SCRIPT_NAME diff --tables users,orders Compare specific tables $SCRIPT_NAME diff --tables users,orders Compare specific tables
$SCRIPT_NAME diff --ignore-migration-fail Continue despite DB1 migration failure
$SCRIPT_NAME diff -m Compare with external APIs mocked $SCRIPT_NAME diff -m Compare with external APIs mocked
$SCRIPT_NAME diff log Show logs from diff operation $SCRIPT_NAME diff log Show logs from diff operation
@@ -850,11 +805,6 @@ subcommand_restore() {
RESTORE_BACKUP_PATH="$DEFAULT_BACKUP_PATH" RESTORE_BACKUP_PATH="$DEFAULT_BACKUP_PATH"
fi fi
local restore_database="${PG_DATABASE:-}"
if [ -f "${RESTORE_BACKUP_PATH:?}/database_name" ]; then
restore_database=$(tr -d '\n' < "${RESTORE_BACKUP_PATH:?}/database_name")
fi
postgres-cleanup postgres-cleanup
local data="${PG_WORKING_DIR:?}/data" local data="${PG_WORKING_DIR:?}/data"
@@ -867,243 +817,12 @@ subcommand_restore() {
tar -xzf "${RESTORE_BACKUP_PATH:?}/pg_wal.tar.gz" -C "${data}/pg_wal" tar -xzf "${RESTORE_BACKUP_PATH:?}/pg_wal.tar.gz" -C "${data}/pg_wal"
fi fi
env PG_REUSE= PG_DATABASE="$restore_database" postgres-init env PG_REUSE= postgres-init
rm -f "${data}/standby.signal" "${data}/recovery.signal" rm -f "${data}/standby.signal" "${data}/recovery.signal"
restore_namespace restore_namespace
} }
___sql_literal() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
}
___normalize_backup_remove_leftovers() {
local pgdata="$1"
rm -f \
"$pgdata/postmaster.pid" \
"$pgdata/postmaster.opts" \
"$pgdata/recovery.signal" \
"$pgdata/standby.signal" \
"$pgdata/backup_label.old"
rm -f "$pgdata/pg_wal/archive_status"/*.ready 2>/dev/null || :
}
___normalize_backup_cleanup() {
local tmpdir="$1"
local pgdata="$2"
if [ -n "$pgdata" ] && [ -d "$pgdata" ]; then
pg_ctl -D "$pgdata" -m fast -w stop >/dev/null 2>&1 || :
fi
if [ -n "$tmpdir" ]; then
rm -rf "$tmpdir"
fi
}
___normalize_backup_refresh_collation() {
local sockdir="$1"
local port="$2"
local admin_role="$3"
local database_name="$4"
psql -h "$sockdir" -p "$port" -U "$admin_role" -d postgres \
-v ON_ERROR_STOP=1 -c "ALTER DATABASE \"$database_name\" REFRESH COLLATION VERSION;" \
>/dev/null 2>&1 || :
}
subcommand_normalize_backup() {
change_namespace 'db normalize-backup'
NORMALIZE_INPUT_PATH=""
NORMALIZE_OUTPUT_PATH="$DEFAULT_NORMALIZED_BACKUP_PATH"
NORMALIZE_ROLE="$(id -un)"
NORMALIZE_DATABASE="${PG_DATABASE:-testdb}"
NORMALIZE_ADMIN_ROLE="postgres"
NORMALIZE_PORT="${PG_PORT:-5432}"
NORMALIZE_PRELOAD_LIBRARIES="${PG_SHARED_PRELOAD_LIBRARIES:-pg_cron}"
NORMALIZE_DATABASE_EXPLICIT=0
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_normalize_backup
exit 0
;;
-o|--output)
if [ $# -lt 2 ]; then
log error "normalize-backup: --output requires a path"
exit 3
fi
NORMALIZE_OUTPUT_PATH="$2"
shift 2
;;
--role)
if [ $# -lt 2 ]; then
log error "normalize-backup: --role requires a name"
exit 3
fi
NORMALIZE_ROLE="$2"
shift 2
;;
--database)
if [ $# -lt 2 ]; then
log error "normalize-backup: --database requires a name"
exit 3
fi
NORMALIZE_DATABASE="$2"
NORMALIZE_DATABASE_EXPLICIT=1
shift 2
;;
--admin-role)
if [ $# -lt 2 ]; then
log error "normalize-backup: --admin-role requires a name"
exit 3
fi
NORMALIZE_ADMIN_ROLE="$2"
shift 2
;;
--*|-*)
log error "normalize-backup argument $1 does not exist"
exit 9
;;
*)
if [ -n "$NORMALIZE_INPUT_PATH" ]; then
log error "normalize-backup: unexpected argument $1"
exit 9
fi
NORMALIZE_INPUT_PATH="$1"
shift
;;
esac
done
if [ -z "$NORMALIZE_INPUT_PATH" ]; then
NORMALIZE_INPUT_PATH="$DEFAULT_BACKUP_PATH"
fi
if [ -z "$NORMALIZE_ROLE" ] || [ -z "$NORMALIZE_DATABASE" ] || [ -z "$NORMALIZE_ADMIN_ROLE" ]; then
log error "normalize-backup: role, database, and admin role must be non-empty"
exit 3
fi
if [ ! -d "$NORMALIZE_INPUT_PATH" ]; then
log error "normalize-backup: input backup directory not found: $NORMALIZE_INPUT_PATH"
exit 3
fi
if [ ! -f "$NORMALIZE_INPUT_PATH/base.tar.gz" ]; then
log error "normalize-backup: missing $NORMALIZE_INPUT_PATH/base.tar.gz"
exit 3
fi
NORMALIZE_INPUT_ABS=$(realpath "$NORMALIZE_INPUT_PATH")
NORMALIZE_OUTPUT_ABS=$(realpath -m "$NORMALIZE_OUTPUT_PATH")
if [ "$NORMALIZE_INPUT_ABS" = "$NORMALIZE_OUTPUT_ABS" ]; then
log error "normalize-backup: input and output paths must differ"
exit 1
fi
case "$NORMALIZE_OUTPUT_ABS/" in
"$NORMALIZE_INPUT_ABS"/*)
log error "normalize-backup: output path must not be inside input backup"
exit 1
;;
esac
case "$NORMALIZE_INPUT_ABS/" in
"$NORMALIZE_OUTPUT_ABS"/*)
log error "normalize-backup: input backup must not be inside output path"
exit 1
;;
esac
mkdir -p "$(dirname "$NORMALIZE_OUTPUT_ABS")"
NORMALIZE_TMPDIR=$(mktemp -d)
NORMALIZE_PGDATA="$NORMALIZE_TMPDIR/data"
NORMALIZE_SOCKDIR="$NORMALIZE_TMPDIR/sock"
NORMALIZE_LOG="$NORMALIZE_TMPDIR/postgres.log"
trap '___normalize_backup_cleanup "$NORMALIZE_TMPDIR" "$NORMALIZE_PGDATA"' EXIT INT HUP
mkdir -m 700 "$NORMALIZE_PGDATA"
mkdir -p "$NORMALIZE_SOCKDIR" "$NORMALIZE_PGDATA/pg_wal"
log notice "extracting backup into temporary PGDATA"
tar -xzf "$NORMALIZE_INPUT_ABS/base.tar.gz" -C "$NORMALIZE_PGDATA"
if [ -f "$NORMALIZE_INPUT_ABS/pg_wal.tar.gz" ]; then
tar -xzf "$NORMALIZE_INPUT_ABS/pg_wal.tar.gz" -C "$NORMALIZE_PGDATA/pg_wal"
fi
___normalize_backup_remove_leftovers "$NORMALIZE_PGDATA"
if [ ! -f "$NORMALIZE_PGDATA/postgresql.conf" ]; then
cat > "$NORMALIZE_PGDATA/postgresql.conf" <<EOF
listen_addresses = ''
port = $NORMALIZE_PORT
unix_socket_directories = '$NORMALIZE_SOCKDIR'
logging_collector = off
shared_preload_libraries = '$NORMALIZE_PRELOAD_LIBRARIES'
cron.database_name = '$NORMALIZE_DATABASE'
cron.host = '$NORMALIZE_SOCKDIR'
EOF
fi
cat > "$NORMALIZE_PGDATA/pg_hba.conf" <<EOF
local all all trust
EOF
: > "$NORMALIZE_PGDATA/pg_ident.conf"
cat > "$NORMALIZE_PGDATA/postgresql.auto.conf" <<EOF
# Local-dev normalized backup overrides.
listen_addresses = ''
port = '$NORMALIZE_PORT'
unix_socket_directories = '$NORMALIZE_SOCKDIR'
logging_collector = 'off'
hba_file = '$NORMALIZE_PGDATA/pg_hba.conf'
ident_file = '$NORMALIZE_PGDATA/pg_ident.conf'
shared_preload_libraries = '$NORMALIZE_PRELOAD_LIBRARIES'
cron.database_name = '$NORMALIZE_DATABASE'
cron.host = '$NORMALIZE_SOCKDIR'
EOF
log notice "starting temporary local cluster"
with_closed_fds pg_ctl -D "$NORMALIZE_PGDATA" -o "-F" -w start > "$NORMALIZE_LOG" 2>&1 || {
log error "normalize-backup: failed to start temporary cluster; see $NORMALIZE_LOG"
exit 1
}
___normalize_backup_refresh_collation "$NORMALIZE_SOCKDIR" "$NORMALIZE_PORT" "$NORMALIZE_ADMIN_ROLE" postgres
___normalize_backup_refresh_collation "$NORMALIZE_SOCKDIR" "$NORMALIZE_PORT" "$NORMALIZE_ADMIN_ROLE" template1
___normalize_backup_refresh_collation "$NORMALIZE_SOCKDIR" "$NORMALIZE_PORT" "$NORMALIZE_ADMIN_ROLE" template0
if [ "$NORMALIZE_DATABASE_EXPLICIT" -eq 0 ]; then
detected_database=$(psql -h "$NORMALIZE_SOCKDIR" -p "$NORMALIZE_PORT" -U "$NORMALIZE_ADMIN_ROLE" -d postgres \
-tAc "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate AND datname <> 'postgres' ORDER BY oid LIMIT 1" 2>/dev/null | sed '/^$/d' | head -n 1)
if [ -n "$detected_database" ]; then
NORMALIZE_DATABASE="$detected_database"
fi
fi
NORMALIZE_ROLE_SQL=$(___sql_literal "$NORMALIZE_ROLE")
psql -h "$NORMALIZE_SOCKDIR" -p "$NORMALIZE_PORT" -U "$NORMALIZE_ADMIN_ROLE" -d postgres \
-v ON_ERROR_STOP=1 -c "DO \$\$ DECLARE v_role text := $NORMALIZE_ROLE_SQL; BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = v_role) THEN EXECUTE format('CREATE ROLE %I LOGIN SUPERUSER', v_role); ELSE EXECUTE format('ALTER ROLE %I LOGIN SUPERUSER', v_role); END IF; END \$\$;"
NORMALIZE_DATABASE_SQL=$(___sql_literal "$NORMALIZE_DATABASE")
NORMALIZE_DATABASE_EXISTS=$(psql -h "$NORMALIZE_SOCKDIR" -p "$NORMALIZE_PORT" -U "$NORMALIZE_ADMIN_ROLE" -d postgres \
-tAc "SELECT 1 FROM pg_database WHERE datname = $NORMALIZE_DATABASE_SQL" 2>/dev/null || true)
if [ "$NORMALIZE_DATABASE_EXISTS" != "1" ]; then
createdb -h "$NORMALIZE_SOCKDIR" -p "$NORMALIZE_PORT" -U "$NORMALIZE_ADMIN_ROLE" \
-O "$NORMALIZE_ROLE" "$NORMALIZE_DATABASE"
fi
___normalize_backup_refresh_collation "$NORMALIZE_SOCKDIR" "$NORMALIZE_PORT" "$NORMALIZE_ADMIN_ROLE" "$NORMALIZE_DATABASE"
log notice "stopping temporary local cluster"
pg_ctl -D "$NORMALIZE_PGDATA" -m fast -w stop >> "$NORMALIZE_LOG" 2>&1
___normalize_backup_remove_leftovers "$NORMALIZE_PGDATA"
log notice "writing normalized backup to $WHITE$NORMALIZE_OUTPUT_ABS$NC"
rm -rf "$NORMALIZE_OUTPUT_ABS"
mkdir -p "$NORMALIZE_OUTPUT_ABS"
tar -czf "$NORMALIZE_OUTPUT_ABS/base.tar.gz" -C "$NORMALIZE_PGDATA" .
printf '%s\n' "$NORMALIZE_DATABASE" > "$NORMALIZE_OUTPUT_ABS/database_name"
___normalize_backup_cleanup "$NORMALIZE_TMPDIR" ""
trap - EXIT INT HUP
restore_namespace
}
subcommand_log() { subcommand_log() {
change_namespace 'db log' change_namespace 'db log'
: "${PG_WORKING_DIR:="$LOCAL_DIR/focus/postgresql"}" : "${PG_WORKING_DIR:="$LOCAL_DIR/focus/postgresql"}"
@@ -1456,7 +1175,6 @@ ___diff_dump_schema() {
local port="$2" local port="$2"
local output_file="$3" local output_file="$3"
local tables="${4:-}" local tables="${4:-}"
local database_name="${5:-${PG_DATABASE:-testdb}}"
log info "dumping schema to $WHITE$output_file$NC" log info "dumping schema to $WHITE$output_file$NC"
@@ -1471,17 +1189,17 @@ ___diff_dump_schema() {
IFS="$old_IFS" IFS="$old_IFS"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
pg_dump -h "$socket_dir" -p "$port" "$database_name" \ pg_dump -h "$socket_dir" -p "$port" testdb \
--schema-only --no-owner --no-privileges \ --schema-only --no-owner --no-privileges \
$table_args > "$output_file" 2>/dev/null $table_args > "$output_file" 2>/dev/null
# shellcheck disable=SC2086 # shellcheck disable=SC2086
pg_dump -h "$socket_dir" -p "$port" "$database_name" \ pg_dump -h "$socket_dir" -p "$port" testdb \
--data-only --no-owner --no-privileges \ --data-only --no-owner --no-privileges \
$table_args >> "$output_file" 2>/dev/null $table_args >> "$output_file" 2>/dev/null
else else
# Schema only # Schema only
pg_dump -h "$socket_dir" -p "$port" "$database_name" \ pg_dump -h "$socket_dir" -p "$port" testdb \
--schema-only --no-owner --no-privileges \ --schema-only --no-owner --no-privileges \
> "$output_file" 2>/dev/null > "$output_file" 2>/dev/null
fi fi
@@ -1490,8 +1208,7 @@ ___diff_dump_schema() {
___diff_immutable_tables() { ___diff_immutable_tables() {
local socket_dir="$1" local socket_dir="$1"
local port="$2" local port="$2"
local database_name="${3:-${PG_DATABASE:-testdb}}" psql -h "$socket_dir" -p "$port" -d testdb -tAv ON_ERROR_STOP=1 -c "$(cat <<'SQL'
psql -h "$socket_dir" -p "$port" -d "$database_name" -tAv ON_ERROR_STOP=1 -c "$(cat <<'SQL'
SELECT n.nspname || '.' || c.relname SELECT n.nspname || '.' || c.relname
FROM pg_inherits i FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid JOIN pg_class c ON c.oid = i.inhrelid
@@ -1509,9 +1226,8 @@ ___diff_immutable_data() {
local sock2="$3" local sock2="$3"
local port2="$4" local port2="$4"
local out_file="$5" local out_file="$5"
local database_name="${6:-${PG_DATABASE:-testdb}}"
if ! psql -h "$sock1" -p "$port1" -d "$database_name" -tAc \ if ! psql -h "$sock1" -p "$port1" -d testdb -tAc \
"SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='hectic' AND c.relname='immutable';" \ "SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='hectic' AND c.relname='immutable';" \
>/dev/null 2>&1 >/dev/null 2>&1
then then
@@ -1540,10 +1256,10 @@ ___diff_immutable_data() {
log info " $tbl" log info " $tbl"
: > "$data1" : > "$data1"
: > "$data2" : > "$data2"
pg_dump -h "$sock1" -p "$port1" "$database_name" \ pg_dump -h "$sock1" -p "$port1" testdb \
--data-only --no-owner --no-privileges --column-inserts -t "$tbl" \ --data-only --no-owner --no-privileges --column-inserts -t "$tbl" \
> "$data1" 2>/dev/null || : > "$data1" 2>/dev/null || :
pg_dump -h "$sock2" -p "$port2" "$database_name" \ pg_dump -h "$sock2" -p "$port2" testdb \
--data-only --no-owner --no-privileges --column-inserts -t "$tbl" \ --data-only --no-owner --no-privileges --column-inserts -t "$tbl" \
> "$data2" 2>/dev/null || : > "$data2" 2>/dev/null || :
{ {
@@ -1689,8 +1405,6 @@ subcommand_diff() {
DIFF_TABLES="" # TODO: add cron table DIFF_TABLES="" # TODO: add cron table
DIFF_NO_CRON=0 # TODO: useless option DIFF_NO_CRON=0 # TODO: useless option
DIFF_BACKUP_PATH="" DIFF_BACKUP_PATH=""
DIFF_IGNORE_MIGRATION_FAIL=0
DIFF_DATABASE="${PG_DATABASE:-testdb}"
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case $1 in case $1 in
@@ -1706,10 +1420,6 @@ subcommand_diff() {
DIFF_NO_CRON=1 DIFF_NO_CRON=1
shift shift
;; ;;
--ignore-migration-fail)
DIFF_IGNORE_MIGRATION_FAIL=1
shift
;;
-m|--mock) -m|--mock)
HYDRATE_USE_MOCK=1 HYDRATE_USE_MOCK=1
shift shift
@@ -1723,8 +1433,9 @@ subcommand_diff() {
exit 9 exit 9
;; ;;
*) *)
if [ -z "$DIFF_BACKUP_PATH" ]; then # NOTE: yes, RESTORE, not DIFF prefix
DIFF_BACKUP_PATH="$1" if [ -z "$RESTORE_BACKUP_PATH" ]; then
RESTORE_BACKUP_PATH="$1"
shift shift
else else
log error "diff: unexpected argument $1" log error "diff: unexpected argument $1"
@@ -1734,21 +1445,14 @@ subcommand_diff() {
esac esac
done done
if [ -z "$DIFF_BACKUP_PATH" ]; then
DIFF_BACKUP_PATH="$DEFAULT_BACKUP_PATH"
fi
if [ -f "$DIFF_BACKUP_PATH/database_name" ]; then
DIFF_DATABASE=$(tr -d '\n' < "$DIFF_BACKUP_PATH/database_name")
fi
DIFF_TMPDIR="${LOCAL_DIR}/focus/database-diff-operation" DIFF_TMPDIR="${LOCAL_DIR}/focus/database-diff-operation"
DIFF_PGDATA1="$DIFF_TMPDIR/pgdata1" DIFF_PGDATA1="$DIFF_TMPDIR/pgdata1"
DIFF_PGDATA2="$DIFF_TMPDIR/pgdata2" DIFF_PGDATA2="$DIFF_TMPDIR/pgdata2"
# TODO: suka, logi drugie # TODO: suka, logi drugie
DIFF_PGLOGFILE1="$DIFF_PGDATA1/logfile" DIFF_PGLOGFILE1="$DIFF_PGDATA1/logfile"
DIFF_PGLOGFILE2="$DIFF_PGDATA2/logfile" DIFF_PGLOGFILE2="$DIFF_PGDATA2/logfile"
DIFF_PGURL1="postgresql://localhost:5432/$DIFF_DATABASE?host=$DIFF_PGDATA1/sock" DIFF_PGURL1="postgresql://localhost:5432/testdb?host=$DIFF_PGDATA1/sock"
DIFF_PGURL2="postgresql://localhost:5432/$DIFF_DATABASE?host=$DIFF_PGDATA2/sock" DIFF_PGURL2="postgresql://localhost:5432/testdb?host=$DIFF_PGDATA2/sock"
if [ "${DIFF_LOG+x}" ]; then if [ "${DIFF_LOG+x}" ]; then
log_pager -O "$DIFF_PGLOGFILE1" "$DIFF_PGLOGFILE2" log_pager -O "$DIFF_PGLOGFILE1" "$DIFF_PGLOGFILE2"
@@ -1770,26 +1474,21 @@ subcommand_diff() {
log notice "provisioning ${WHITE}DB1$NC (backup + migrations)" log notice "provisioning ${WHITE}DB1$NC (backup + migrations)"
PG_WORKING_DIR="$DIFF_PGDATA1" PG_LOG_PATH="$DIFF_PGDATA1" subcommand_restore "$DIFF_BACKUP_PATH" PG_WORKING_DIR="$DIFF_PGDATA1" PG_LOG_PATH="$DIFF_PGDATA1" subcommand_restore
log info "applying migrations to ${WHITE}DB1$NC" log info "applying migrations to ${WHITE}DB1$NC"
subcommand_migrator migrate up all \ subcommand_migrator migrate up all \
--db-url \ --db-url \
"$DIFF_PGURL1" \ "$DIFF_PGURL1" \
|| { || {
if [ "$DIFF_IGNORE_MIGRATION_FAIL" = "1" ]; then log warn "migrations failed or none to apply"
log warn "migrations failed; continuing because --ignore-migration-fail is set"
else
log error "migrations failed"
exit 1
fi
} }
log notice "provisioning ${WHITE}DB2$NC (current sources)" log notice "provisioning ${WHITE}DB2$NC (current sources)"
log info "initializing ${WHITE}DB2$NC with postgres-init" log info "initializing ${WHITE}DB2$NC with postgres-init"
PG_WORKING_DIR="$DIFF_PGDATA2" \ PG_WORKING_DIR="$DIFF_PGDATA2" \
PG_DATABASE="$DIFF_DATABASE" \ PG_DATABASE="testdb" \
PG_DISABLE_LOGGING=1 \ PG_DISABLE_LOGGING=1 \
postgres-init || { postgres-init || {
log error "failed to initialize ${WHITE}DB2$NC" log error "failed to initialize ${WHITE}DB2$NC"
@@ -1815,11 +1514,8 @@ subcommand_diff() {
DIFF_DUMP1="$DIFF_TMPDIR/target.sql" DIFF_DUMP1="$DIFF_TMPDIR/target.sql"
DIFF_DUMP2="$DIFF_TMPDIR/source.sql" DIFF_DUMP2="$DIFF_TMPDIR/source.sql"
___diff_dump_schema "$DIFF_PGDATA1/sock" "5432" "$DIFF_DUMP1" "$DIFF_TABLES" "$DIFF_DATABASE" ___diff_dump_schema "$DIFF_PGDATA1/sock" "5432" "$DIFF_DUMP1" "$DIFF_TABLES"
___diff_dump_schema "$DIFF_PGDATA2/sock" "5432" "$DIFF_DUMP2" "$DIFF_TABLES" "$DIFF_DATABASE" ___diff_dump_schema "$DIFF_PGDATA2/sock" "5432" "$DIFF_DUMP2" "$DIFF_TABLES"
sed -i '/^\\restrict /d;/^\\unrestrict /d' "$DIFF_DUMP1" || exit 1
sed -i '/^\\restrict /d;/^\\unrestrict /d' "$DIFF_DUMP2" || exit 1
# Optional: filter out cron tables # Optional: filter out cron tables
if [ "$DIFF_NO_CRON" = "1" ]; then if [ "$DIFF_NO_CRON" = "1" ]; then
@@ -1845,8 +1541,7 @@ subcommand_diff() {
___diff_immutable_data \ ___diff_immutable_data \
"$DIFF_PGDATA1/sock" "5432" \ "$DIFF_PGDATA1/sock" "5432" \
"$DIFF_PGDATA2/sock" "5432" \ "$DIFF_PGDATA2/sock" "5432" \
"$DIFF_TMPDIR/diff" \ "$DIFF_TMPDIR/diff"
"$DIFF_DATABASE"
data_status=$? data_status=$?
if [ "$schema_differs" = 0 ] && [ "$data_status" = 0 ]; then if [ "$schema_differs" = 0 ] && [ "$data_status" = 0 ]; then
@@ -1878,14 +1573,6 @@ if ! [ "${AS_LIBRARY+x}" ]; then
DB_URL=$2 DB_URL=$2
shift 2 shift 2
;; ;;
normalize-backup)
if [ "${SUBCOMMAND+x}" ]; then
REMAINING_ARGS="$REMAINING_ARGS $(quote "$1")"
else
SUBCOMMAND="normalize_backup"
fi
shift
;;
deploy|replay|restore|patch|hydrate|backup|log|migrator|diff|pull_staging|test|check|cleanup|init) deploy|replay|restore|patch|hydrate|backup|log|migrator|diff|pull_staging|test|check|cleanup|init)
if [ "${SUBCOMMAND+x}" ]; then if [ "${SUBCOMMAND+x}" ]; then
REMAINING_ARGS="$REMAINING_ARGS $(quote "$1")" REMAINING_ARGS="$REMAINING_ARGS $(quote "$1")"
-222
View File
@@ -1,222 +0,0 @@
# shellcheck shell=dash
: "${SCRIPT_NAME:=$(basename "$0")}"
help() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME [OPTIONS] <SUBCOMMAND> [OPTIONS]
PostgreSQL operations utility.
${BGREEN}Global Options:${NC}
${BCYAN}-h${NC}, ${BCYAN}--help${NC} Show this help message
${BCYAN}-u${NC}, ${BCYAN}--url${NC} <url> PostgreSQL connection string
${BGREEN}Subcommands:${NC}
${BCYAN}secrets load${NC} [OPTIONS] Apply hectic bundle and load secrets
${BGREEN}Environment:${NC}
${BBLACK}PGURL${NC} PostgreSQL connection string fallback
${BBLACK}DB_URL${NC} PostgreSQL connection string fallback
${BBLACK}HECTIC_DOTENV_CONTENT${NC} Raw dotenv content to load
${BBLACK}HECTIC_DOTENV_FILE${NC} Dotenv file to read when readable
${BBLACK}LOCAL_DIR${NC} Used with ENVIRONMENT for .env fallback
${BBLACK}ENVIRONMENT${NC} Falls back to ${BBLACK}\$LOCAL_DIR/.env.\$ENVIRONMENT${NC}
EOF
)"
}
help_secrets_load() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME ${BCYAN}secrets load${NC} [OPTIONS]
Apply the hectic SQL bundle and load dotenv-backed secrets into
${BBLACK}hectic.secret${NC}.
${BGREEN}Options:${NC}
${BCYAN}-h${NC}, ${BCYAN}--help${NC} Show this help message
${BCYAN}-u${NC}, ${BCYAN}--url${NC} <url> PostgreSQL connection string
${BCYAN}-f${NC}, ${BCYAN}--dotenv-file${NC} <path> Dotenv file path
${BGREEN}Resolution order:${NC}
1. ${BBLACK}HECTIC_DOTENV_CONTENT${NC}
2. ${BBLACK}--dotenv-file${NC} / ${BBLACK}HECTIC_DOTENV_FILE${NC}
3. ${BBLACK}\$LOCAL_DIR/.env.\$ENVIRONMENT${NC}
Fails with exit code ${BBLACK}3${NC} when neither a PostgreSQL URL nor a dotenv
source can be resolved.
EOF
)"
}
resolve_pgurl() {
if [ -n "${PGURL:-}" ]; then
printf '%s' "$PGURL"
elif [ -n "${DB_URL:-}" ]; then
printf '%s' "$DB_URL"
else
return 1
fi
}
resolve_dotenv_content() {
dotenv_file="${1:-}"
if [ -n "${HECTIC_DOTENV_CONTENT:-}" ]; then
printf '%s' "$HECTIC_DOTENV_CONTENT"
elif [ -n "$dotenv_file" ]; then
if [ ! -f "$dotenv_file" ] || [ ! -r "$dotenv_file" ]; then
return 2
fi
cat "$dotenv_file"
elif [ -n "${ENVIRONMENT:-}" ] && [ -n "${LOCAL_DIR:-}" ] && [ -r "${LOCAL_DIR}/.env.${ENVIRONMENT}" ]; then
cat "${LOCAL_DIR}/.env.${ENVIRONMENT}"
else
return 1
fi
}
subcommand_secrets_load() {
change_namespace 'db ops secrets load'
dotenv_file="${HECTIC_DOTENV_FILE:-}"
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_secrets_load
restore_namespace
exit 0
;;
-u|--url)
if [ $# -lt 2 ]; then
log error "missing value for $1"
restore_namespace
exit 3
fi
PGURL=$2
DB_URL=$2
shift 2
;;
-f|--dotenv-file)
if [ $# -lt 2 ]; then
log error "missing value for $1"
restore_namespace
exit 3
fi
dotenv_file=$2
shift 2
;;
--*|-*)
log error "secrets load argument $1 does not exist"
restore_namespace
exit 9
;;
*)
log error "secrets load subcommand $1 does not exist"
restore_namespace
exit 9
;;
esac
done
if ! pgurl="$(resolve_pgurl)"; then
log error "PGURL or DB_URL is required"
restore_namespace
exit 3
fi
if dotenv_content="$(resolve_dotenv_content "$dotenv_file")"; then
:
else
dotenv_content_exit_code=$?
if [ "$dotenv_content_exit_code" -eq 2 ]; then
log error "dotenv file is not readable: $dotenv_file"
else
log error "dotenv source is required (HECTIC_DOTENV_CONTENT, readable dotenv file, or \$LOCAL_DIR/.env.\$ENVIRONMENT)"
fi
restore_namespace
exit 3
fi
log notice "apply hectic bundle and load secrets"
apply_hectic_bundle "$pgurl" "$dotenv_content"
restore_namespace
}
subcommand_secrets() {
change_namespace 'db ops secrets'
if [ $# -eq 0 ]; then
help_secrets_load
restore_namespace
exit 0
fi
subcommand=$1
shift
case $subcommand in
load)
subcommand_secrets_load "$@"
;;
-h|--help)
help_secrets_load
restore_namespace
exit 0
;;
*)
log error "secrets subcommand $subcommand does not exist"
restore_namespace
exit 9
;;
esac
}
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help
exit 0
;;
-u|--url)
if [ $# -lt 2 ]; then
log error "missing value for $1"
exit 3
fi
PGURL=$2
DB_URL=$2
shift 2
;;
--*|-*)
log error "argument $1 does not exist"
exit 9
;;
*)
break
;;
esac
done
subcommand="${1:-}"
if [ -z "$subcommand" ]; then
help
exit 0
fi
shift
case $subcommand in
secrets)
subcommand_secrets "$@"
;;
*)
log error "subcommand $subcommand does not exist"
exit 9
;;
esac
+24 -31
View File
@@ -1,14 +1,32 @@
{ dash, hectic, postgresql_17, neovim, openssh, coreutils, gawk, lib, runCommand, self }: { dash, hectic, postgresql_17, neovim, openssh, coreutils, gawk, lib, runCommand, self }:
let let
shell = "${dash}/bin/dash"; shell = "${dash}/bin/dash";
hecticInheritanceSqlPath = ../../lib/hook/sql/hectic-inheritance.sql;
hecticInheritance = runCommand "hectic-inheritance" { } '' hecticInheritance = runCommand "hectic-inheritance" { } ''
mkdir -p "$out/share/hectic" mkdir -p "$out/share/hectic"
cp ${self.lib.hectic.inheritance.path} "$out/share/hectic/hectic-inheritance.sql" cp ${hecticInheritanceSqlPath} "$out/share/hectic/hectic-inheritance.sql"
''; '';
applyBundle = self.lib.hectic.applyBundleScript; # Materialize the templated version SQL into the Nix store as a real file
# so it can be passed by path to psql -f (alongside the static siblings).
hecticVersionSqlFile = pkgs-writeText "hectic-version.sql" self.lib.hectic.version.sql;
pkgs-writeText = name: text: runCommand name { inherit text; passAsFile = [ "text" ]; } ''
cp "$textPath" "$out"
'';
mkDbDev = hecticEnv = ''
HECTIC_VERSION_SQL=${hecticVersionSqlFile}
HECTIC_SECRET_SQL=${self.lib.hectic.secret.path}
HECTIC_MIGRATION_SQL=${self.lib.hectic.migration.path}
HECTIC_INHERITANCE_SQL=${self.lib.hectic.inheritance.path}
export HECTIC_VERSION_SQL HECTIC_SECRET_SQL HECTIC_MIGRATION_SQL HECTIC_INHERITANCE_SQL
'';
applyBundle = builtins.readFile self.lib.hectic.applyBundleScript;
mkDatabase =
{ postgresql ? postgresql_17 }: { postgresql ? postgresql_17 }:
hectic.writeShellApplication { hectic.writeShellApplication {
inherit shell; inherit shell;
@@ -26,8 +44,9 @@ let
${builtins.readFile hectic.helpers.posix-shell.quote} ${builtins.readFile hectic.helpers.posix-shell.quote}
${builtins.readFile hectic.helpers.posix-shell.pager_or_cat} ${builtins.readFile hectic.helpers.posix-shell.pager_or_cat}
${builtins.readFile hectic.helpers.posix-shell.with_closed_fds} ${builtins.readFile hectic.helpers.posix-shell.with_closed_fds}
${hecticEnv}
${applyBundle} ${applyBundle}
${builtins.readFile ./db-dev.sh} ${builtins.readFile ./database.sh}
''; '';
meta = { meta = {
@@ -36,31 +55,6 @@ let
}; };
}; };
mkDbOps =
{ postgresql ? postgresql_17 }:
hectic.writeShellApplication {
inherit shell;
bashOptions = [
"errexit"
"nounset"
];
excludeShellChecks = [ "SC2209" ];
name = "db-ops";
runtimeInputs = [ postgresql coreutils ];
text = ''
${builtins.readFile hectic.helpers.posix-shell.log}
${builtins.readFile hectic.helpers.posix-shell.change_namespace}
${applyBundle}
${builtins.readFile ./db-ops.sh}
'';
meta = {
description = "PostgreSQL operations utility";
mainProgram = "db-ops";
};
};
mkPostgresInit = mkPostgresInit =
{ postgresql ? postgresql_17 }: { postgresql ? postgresql_17 }:
hectic.writeShellApplication { hectic.writeShellApplication {
@@ -98,8 +92,7 @@ let
}; };
in in
{ {
"db-dev" = lib.makeOverridable mkDbDev { }; "db-tool" = lib.makeOverridable mkDatabase { };
"db-ops" = lib.makeOverridable mkDbOps { };
"postgres-init" = lib.makeOverridable mkPostgresInit { }; "postgres-init" = lib.makeOverridable mkPostgresInit { };
"postgres-cleanup" = lib.makeOverridable mkPostgresCleanup { }; "postgres-cleanup" = lib.makeOverridable mkPostgresCleanup { };
"hectic-inheritance" = hecticInheritance; "hectic-inheritance" = hecticInheritance;
-37
View File
@@ -1,46 +1,9 @@
#!/bin/dash #!/bin/dash
# Stop a local PostgreSQL cluster started by postgres-init.
#
# Public contract:
# - Accepts PG_WORKING_DIR directly, or derives it from LOCAL_DIR.
# - If no cluster root can be resolved, exits successfully without doing
# anything. This keeps cleanup safe in traps and partial-init flows.
# - If no postmaster.pid exists, treats the cluster as already stopped.
# - NO_TTY=1 redirects pg_ctl output into a temp log file instead of writing to
# the current terminal.
postgres_cleanup_help() {
cat <<'EOF'
Usage: postgres-cleanup
Stop a local PostgreSQL cluster if it is running.
Environment:
PG_WORKING_DIR Cluster root directory
LOCAL_DIR Fallback root used to derive PG_WORKING_DIR
NO_TTY Redirect pg_ctl output to a temp file
Behavior:
- Returns success if the cluster directory cannot be resolved.
- Returns success if postmaster.pid is already absent.
- Ignores pg_ctl stop errors so cleanup remains trap-safe.
EOF
}
postgres_cleanup_main() { postgres_cleanup_main() {
case "${1:-}" in
-h|--help)
postgres_cleanup_help
return 0
;;
esac
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then return 0; fi if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then return 0; fi
: "${PG_WORKING_DIR:=$LOCAL_DIR/focus/postgresql}" : "${PG_WORKING_DIR:=$LOCAL_DIR/focus/postgresql}"
if [ -f "${PG_WORKING_DIR}/data/postmaster.pid" ]; then if [ -f "${PG_WORKING_DIR}/data/postmaster.pid" ]; then
# Cleanup is intentionally forgiving so it can be used in traps after
# partial startup failures without masking the real error.
if [ "${NO_TTY:-0}" = "1" ]; then if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-cleanup.XXXXXX.log)" _pg_log="$(mktemp /tmp/postgres-cleanup.XXXXXX.log)"
pg_ctl -D "${PG_WORKING_DIR}/data" -m fast -w stop > "$_pg_log" 2>&1 || : pg_ctl -D "${PG_WORKING_DIR}/data" -m fast -w stop > "$_pg_log" 2>&1 || :
+5 -181
View File
@@ -1,58 +1,6 @@
#!/bin/dash #!/bin/dash
# Initialize or reuse a local PostgreSQL cluster for development workflows.
#
# Public contract:
# - Requires either PG_WORKING_DIR or LOCAL_DIR.
# - Produces a ready-to-use database and exports:
# POSTGRESQL_HOST, POSTGRESQL_PORT, POSTGRESQL_USER, POSTGRESQL_DATABASE,
# PGURL, and also the variable named by PG_URL_VAR.
# - Reuses an existing cluster only when PG_REUSE is set and PG_VERSION exists.
# - If a reused cluster cannot start, it is reinitialized automatically.
#
# Important knobs:
# - PG_WORKING_DIR: cluster root (defaults to $LOCAL_DIR/focus/postgresql)
# - PG_DATABASE: database to create/ensure (default: testdb)
# - PG_PORT: socket port (default: 5432)
# - PG_URL_VAR: alternate URL variable to export in addition to PGURL
# - PG_CONF_FILE: base postgresql.conf to copy on fresh init
# - PG_SHARED_PRELOAD_LIBRARIES: preload list; empty string disables pg_cron
# - PG_DISABLE_LOGGING=1: disable logging_collector in generated config
# - NO_TTY=1: redirect pg_ctl stop/start output into temp log files
postgres_init_help() {
cat <<'EOF'
Usage: postgres-init
Initialize or reuse a local PostgreSQL cluster.
Environment:
PG_WORKING_DIR Cluster root directory
LOCAL_DIR Fallback root used to derive PG_WORKING_DIR
PG_DATABASE Database name to create/ensure (default: testdb)
PG_PORT PostgreSQL port / unix socket port (default: 5432)
PG_URL_VAR Extra URL variable to export alongside PGURL
PG_CONF_FILE Base postgresql.conf copied on fresh init only
PG_SHARED_PRELOAD_LIBRARIES Preload list; empty disables pg_cron preload
PG_DISABLE_LOGGING Set to 1 to disable logging_collector
PG_REUSE Reuse existing cluster if PG_VERSION exists
NO_TTY Redirect pg_ctl output to temp files
Behavior:
- Rewrites runtime socket/port/cron settings on every launch.
- Creates the target database if missing.
- If a reused cluster exists but cannot start, reinitializes it automatically.
EOF
}
postgres_init_main() { postgres_init_main() {
case "${1:-}" in
-h|--help)
postgres_init_help
return 0
;;
esac
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then
printf '%s\n' 'postgres-init: PG_WORKING_DIR or LOCAL_DIR is required' >&2 printf '%s\n' 'postgres-init: PG_WORKING_DIR or LOCAL_DIR is required' >&2
return 1 return 1
@@ -77,149 +25,29 @@ postgres_init_main() {
fi fi
mkdir -p "$sockdir" || return 1 mkdir -p "$sockdir" || return 1
# Reuse is optimistic: if a cluster exists on disk, try to start it first.
# We only destroy state after a real startup failure proves the cluster is
# broken, which keeps long-lived local DBs intact when possible.
if [ "${PG_REUSE+x}" ] && [ -f "$data/PG_VERSION" ]; then PG_REUSE=1; else PG_REUSE=0; fi if [ "${PG_REUSE+x}" ] && [ -f "$data/PG_VERSION" ]; then PG_REUSE=1; else PG_REUSE=0; fi
if [ "$PG_REUSE" -eq 0 ]; then if [ "$PG_REUSE" -eq 0 ]; then
rm -rf "$data" "$sockdir" || return 1 rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1 mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1 initdb -D "$data" --no-locale -E UTF8 || return 1
if [ -n "${PG_CONF_FILE:-}" ]; then if [ -n "${PG_CONF_FILE:-}" ]; then
# PG_CONF_FILE replaces the base postgresql.conf on fresh init only. We
# still append runtime values later so the helper can relocate sockets,
# ports, and cron settings regardless of the custom file contents.
[ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; } [ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; }
cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1 cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1
else else
{ { printf '%s\n' "listen_addresses = ''"; [ "$PG_DISABLE_LOGGING" -eq 0 ] && { printf '%s\n' 'logging_collector = on'; printf '%s\n' "log_directory = 'log'"; }; [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ] && { printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"; printf '%s\n' "cron.database_name = '$db'"; printf '%s\n' "cron.host = '$sockdir'"; }; :; } >> "$data/postgresql.conf" || return 1
printf '%s\n' "listen_addresses = ''"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' 'logging_collector = on'
printf '%s\n' "log_directory = 'log'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.conf" || return 1
fi fi
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1 sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
fi fi
# Rewrite the runtime knobs on every launch. Reused clusters may have been
# started from another workspace/session, so we must override stale socket,
# port, and pg_cron wiring before attempting startup.
[ -f "$data/postgresql.auto.conf" ] || : > "$data/postgresql.auto.conf"
[ -f "$data/pg_ident.conf" ] || : > "$data/pg_ident.conf"
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1 sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1 sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*hba_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*ident_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*shared_preload_libraries[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.database_name[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.host[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
{ printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1 { printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1
{
printf '%s\n' "port = '$PG_PORT'"
printf '%s\n' "unix_socket_directories = '$sockdir'"
printf '%s\n' "hba_file = '$data/pg_hba.conf'"
printf '%s\n' "ident_file = '$data/pg_ident.conf'"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' "logging_collector = 'on'"
else
printf '%s\n' "logging_collector = 'off'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.auto.conf" || return 1
_pg_start_exit=0
if [ "${NO_TTY:-0}" = "1" ]; then if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)" _pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)"
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || _pg_start_exit=$? with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || return 2
printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2 printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2
else else
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || _pg_start_exit=$? with_closed_fds pg_ctl -D "$data" -o "-F" -w start || return 2
fi
if [ "$_pg_start_exit" -ne 0 ]; then
if [ "$PG_REUSE" -eq 1 ]; then
# Self-heal path: the on-disk cluster exists but cannot complete startup
# (for example bad WAL / invalid checkpoint). At this point preserving
# the broken state is less useful than reinitializing automatically.
printf '%s\n' "postgres-init: reused cluster failed to start; reinitializing $wd" >&2
PG_REUSE=0
rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1
if [ -n "${PG_CONF_FILE:-}" ]; then
[ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; }
cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1
else
{
printf '%s\n' "listen_addresses = ''"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' 'logging_collector = on'
printf '%s\n' "log_directory = 'log'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.conf" || return 1
fi
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
[ -f "$data/postgresql.auto.conf" ] || : > "$data/postgresql.auto.conf"
[ -f "$data/pg_ident.conf" ] || : > "$data/pg_ident.conf"
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*hba_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*ident_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*shared_preload_libraries[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.database_name[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.host[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
{ printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1
{
printf '%s\n' "port = '$PG_PORT'"
printf '%s\n' "unix_socket_directories = '$sockdir'"
printf '%s\n' "hba_file = '$data/pg_hba.conf'"
printf '%s\n' "ident_file = '$data/pg_ident.conf'"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' "logging_collector = 'on'"
else
printf '%s\n' "logging_collector = 'off'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.auto.conf" || return 1
_pg_start_exit=0
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)"
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || _pg_start_exit=$?
printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2
else
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || _pg_start_exit=$?
fi
[ "$_pg_start_exit" -eq 0 ] || return 2
printf '%s\n' "postgres-init: reinitialized broken cluster at $wd" >&2
else
return 2
fi
fi fi
user="$(id -un)" || return 1 user="$(id -un)" || return 1
@@ -233,13 +61,9 @@ postgres_init_main() {
psql -h "$sockdir" -p "$PG_PORT" -d "$db" -v ON_ERROR_STOP=1 -c 'select 1;' || return 1 psql -h "$sockdir" -p "$PG_PORT" -d "$db" -v ON_ERROR_STOP=1 -c 'select 1;' || return 1
export POSTGRESQL_HOST="$sockdir" POSTGRESQL_PORT="$PG_PORT" POSTGRESQL_USER="$user" POSTGRESQL_DATABASE="$db" export POSTGRESQL_HOST="$sockdir" POSTGRESQL_PORT="$PG_PORT" POSTGRESQL_USER="$user" POSTGRESQL_DATABASE="$db"
# Export both names for compatibility: some callers consume plain PGURL, _pg_url="postgresql://${POSTGRESQL_USER}@/${POSTGRESQL_DATABASE}?host=${POSTGRESQL_HOST}&port=${POSTGRESQL_PORT}"
# while multi-cluster shells also need a project-specific variable like
# WIPE_PGURL or BMSEARCH_PGURL in the same environment.
PGURL="postgresql://${POSTGRESQL_USER}@/${POSTGRESQL_DATABASE}?host=${POSTGRESQL_HOST}&port=${POSTGRESQL_PORT}"
export PGURL
case $PG_URL_VAR in ''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_]* ) printf '%s\n' 'postgres-init: invalid PG_URL_VAR' >&2; return 1 ;; esac case $PG_URL_VAR in ''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_]* ) printf '%s\n' 'postgres-init: invalid PG_URL_VAR' >&2; return 1 ;; esac
export "${PG_URL_VAR}=${PGURL}" || return 1 export "${PG_URL_VAR}=${_pg_url}" || return 1
return 0 return 0
} }
+1 -9
View File
@@ -123,7 +123,6 @@ in {
py3-swifter = pkgs.callPackage ./py3-swifter.nix {}; py3-swifter = pkgs.callPackage ./py3-swifter.nix {};
py3-aiogram-newsletter = pkgs.callPackage ./py3-aiogram-newsletter.nix {}; py3-aiogram-newsletter = pkgs.callPackage ./py3-aiogram-newsletter.nix {};
py3-openai-shap-e = pkgs.callPackage ./py3-openai-shap-e.nix {}; py3-openai-shap-e = pkgs.callPackage ./py3-openai-shap-e.nix {};
hiddify-core = pkgs.callPackage ./hiddify-core {};
nvim-alias = pkgs.callPackage ./nvim-alias.nix {}; nvim-alias = pkgs.callPackage ./nvim-alias.nix {};
bolt-unpack = pkgs.callPackage ./bolt-unpack.nix {}; bolt-unpack = pkgs.callPackage ./bolt-unpack.nix {};
merge-archive = pkgs.callPackage ./merge-archive {}; merge-archive = pkgs.callPackage ./merge-archive {};
@@ -132,7 +131,6 @@ in {
github-gh-tl = pkgs.callPackage ./github/gh-tl.nix {}; github-gh-tl = pkgs.callPackage ./github/gh-tl.nix {};
supabase-with-env-collection = pkgs.callPackage ./supabase-with-env-collection.nix {}; supabase-with-env-collection = pkgs.callPackage ./supabase-with-env-collection.nix {};
migration-name = pkgs.callPackage ./migration-name.nix {}; migration-name = pkgs.callPackage ./migration-name.nix {};
plgo = pkgs.callPackage ./plgo {};
pg-from = pkgs.callPackage ./postgres/pg-from/default.nix rust.commonArgs; pg-from = pkgs.callPackage ./postgres/pg-from/default.nix rust.commonArgs;
pg-schema = pkgs.callPackage ./postgres/pg-schema/default.nix rust.commonArgs; pg-schema = pkgs.callPackage ./postgres/pg-schema/default.nix rust.commonArgs;
pg_wdumpall = pkgs.callPackage ./postgres/pg_wdumpall.nix rust.commonArgs; pg_wdumpall = pkgs.callPackage ./postgres/pg_wdumpall.nix rust.commonArgs;
@@ -142,19 +140,14 @@ in {
c-hectic = pkgs.callPackage ./c/hectic/default.nix {}; c-hectic = pkgs.callPackage ./c/hectic/default.nix {};
watch = pkgs.callPackage ./c/watch/default.nix {}; watch = pkgs.callPackage ./c/watch/default.nix {};
support-bot = pkgs.callPackage ./support-bot {}; support-bot = pkgs.callPackage ./support-bot {};
gitea-heatmap = pkgs.callPackage ./gitea {};
gitea-runner-nix-image = pkgs.callPackage ./gitea-runner-nix-image {};
nix-derivation-hash = pkgs.callPackage ./nix-derivation-hash {}; nix-derivation-hash = pkgs.callPackage ./nix-derivation-hash {};
"sentinèlla" = pkgs.callPackage (./. + "/sentinèlla") {}; "sentinèlla" = pkgs.callPackage (./. + "/sentinèlla") {};
deploy = pkgs.callPackage ./deploy { inherit inputs; }; deploy = pkgs.callPackage ./deploy { inherit inputs; };
element-web = pkgs.callPackage ./element-web {};
shellplot = pkgs.callPackage ./shellplot {}; shellplot = pkgs.callPackage ./shellplot {};
which-country-rs = pkgs.callPackage ./which-country-rs {};
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {}; onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
migrator = pkgs.callPackage ./migrator { inherit self; }; migrator = pkgs.callPackage ./migrator { inherit self; };
"parse-uri" = pkgs.callPackage ./parse-uri {}; "parse-uri" = pkgs.callPackage ./parse-uri {};
"db-dev" = dbToolPkgs."db-dev"; "db-tool" = dbToolPkgs."db-tool";
"db-ops" = dbToolPkgs."db-ops";
"postgres-init" = dbToolPkgs."postgres-init"; "postgres-init" = dbToolPkgs."postgres-init";
"postgres-cleanup" = dbToolPkgs."postgres-cleanup"; "postgres-cleanup" = dbToolPkgs."postgres-cleanup";
"hectic-inheritance" = dbToolPkgs."hectic-inheritance"; "hectic-inheritance" = dbToolPkgs."hectic-inheritance";
@@ -176,6 +169,5 @@ in {
pg-15-ext-smtp-client = buildSmtpExt pkgs "15"; pg-15-ext-smtp-client = buildSmtpExt pkgs "15";
pg-15-ext-plhaskell = buildPlHaskellExt pkgs "15"; pg-15-ext-plhaskell = buildPlHaskellExt pkgs "15";
pg-15-ext-plsh = buildPlShExt pkgs "15"; pg-15-ext-plsh = buildPlShExt pkgs "15";
stable-video-diffusion-api = pkgs.callPackage ./stable-video-diffusion-api {};
media-browser = pkgs.callPackage ./media-browser {}; media-browser = pkgs.callPackage ./media-browser {};
} }
-114
View File
@@ -1,114 +0,0 @@
{
lib,
stdenv,
fetchFromGitHub,
jq,
nodejs,
jitsi-meet,
fetchPnpmDeps,
pnpm_10,
pnpmConfigHook,
faketty,
config,
conf ? config.element-web.conf or { },
}:
let
pnpm = pnpm_10;
patchDir = ./patches;
patches = if builtins.pathExists patchDir then map (name: patchDir + "/${name}") (
builtins.filter (name: lib.hasSuffix ".patch" name) (builtins.attrNames (builtins.readDir patchDir))
) else [ ];
noPhoningHome = {
disable_guests = true;
};
jitsi-meet-override = jitsi-meet.overrideAttrs (previousAttrs: {
meta = removeAttrs previousAttrs.meta [ "knownVulnerabilities" ];
});
element-web-unwrapped = stdenv.mkDerivation (finalAttrs: {
pname = "element-web";
version = "1.12.20";
src = fetchFromGitHub {
owner = "element-hq";
repo = "element-web";
tag = "v${finalAttrs.version}";
hash = "sha256-pbzuPgKJ0DmrDSTO7ZTDArX+Xr9k/ndAGZvQg2kMTMQ=";
};
#inherit patches; #WIP
pnpmDeps = fetchPnpmDeps {
pname = "element";
inherit (finalAttrs) version src;
inherit pnpm;
fetcherVersion = 3;
hash = "sha256-snm7vaHCVX6vYrkmsz5HlYMKx5Ks3K9jvUinkJ41CU0=";
};
nativeBuildInputs = [
jq
nodejs
pnpm
pnpmConfigHook
faketty
];
buildPhase = ''
runHook preBuild
cd apps/web
export VERSION=${finalAttrs.version}
faketty pnpm run build
runHook postBuild
'';
installPhase = ''
runHook preInstall
cp -R webapp $out
cp ${jitsi-meet-override}/libs/external_api.min.js $out/jitsi_external_api.min.js
echo "${finalAttrs.version}" > "$out/version"
jq -s '.[0] * $conf' "config.sample.json" --argjson "conf" '${builtins.toJSON noPhoningHome}' > "$out/config.json"
runHook postInstall
'';
meta = {
description = "Glossy Matrix collaboration client for the web";
homepage = "https://element.io/";
changelog = "https://github.com/element-hq/element-web/blob/v${finalAttrs.version}/CHANGELOG.md";
teams = [ lib.teams.matrix ];
license = lib.licenses.agpl3Plus;
platforms = lib.platforms.all;
};
});
in
if conf == { } then
element-web-unwrapped
else
stdenv.mkDerivation {
pname = "${element-web-unwrapped.pname}-wrapped";
inherit (element-web-unwrapped) version meta;
dontUnpack = true;
nativeBuildInputs = [ jq ];
installPhase = ''
runHook preInstall
mkdir -p $out
ln -s ${element-web-unwrapped}/* $out
rm $out/config.json
jq -s '.[0] * $conf' "${element-web-unwrapped}/config.json" --argjson "conf" ${lib.escapeShellArg (builtins.toJSON conf)} > "$out/config.json"
runHook postInstall
'';
passthru = {
inherit conf;
};
}
@@ -1,481 +0,0 @@
diff --git a/apps/web/src/IConfigOptions.ts b/apps/web/src/IConfigOptions.ts
index a3a6a32..c61c24f 100644
--- a/apps/web/src/IConfigOptions.ts
+++ b/apps/web/src/IConfigOptions.ts
@@ -105,6 +105,15 @@ export interface IConfigOptions {
show_labs_settings: boolean;
features?: Record<string, boolean>; // <FeatureName, EnabledBool>
+ hectic?: {
+ // This local package config intentionally uses the documented camelCase path
+ // `hectic.videoMessages.enabled`.
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ videoMessages?: {
+ enabled?: boolean;
+ };
+ };
+
/**
* Bug report endpoint URL. "local" means the logs should not be uploaded.
*/
diff --git a/apps/web/src/SdkConfig.ts b/apps/web/src/SdkConfig.ts
index 4be6464..9f48743 100644
--- a/apps/web/src/SdkConfig.ts
+++ b/apps/web/src/SdkConfig.ts
@@ -28,6 +28,12 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
integrations_ui_url: "https://scalar.vector.im/",
integrations_rest_url: "https://scalar.vector.im/api",
show_labs_settings: false,
+ hectic: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ videoMessages: {
+ enabled: false,
+ },
+ },
force_verification: false,
jitsi: {
diff --git a/apps/web/src/components/views/rooms/MessageComposer.tsx b/apps/web/src/components/views/rooms/MessageComposer.tsx
index 06c843f..eea76b7 100644
--- a/apps/web/src/components/views/rooms/MessageComposer.tsx
+++ b/apps/web/src/components/views/rooms/MessageComposer.tsx
@@ -33,6 +33,7 @@ import ReplyPreview from "./ReplyPreview";
import { UserIdentityWarning } from "./UserIdentityWarning";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
+import VideoRecordComposerTile from "./VideoRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../audio/VoiceRecording";
import type ResizeNotifier from "../../../utils/ResizeNotifier";
@@ -92,6 +93,7 @@ interface IState {
composerContent: string;
isComposerEmpty: boolean;
haveRecording: boolean;
+ haveVideoRecording: boolean;
recordingTimeLeftSeconds?: number;
me?: RoomMember;
isMenuOpen: boolean;
@@ -113,6 +115,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
+ private videoRecordingButton = createRef<VideoRecordComposerTile>();
private ref = createRef<HTMLDivElement>();
private instanceId: number;
@@ -144,6 +147,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
isComposerEmpty: initialComposerContent?.length === 0,
composerContent: initialComposerContent,
haveRecording: false,
+ haveVideoRecording: false,
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
isMenuOpen: false,
isStickerPickerOpen: false,
@@ -526,6 +530,18 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
};
+ private onVideoRecordStartClick = (): void => {
+ this.videoRecordingButton.current?.onRecordStartEndClick();
+
+ if (this.context.narrow) {
+ this.toggleButtonMenu();
+ }
+ };
+
+ private onVideoRecordingStateChange = (haveVideoRecording: boolean): void => {
+ this.setState({ haveVideoRecording });
+ };
+
public render(): React.ReactNode {
let leftIcon: false | JSX.Element = false;
if (!this.state.isWysiwygLabEnabled) {
@@ -561,13 +577,14 @@ export class MessageComposer extends React.Component<IProps, IState> {
const menuPosition = this.getMenuPosition();
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
+ const hasActiveRecording = this.state.haveRecording || this.state.haveVideoRecording;
let composer: ReactNode;
if (canSendMessages) {
if (this.state.isWysiwygLabEnabled && menuPosition) {
composer = (
<SendWysiwygComposer
key="controls_input"
- disabled={this.state.haveRecording}
+ disabled={hasActiveRecording}
onChange={this.onWysiwygChange}
onSend={this.sendMessage}
isRichTextEnabled={this.state.isRichTextEnabled}
@@ -588,7 +605,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
relation={this.props.relation}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
- disabled={this.state.haveRecording}
+ disabled={hasActiveRecording}
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
/>
);
@@ -608,6 +625,11 @@ export class MessageComposer extends React.Component<IProps, IState> {
replyToEvent={this.props.replyToEvent}
/>
</Tooltip>,
+ <VideoRecordComposerTile
+ key="controls_video_record"
+ ref={this.videoRecordingButton}
+ onRecordingStateChange={this.onVideoRecordingStateChange}
+ />,
);
} else if (this.context.tombstone) {
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
@@ -688,12 +710,13 @@ export class MessageComposer extends React.Component<IProps, IState> {
{canSendMessages && (
<MessageComposerButtons
addEmoji={this.addEmoji}
- haveRecording={this.state.haveRecording}
+ haveRecording={hasActiveRecording}
isMenuOpen={this.state.isMenuOpen}
isStickerPickerOpen={this.state.isStickerPickerOpen}
menuPosition={menuPosition}
relation={this.props.relation}
onRecordStartEndClick={this.onRecordStartEndClick}
+ onVideoRecordStartClick={this.onVideoRecordStartClick}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={
!window.electron && SettingsStore.getValue(UIFeature.LocationSharing)
diff --git a/apps/web/src/components/views/rooms/MessageComposerButtons.tsx b/apps/web/src/components/views/rooms/MessageComposerButtons.tsx
index 6f4f5d1..0c1b7ff 100644
--- a/apps/web/src/components/views/rooms/MessageComposerButtons.tsx
+++ b/apps/web/src/components/views/rooms/MessageComposerButtons.tsx
@@ -44,6 +44,8 @@ import { useSettingValue } from "../../../hooks/useSettings";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
import { useRoomUploadViewModel } from "../../../viewmodels/room/RoomUploadViewModel.tsx";
+import SdkConfig from "../../../SdkConfig";
+import { isVideoMessageRecorderSupported } from "./VideoRecordComposerTile";
interface IProps {
addEmoji: (emoji: string) => boolean;
@@ -52,6 +54,7 @@ interface IProps {
isStickerPickerOpen: boolean;
menuPosition?: MenuProps;
onRecordStartEndClick: () => void;
+ onVideoRecordStartClick: () => void;
relation?: IEventRelation;
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
showLocationButton: boolean;
@@ -103,6 +106,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
)),
showStickersButton(props),
voiceRecordingButton(props, narrow),
+ videoRecordingButton(props, narrow),
props.showPollsButton ? pollButton(room, props.relation) : null,
showLocationButton(props, room, matrixClient),
];
@@ -122,6 +126,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
moreButtons = [
showStickersButton(props),
voiceRecordingButton(props, narrow),
+ videoRecordingButton(props, narrow),
props.showPollsButton ? pollButton(room, props.relation) : null,
showLocationButton(props, room, matrixClient),
];
@@ -203,6 +208,25 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | nu
);
}
+function videoRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
+ // The current recorder skeleton follows the voice recorder and stays out of narrow mode.
+ if (narrow || SdkConfig.get("hectic")?.videoMessages?.enabled !== true || !isVideoMessageRecorderSupported()) {
+ return null;
+ }
+
+ return (
+ <CollapsibleButton
+ key="video_message_send"
+ className="mx_MessageComposer_button"
+ data-testid="video-message-button"
+ onClick={props.onVideoRecordStartClick}
+ title={_t("composer|video_message_button")}
+ >
+ <span aria-hidden="true">●</span>
+ </CollapsibleButton>
+ );
+}
+
function pollButton(room: Room, relation?: IEventRelation): ReactElement {
return <PollButton key="polls" room={room} relation={relation} />;
}
diff --git a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
new file mode 100644
index 0000000..c5f0adc
--- /dev/null
+++ b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
@@ -0,0 +1,249 @@
+/*
+Copyright 2026 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import React, { type ReactNode } from "react";
+import { logger } from "matrix-js-sdk/src/logger";
+import { DeleteIcon, SendSolidIcon, StopSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+
+interface IProps {
+ onRecordingStateChange: (haveVideoRecording: boolean) => void;
+}
+
+interface IState {
+ error?: string;
+ isRecording: boolean;
+ previewUrl?: string;
+}
+
+export function isVideoMessageRecorderSupported(): boolean {
+ return (
+ typeof window !== "undefined" &&
+ typeof MediaRecorder !== "undefined" &&
+ typeof navigator !== "undefined" &&
+ !!navigator.mediaDevices?.getUserMedia
+ );
+}
+
+/**
+ * Container tile for rendering the config-gated video message recorder skeleton in the composer.
+ */
+export default class VideoRecordComposerTile extends React.PureComponent<IProps, IState> {
+ private chunks: Blob[] = [];
+ private mediaRecorder?: MediaRecorder;
+ private stream?: MediaStream;
+ private isRequestingMedia = false;
+ private isUnmounted = false;
+ private shouldDiscardRecording = false;
+
+ public constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ isRecording: false,
+ };
+ }
+
+ public componentWillUnmount(): void {
+ this.isUnmounted = true;
+ this.disposeRecording();
+ }
+
+ public onRecordStartEndClick = async (): Promise<void> => {
+ if (this.state.isRecording) {
+ this.stopRecording();
+ return;
+ }
+
+ await this.startRecording();
+ };
+
+ private startRecording = async (): Promise<void> => {
+ if (this.isRequestingMedia || this.mediaRecorder) return;
+
+ if (!isVideoMessageRecorderSupported()) {
+ if (!this.isUnmounted) {
+ this.setState({ error: _t("composer|video_message_error") });
+ this.props.onRecordingStateChange(false);
+ }
+ return;
+ }
+
+ this.isRequestingMedia = true;
+ this.shouldDiscardRecording = false;
+ this.revokePreviewUrl();
+ this.chunks = [];
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
+ if (this.isUnmounted) {
+ stream.getTracks().forEach((track) => track.stop());
+ return;
+ }
+
+ this.stream = stream;
+ const mediaRecorder = new MediaRecorder(stream);
+
+ mediaRecorder.ondataavailable = (ev: BlobEvent): void => {
+ if (ev.data.size > 0) {
+ this.chunks.push(ev.data);
+ }
+ };
+ mediaRecorder.onerror = (ev: Event): void => {
+ logger.error("Error recording video message:", ev);
+ this.setState({ error: _t("composer|video_message_error") });
+ };
+ mediaRecorder.onstop = this.onRecorderStop;
+
+ this.mediaRecorder = mediaRecorder;
+ mediaRecorder.start();
+
+ this.setState({ error: undefined, isRecording: true, previewUrl: undefined });
+ this.props.onRecordingStateChange(true);
+ } catch (e) {
+ logger.error("Error starting video message recording:", e);
+ this.stopStream();
+ if (!this.isUnmounted) {
+ this.setState({ error: _t("composer|video_message_error"), isRecording: false, previewUrl: undefined });
+ this.props.onRecordingStateChange(false);
+ }
+ } finally {
+ this.isRequestingMedia = false;
+ }
+ };
+
+ private stopRecording = (): void => {
+ if (!this.mediaRecorder) return;
+
+ if (this.mediaRecorder.state === "inactive") {
+ this.onRecorderStop();
+ } else {
+ this.mediaRecorder.stop();
+ }
+ };
+
+ private onRecorderStop = (): void => {
+ const shouldDiscardRecording = this.shouldDiscardRecording;
+ const hasRecording = this.chunks.length > 0 && !shouldDiscardRecording;
+
+ this.mediaRecorder = undefined;
+ this.stopStream();
+
+ if (this.isUnmounted) {
+ this.chunks = [];
+ return;
+ }
+
+ if (!hasRecording) {
+ this.chunks = [];
+ this.setState({ isRecording: false, previewUrl: undefined });
+ this.props.onRecordingStateChange(false);
+ return;
+ }
+
+ const blob = new Blob(this.chunks, { type: this.chunks[0]?.type || "video/webm" });
+ const previewUrl = URL.createObjectURL(blob);
+
+ this.chunks = [];
+ this.revokePreviewUrl();
+ this.setState({ isRecording: false, previewUrl });
+ this.props.onRecordingStateChange(true);
+ };
+
+ private onCancel = (): void => {
+ this.shouldDiscardRecording = true;
+ this.disposeRecording();
+ this.setState({ error: undefined, isRecording: false, previewUrl: undefined });
+ this.props.onRecordingStateChange(false);
+ };
+
+ private disposeRecording(): void {
+ this.chunks = [];
+ this.stopStream();
+ this.revokePreviewUrl();
+
+ if (this.mediaRecorder) {
+ this.mediaRecorder.ondataavailable = null;
+ this.mediaRecorder.onerror = null;
+ this.mediaRecorder.onstop = null;
+
+ if (this.mediaRecorder.state !== "inactive") {
+ this.mediaRecorder.stop();
+ }
+ this.mediaRecorder = undefined;
+ }
+ }
+
+ private stopStream(): void {
+ this.stream?.getTracks().forEach((track) => track.stop());
+ this.stream = undefined;
+ }
+
+ private revokePreviewUrl(): void {
+ if (this.state.previewUrl) {
+ URL.revokeObjectURL(this.state.previewUrl);
+ }
+ }
+
+ public render(): ReactNode {
+ if (!this.state.isRecording && !this.state.previewUrl && !this.state.error) return null;
+
+ const stopButton = this.state.isRecording ? (
+ <AccessibleButton
+ className="mx_VoiceRecordComposerTile_stop"
+ data-testid="video-message-stop-button"
+ onClick={this.onRecordStartEndClick}
+ title={_t("composer|stop_video_message")}
+ >
+ <StopSolidIcon />
+ </AccessibleButton>
+ ) : null;
+
+ const sendButton = this.state.previewUrl ? (
+ <AccessibleButton
+ className="mx_VoiceRecordComposerTile_stop"
+ data-testid="video-message-send-button"
+ disabled={true}
+ onClick={null}
+ title={_t("composer|send_video_message")}
+ >
+ <SendSolidIcon />
+ </AccessibleButton>
+ ) : null;
+
+ return (
+ <>
+ {this.state.error && (
+ <span className="mx_VoiceRecordComposerTile_failedState" data-testid="video-message-error">
+ {this.state.error}
+ </span>
+ )}
+ <AccessibleButton
+ className="mx_VoiceRecordComposerTile_delete"
+ data-testid="video-message-cancel-button"
+ onClick={this.onCancel}
+ title={_t("composer|video_message_cancel")}
+ >
+ <DeleteIcon />
+ </AccessibleButton>
+ {stopButton}
+ {sendButton}
+ {this.state.previewUrl && (
+ <video
+ aria-label={_t("composer|video_message_preview")}
+ className="mx_VoiceMessagePrimaryContainer"
+ controls={true}
+ data-testid="video-message-preview"
+ src={this.state.previewUrl}
+ />
+ )}
+ </>
+ );
+ }
+}
diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json
index 4ed51db..e11fe90 100644
--- a/apps/web/src/i18n/strings/en_EN.json
+++ b/apps/web/src/i18n/strings/en_EN.json
@@ -641,8 +641,14 @@
"room_upgraded_notice": "This room has been replaced and is no longer active.",
"send_button_title": "Send message",
"send_button_voice_message": "Send voice message",
+ "send_video_message": "Send video message",
"send_voice_message": "Send voice message",
+ "stop_video_message": "Stop video recording",
"stop_voice_message": "Stop recording",
+ "video_message_button": "Record video message",
+ "video_message_cancel": "Cancel video message",
+ "video_message_error": "Unable to record video message",
+ "video_message_preview": "Video message preview",
"voice_message_button": "Voice Message"
},
"console_dev_note": "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!",
@@ -1,201 +0,0 @@
diff --git a/apps/web/src/components/views/rooms/MessageComposer.tsx b/apps/web/src/components/views/rooms/MessageComposer.tsx
index eea76b7..3483c28 100644
--- a/apps/web/src/components/views/rooms/MessageComposer.tsx
+++ b/apps/web/src/components/views/rooms/MessageComposer.tsx
@@ -629,6 +629,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
key="controls_video_record"
ref={this.videoRecordingButton}
onRecordingStateChange={this.onVideoRecordingStateChange}
+ relation={this.props.relation}
+ replyToEvent={this.props.replyToEvent}
+ room={this.props.room}
/>,
);
} else if (this.context.tombstone) {
diff --git a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
index c5f0adc..851c64e 100644
--- a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
+++ b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
@@ -6,22 +6,39 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
+import { type IEventRelation, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { DeleteIcon, SendSolidIcon, StopSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import ContentMessages from "../../../ContentMessages";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
onRecordingStateChange: (haveVideoRecording: boolean) => void;
+ relation?: IEventRelation;
+ replyToEvent?: MatrixEvent;
+ room: Room;
}
interface IState {
error?: string;
isRecording: boolean;
+ isSending: boolean;
previewUrl?: string;
}
+const PREFERRED_VIDEO_MIME_TYPES = ["video/webm;codecs=vp8,opus", "video/webm"];
+
+function preferredVideoMimeType(): string | undefined {
+ for (const mimeType of PREFERRED_VIDEO_MIME_TYPES) {
+ if (MediaRecorder.isTypeSupported(mimeType)) {
+ return mimeType;
+ }
+ }
+}
+
export function isVideoMessageRecorderSupported(): boolean {
return (
typeof window !== "undefined" &&
@@ -37,6 +54,7 @@ export function isVideoMessageRecorderSupported(): boolean {
export default class VideoRecordComposerTile extends React.PureComponent<IProps, IState> {
private chunks: Blob[] = [];
private mediaRecorder?: MediaRecorder;
+ private recordedFile?: File;
private stream?: MediaStream;
private isRequestingMedia = false;
private isUnmounted = false;
@@ -47,6 +65,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
this.state = {
isRecording: false,
+ isSending: false,
};
}
@@ -78,6 +97,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
this.isRequestingMedia = true;
this.shouldDiscardRecording = false;
this.revokePreviewUrl();
+ this.recordedFile = undefined;
this.chunks = [];
try {
@@ -88,7 +108,8 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
}
this.stream = stream;
- const mediaRecorder = new MediaRecorder(stream);
+ const mimeType = preferredVideoMimeType();
+ const mediaRecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
mediaRecorder.ondataavailable = (ev: BlobEvent): void => {
if (ev.data.size > 0) {
@@ -104,13 +125,18 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
this.mediaRecorder = mediaRecorder;
mediaRecorder.start();
- this.setState({ error: undefined, isRecording: true, previewUrl: undefined });
+ this.setState({ error: undefined, isRecording: true, isSending: false, previewUrl: undefined });
this.props.onRecordingStateChange(true);
} catch (e) {
logger.error("Error starting video message recording:", e);
this.stopStream();
if (!this.isUnmounted) {
- this.setState({ error: _t("composer|video_message_error"), isRecording: false, previewUrl: undefined });
+ this.setState({
+ error: _t("composer|video_message_error"),
+ isRecording: false,
+ isSending: false,
+ previewUrl: undefined,
+ });
this.props.onRecordingStateChange(false);
}
} finally {
@@ -129,6 +155,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
};
private onRecorderStop = (): void => {
+ const mimeType = this.mediaRecorder?.mimeType;
const shouldDiscardRecording = this.shouldDiscardRecording;
const hasRecording = this.chunks.length > 0 && !shouldDiscardRecording;
@@ -142,29 +169,63 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
if (!hasRecording) {
this.chunks = [];
- this.setState({ isRecording: false, previewUrl: undefined });
+ this.recordedFile = undefined;
+ this.setState({ isRecording: false, isSending: false, previewUrl: undefined });
this.props.onRecordingStateChange(false);
return;
}
- const blob = new Blob(this.chunks, { type: this.chunks[0]?.type || "video/webm" });
- const previewUrl = URL.createObjectURL(blob);
+ const blob = new Blob(this.chunks, { type: mimeType || this.chunks[0]?.type || "video/webm" });
+ const file = new File([blob], `video-message-${Date.now()}.webm`, { type: blob.type || "video/webm" });
+ const previewUrl = URL.createObjectURL(file);
this.chunks = [];
+ this.recordedFile = file;
this.revokePreviewUrl();
- this.setState({ isRecording: false, previewUrl });
+ this.setState({ isRecording: false, isSending: false, previewUrl });
this.props.onRecordingStateChange(true);
};
+ private onSend = async (): Promise<void> => {
+ if (!this.recordedFile || this.state.isSending) return;
+
+ this.setState({ isSending: true });
+
+ try {
+ await ContentMessages.sharedInstance().sendContentToRoom(
+ this.recordedFile,
+ this.props.room.roomId,
+ this.props.relation,
+ MatrixClientPeg.safeGet(),
+ this.props.replyToEvent,
+ );
+ this.clearRecording();
+ this.props.onRecordingStateChange(false);
+ } catch (e) {
+ logger.error("Error sending video message recording:", e);
+ if (!this.isUnmounted) {
+ this.setState({ error: _t("composer|video_message_error"), isSending: false });
+ }
+ }
+ };
+
private onCancel = (): void => {
this.shouldDiscardRecording = true;
this.disposeRecording();
- this.setState({ error: undefined, isRecording: false, previewUrl: undefined });
+ this.setState({ error: undefined, isRecording: false, isSending: false, previewUrl: undefined });
this.props.onRecordingStateChange(false);
};
+ private clearRecording(): void {
+ this.chunks = [];
+ this.recordedFile = undefined;
+ this.revokePreviewUrl();
+ this.setState({ error: undefined, isRecording: false, isSending: false, previewUrl: undefined });
+ }
+
private disposeRecording(): void {
this.chunks = [];
+ this.recordedFile = undefined;
this.stopStream();
this.revokePreviewUrl();
@@ -209,8 +270,8 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
<AccessibleButton
className="mx_VoiceRecordComposerTile_stop"
data-testid="video-message-send-button"
- disabled={true}
- onClick={null}
+ disabled={!this.recordedFile || this.state.isSending}
+ onClick={this.onSend}
title={_t("composer|send_video_message")}
>
<SendSolidIcon />
@@ -1,195 +0,0 @@
diff --git a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
index 851c64e..4285c72 100644
--- a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
+++ b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx
@@ -30,6 +30,7 @@ interface IState {
}
const PREFERRED_VIDEO_MIME_TYPES = ["video/webm;codecs=vp8,opus", "video/webm"];
+const MAX_VIDEO_RECORDING_MS = 120000;
function preferredVideoMimeType(): string | undefined {
for (const mimeType of PREFERRED_VIDEO_MIME_TYPES) {
@@ -39,6 +40,18 @@ function preferredVideoMimeType(): string | undefined {
}
}
+function isPermissionDeniedError(error: unknown): boolean {
+ if (error instanceof DOMException) {
+ return error.name === "NotAllowedError" || error.name === "PermissionDeniedError";
+ }
+
+ if (error instanceof Error) {
+ return /permission denied|not allowed|denied permission/i.test(error.message);
+ }
+
+ return false;
+}
+
export function isVideoMessageRecorderSupported(): boolean {
return (
typeof window !== "undefined" &&
@@ -56,6 +69,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
private mediaRecorder?: MediaRecorder;
private recordedFile?: File;
private stream?: MediaStream;
+ private durationCapTimer?: number;
private isRequestingMedia = false;
private isUnmounted = false;
private shouldDiscardRecording = false;
@@ -72,6 +86,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
public componentWillUnmount(): void {
this.isUnmounted = true;
this.disposeRecording();
+ this.props.onRecordingStateChange(false);
}
public onRecordStartEndClick = async (): Promise<void> => {
@@ -96,6 +111,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
this.isRequestingMedia = true;
this.shouldDiscardRecording = false;
+ this.clearDurationCapTimer();
this.revokePreviewUrl();
this.recordedFile = undefined;
this.chunks = [];
@@ -108,6 +124,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
}
this.stream = stream;
+ this.bindStreamEndedHandlers(stream);
const mimeType = preferredVideoMimeType();
const mediaRecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
@@ -118,12 +135,13 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
};
mediaRecorder.onerror = (ev: Event): void => {
logger.error("Error recording video message:", ev);
- this.setState({ error: _t("composer|video_message_error") });
+ this.failRecording(_t("composer|video_message_error"));
};
mediaRecorder.onstop = this.onRecorderStop;
this.mediaRecorder = mediaRecorder;
mediaRecorder.start();
+ this.durationCapTimer = window.setTimeout(this.onDurationCap, MAX_VIDEO_RECORDING_MS);
this.setState({ error: undefined, isRecording: true, isSending: false, previewUrl: undefined });
this.props.onRecordingStateChange(true);
@@ -132,7 +150,9 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
this.stopStream();
if (!this.isUnmounted) {
this.setState({
- error: _t("composer|video_message_error"),
+ error: isPermissionDeniedError(e)
+ ? _t("composer|video_message_permission_denied")
+ : _t("composer|video_message_error"),
isRecording: false,
isSending: false,
previewUrl: undefined,
@@ -144,6 +164,21 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
}
};
+ private onDurationCap = (): void => {
+ this.durationCapTimer = undefined;
+
+ if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") return;
+
+ this.stopRecording();
+ };
+
+ private onStreamTrackEnded = (): void => {
+ if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") return;
+
+ logger.warn("Video message media stream ended before recording stopped");
+ this.failRecording(_t("composer|video_message_error"));
+ };
+
private stopRecording = (): void => {
if (!this.mediaRecorder) return;
@@ -160,6 +195,7 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
const hasRecording = this.chunks.length > 0 && !shouldDiscardRecording;
this.mediaRecorder = undefined;
+ this.clearDurationCapTimer();
this.stopStream();
if (this.isUnmounted) {
@@ -200,7 +236,9 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
this.props.replyToEvent,
);
this.clearRecording();
- this.props.onRecordingStateChange(false);
+ if (!this.isUnmounted) {
+ this.props.onRecordingStateChange(false);
+ }
} catch (e) {
logger.error("Error sending video message recording:", e);
if (!this.isUnmounted) {
@@ -219,13 +257,18 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
private clearRecording(): void {
this.chunks = [];
this.recordedFile = undefined;
+ this.clearDurationCapTimer();
+ this.stopStream();
this.revokePreviewUrl();
+ if (this.isUnmounted) return;
+
this.setState({ error: undefined, isRecording: false, isSending: false, previewUrl: undefined });
}
private disposeRecording(): void {
this.chunks = [];
this.recordedFile = undefined;
+ this.clearDurationCapTimer();
this.stopStream();
this.revokePreviewUrl();
@@ -241,8 +284,32 @@ export default class VideoRecordComposerTile extends React.PureComponent<IProps,
}
}
+ private failRecording(error: string): void {
+ this.shouldDiscardRecording = true;
+ this.disposeRecording();
+
+ if (this.isUnmounted) return;
+
+ this.setState({ error, isRecording: false, isSending: false, previewUrl: undefined });
+ this.props.onRecordingStateChange(false);
+ }
+
+ private clearDurationCapTimer(): void {
+ if (this.durationCapTimer !== undefined) {
+ window.clearTimeout(this.durationCapTimer);
+ this.durationCapTimer = undefined;
+ }
+ }
+
+ private bindStreamEndedHandlers(stream: MediaStream): void {
+ stream.getTracks().forEach((track) => track.addEventListener("ended", this.onStreamTrackEnded));
+ }
+
private stopStream(): void {
- this.stream?.getTracks().forEach((track) => track.stop());
+ this.stream?.getTracks().forEach((track) => {
+ track.removeEventListener("ended", this.onStreamTrackEnded);
+ track.stop();
+ });
this.stream = undefined;
}
diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json
index e11fe90..83b7c21 100644
--- a/apps/web/src/i18n/strings/en_EN.json
+++ b/apps/web/src/i18n/strings/en_EN.json
@@ -648,6 +648,7 @@
"video_message_button": "Record video message",
"video_message_cancel": "Cancel video message",
"video_message_error": "Unable to record video message",
+ "video_message_permission_denied": "Camera permission denied",
"video_message_preview": "Video message preview",
"voice_message_button": "Voice Message"
},
-17
View File
@@ -1,17 +0,0 @@
# Element Web patches
Put ordered local patch files in this directory as `*.patch`.
- Files are applied in lexical order.
- Use zero-padded numeric prefixes when patch order matters, e.g. `0001-...patch`.
- Keep non-patch files out of the order by using a different extension; `README.md` is ignored by the Nix filter.
## Regenerating a patch
Create a clean checkout of upstream Element Web, make the change under `apps/web`, then run from the repository root:
```sh
git diff -- apps/web > package/element-web/patches/0001-description.patch
```
Use one patch per logical change so the sequence stays reproducible and reviewable.
@@ -1,81 +0,0 @@
{
bash,
cacert,
coreutils,
dockerTools,
git,
lib,
nix,
symlinkJoin,
}:
let
name = "gitea-runner-nix-image";
tag = "2026-06-07";
root = symlinkJoin {
name = "${name}-root";
paths = [
bash
cacert
coreutils
git
nix
];
};
in
dockerTools.buildLayeredImageWithNixDb {
inherit name tag;
contents = [ root ];
fakeRootCommands = ''
mkdir -p ./etc
cat > ./etc/passwd <<'EOF'
root:x:0:0:root:/root:/bin/bash
nobody:x:65534:65534:nobody:/var/empty:/bin/false
EOF
cat > ./etc/group <<'EOF'
root:x:0:
nixbld:x:30000:
nobody:x:65534:
EOF
for n in $(seq 1 10); do
echo "nixbld$n:x:$((30000 + n)):30000:Nix build user $n:/var/empty:/bin/false" >> ./etc/passwd
sed -i "s/^nixbld:x:30000:.*/&,nixbld$n/" ./etc/group
done
mkdir -p ./nix/var/nix/{gcroots,profiles,temproots,userpool,db}
mkdir -p ./nix/var/log/nix/drvs
chmod 1777 ./nix/var/nix/gcroots ./nix/var/nix/profiles
'';
extraCommands = ''
mkdir -p etc/nix root tmp
chmod 1777 tmp
cat > etc/nix/nix.conf <<'EOF'
accept-flake-config = true
experimental-features = nix-command flakes
substituters = https://cache.nixos.org https://cache.hectic-lab.com/hectic
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gW4x6l1xP+GxgH0r7u+f6p1VFlr0= hectic:KMQsKow4SoA9K2vOJlOljmx7/Zpf91Yy+5qEtxDDCzA=
trusted-users = root
sandbox = false
EOF
'';
config = {
Cmd = [ "${bash}/bin/bash" ];
Env = [
"HOME=/root"
"PATH=${lib.makeBinPath [ bash coreutils git nix ]}"
"SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt"
"NIX_SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt"
"USER=root"
];
Labels = {
"org.opencontainers.image.description" = "Nix-capable Gitea Actions job image";
"org.opencontainers.image.ref.name" = "${name}:${tag}";
"org.opencontainers.image.source" = "https://gitea.hectic-lab.com/hectic-lab/util.nix";
};
WorkingDir = "/workspace";
};
}
-128
View File
@@ -1,128 +0,0 @@
{
lib,
buildGo126Module,
makeWrapper,
git,
bash,
coreutils,
gzip,
nodejs,
openssh,
fetchPnpmDeps,
pnpmConfigHook,
pnpm_10,
stdenv,
sqliteSupport ? true,
nixosTests,
}:
let
pname = "gitea";
version = "1.26.2";
src = ./source;
pnpm = pnpm_10;
frontend = stdenv.mkDerivation {
pname = "gitea-frontend";
inherit src version;
pnpmDeps = fetchPnpmDeps {
pname = "gitea-frontend";
inherit version src;
inherit pnpm;
fetcherVersion = 3;
hash = "sha256-Qo0DLuZv+2GVLsBfCv/6CC9E/qhSE4HwV4StQL4HX4Y=";
};
nativeBuildInputs = [
nodejs
pnpmConfigHook
pnpm
];
buildPhase = ''
make frontend
'';
installPhase = ''
mkdir -p $out
cp -R public $out/
'';
};
in
buildGo126Module rec {
inherit pname version src;
proxyVendor = true;
vendorHash = "sha256-7+M1n8RSgB3gZ/2na4RF9kYOf90H0bnsJZMDKpgAy64=";
outputs = [
"out"
"data"
];
patches = [ ./static-root-path.patch ];
overrideModAttrs = _: {
postPatch = ''
substituteInPlace go.mod \
--replace-fail "go 1.26.3" "go 1.26"
'';
};
postPatch = ''
substituteInPlace modules/setting/server.go --subst-var data
substituteInPlace go.mod \
--replace-fail "go 1.26.3" "go 1.26"
'';
subPackages = [ "." ];
nativeBuildInputs = [ makeWrapper ];
tags = lib.optionals sqliteSupport [
"sqlite"
"sqlite_unlock_notify"
];
ldflags = [
"-s"
"-w"
"-X main.Version=${version}"
"-X 'main.Tags=${lib.concatStringsSep " " tags}'"
];
postInstall = ''
mkdir $data
ln -s ${frontend}/public $data/public
cp -R ./{templates,options} $data
mkdir -p $out
cp -R ./options/locale $out/locale
wrapProgram $out/bin/gitea \
--prefix PATH : ${
lib.makeBinPath [
bash
coreutils
git
gzip
openssh
]
}
'';
passthru = {
tests = nixosTests.gitea;
};
meta = {
description = "Git with a cup of tea";
homepage = "https://about.gitea.com";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [
techknowlogick
SuperSandro2000
];
mainProgram = "gitea";
};
}
-26
View File
@@ -1,26 +0,0 @@
root = "."
tmp_dir = ".air"
[build]
pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs
cmd = "make --no-print-directory backend"
entrypoint = ["./gitea"]
delay = 2000
include_ext = ["go", "tmpl"]
include_file = ["main.go"]
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
exclude_dir = [
"models/fixtures",
"models/migrations/fixtures",
"modules/avatar/identicon/testdata",
"modules/avatar/testdata",
"modules/git/tests",
"modules/migration/file_format_testdata",
"routers/private/tests",
"services/gitdiff/testdata",
]
exclude_regex = ["_test.go$", "_gen.go$"]
stop_on_error = true
[log]
main_only = true
-62
View File
@@ -1,62 +0,0 @@
# The full repository name
repo: go-gitea/gitea
# Service type (gitea or github)
service: github
# Base URL for Gitea instance if using gitea service type (optional)
# Default: https://gitea.com
base-url:
# Changelog groups and which labeled PRs to add to each group
groups:
-
name: BREAKING
labels:
- pr/breaking
-
name: SECURITY
labels:
- topic/security
-
name: FEATURES
labels:
- type/feature
-
name: ENHANCEMENTS
labels:
- type/enhancement
-
name: PERFORMANCE
labels:
- performance/memory
- performance/speed
- performance/bigrepo
- performance/cpu
-
name: BUGFIXES
labels:
- type/bug
-
name: API
labels:
- modifies/api
-
name: TESTING
labels:
- type/testing
-
name: BUILD
labels:
- topic/build
- topic/code-linting
-
name: DOCS
labels:
- type/docs
-
name: MISC
default: true
# regex indicating which labels to skip for the changelog
skip-labels: skip-changelog|backport\/.+
@@ -1,45 +0,0 @@
{
"name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
"containerEnv": {
// override "local" from packaged version
"GOTOOLCHAIN": "auto"
},
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.14"
},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
},
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"golang.go",
"stylelint.vscode-stylelint",
"DavidAnson.vscode-markdownlint",
"Vue.volar",
"ms-azuretools.vscode-docker",
"vitest.explorer",
"cweijan.vscode-database-client2",
"GitHub.vscode-pull-request-github",
"Azurite.azurite"
]
}
},
"portsAttributes": {
"3000": {
"label": "Gitea Web",
"onAutoForward": "notify"
}
},
"postCreateCommand": "make deps"
}
-98
View File
@@ -1,98 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# IntelliJ
.idea
# Goland's output filename can not be set manually
/go_build_*
# MS VSCode
.vscode
__debug_bin*
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*coverage.out
coverage.all
cpu.out
*.db
*.log
/gitea
/gitea-vet
/debug
/integrations.test
/bin
/dist
/custom/*
!/custom/conf
/custom/conf/*
!/custom/conf/app.example.ini
/data
/indexers
/log
/tests/integration/gitea-integration-*
/tests/integration/indexers-*
/tests/e2e/gitea-e2e-*
/tests/e2e/indexers-*
/tests/e2e/reports
/tests/e2e/test-artifacts
/tests/e2e/test-snapshots
/tests/*.ini
/node_modules
/yarn.lock
/yarn-error.log
/npm-debug.log*
/pnpm-debug.log*
/public/assets/js
/public/assets/css
/public/assets/fonts
/public/assets/img/avatar
/vendor
/VERSION
/.air
/.go-licenses
/Dockerfile
/Dockerfile.rootless
/.venv
# Files and folders that were previously generated
/public/assets/img/webpack
# Snapcraft
snap/.snapcraft/
parts/
stage/
prime/
*.snap
*.snap-build
*_source.tar.bz2
.DS_Store
# Make evidence files
/.make_evidence
# Manpage
/man
-36
View File
@@ -1,36 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{go,tmpl,html}]
indent_style = tab
[go.*]
indent_style = tab
[templates/custom/*.tmpl]
insert_final_newline = false
[templates/swagger/v1_json.tmpl]
indent_style = space
insert_final_newline = false
[templates/user/auth/oidc_wellknown.tmpl]
indent_style = space
[templates/shared/actions/runner_badge_*.tmpl]
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
charset = unset
[Makefile]
indent_style = tab
[*.svg]
insert_final_newline = false
-1
View File
@@ -1 +0,0 @@
use flake
-11
View File
@@ -1,11 +0,0 @@
* text=auto eol=lf
*.tmpl linguist-language=Handlebars
*.pb.go linguist-generated
/assets/*.json linguist-generated
/public/assets/img/svg/*.svg linguist-generated
/templates/swagger/v1_json.tmpl linguist-generated
/options/fileicon/** linguist-generated
/vendor/** -text -eol linguist-vendored
/web_src/js/vendor/** -text -eol linguist-vendored
Dockerfile.* linguist-language=Dockerfile
Makefile.* linguist-language=Makefile
@@ -1,42 +0,0 @@
<!-- NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue -->
<!--
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report.
-->
- Gitea version (or commit ref):
- Git version:
- Operating system:
<!-- Please include information on whether you built gitea yourself, used one of our downloads or are using some other package -->
<!-- Please also tell us how you are running gitea, e.g. if it is being run from docker, a command-line, systemd etc. --->
<!-- If you are using a package or systemd tell us what distribution you are using -->
- Database (use `[x]`):
- [ ] PostgreSQL
- [ ] MySQL
- [ ] MSSQL
- [ ] SQLite
- Can you reproduce the bug at https://demo.gitea.com:
- [ ] Yes (provide example URL)
- [ ] No
- Log gist:
<!-- It really is important to provide pertinent logs -->
<!-- Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help -->
<!-- In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -->
## Description
<!-- If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please
disable the proxy/CDN fully and connect to gitea directly to confirm
the issue still persists without those services. -->
...
## Screenshots
<!-- **If this issue involves the Web Interface, please include a screenshot** -->
-1
View File
@@ -1 +0,0 @@
open_collective: gitea
@@ -1,91 +0,0 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: ["type/bug"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support),
incomplete details will be handled as an invalid report.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services.
- type: input
id: gitea-ver
attributes:
label: Gitea Version
description: Gitea version (or commit reference) of your instance
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
validations:
required: true
- type: markdown
attributes:
value: |
It's really important to provide pertinent logs
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
- type: input
id: logs
attributes:
label: Log Gist
description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots
- type: input
id: git-ver
attributes:
label: Git Version
description: The version of git running on the server
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to run Gitea
- type: textarea
id: run-info
attributes:
label: How are you running Gitea?
description: |
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
If you are using a package or systemd tell us what distribution you are using
validations:
required: true
- type: dropdown
id: database
attributes:
label: Database
description: What database system are you running?
options:
- PostgreSQL
- MySQL/MariaDB
- MSSQL
- SQLite
-17
View File
@@ -1,17 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Security Concern
url: https://tinyurl.com/security-gitea
about: For security concerns, please send a mail to security@gitea.io instead of opening a public issue.
- name: Discord Server
url: https://discord.gg/Gitea
about: Please ask questions and discuss configuration or deployment problems here.
- name: Discourse Forum
url: https://forum.gitea.com
about: Questions and configuration or deployment problems can also be discussed on our forum.
- name: Frequently Asked Questions
url: https://docs.gitea.com/help/faq
about: Please check if your question isn't mentioned here.
- name: Crowdin Translations
url: https://translate.gitea.com
about: Translations are managed here.
@@ -1,24 +0,0 @@
name: Feature Request
description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here!
labels: ["type/proposal"]
body:
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your feature hasn't already been suggested.
- type: textarea
id: description
attributes:
label: Feature Description
placeholder: |
I think it would be great if Gitea had...
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If you can, provide screenshots of an implementation on another site e.g. GitHub
@@ -1,66 +0,0 @@
name: Web Interface Bug Report
description: Something doesn't look quite as it should? Report it here!
labels: ["type/bug", "topic/ui"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report.
6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript
error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us
DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help)
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services.
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please provide at least 1 screenshot showing the issue.
validations:
required: true
- type: input
id: gitea-ver
attributes:
label: Gitea Version
description: Gitea version (or commit reference) your instance is running
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
validations:
required: true
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to access Gitea
- type: input
id: browser-ver
attributes:
label: Browser Version
description: The browser and version that you are using to access Gitea
validations:
required: true
-7
View File
@@ -1,7 +0,0 @@
self-hosted-runner:
labels:
- actuated-4cpu-8gb
- actuated-4cpu-16gb
- nscloud
- namespace-profile-gitea-release-docker
- namespace-profile-gitea-release-binary
-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: [modifies/dependencies]
directory: /
schedule:
interval: daily
cooldown:
default-days: 5
-91
View File
@@ -1,91 +0,0 @@
modifies/docs:
- changed-files:
- any-glob-to-any-file:
- "**/*.md"
- "docs/**"
modifies/templates:
- changed-files:
- all-globs-to-any-file:
- "templates/**"
- "!templates/swagger/v1_json.tmpl"
modifies/api:
- changed-files:
- any-glob-to-any-file:
- "routers/api/**"
- "templates/swagger/v1_json.tmpl"
modifies/cli:
- changed-files:
- any-glob-to-any-file:
- "cmd/**"
modifies/translation:
- changed-files:
- any-glob-to-any-file:
- "options/locale/*.ini"
modifies/migrations:
- changed-files:
- any-glob-to-any-file:
- "models/migrations/**"
modifies/internal:
- changed-files:
- any-glob-to-any-file:
- ".air.toml"
- "Makefile"
- "Dockerfile"
- "Dockerfile.rootless"
- ".dockerignore"
- "docker/**"
- ".editorconfig"
- ".eslintrc.cjs"
- ".golangci.yml"
- ".markdownlint.yaml"
- ".spectral.yaml"
- "stylelint.config.*"
- ".yamllint.yaml"
- ".github/**"
- ".gitea/**"
- ".devcontainer/**"
- "build/**"
- "contrib/**"
modifies/dependencies:
- changed-files:
- any-glob-to-any-file:
- "package.json"
- "pnpm-lock.yaml"
- "pyproject.toml"
- "uv.lock"
- "go.mod"
- "go.sum"
modifies/go:
- changed-files:
- any-glob-to-any-file:
- "**/*.go"
modifies/frontend:
- changed-files:
- any-glob-to-any-file:
- "*.js"
- "*.ts"
- "web_src/**"
docs-update-needed:
- changed-files:
- any-glob-to-any-file:
- "custom/conf/app.example.ini"
topic/code-linting:
- changed-files:
- any-glob-to-any-file:
- ".golangci.yml"
- ".markdownlint.yaml"
- ".spectral.yaml"
- ".yamllint.yaml"
- "eslint*.config.*"
- "stylelint.config.*"
-10
View File
@@ -1,10 +0,0 @@
<!-- start tips -->
Please check the following:
1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports.
2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md .
3. For documentations contribution, please go to https://gitea.com/gitea/docs
4. Describe what your pull request does and which issue you're targeting (if any).
5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`.
7. Delete all these tips before posting.
<!-- end tips -->
@@ -1,22 +0,0 @@
name: cron-flake-updater
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # runs weekly on Sunday at 00:00
jobs:
nix-flake-update:
permissions:
contents: write
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: DeterminateSystems/determinate-nix-action@v3
- uses: DeterminateSystems/update-flake-lock@main
with:
pr-title: "Update Nix flake"
pr-labels: |
dependencies
@@ -1,31 +0,0 @@
name: cron-licenses
on:
# schedule:
# - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch:
jobs:
cron-licenses:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make generate-gitignore
timeout-minutes: 40
- name: push translations to repo
uses: appleboy/git-push-action@v1.2.0
with:
author_email: "teabot@gitea.io"
author_name: GiteaBot
branch: main
commit: true
commit_message: "[skip ci] Updated licenses and gitignores"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
@@ -1,40 +0,0 @@
name: cron-translations
on:
schedule:
- cron: "7 0 * * *" # every day at 00:07 UTC
workflow_dispatch:
jobs:
crowdin-pull:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_sources: false
download_translations: true
push_translations: false
push_sources: false
create_pull_request: false
config: crowdin.yml
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
- name: update locales
run: ./build/update-locales.sh
- name: push translations to repo
uses: appleboy/git-push-action@v1.2.0
with:
author_email: "teabot@gitea.io"
author_name: GiteaBot
branch: main
commit: true
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
-109
View File
@@ -1,109 +0,0 @@
name: files-changed
on:
workflow_call:
outputs:
backend:
value: ${{ jobs.detect.outputs.backend }}
frontend:
value: ${{ jobs.detect.outputs.frontend }}
docs:
value: ${{ jobs.detect.outputs.docs }}
actions:
value: ${{ jobs.detect.outputs.actions }}
templates:
value: ${{ jobs.detect.outputs.templates }}
docker:
value: ${{ jobs.detect.outputs.docker }}
swagger:
value: ${{ jobs.detect.outputs.swagger }}
yaml:
value: ${{ jobs.detect.outputs.yaml }}
json:
value: ${{ jobs.detect.outputs.json }}
jobs:
detect:
runs-on: ubuntu-latest
timeout-minutes: 3
permissions:
contents: read
outputs:
backend: ${{ steps.changes.outputs.backend }}
frontend: ${{ steps.changes.outputs.frontend }}
docs: ${{ steps.changes.outputs.docs }}
actions: ${{ steps.changes.outputs.actions }}
templates: ${{ steps.changes.outputs.templates }}
docker: ${{ steps.changes.outputs.docker }}
swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
json: ${{ steps.changes.outputs.json }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
id: changes
with:
filters: |
backend:
- "**/*.go"
- "templates/**/*.tmpl"
- "assets/emoji.json"
- "go.mod"
- "go.sum"
- "Makefile"
- ".golangci.yml"
- ".editorconfig"
- "options/locale/locale_en-US.json"
frontend:
- "*.js"
- "*.ts"
- "web_src/**"
- "tools/*.js"
- "tools/*.ts"
- "assets/emoji.json"
- "package.json"
- "pnpm-lock.yaml"
- "Makefile"
- ".eslintrc.cjs"
- ".npmrc"
docs:
- "**/*.md"
- ".markdownlint.yaml"
- "package.json"
- "pnpm-lock.yaml"
actions:
- ".github/workflows/*"
- "Makefile"
templates:
- "tools/lint-templates-*.js"
- "templates/**/*.tmpl"
- "pyproject.toml"
- "uv.lock"
docker:
- ".github/workflows/pull-docker-dryrun.yml"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
- "Makefile"
swagger:
- "templates/swagger/v1_json.tmpl"
- "templates/swagger/v1_input.json"
- "Makefile"
- "package.json"
- "pnpm-lock.yaml"
- ".spectral.yaml"
yaml:
- "**/*.yml"
- "**/*.yaml"
- ".yamllint.yaml"
- "pyproject.toml"
json:
- "**/*.json"
@@ -1,246 +0,0 @@
name: compliance
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
permissions:
contents: read
lint-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make deps-backend deps-tools
- run: make lint-backend
env:
TAGS: bindata sqlite sqlite_unlock_notify
lint-templates:
if: needs.files-changed.outputs.templates == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.0.0
- run: uv python install 3.14
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-py
- run: make deps-frontend
- run: make lint-templates
lint-yaml:
if: needs.files-changed.outputs.yaml == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.0.0
- run: uv python install 3.14
- run: make deps-py
- run: make lint-yaml
lint-json:
if: needs.files-changed.outputs.json == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v5
with:
node-version: 24
- run: make deps-frontend
- run: make lint-json
lint-swagger:
if: needs.files-changed.outputs.swagger == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make lint-swagger
lint-spell:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make lint-spell
lint-go-windows:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make deps-backend deps-tools
- run: make lint-go-windows lint-go-gitea-vet
env:
TAGS: bindata sqlite sqlite_unlock_notify
GOOS: windows
GOARCH: amd64
lint-go-gogit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make deps-backend deps-tools
- run: make lint-go
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
checks-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
frontend:
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
- run: make test-frontend
- run: make frontend
backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
# no frontend build here as backend should be able to build
# even without any frontend files
- run: make deps-backend
- run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag
- name: build-backend-arm64
run: make backend # test cross compile
env:
GOOS: linux
GOARCH: arm64
TAGS: bindata gogit
- name: build-backend-windows
run: go build -o gitea_windows
env:
GOOS: windows
GOARCH: amd64
TAGS: bindata gogit
- name: build-backend-386
run: go build -o gitea_linux_386 # test if compatible with 32 bit
env:
GOOS: linux
GOARCH: 386
docs:
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make lint-md
actions:
if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make lint-actions
-245
View File
@@ -1,245 +0,0 @@
name: db-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
permissions:
contents: read
test-pgsql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
pgsql:
image: postgres:14
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
ldap:
image: gitea/test-openldap:latest
ports:
- "389:389"
- "636:636"
minio:
# as github actions doesn't support "entrypoint", we need to use a non-official image
# that has a custom entrypoint set to "minio server /data"
image: bitnamilegacy/minio:2023.8.31
env:
MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678
ports:
- "9000:9000"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: make test-pgsql-migration
- name: run tests
run: make test-pgsql
timeout-minutes: 50
env:
TAGS: bindata gogit
RACE_ENABLED: true
TEST_TAGS: gogit
TEST_LDAP: 1
test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- run: make deps-backend
- run: GOEXPERIMENT='' make backend
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
- name: run migration tests
run: make test-sqlite-migration
- name: run tests
run: GOEXPERIMENT='' make test-sqlite
timeout-minutes: 50
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
RACE_ENABLED: true
TEST_TAGS: gogit sqlite sqlite_unlock_notify
test-unit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
elasticsearch:
image: elasticsearch:7.5.0
env:
discovery.type: single-node
ports:
- "9200:9200"
meilisearch:
image: getmeili/meilisearch:v1
env:
MEILI_ENV: development # disable auth
ports:
- "7700:7700"
redis:
image: redis
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
ports:
- 6379:6379
minio:
image: bitnamilegacy/minio:2021.3.17
env:
MINIO_ACCESS_KEY: 123456
MINIO_SECRET_KEY: 12345678
ports:
- "9000:9000"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- 10000:10000
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: unit-tests
run: make unit-test-coverage test-check
env:
TAGS: bindata
RACE_ENABLED: true
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: GOEXPERIMENT='' make unit-test-coverage test-check
env:
TAGS: bindata gogit
RACE_ENABLED: true
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
test-mysql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
mysql:
# the bitnami mysql image has more options than the official one, it's easier to customize
image: bitnamilegacy/mysql:8.0
env:
ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: testgitea
ports:
- "3306:3306"
options: >-
--mount type=tmpfs,destination=/bitnami/mysql/data
elasticsearch:
image: elasticsearch:7.5.0
env:
discovery.type: single-node
ports:
- "9200:9200"
smtpimap:
image: tabascoterrier/docker-imap-devel:latest
ports:
- "25:25"
- "143:143"
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: make test-mysql-migration
- name: run tests
# run: make integration-test-coverage (at the moment, no coverage is really handled)
run: make test-mysql
env:
TAGS: bindata
RACE_ENABLED: true
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
env:
ACCEPT_EULA: Y
MSSQL_PID: Standard
SA_PASSWORD: MwantsaSecurePassword1
ports:
- "1433:1433"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- 10000:10000
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- run: make test-mssql-migration
- name: run tests
run: make test-mssql
timeout-minutes: 50
env:
TAGS: bindata
@@ -1,40 +0,0 @@
name: docker-dryrun
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
permissions:
contents: read
container:
if: needs.files-changed.outputs.docker == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Build regular container image
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: false
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless container image
uses: docker/build-push-action@v7
with:
context: .
push: false
platforms: linux/amd64,linux/arm64,linux/riscv64
file: Dockerfile.rootless
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless

Some files were not shown because too many files have changed in this diff Show More