# Forgejo (self-hosted Git + CI/CD) as a Docker Swarm stack — the only workload
# this Swarm cluster runs (see deploy/swarm/README.md). Deliberately separate
# from the Terraform-managed docker-compose.yml that runs the Thermograph app
# itself: this stack's only job is Forgejo and its container registry.
#
# The Actions RUNNER is deliberately NOT a service in this stack. Per
# thermograph-docs/runbooks/implementation-handoff.md (Track B step 5), the canonical
# design registers the runner on the desktop node as a plain systemd service
# (deploy/forgejo/register-lan-runner.sh) — the same place the pre-Forgejo
# GitHub self-hosted runner already lived, not a Swarm-scheduled container.
#
# No Traefik here. Forgejo is pinned to vps1 (node.labels.role == forge)
# because that's what was chosen, and vps1 is ALSO the emigriffith.dev portfolio
# host — Caddy already owns its ports 80/443 (see /etc/caddy/Caddyfile on that
# box). A second reverse proxy binding those same ports would either fail to
# start or fight Caddy. Instead: forgejo's web port publishes to
# 127.0.0.1:3080 only (host-local, mode: host), and vps1's existing Caddy gets
# a new site block reverse-proxying git.thermograph.org -> 127.0.0.1:3080,
# same pattern as its other blocks. TLS is Caddy's existing automatic-HTTPS
# (HTTP-01), not a second ACME flow.
#
# Deploy from the manager node (vps2, which runs prod), after all three nodes
# (vps1, vps2, desktop) have joined the swarm and vps1 is labeled role=forge:
#
#   docker stack deploy -c deploy/forgejo/docker-stack.yml forgejo
#
# Requires this Swarm secret to exist first (see deploy/forgejo/README.md):
#   forgejo_db_password
#
# Registry exposure (hazard #15 in thermograph-docs/runbooks/hop1-forgejo-registry-cutover.md):
# resolved as the runbook's first listed option — serve Git/UI publicly but
# firewall the /v2/ registry API paths so only WireGuard-mesh clients can
# reach them. That's implemented in the Caddy site block (see
# deploy/forgejo/README.md), not here — nothing in this stack file is
# registry-specific, the restriction lives entirely in Caddy's config on vps1.

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: forgejo
      POSTGRES_DB: forgejo
      POSTGRES_PASSWORD_FILE: /run/secrets/forgejo_db_password
    secrets: [forgejo_db_password]
    volumes:
      - forgejo_db:/var/lib/postgresql/data
    networks: [forgejo_net]
    deploy:
      placement:
        constraints: [node.labels.role == forge]
      resources:
        limits:
          cpus: "${FORGEJO_DB_CPUS:-1}"
          memory: ${FORGEJO_DB_MEMORY:-1g}
      restart_policy:
        condition: on-failure

  forgejo:
    image: codeberg.org/forgejo/forgejo:9-rootless
    depends_on: [db]
    environment:
      FORGEJO__database__DB_TYPE: postgres
      FORGEJO__database__HOST: db:5432
      FORGEJO__database__NAME: forgejo
      FORGEJO__database__USER: forgejo
      FORGEJO__server__DOMAIN: "${FORGEJO_DOMAIN:-git.thermograph.org}"
      FORGEJO__server__ROOT_URL: "https://${FORGEJO_DOMAIN:-git.thermograph.org}/"
      FORGEJO__server__SSH_PORT: "2222"
      # Without this, Forgejo starts no SSH server at all despite SSH_PORT
      # being set — git@ clones get "connection refused", not a slow failure.
      FORGEJO__server__START_SSH_SERVER: "true"
      # Config is fully supplied via env, so lock the install wizard rather
      # than leave it open on the public internet waiting for someone to
      # complete it first (DB config alone does NOT imply this).
      FORGEJO__security__INSTALL_LOCK: "true"
      # Actions on: repo/org/user-level runners register against this instance.
      FORGEJO__actions__ENABLED: "true"
      # --- Access model: OAuth-only login, multi-user via an approval gate ---
      # Login is Google SSO only. Forgejo 9.0.3 has no setting to hide the
      # password sign-in form, so "OAuth-only" is enforced operationally: every
      # account is given an unusable (random) password and no NEW local account
      # can be created (ALLOW_ONLY_EXTERNAL_REGISTRATION). ENABLE_PASSWORD_SIGNIN_FORM
      # is set anyway — ignored on 9.0.3, effective if the instance is upgraded.
      # Auto-registration stays ON so a Google login auto-links (by verified email)
      # to a PRE-CREATED account (`forgejo admin user create --username X --email
      # X@gmail.com --random-password`). A single required-claim-value can't
      # allowlist more than one email, so instead of pinning one address the
      # required-claim-value is CLEARED (a DB change on the auth source, persisted
      # in the forgejo_data volume — not re-applied from here) and every NEW signup
      # is gated: a stranger who signs in with Google lands INACTIVE
      # (REGISTER_MANUAL_CONFIRM) and RESTRICTED (DEFAULT_USER_IS_RESTRICTED)
      # pending admin approval, so an un-provisioned Google user gets zero access.
      FORGEJO__oauth2_client__ACCOUNT_LINKING: "auto"
      FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
      FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true"
      FORGEJO__service__ENABLE_PASSWORD_SIGNIN_FORM: "false"
      FORGEJO__service__REGISTER_MANUAL_CONFIRM: "true"
      FORGEJO__service__DEFAULT_USER_IS_RESTRICTED: "true"
      # Outbound mail via vps2's Postfix null client (prod's box) over the
      # WireGuard mesh (10.10.0.1:25 — mynetworks permits vps1, 10.10.0.2,
      # where this Forgejo runs); self-signed cert on :25, so trust it.
      # Send-only; used for admin/approval and notification mail.
      FORGEJO__mailer__ENABLED: "true"
      FORGEJO__mailer__PROTOCOL: "smtp"
      FORGEJO__mailer__SMTP_ADDR: "10.10.0.1"
      FORGEJO__mailer__SMTP_PORT: "25"
      FORGEJO__mailer__FORCE_TRUST_SERVER_CERT: "true"
      FORGEJO__mailer__FROM: "Thermograph Git <no-reply@thermograph.org>"
      # Gitea/Forgejo's app.ini env-mapping honors a __FILE suffix to read a
      # value from a file instead of the literal env var — same convention as
      # the official Postgres image's POSTGRES_PASSWORD_FILE above.
      FORGEJO__database__PASSWD__FILE: /run/secrets/forgejo_db_password
    secrets:
      - source: forgejo_db_password
        target: forgejo_db_password
    volumes:
      - forgejo_data:/var/lib/gitea
    networks: [forgejo_net]
    ports:
      # SSH for git@ clones — published on whichever node the task lands on
      # (pinned to vps1 by the placement constraint below, so effectively
      # always vps1's public IP, port 2222).
      - target: 2222
        published: 2222
        protocol: tcp
        mode: host
      # Web UI/API. Swarm's port schema has no host_ip scoping, so this binds
      # 0.0.0.0:3080 on vps1 — NOT actually localhost-only by itself. A
      # DOCKER-USER iptables rule (see deploy/forgejo/README.md) is what
      # actually restricts it, since Docker's own iptables rules bypass ufw
      # for published ports. vps1's Caddy reverse-proxies to 127.0.0.1:3080.
      - target: 3000
        published: 3080
        protocol: tcp
        mode: host
    deploy:
      placement:
        constraints: [node.labels.role == forge]
      resources:
        limits:
          cpus: "${FORGEJO_CPUS:-2}"
          memory: ${FORGEJO_MEMORY:-2g}
      restart_policy:
        condition: on-failure

networks:
  forgejo_net:
    driver: overlay
    attachable: false

volumes:
  forgejo_db:
  forgejo_data:

secrets:
  forgejo_db_password:
    external: true
