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:
- Given the
forgejomachine of thedevgroup with the component enabled, When the node deploys, Then Forgejo listens on127.0.0.1:3000and nginx serveshttps://<cfg.domain>/(git.fediversity.euin thedevgroup). - Given the component, When the
sslresource resolves, Then the certificate coverscfg.domainand each name incfg.extraDomains. In thedevgroup, these are the git-pages sites. - Given the
smtpcontract result forforgejo, When Forgejo sends a notification, Then it usessmtp+starttlswith the host, port and user of that result, and the sendercfg.sender(git@fediversity.euin thedevgroup). - Given the component, When a scanner requests
https://<cfg.domain>/.well-known/security.txt, Then nginx serves it with no Anubis challenge, withContactfromcfg.securityContact,Policyfromcfg.securityPolicy, andEncryptionandCanonicalbelowhttps://<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:
- Given a browser user agent and no Anubis cookie, When it requests a
/raw/path, Then the request reaches Forgejo. - Given a browser user agent and no Anubis cookie, When it requests
info/refs, Then the request reaches Forgejo. - 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:
- Given the patched package, When a client sends
POST /jobs/{job_id}/rerunon the run API, Then Forgejo reruns that job. - Given the patched package, When Forgejo starts, Then it creates an index on
action_run.commit_sha.
Edge Cases
- The nixpkgs
forgejoversion is 17 or higher:package.nixfails the evaluation through an assertion, so the patches get a review. - Anubis restarts: the ed25519 key from the
anubis-forgejo-keysecret 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.nixappends its rules after the default rules. - Static paths (
assets,avatars,repo-avatars,attachments) bypass Anubis and go to:3000directly. - Long requests on
/get aproxy_read_timeoutof 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.servicebefore a restore and start it after both restores. - An offline runner stays registered for
168hbeforecron.cleanup_offline_runnersremoves it.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
components.forgejowithname = "Forgejo",package = import ./package.nix, and the options ofsharedOptions.nixwithoutdefaults.subdomain. The deployment statesdomainas a fully-qualified name:core/setups/groups/dev.nixsetsgit.fediversity.eu. - FR-002: When
cfg.enableis false, theimplementationMUST return no requests. - FR-003: The component MUST request
tls.ssl.domainswithcfg.domainpluscfg.extraDomains.extraDomainsMUST be a list of strings with the default[ ]. Thedevgroup sets it to thesslDomainsof git-pages, seegit-pages.md. - FR-004: The component MUST request
web.firewall.publicTCPPorts = [ 80 443 ]. - FR-005: The component MUST request the
secretsanubis-forgejo-key(ownerroot, mode400),forgejo-database-password(ownerforgejo) andforgejo-email-password(ownerforgejo). - FR-006: The component MUST request
forgejo-smtpfrom thesmtpresource with the sendercfg.sender, and MUST setservices.forgejo.settings.mailerfromcontracts.smtp.results.forgejo.default.senderMUST be a string with no default. Thedevgroup sets it togit@fediversity.eufrom the same value as theusernameof its smarthost. - FR-006a: The component MUST serve
/.well-known/security.txt(RFC 9116) as an exact nginx location on the virtual hostcfg.domain, and/.well-known/pgp-key.txtfromcfg.securityKey. It MUST rendersecurity.txtwithContactfromcfg.securityContact,Encryptionashttps://<cfg.domain>/.well-known/pgp-key.txt, a fixedExpires,Preferred-Languages: en,Canonicalashttps://<cfg.domain>/.well-known/security.txt, andPolicyfromcfg.securityPolicy.securityContactandsecurityPolicyMUST be strings andsecurityKeyMUST be a path, all with no default. Thedevgroup setsmailto:security@fediversity.eu, the URL ofSECURITY.mdon the forge, andcore/setups/groups/dev-pgp-key.txt. The component MUST give these values to the NixOS modulesecurity-txt.nixas data, and that module MUST rendersecurity.txton the node, because a plan holds only data. - FR-007: The component MUST import
forgejo.nix,backup.nix,anubis.nixandsmtp.nixthrough thenixos-moduleresource. It MUST NOT import the git-pages module. - FR-008: Forgejo MUST listen on
127.0.0.1with LFS enabled,MAX_RESPONSE_ITEMS = 100, and amysqldatabase over the local socket that reads its password from thefileSecretscontract. - FR-009: MariaDB MUST set
innodb_buffer_pool_size = "4G", and journald MUST setSystemMaxUse = "1G". - FR-008a: Forgejo MUST take
DOMAINfromcfg.domain, andROOT_URLMUST behttps://<DOMAIN>/. - FR-010: nginx MUST listen only on the addresses that the
networkresource gives the node, MUST send/to Anubis on127.0.0.1:3001and the static paths to Forgejo on127.0.0.1:3000, and MUST add the HSTS header. - FR-011: nginx MUST keep one
access_logathttplevel that drops the runner RPC requests through the$forgejo_access_logmap. - FR-012: Anubis instance
forgejoMUST bind:3001, target:3000, serve metrics on:9001, load its key throughLoadCredential, 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) andforgejo-actions-rpc(^/api/actions/) after the default rules. - FR-014:
package.nixMUST add the rerun API and thexorm:"index"tag onaction_run.CommitSHA, and MUST assert that the version is below 17. - FR-015: When
contracts.s3.resultshasforgejo-backups,backup.nixMUST request thestreamingBackupforgejo.mysql(amysqldump --single-transactionasroot) and thefileBackupforgejo.files(services.forgejo.stateDirasforgejo, withoutdata/repo-archive,data/indexers,data/tmpandlog), both through restic into that bucket, with the files at the subpathfiles. Without that result,backup.nixMUST set nothing.
Key Entities
components.forgejo: The component declaration with itsimplementation = 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_mysqlandforgejo_filesin theforgejo-backupsbucket. forgejomachine: The roster entry incore/setups/groups/dev.nixwithrunsJobs = false. Its host config imports the git-pages module.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
test-forgejo-servicepasses. - SC-002: The
devgroup evaluates with theforgejocomponent enabled and thesmtpwant satisfied by theexternal-relayprovider. - SC-003: Access logs contain no runner RPC lines.
test-forgejo-servicechecks this with a runner RPC and an API request.
Assumptions
- The
devgroup has no maddy. Thesmtprequest resolves to theexternal-relayprovider, which suppliesfediversity.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-backupsbucket, thus the offsite backup is off. The dev group has no garage that makes the key, and the forge node does not use OpenBao, wheremkOffsiteBackupreads the key and the repository password (#29).