Feature Specification: Component forgejo

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/components/dev/default.nix, core/components/dev/forgejo/default.nix, core/components/dev/forgejo/package.nix, core/components/dev/forgejo/forgejo.nix, core/components/dev/forgejo/anubis.nix, core/components/dev/forgejo/anubis-allow.nix, core/components/dev/forgejo/anubis-allow-test.nix, core/setups/groups/dev.nix, core/checks/default.nix), pull requests #1383, #1466, #1475, #1547, #1583, #1589, #1595, #1852, #1956, #1992 and #1994.

This spec describes one component of the dev group. The family spec ../spec.md describes what all components share. The group roster is in core/setups/groups/dev.nix, see ../../005-setups-and-wiring/spec.md.

User Scenarios & Testing (mandatory)

User Story 1 - Contributors use the forge over HTTPS (Priority: P1)

A contributor opens the forge in a browser, clones and pushes over HTTPS, and gets mail notifications. The forgejo component runs Forgejo with a MariaDB database behind nginx. nginx terminates TLS with the certificate of the ssl resource. Port 80 and port 443 are public through the firewall resource.

Why this priority: The forge holds the source code and the CI of the project. Without it, no contributor can work.

Independent Test: The test-forgejo-service NixOS test (core/components/dev/forgejo/test.nix) runs the NixOS modules of the component on one node, with stand-ins for the contract results and the SMTP relay, and with plain HTTP in place of TLS. It checks that Forgejo answers on its domain through nginx and gives https://<DOMAIN>/ in its URLs, that nginx and Forgejo are not reachable on other addresses or ports of the node, that the repositories are in MariaDB, that security.txt has the configured fields and that the key is served, both without a challenge, and that a message from Forgejo arrives at the relay with the sender and the credential of the smtp result. It does not cover TLS, HSTS or the backup. The setups-dev-config nix-unit suite (core/setups/dev-config-test.nix) evaluates the dev-forgejo node and pins its domain, its listen addresses, its mail sender and its security.txt.

Acceptance Scenarios:

  1. Given the forgejo machine of the dev group with the component enabled, When the node deploys, Then Forgejo listens on 127.0.0.1:3000 and nginx serves https://<cfg.domain>/ (git.fediversity.eu in the dev group).
  2. Given the component, When the ssl resource resolves, Then the certificate covers cfg.domain and each name in cfg.extraDomains. In the dev group, these are the git-pages sites.
  3. Given the smtp contract result for forgejo, When Forgejo sends a notification, Then it uses smtp+starttls with the host, port and user of that result, and the sender cfg.sender (git@fediversity.eu in the dev group).
  4. Given the component, When a scanner requests https://<cfg.domain>/.well-known/security.txt, Then nginx serves it with no Anubis challenge, with Contact from cfg.securityContact, Policy from cfg.securityPolicy, and Encryption and Canonical below https://<cfg.domain>/.well-known/.

User Story 2 - A proof-of-work gate protects the expensive views (Priority: P1)

The forge operator must keep scrapers off the expensive HTML views. A browser user agent with no Anubis cookie gets a proof-of-work challenge on /. Git clients, CI runners and raw file reads run no JavaScript, so their paths pass the gate without a challenge.

Why this priority: Without the gate, scraper load makes the forge slow for all users. Without the allow rules, the gate breaks clones and CI.

Independent Test: test-forgejo-service (core/components/dev/forgejo/test.nix). With a browser user agent and no Anubis cookie, it gets a raw file, info/refs, a .tar.gz archive of a branch and a .zip archive from Forgejo, and gets a challenge for a .bundle archive and a file view.

Acceptance Scenarios:

  1. Given a browser user agent and no Anubis cookie, When it requests a /raw/ path, Then the request reaches Forgejo.
  2. Given a browser user agent and no Anubis cookie, When it requests info/refs, Then the request reaches Forgejo.
  3. Given a browser user agent and no Anubis cookie, When it requests a file view, Then the response contains anubis_challenge.

User Story 3 - CI tools rerun jobs and find runs by commit (Priority: P2)

CI tooling must rerun a failed Actions run or job through the API, and must find runs by commit fast. The component builds a patched Forgejo package for this.

Why this priority: These are operational aids. The forge works without them, but CI recovery is slower.

Independent Test: No check covers the package patches.

Acceptance Scenarios:

  1. Given the patched package, When a client sends POST /jobs/{job_id}/rerun on the run API, Then Forgejo reruns that job.
  2. Given the patched package, When Forgejo starts, Then it creates an index on action_run.commit_sha.

Edge Cases

  • The nixpkgs forgejo version is 17 or higher: package.nix fails the evaluation through an assertion, so the patches get a review.
  • Anubis restarts: the ed25519 key from the anubis-forgejo-key secret stays the same, so the 7-day visitor cookies stay valid.
  • An upstream Anubis DENY rule matches a path in the allow list: the DENY rule wins, because anubis-allow.nix appends its rules after the default rules.
  • Static paths (assets, avatars, repo-avatars, attachments) bypass Anubis and go to :3000 directly.
  • Long requests on / get a proxy_read_timeout of 600s.
  • A restore of the offsite backup: the dump and the copy of the files are from different times, thus the database can refer to a file that is not in the copy. Stop forgejo.service before a restore and start it after both restores.
  • An offline runner stays registered for 168h before cron.cleanup_offline_runners removes it.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare components.forgejo with name = "Forgejo", package = import ./package.nix, and the options of sharedOptions.nix without defaults.subdomain. The deployment states domain as a fully-qualified name: core/setups/groups/dev.nix sets git.fediversity.eu.
  • FR-002: When cfg.enable is false, the implementation MUST return no requests.
  • FR-003: The component MUST request tls.ssl.domains with cfg.domain plus cfg.extraDomains. extraDomains MUST be a list of strings with the default [ ]. The dev group sets it to the sslDomains of git-pages, see git-pages.md.
  • FR-004: The component MUST request web.firewall.publicTCPPorts = [ 80 443 ].
  • FR-005: The component MUST request the secrets anubis-forgejo-key (owner root, mode 400), forgejo-database-password (owner forgejo) and forgejo-email-password (owner forgejo).
  • FR-006: The component MUST request forgejo-smtp from the smtp resource with the sender cfg.sender, and MUST set services.forgejo.settings.mailer from contracts.smtp.results.forgejo.default. sender MUST be a string with no default. The dev group sets it to git@fediversity.eu from the same value as the username of its smarthost.
  • FR-006a: The component MUST serve /.well-known/security.txt (RFC 9116) as an exact nginx location on the virtual host cfg.domain, and /.well-known/pgp-key.txt from cfg.securityKey. It MUST render security.txt with Contact from cfg.securityContact, Encryption as https://<cfg.domain>/.well-known/pgp-key.txt, a fixed Expires, Preferred-Languages: en, Canonical as https://<cfg.domain>/.well-known/security.txt, and Policy from cfg.securityPolicy. securityContact and securityPolicy MUST be strings and securityKey MUST be a path, all with no default. The dev group sets mailto:security@fediversity.eu, the URL of SECURITY.md on the forge, and core/setups/groups/dev-pgp-key.txt. The component MUST give these values to the NixOS module security-txt.nix as data, and that module MUST render security.txt on the node, because a plan holds only data.
  • FR-007: The component MUST import forgejo.nix, backup.nix, anubis.nix and smtp.nix through the nixos-module resource. It MUST NOT import the git-pages module.
  • FR-008: Forgejo MUST listen on 127.0.0.1 with LFS enabled, MAX_RESPONSE_ITEMS = 100, and a mysql database over the local socket that reads its password from the fileSecrets contract.
  • FR-009: MariaDB MUST set innodb_buffer_pool_size = "4G", and journald MUST set SystemMaxUse = "1G".
  • FR-008a: Forgejo MUST take DOMAIN from cfg.domain, and ROOT_URL MUST be https://<DOMAIN>/.
  • FR-010: nginx MUST listen only on the addresses that the network resource gives the node, MUST send / to Anubis on 127.0.0.1:3001 and the static paths to Forgejo on 127.0.0.1:3000, and MUST add the HSTS header.
  • FR-011: nginx MUST keep one access_log at http level that drops the runner RPC requests through the $forgejo_access_log map.
  • FR-012: Anubis instance forgejo MUST bind :3001, target :3000, serve metrics on :9001, load its key through LoadCredential, and keep its store at /var/lib/anubis-forgejo/data.bdb.
  • FR-013: Anubis MUST allow forgejo-machine-paths (raw, media, archive, releases/download, info/refs, info/lfs, git-upload-pack, git-receive-pack) and forgejo-actions-rpc (^/api/actions/) after the default rules.
  • FR-014: package.nix MUST add the rerun API and the xorm:"index" tag on action_run.CommitSHA, and MUST assert that the version is below 17.
  • FR-015: When contracts.s3.results has forgejo-backups, backup.nix MUST request the streamingBackup forgejo.mysql (a mysqldump --single-transaction as root) and the fileBackup forgejo.files (services.forgejo.stateDir as forgejo, without data/repo-archive, data/indexers, data/tmp and log), both through restic into that bucket, with the files at the subpath files. Without that result, backup.nix MUST set nothing.

Key Entities

  • components.forgejo: The component declaration with its implementation = cfg: { ... } requests.
  • Anubis instance forgejo: The proof-of-work gate between nginx and Forgejo.
  • anubis-allow.nix: The list of ALLOW rules for machine paths.
  • Offsite backup: The restic repositories forgejo_mysql and forgejo_files in the forgejo-backups bucket.
  • forgejo machine: The roster entry in core/setups/groups/dev.nix with runsJobs = false. Its host config imports the git-pages module.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: test-forgejo-service passes.
  • SC-002: The dev group evaluates with the forgejo component enabled and the smtp want satisfied by the external-relay provider.
  • SC-003: Access logs contain no runner RPC lines. test-forgejo-service checks this with a runner RPC and an API request.

Assumptions

  • The dev group has no maddy. The smtp request resolves to the external-relay provider, which supplies fediversity.smtp-relay.passwordFile.
  • Contributors reach the forge from the internet, so the ports are public in every environment.

Known Deviations

  • The package carries local patches and a version assertion. A nixpkgs bump to version 17 fails the evaluation on purpose.
  • The component requests no forgejo-backups bucket, thus the offsite backup is off. The dev group has no garage that makes the key, and the forge node does not use OpenBao, where mkOffsiteBackup reads the key and the repository password (#29).