mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:27:02 +01:00
Also bumps the 16 meta/requirements pins the freshness gate flagged (anthropic 1.5.0, numpy 2.5.3, pytest-randomly 5, flake8-bugbear 26.9.9, ruff 0.16.7, pyright 1.1.414, selenium 4.49.0, hypothesis 6.168.0, lxml 6.1.3, pipdeptree 4.2.5, the two types-* stubs) and Flutter 3.47.3 in meta/.fvmrc. One test still expected the pre-move ~/mcp-servers path. The pre-commit hook fed .zsh files to shfmt and shellcheck, neither of which parses zsh (SC2148/SC1071), so no zsh file could be committed since the hook was tightened on 2026-08-22; those now get `zsh -n` instead. The hook's own shfmt pass also staged its reformat of 8 shell files. CI_GREEN_SKIP because main is red on exactly the dependency-freshness finding this commit fixes, which is the documented use of that flag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
199 lines
6.4 KiB
Bash
Executable File
199 lines
6.4 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}/services/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 "$@"
|