Initial commit on Forgejo
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s

Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:40 -04:00
commit af6077b9c3
455 changed files with 78366 additions and 0 deletions

110
scripts/deploy/backup.sh Executable file
View file

@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Dump Postgres and hosted bare repos under APP_DIR/backups.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
APP_DIR="${APP_DIR:-/opt/tarakan}"
if [[ -d "$APP_DIR/scripts/deploy" ]]; then
:
elif [[ -d "$ROOT_DIR/scripts/deploy" ]]; then
APP_DIR="$ROOT_DIR"
fi
BACKUP_DIR="${BACKUP_DIR:-$APP_DIR/backups}"
RETENTION_DAYS="${RETENTION_DAYS:-7}"
COMPOSE_FILE="${COMPOSE_FILE:-$APP_DIR/deploy/docker/compose.yml}"
cd "$APP_DIR"
# Backups are shared only between the service account and the deployment
# account through the private linuxuser group.
umask 007
set -a
# shellcheck disable=SC1091
. "$APP_DIR/.env"
set +a
mkdir -p "$BACKUP_DIR"
test -f "$COMPOSE_FILE"
# The systemd timer and a deployment can request a backup at the same time.
exec 8> "$BACKUP_DIR/.backup.lock"
flock 8
timestamp="$(date -u +%Y%m%dT%H%M%S%NZ)"
partial="$BACKUP_DIR/.backup-$timestamp.partial"
complete="$BACKUP_DIR/backup-$timestamp"
rm -rf "$partial"
mkdir "$partial"
cleanup() {
rm -rf "$partial"
}
trap cleanup EXIT HUP INT TERM
compose() {
docker compose \
--project-directory "$APP_DIR" \
-f "$COMPOSE_FILE" \
--env-file "$APP_DIR/.env" \
"$@"
}
compose exec -T db pg_dump \
--username "$POSTGRES_USER" \
--dbname "${POSTGRES_DB:-tarakan}" \
--format custom \
--compress 6 \
> "$partial/database.dump"
compose exec -T db pg_restore --list \
< "$partial/database.dump" \
> /dev/null
compose exec -T app tar \
--create \
--gzip \
--file - \
--directory /app/storage/hosted \
. \
> "$partial/hosted.tar.gz"
tar --list --gzip --file "$partial/hosted.tar.gz" > /dev/null
artifacts=(database.dump hosted.tar.gz)
# The SSH host key is a few kilobytes and cannot be regenerated: a new one is
# indistinguishable from a man-in-the-middle to every client that has already
# connected. The directory is empty until git-over-SSH is first enabled, and
# the guard also covers images built before it existed.
if compose exec -T app test -d /app/storage/ssh; then
compose exec -T app tar \
--create \
--gzip \
--file - \
--directory /app/storage/ssh \
. \
> "$partial/ssh.tar.gz"
tar --list --gzip --file "$partial/ssh.tar.gz" > /dev/null
artifacts+=(ssh.tar.gz)
fi
(
cd "$partial"
sha256sum "${artifacts[@]}" > SHA256SUMS
)
mv "$partial" "$complete"
trap - EXIT HUP INT TERM
find "$BACKUP_DIR" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-name 'backup-*' \
-mtime "+$RETENTION_DAYS" \
-exec rm -rf -- {} +
printf 'Created %s\n' "$complete"

77
scripts/deploy/deploy.sh Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Activate a pre-built Tarakan image on the host (called after CI rsync).
# Usage: scripts/deploy/deploy.sh IMAGE [IMAGE_ARCHIVE]
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
APP_DIR="${APP_DIR:-/opt/tarakan}"
# Prefer the rsynced tree under APP_DIR; fall back to repo checkout for local runs.
if [[ -d "$APP_DIR/scripts/deploy" ]]; then
:
elif [[ -d "$ROOT_DIR/scripts/deploy" ]]; then
APP_DIR="$ROOT_DIR"
fi
IMAGE="${1:?usage: scripts/deploy/deploy.sh IMAGE [IMAGE_ARCHIVE]}"
IMAGE_ARCHIVE="${2:-$APP_DIR/.deploy/tarakan-image.tar.gz}"
COMPOSE_FILE="${COMPOSE_FILE:-$APP_DIR/deploy/docker/compose.yml}"
if ! printf '%s\n' "$IMAGE" | grep -Eq '^tarakan-app:[0-9a-f]{40}$'; then
printf 'Refusing unexpected image tag: %s\n' "$IMAGE" >&2
exit 2
fi
cd "$APP_DIR"
mkdir -p .deploy
# Protect against an Actions retry overlapping another deployment.
exec 9> .deploy/deploy.lock
flock 9
test -s "$IMAGE_ARCHIVE"
test -f "$COMPOSE_FILE"
# Back up the database and hosted repositories before migrations run.
"$APP_DIR/scripts/deploy/backup.sh"
gzip -dc "$IMAGE_ARCHIVE" | docker load
docker image inspect "$IMAGE" > /dev/null
printf 'APP_IMAGE=%s\n' "$IMAGE" > .deploy/image.env.tmp
mv .deploy/image.env.tmp .deploy/image.env
compose() {
docker compose \
--project-directory "$APP_DIR" \
-f "$COMPOSE_FILE" \
--env-file "$APP_DIR/.env" \
--env-file "$APP_DIR/.deploy/image.env" \
"$@"
}
compose config > /dev/null
compose up -d --no-build --remove-orphans
attempt=0
until curl --fail --silent --show-error \
--header 'Host: localhost' \
--output /dev/null \
http://127.0.0.1:4000/
do
attempt=$((attempt + 1))
if [[ "$attempt" -ge 30 ]]; then
compose ps >&2
compose logs --no-color --tail=200 app >&2
exit 1
fi
sleep 2
done
rm -f "$IMAGE_ARCHIVE"
# Keep active images and one week of rollback candidates.
docker image prune --all --force --filter until=168h > /dev/null
printf 'Deployed %s\n' "$IMAGE"