Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,11 @@ services:
- gittensory-backups:/backups
- ./scripts/backup.sh:/backup.sh:ro
- ./scripts/verify-backup.sh:/verify-backup.sh:ro
# Shared url_decode/pgpass_escape helpers (#2910), sourced by both scripts above via
# `. "$(dirname "$0")/selfhost-pg-url.sh"` -- must be mounted at container root alongside them so that
# dirname-relative resolution finds it at /selfhost-pg-url.sh, the same way it finds
# scripts/selfhost-pg-url.sh when either script is run directly from a repo checkout.
- ./scripts/selfhost-pg-url.sh:/selfhost-pg-url.sh:ro
# `docker compose run --rm backup sh /backup.sh` (or /verify-backup.sh) REPLACES `command:`, not
# `entrypoint:`, so the package install must live in the entrypoint or an on-demand run gets a bare
# container with no pg_restore/sqlite3/psql. The entrypoint installs packages once, then `exec "$@"`
Expand Down
36 changes: 10 additions & 26 deletions scripts/backup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# Backups land in the `gittensory-backups` volume at /backups/{postgres,sqlite,qdrant}.
set -eu

# shellcheck source=selfhost-pg-url.sh
. "$(dirname "$0")/selfhost-pg-url.sh"

normalize_backup_retain() {
retain_value=$1
case "$retain_value" in
Expand Down Expand Up @@ -54,32 +57,6 @@ mkdir -p "$OUT/postgres" "$OUT/sqlite" "$OUT/qdrant"
# (never delete the last good backup) and still exit non-zero at the end (fail loudly).
SQLITE_BACKUP_FAILED=0

# Percent-decodes a URI userinfo component (RFC 3986). Deliberately does NOT treat '+' as a space -- that
# convention is specific to application/x-www-form-urlencoded query values, not URI userinfo, where '+' is
# an ordinary sub-delims character allowed unencoded; the only caller of this function decodes a password
# extracted from the userinfo section, and a literal '+' there must stay a '+', not become a space.
url_decode() {
printf '%s' "$1" | awk '
BEGIN { for (i = 0; i < 256; i++) hex[sprintf("%02X", i)] = sprintf("%c", i); }
{
out = "";
for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1);
if (c == "%" && i + 2 <= length($0)) {
h = toupper(substr($0, i + 1, 2));
if (h in hex) { out = out hex[h]; i += 2; } else { out = out c; }
} else {
out = out c;
}
}
printf "%s", out;
}'
}

pgpass_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/:/\\:/g'
}

json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
Expand Down Expand Up @@ -136,6 +113,13 @@ write_manifest() {
# this script makes, and is deleted immediately after via the `cleanup` trap, so there's no scoped value
# in re-deriving the exact host/port/dbname libpq will resolve -- which the query string can override
# anyway -- just to match them precisely).
#
# NOT extracted alongside url_decode/pgpass_escape into selfhost-pg-url.sh (#2910): verify-backup.sh's
# pg_connect_arg() shares this exact URI-parsing algorithm, but the two are not safe to collapse into one
# function -- this one reads $PG_DB from a global and runs once per invocation; pg_connect_arg() takes the
# URL as an argument, unsets PGPASSFILE at the top of every call (a reentrancy guard this script doesn't
# need, since it only ever connects once), and tracks a LIST of created passfiles instead of one. See
# verify-backup.sh's pg_connect_arg for the full rationale.
prepare_pg_env() {
pg_rest=${PG_DB#postgres://}
pg_rest=${pg_rest#postgresql://}
Expand Down
94 changes: 3 additions & 91 deletions scripts/deploy-selfhost-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,97 +17,9 @@ SERVICE="${SELFHOST_SERVICE:-gittensory}"
HEALTH_TIMEOUT_SECONDS="${SELFHOST_HEALTH_TIMEOUT_SECONDS:-180}"
DEFAULT_IMAGE="ghcr.io/jsonbored/gittensory-selfhost:latest"

require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: required command not found: $1" >&2
exit 1
fi
}

env_get() {
local key="$1"
local file="${2:-$ENV_FILE}"

[ -f "$file" ] || return 1

awk -v key="$key" '
/^[[:space:]]*(#|$)/ { next }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line !~ "^" key "[[:space:]]*=") {
next
}
sub(/^[^=]*=/, "", line)
sub(/^[[:space:]]*/, "", line)
sub(/[[:space:]]*$/, "", line)
if (length(line) >= 2) {
first = substr(line, 1, 1)
last = substr(line, length(line), 1)
if ((first == "\"" && last == "\"") || (first == "'\''" && last == "'\''")) {
line = substr(line, 2, length(line) - 2)
}
}
print line
found = 1
exit
}
END { exit found ? 0 : 1 }
' "$file"
}

env_put() {
local key="$1"
local value="$2"
local file="${3:-$ENV_FILE}"
local dir base tmp

touch "$file"
dir="$(dirname "$file")"
base="$(basename "$file")"
tmp="$(mktemp "$dir/.${base}.tmp.XXXXXX")"
awk -v key="$key" -v value="$value" '
BEGIN { written = 0 }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line ~ "^" key "[[:space:]]*=") {
print key "=" value
written = 1
} else {
print $0
}
}
END {
if (!written) {
print key "=" value
}
}
' "$file" >"$tmp"
cat "$tmp" >"$file"
rm -f "$tmp"
}

compose_file_args() {
local files=()
local file

if [ -n "${SELFHOST_COMPOSE_FILES:-}" ]; then
# shellcheck disable=SC2206
files=(${SELFHOST_COMPOSE_FILES})
else
files=(docker-compose.yml)
[ -f docker-compose.override.yml ] && files+=(docker-compose.override.yml)
fi

for file in "${files[@]}"; do
if [ ! -f "$file" ]; then
echo "error: compose file not found: $file" >&2
exit 1
fi
printf '%s\n' -f "$file"
done
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/selfhost-deploy-common.sh
. "$SCRIPT_DIR/lib/selfhost-deploy-common.sh"

resolve_image() {
local env_file_image
Expand Down
92 changes: 3 additions & 89 deletions scripts/deploy-selfhost-prebuilt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,95 +16,9 @@ SERVICE="${SELFHOST_SERVICE:-gittensory}"
SKIP_SENTRY_UPLOAD="${SELFHOST_SKIP_SENTRY_UPLOAD:-0}"
SENTRY_CLI_PACKAGE="${SENTRY_CLI_PACKAGE:-@sentry/cli@3.6.0}"

require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: required command not found: $1" >&2
exit 1
fi
}

env_get() {
local key="$1"
local file="${2:-$ENV_FILE}"

[ -f "$file" ] || return 1

awk -v key="$key" '
/^[[:space:]]*(#|$)/ { next }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line !~ "^" key "[[:space:]]*=") {
next
}
sub(/^[^=]*=/, "", line)
sub(/^[[:space:]]*/, "", line)
sub(/[[:space:]]*$/, "", line)
if (length(line) >= 2) {
first = substr(line, 1, 1)
last = substr(line, length(line), 1)
if ((first == "\"" && last == "\"") || (first == "'\''" && last == "'\''")) {
line = substr(line, 2, length(line) - 2)
}
}
print line
found = 1
exit
}
END { exit found ? 0 : 1 }
' "$file"
}

env_put() {
local key="$1"
local value="$2"
local file="${3:-$ENV_FILE}"
local tmp

touch "$file"
tmp="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { written = 0 }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line ~ "^" key "[[:space:]]*=") {
print key "=" value
written = 1
} else {
print $0
}
}
END {
if (!written) {
print key "=" value
}
}
' "$file" >"$tmp"
cat "$tmp" >"$file"
rm -f "$tmp"
}

compose_file_args() {
local files=()
local file

if [ -n "${SELFHOST_COMPOSE_FILES:-}" ]; then
# shellcheck disable=SC2206
files=(${SELFHOST_COMPOSE_FILES})
else
files=(docker-compose.yml)
[ -f docker-compose.override.yml ] && files+=(docker-compose.override.yml)
fi

for file in "${files[@]}"; do
if [ ! -f "$file" ]; then
echo "error: compose file not found: $file" >&2
exit 1
fi
printf '%s\n' -f "$file"
done
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/selfhost-deploy-common.sh
. "$SCRIPT_DIR/lib/selfhost-deploy-common.sh"

run_node_build() {
local uid gid
Expand Down
101 changes: 101 additions & 0 deletions scripts/lib/selfhost-deploy-common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Shared helpers for the self-host deploy scripts (deploy-selfhost-image.sh, deploy-selfhost-prebuilt.sh).
# Sourced, not executed: this file has no shebang-driven side effects and defines functions only.
# Both callers set ENV_FILE before sourcing this; env_get/env_put fall back to it when no file arg is given.

require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: required command not found: $1" >&2
exit 1
fi
}

env_get() {
local key="$1"
local file="${2:-$ENV_FILE}"

[ -f "$file" ] || return 1

awk -v key="$key" '
/^[[:space:]]*(#|$)/ { next }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line !~ "^" key "[[:space:]]*=") {
next
}
sub(/^[^=]*=/, "", line)
sub(/^[[:space:]]*/, "", line)
sub(/[[:space:]]*$/, "", line)
if (length(line) >= 2) {
first = substr(line, 1, 1)
last = substr(line, length(line), 1)
if ((first == "\"" && last == "\"") || (first == "'\''" && last == "'\''")) {
line = substr(line, 2, length(line) - 2)
}
}
print line
found = 1
exit
}
END { exit found ? 0 : 1 }
' "$file"
}

# Same-directory temp file (not the system tmpdir): guarantees `cat "$tmp" >"$file"` never crosses a
# filesystem boundary, which a plain `mktemp` could when $ENV_FILE lives on a different mount than the
# default tmp directory (#2910 -- this was previously only true for deploy-selfhost-image.sh's copy of
# this function; deploy-selfhost-prebuilt.sh's copy used a plain `mktemp` with no documented reason for
# the difference, so consolidating adopts the more defensive behavior for both callers).
env_put() {
local key="$1"
local value="$2"
local file="${3:-$ENV_FILE}"
local dir base tmp

touch "$file"
dir="$(dirname "$file")"
base="$(basename "$file")"
tmp="$(mktemp "$dir/.${base}.tmp.XXXXXX")"
awk -v key="$key" -v value="$value" '
BEGIN { written = 0 }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line ~ "^" key "[[:space:]]*=") {
print key "=" value
written = 1
} else {
print $0
}
}
END {
if (!written) {
print key "=" value
}
}
' "$file" >"$tmp"
cat "$tmp" >"$file"
rm -f "$tmp"
}

compose_file_args() {
local files=()
local file

if [ -n "${SELFHOST_COMPOSE_FILES:-}" ]; then
# shellcheck disable=SC2206
files=(${SELFHOST_COMPOSE_FILES})
else
files=(docker-compose.yml)
[ -f docker-compose.override.yml ] && files+=(docker-compose.override.yml)
fi

for file in "${files[@]}"; do
if [ ! -f "$file" ]; then
echo "error: compose file not found: $file" >&2
exit 1
fi
printf '%s\n' -f "$file"
done
}
Loading
Loading