testsAndMisc/linux_configuration/features/setup_gitea.sh
Krzysztof kuhy Rudnicki 4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:53:30 +02:00

199 lines
6.3 KiB
Bash
Executable File

#!/bin/bash
# Self-hosted Gitea git server, mirroring GitHub repos, exposed publicly via
# DuckDNS + Caddy automatic HTTPS.
#
# Companion to setup_wireguard_ssh.sh, which owns /etc/nftables.conf and
# provides the 'allow-web' firewall rule this script depends on.
#
# DNS note: kuhy.duckdns.org is kept updated by an existing cron job installed
# by install_joplin.sh (~/.joplin-server/duckdns-update.sh) -- this script
# does not manage DuckDNS itself.
#
# Networking note: Gitea and Caddy run with `network_mode: host`, not a
# Docker bridge network. This host's custom nftables ruleset has a
# default-drop FORWARD chain with no rules; Docker's own forwarding rules
# (separate iptables-legacy tables) don't override it, since both hook the
# same netfilter point and a DROP verdict from either is terminal. That
# silently blocks all bridge-networked container egress -- breaking both
# Caddy's ACME renewal and Gitea's GitHub pull-mirroring. Host networking
# sidesteps the FORWARD chain entirely (uses INPUT/OUTPUT only, both already
# permissive) without touching the shared firewall script. Gitea binds to
# 127.0.0.1:3000 only -- never exposed directly, only reachable via Caddy.
#
# Usage:
# ./setup_gitea.sh setup - full first-time setup (idempotent, re-runnable)
# ./setup_gitea.sh status - show container + certificate status
# ./setup_gitea.sh help
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
readonly GITEA_DOMAIN="gitea.kuhy.duckdns.org"
readonly GITEA_ADMIN_USER="kuhyx"
readonly GITEA_ADMIN_EMAIL="krzysztofrudnicki0@gmail.com"
readonly GITEA_DATA_DIR="${HOME}/gitea"
readonly COMPOSE_FILE="${GITEA_DATA_DIR}/docker-compose.yml"
readonly CADDYFILE="${GITEA_DATA_DIR}/Caddyfile"
# Per-service Caddy site blocks live here as individual *.caddy snippets so
# adding/removing a service never clobbers another's block (see the root
# Caddyfile's `import` below). Every service on this edge owns one file.
readonly SITES_DIR="${GITEA_DATA_DIR}/sites"
readonly ADMIN_PASSWORD_FILE="${GITEA_DATA_DIR}/.admin_password"
readonly ADMIN_TOKEN_FILE="${GITEA_DATA_DIR}/.admin_token"
readonly WIREGUARD_SCRIPT="${SCRIPT_DIR}/setup_wireguard_ssh.sh"
die() {
log_error "$1"
exit 1
}
write_compose_files() {
ensure_dir "$GITEA_DATA_DIR"
cat >"$COMPOSE_FILE" <<EOF
services:
gitea:
image: gitea/gitea:1.22.3
container_name: gitea
restart: unless-stopped
network_mode: host
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=${GITEA_DOMAIN}
- GITEA__server__ROOT_URL=https://${GITEA_DOMAIN}/
- GITEA__server__HTTP_ADDR=127.0.0.1
- GITEA__server__HTTP_PORT=3000
- GITEA__server__DISABLE_SSH=true
- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__service__REGISTER_EMAIL_CONFIRM=false
- GITEA__security__INSTALL_LOCK=true
- GITEA__mirror__ENABLED=true
- GITEA__mirror__DEFAULT_INTERVAL=10m
- GITEA__mirror__MIN_INTERVAL=10m
volumes:
- ./gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
caddy:
image: caddy:2.8
container_name: gitea-caddy
restart: unless-stopped
network_mode: host
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./sites:/etc/caddy/sites:ro
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:
EOF
# The root Caddyfile only imports per-service snippets; every service
# (gitea, dufs, syncyomi, the personal website, ...) owns its own file in
# sites/ so re-running any one setup script never drops another's block.
ensure_dir "$SITES_DIR"
cat >"$CADDYFILE" <<'EOF'
# Managed by setup_gitea.sh — do not edit by hand.
# Each service on this edge owns one snippet in sites/*.caddy.
import /etc/caddy/sites/*.caddy
EOF
cat >"${SITES_DIR}/gitea.caddy" <<EOF
# Managed by setup_gitea.sh — do not edit by hand.
${GITEA_DOMAIN} {
reverse_proxy 127.0.0.1:3000
}
EOF
log_ok "Wrote ${COMPOSE_FILE}, ${CADDYFILE}, and ${SITES_DIR}/gitea.caddy."
}
start_containers() {
docker compose -f "$COMPOSE_FILE" up -d
}
wait_for_gitea() {
log_info "Waiting for Gitea to become ready..."
local attempt logs
for ((attempt = 1; attempt <= 60; attempt++)); do
# Capture into a variable rather than piping straight into
# `grep -q`: grep quits at the first match, SIGPIPE-ing `docker
# logs` before it finishes writing, and under `pipefail` that
# upstream non-zero exit clobbers grep's own success -- the `if`
# never sees a match even though one is right there in the log.
logs=$(docker logs gitea 2>&1)
if grep -q "Listen: http" <<<"$logs"; then
log_ok "Gitea is ready."
return 0
fi
sleep 2
done
die "Gitea did not become ready within 2 minutes -- check 'docker logs gitea'."
}
bootstrap_admin() {
if docker exec -u git gitea gitea admin user list 2>/dev/null | grep -q "$GITEA_ADMIN_USER"; then
log_info "Admin user '${GITEA_ADMIN_USER}' already exists -- not recreating."
return 0
fi
umask 077
openssl rand -base64 24 >"$ADMIN_PASSWORD_FILE"
chmod 600 "$ADMIN_PASSWORD_FILE"
docker exec -u git gitea gitea admin user create \
--username "$GITEA_ADMIN_USER" --password "$(cat "$ADMIN_PASSWORD_FILE")" \
--email "$GITEA_ADMIN_EMAIL" --admin --must-change-password=false
log_ok "Created admin user '${GITEA_ADMIN_USER}'. Password saved to ${ADMIN_PASSWORD_FILE} (chmod 600)."
}
mint_api_token() {
if [[ -f $ADMIN_TOKEN_FILE ]]; then
log_info "API token already exists at ${ADMIN_TOKEN_FILE} -- not regenerating."
return 0
fi
umask 077
docker exec -u git gitea gitea admin user generate-access-token \
--username "$GITEA_ADMIN_USER" --token-name automation --scopes all --raw >"$ADMIN_TOKEN_FILE"
chmod 600 "$ADMIN_TOKEN_FILE"
log_ok "API token saved to ${ADMIN_TOKEN_FILE} (chmod 600)."
}
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/gitea_network.sh
source "$SCRIPT_DIR/lib/gitea_network.sh"
main() {
local cmd="${1:-help}"
case "$cmd" in
setup)
write_compose_files
start_containers
wait_for_gitea
bootstrap_admin
mint_api_token
open_firewall
attempt_upnp
print_router_instructions
log_ok "Gitea setup complete."
;;
status)
status_cmd
;;
help | -h | --help)
usage
;;
*)
log_error "Unknown command: $cmd"
usage
exit 1
;;
esac
}
main "$@"