From 5d6a28e2b29dac1d6ebe146e9f3c6267c4852c68 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:36:16 +0000 Subject: [PATCH 1/3] chore(host): add the host's preview and compose scripts as installed Copied unchanged from /usr/local/sbin and /etc/systemd/system on the TF server, so the next commit shows what changes. Co-Authored-By: Claude Opus 5.5 --- scripts/ps-compose.sh | 49 +++++ scripts/ps-image-prune.service | 10 + scripts/ps-image-prune.sh | 7 + scripts/ps-image-prune.timer | 10 + scripts/ps-preview-reap.service | 13 ++ scripts/ps-preview-reap.timer | 10 + scripts/ps-preview-router.Caddyfile | 26 +++ scripts/ps-preview-ssh.sh | 8 + scripts/ps-preview.sh | 285 ++++++++++++++++++++++++++++ 9 files changed, 418 insertions(+) create mode 100755 scripts/ps-compose.sh create mode 100644 scripts/ps-image-prune.service create mode 100755 scripts/ps-image-prune.sh create mode 100644 scripts/ps-image-prune.timer create mode 100644 scripts/ps-preview-reap.service create mode 100644 scripts/ps-preview-reap.timer create mode 100644 scripts/ps-preview-router.Caddyfile create mode 100755 scripts/ps-preview-ssh.sh create mode 100755 scripts/ps-preview.sh diff --git a/scripts/ps-compose.sh b/scripts/ps-compose.sh new file mode 100755 index 00000000..9a3bea92 --- /dev/null +++ b/scripts/ps-compose.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Limited docker compose for the ProvinceSystem main and dev sites. +# Runs as tfmc (a docker-group user) via sudo. Ryan is not in the docker group. +set -euo pipefail + +usage() { + echo "usage: ps-compose [args...]" >&2 + exit 1 +} + +[[ $# -ge 2 ]] || usage +target="$1" +shift +cmd="$1" +shift + +case "$target" in + main) + dir=/home/tfmc/ProvinceSystem + files=(-f "$dir/docker-compose.yml" -f "$dir/docker-compose.override.yml") + ;; + dev) + dir=/home/tfmc/tfmc-staging + files=(-f "$dir/docker-compose.staging.yml" -f "$dir/docker-compose.override.yml") + ;; + *) + usage + ;; +esac + +case "$cmd" in + ps|logs|start|stop|restart|up|build|pull|exec|top) ;; + *) + echo "subcommand not allowed: $cmd" >&2 + usage + ;; +esac + +for arg in "$@"; do + case "$arg" in + -f|--file|--file=*|-H|--host|--host=*|--context|--context=*|--project-directory|--project-directory=*|--project-name|--project-name=*|--env-file|--env-file=*|--privileged|--privileged=*|-v|--volume|--volume=*|--mount|--mount=*) + echo "argument not allowed: $arg" >&2 + exit 1 + ;; + esac +done + +cd "$dir" +exec docker compose --project-directory "$dir" "${files[@]}" "$cmd" "$@" diff --git a/scripts/ps-image-prune.service b/scripts/ps-image-prune.service new file mode 100644 index 00000000..96bdc065 --- /dev/null +++ b/scripts/ps-image-prune.service @@ -0,0 +1,10 @@ +[Unit] +Description=Remove untagged ProvinceSystem website builds and week-old build cache +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +Nice=10 +IOSchedulingClass=idle +ExecStart=/usr/local/sbin/ps-image-prune diff --git a/scripts/ps-image-prune.sh b/scripts/ps-image-prune.sh new file mode 100755 index 00000000..5d4e27f4 --- /dev/null +++ b/scripts/ps-image-prune.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Untagged local builds of the main and dev sites, plus build cache unused for a week. +# Tagged images used by the running containers are left alone. Preview images are handled by ps-preview. +set -euo pipefail +docker image prune -f --filter "label=com.docker.compose.project=provincesystem" +docker image prune -f --filter "label=com.docker.compose.project=tfmc-staging" +docker builder prune -f --filter "until=168h" diff --git a/scripts/ps-image-prune.timer b/scripts/ps-image-prune.timer new file mode 100644 index 00000000..347031f2 --- /dev/null +++ b/scripts/ps-image-prune.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Weekly clean-up of old ProvinceSystem website builds + +[Timer] +OnCalendar=Mon *-*-* 05:00:00 +AccuracySec=1min +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/scripts/ps-preview-reap.service b/scripts/ps-preview-reap.service new file mode 100644 index 00000000..4a149073 --- /dev/null +++ b/scripts/ps-preview-reap.service @@ -0,0 +1,13 @@ +[Unit] +Description=Stop expired ProvinceSystem website previews +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +User=tfmc +Group=tfmc +SupplementaryGroups=docker +Nice=10 +IOSchedulingClass=idle +ExecStart=/usr/local/sbin/ps-preview reap diff --git a/scripts/ps-preview-reap.timer b/scripts/ps-preview-reap.timer new file mode 100644 index 00000000..dfcc2f26 --- /dev/null +++ b/scripts/ps-preview-reap.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Stop expired ProvinceSystem website previews every minute + +[Timer] +OnCalendar=*-*-* *:*:00 +AccuracySec=5s +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/scripts/ps-preview-router.Caddyfile b/scripts/ps-preview-router.Caddyfile new file mode 100644 index 00000000..2ee5acea --- /dev/null +++ b/scripts/ps-preview-router.Caddyfile @@ -0,0 +1,26 @@ +{ + admin off +} + +:80 { + @preview header_regexp host Host ^([a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?)\.tfminecraft\.net$ + handle @preview { + header X-Robots-Tag "noindex, nofollow" + handle_path /api/* { + reverse_proxy pv-{re.host.1}-backend:8000 { + header_up X-Forwarded-Proto https + } + } + handle { + reverse_proxy pv-{re.host.1}-frontend:3000 { + header_up X-Forwarded-Proto https + } + } + } + handle { + respond "Not found" 404 + } + handle_errors 502 503 504 { + respond "This preview isn't running. It has probably expired; redeploy it from GitHub Actions." 503 + } +} diff --git a/scripts/ps-preview-ssh.sh b/scripts/ps-preview-ssh.sh new file mode 100755 index 00000000..d2da0c21 --- /dev/null +++ b/scripts/ps-preview-ssh.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail +read -r -a args <<< "${SSH_ORIGINAL_COMMAND:-}" +case "${args[0]:-}" in + up|down|list) ;; + *) echo "usage: up | down | list" >&2; exit 2 ;; +esac +exec sudo -n -u tfmc /usr/local/sbin/ps-preview "${args[@]}" diff --git a/scripts/ps-preview.sh b/scripts/ps-preview.sh new file mode 100755 index 00000000..fc1239a8 --- /dev/null +++ b/scripts/ps-preview.sh @@ -0,0 +1,285 @@ +#!/bin/bash +# Per-branch ProvinceSystem website previews at https://.tfminecraft.net. +# Runs as tfmc (docker group). Called by psdeploy (via ps-preview-ssh), ryan, and ps-preview-reap.timer. +set -euo pipefail +umask 077 + +export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export HOME=/home/tfmc + +ROOT=/home/tfmc/previews +LOCK="$ROOT/.lock" +NETWORK=ps-previews +DOMAIN=tfminecraft.net +MAX_PREVIEWS=4 +TTL=5400 +IMAGE_PREFIX=ghcr.io/tf-minecraft/provincesystem-preview +HELPER_IMAGE=alpine:3 +RESERVED=(www dev api mail map admin staff play mc status preview t3 portainer) +MOUNTS=(data input defines output) + +die() { echo "ps-preview: $1" >&2; exit "${2:-1}"; } + +usage() { + cat >&2 <<'EOF' +usage: ps-preview up + ps-preview down + ps-preview reap + ps-preview list + ps-preview logs [backend|frontend] +EOF + exit 2 +} + +[[ "$(id -un)" == tfmc ]] || die "must run as tfmc (sudo -u tfmc $0 ...)" +cd / +mkdir -p "$ROOT" + +validate_slug() { + local slug="$1" r + [[ "$slug" =~ ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$ ]] || die "invalid slug: '$slug'" 2 + for r in "${RESERVED[@]}"; do + [[ "$slug" != "$r" ]] || die "slug '$slug' is reserved" 2 + done +} + +take_lock() { + exec 9>"$LOCK" + if [[ "${1:-}" == nowait ]]; then + flock -n 9 || exit 0 + else + flock -w 900 9 || die "timed out waiting for another ps-preview run" + fi +} + +low_prio() { nice -n 10 ionice -c3 "$@"; } + +previews() { + local d + for d in "$ROOT"/*/; do + [[ -d "$d" ]] || continue + basename "$d" + done +} + +# The backend runs as root in its container, so some files under a preview dir are root-owned. +remove_dir() { + local slug="$1" + [[ -e "$ROOT/$slug" ]] || return 0 + rm -rf "${ROOT:?}/$slug" 2>/dev/null && return 0 + docker image inspect "$HELPER_IMAGE" >/dev/null 2>&1 || low_prio docker pull -q "$HELPER_IMAGE" >/dev/null + docker run --rm --network none -v "$ROOT:/previews" "$HELPER_IMAGE" rm -rf "/previews/$slug" +} + +staging_backend() { + local id + id=$(docker ps -a -q \ + --filter label=com.docker.compose.project=tfmc-staging \ + --filter label=com.docker.compose.service=backend | head -n1) + [[ -n "$id" ]] || die "cannot find the tfmc-staging backend container" + echo "$id" +} + +create_data() { + local slug="$1" tmp="$ROOT/.new-$slug" src m container + container=$(staging_backend) + remove_dir ".new-$slug" + mkdir -p "$tmp" + for m in "${MOUNTS[@]}"; do + src=$(docker inspect "$container" \ + --format "{{range .Mounts}}{{if eq .Destination \"/app/src/$m\"}}{{.Source}}{{end}}{{end}}") + [[ -n "$src" && -d "$src" ]] || die "dev backend has no host mount for /app/src/$m" + case "$m" in + # Copied separately below via sqlite's online backup; the other .db files are old backups. + data) low_prio rsync -a --exclude='/*.db' --exclude='/*.db-*' --exclude='/maintenance-backups/' "$src/" "$tmp/$m/" ;; + # Regenerable tile/webp cache, written root-only by the dev backend. + output) low_prio rsync -a --exclude='/_derived/' "$src/" "$tmp/$m/" ;; + *) low_prio rsync -a "$src/" "$tmp/$m/" ;; + esac + if [[ "$m" == data ]]; then + [[ -f "$src/province.db" ]] || die "dev data has no province.db" + low_prio sqlite3 "$src/province.db" ".timeout 30000" ".backup '$tmp/data/province.db'" + fi + done + sqlite3 "$tmp/data/province.db" <<'SQL' +.timeout 30000 +DELETE FROM sessions; +DELETE FROM user_sessions; +DELETE FROM discord_oauth_states; +DELETE FROM discord_link_codes; +VACUUM; +SQL + # Open up the copies so the container (root) and tfmc can both use them. + chmod -R u+rwX,go+rX "$tmp"/{data,input,defines,output} + { + echo "STAFF_KEY=$(openssl rand -hex 32)" + echo "PLUGIN_KEY=$(openssl rand -hex 32)" + } > "$tmp/keys" + mv "$tmp" "$ROOT/$slug" +} + +write_compose() { + local slug="$1" dir="$ROOT/$slug" staff plugin + staff=$(sed -n 's/^STAFF_KEY=//p' "$dir/keys") + plugin=$(sed -n 's/^PLUGIN_KEY=//p' "$dir/keys") + [[ -n "$staff" && -n "$plugin" ]] || die "missing keys in $dir/keys" + cat > "$dir/compose.yml" </dev/null 2>&1 || true + remove_dir "$slug" +} + +cmd_up() { + [[ $# -eq 2 ]] || usage + local slug="$1" sha="$2" dir count=0 p + validate_slug "$slug" + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || die "invalid sha: '$sha'" 2 + dir="$ROOT/$slug" + take_lock + + if [[ ! -d "$dir" ]]; then + while read -r p; do + [[ -n "$p" ]] && count=$((count + 1)) + done < <(previews) + if (( count >= MAX_PREVIEWS )); then + echo "ps-preview: $MAX_PREVIEWS previews are already running; stop one first:" >&2 + previews | sed 's/^/ /' >&2 + exit 3 + fi + fi + + low_prio docker pull -q "$IMAGE_PREFIX-backend:$slug" >/dev/null + low_prio docker pull -q "$IMAGE_PREFIX-frontend:$slug" >/dev/null + docker image prune -f \ + --filter "label=org.opencontainers.image.source=https://github.com/TF-Minecraft/ProvinceSystem" >/dev/null + + [[ -d "$dir" ]] || create_data "$slug" + write_compose "$slug" + docker compose -p "pv-$slug" -f "$dir/compose.yml" up -d --remove-orphans --pull never --quiet-pull >&2 + + echo "$sha" > "$dir/sha" + echo $(( $(date +%s) + TTL )) > "$dir/expires" + echo "https://$slug.$DOMAIN" +} + +cmd_down() { + [[ $# -eq 1 ]] || usage + local slug="$1" + [[ "$slug" =~ ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$ ]] || die "invalid slug: '$slug'" 2 + take_lock + do_down "$slug" + echo "stopped $slug" +} + +cmd_reap() { + [[ $# -eq 0 ]] || usage + local now slug expires project + take_lock nowait + now=$(date +%s) + while read -r slug; do + [[ -n "$slug" ]] || continue + expires=$(cat "$ROOT/$slug/expires" 2>/dev/null || true) + if [[ ! "$expires" =~ ^[0-9]+$ ]] || (( now >= expires )); then + echo "reaping $slug" + do_down "$slug" || echo "failed to reap $slug" >&2 + fi + done < <(previews) + # Compose projects whose preview dir is gone. + while read -r project; do + slug="${project#pv-}" + [[ "$slug" =~ ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$ ]] || continue + [[ -d "$ROOT/$slug" ]] && continue + echo "reaping orphaned project $project" + docker compose -p "$project" down -v --remove-orphans || echo "failed to reap $project" >&2 + done < <(docker ps -a --filter label=com.docker.compose.project \ + --format '{{.Label "com.docker.compose.project"}}' | grep '^pv-' | sort -u) + docker image prune -f \ + --filter "label=org.opencontainers.image.source=https://github.com/TF-Minecraft/ProvinceSystem" >/dev/null +} + +cmd_list() { + [[ $# -eq 0 ]] || usage + local slug sha expires + while read -r slug; do + [[ -n "$slug" ]] || continue + sha=$(cat "$ROOT/$slug/sha" 2>/dev/null || echo -) + expires=$(cat "$ROOT/$slug/expires" 2>/dev/null || true) + if [[ "$expires" =~ ^[0-9]+$ ]]; then + expires=$(date -u -d "@$expires" +%Y-%m-%dT%H:%M:%SZ) + else + expires=- + fi + printf '%s %s %s\n' "$slug" "$sha" "$expires" + done < <(previews) +} + +cmd_logs() { + [[ $# -ge 1 && $# -le 2 ]] || usage + local slug="$1" service="${2:-}" + validate_slug "$slug" + case "$service" in + ""|backend|frontend) ;; + *) usage ;; + esac + [[ -d "$ROOT/$slug" ]] || die "no such preview: $slug" + docker compose -p "pv-$slug" -f "$ROOT/$slug/compose.yml" logs --no-color --tail 200 ${service:+"$service"} +} + +[[ $# -ge 1 ]] || usage +sub="$1" +shift +case "$sub" in + up) cmd_up "$@" ;; + down) cmd_down "$@" ;; + reap) cmd_reap "$@" ;; + list) cmd_list "$@" ;; + logs) cmd_logs "$@" ;; + *) usage ;; +esac From 430cc1c2906cf831fdef9dbd24ed5affdb65f42f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:36:49 +0000 Subject: [PATCH 2/3] fix(host): run previews as tfmc and reap container processes - Preview containers run as tfmc (1001) with init, so tfmc owns every file in a preview folder: the Alpine container that deleted root-owned files and the chmod of copied data are gone. - The www and Dev compose files run both services with init, which reaps the renderer's Chromium processes (they piled up as zombies). - Each host script now says where it is installed and who runs it. - portainer is reserved as a preview name, matching the host. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/preview_slug.py | 2 +- docker-compose.staging.yml | 3 +++ docker-compose.yml | 3 +++ scripts/ps-compose.sh | 5 +++-- scripts/ps-image-prune.sh | 1 + scripts/ps-preview-ssh.sh | 3 +++ scripts/ps-preview.sh | 19 ++++++++----------- 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.github/scripts/preview_slug.py b/.github/scripts/preview_slug.py index 87f906a0..e51c2778 100644 --- a/.github/scripts/preview_slug.py +++ b/.github/scripts/preview_slug.py @@ -13,7 +13,7 @@ DOMAIN = "tfminecraft.net" MAX_LENGTH = 40 # Hostnames with their own DNS records or tunnel rules; a preview there could never be reached. -RESERVED = {"www", "dev", "api", "mail", "map", "admin", "staff", "play", "mc", "status", "preview", "t3"} +RESERVED = {"www", "dev", "api", "mail", "map", "admin", "staff", "play", "mc", "status", "preview", "t3", "portainer"} VALID = re.compile(r"[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?") diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 51a17801..8adb59e8 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -2,6 +2,8 @@ name: tfmc-staging services: backend: + # Reaps the renderer's Chromium processes. + init: true build: context: . dockerfile: backend/Dockerfile @@ -37,6 +39,7 @@ services: restart: unless-stopped frontend: + init: true build: context: . dockerfile: frontend/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 9436ac10..e09e1f04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,7 @@ services: backend: + # Reaps the renderer's Chromium processes. + init: true build: context: . dockerfile: backend/Dockerfile @@ -17,6 +19,7 @@ services: restart: always frontend: + init: true build: context: . dockerfile: frontend/Dockerfile diff --git a/scripts/ps-compose.sh b/scripts/ps-compose.sh index 9a3bea92..2a68c067 100755 --- a/scripts/ps-compose.sh +++ b/scripts/ps-compose.sh @@ -1,6 +1,7 @@ #!/bin/bash -# Limited docker compose for the ProvinceSystem main and dev sites. -# Runs as tfmc (a docker-group user) via sudo. Ryan is not in the docker group. +# Limited docker compose for the ProvinceSystem main and dev sites. Installed as root's +# /usr/local/sbin/ps-compose and run as tfmc (docker group) through sudo, so people +# without docker access can manage the two sites but not mount the host. set -euo pipefail usage() { diff --git a/scripts/ps-image-prune.sh b/scripts/ps-image-prune.sh index 5d4e27f4..06042327 100755 --- a/scripts/ps-image-prune.sh +++ b/scripts/ps-image-prune.sh @@ -1,4 +1,5 @@ #!/bin/bash +# Installed as root's /usr/local/sbin/ps-image-prune; run weekly by ps-image-prune.timer. # Untagged local builds of the main and dev sites, plus build cache unused for a week. # Tagged images used by the running containers are left alone. Preview images are handled by ps-preview. set -euo pipefail diff --git a/scripts/ps-preview-ssh.sh b/scripts/ps-preview-ssh.sh index d2da0c21..0c94cea5 100755 --- a/scripts/ps-preview-ssh.sh +++ b/scripts/ps-preview-ssh.sh @@ -1,4 +1,7 @@ #!/bin/bash +# Forced command for the Preview workflow's key on psdeploy. Installed as root's +# /usr/local/sbin/ps-preview-ssh; sudoers lets psdeploy run ps-preview as tfmc: +# restrict,command="/usr/local/sbin/ps-preview-ssh" ssh-ed25519 ... github-preview-deploy set -euo pipefail read -r -a args <<< "${SSH_ORIGINAL_COMMAND:-}" case "${args[0]:-}" in diff --git a/scripts/ps-preview.sh b/scripts/ps-preview.sh index fc1239a8..82a30c73 100755 --- a/scripts/ps-preview.sh +++ b/scripts/ps-preview.sh @@ -1,6 +1,8 @@ #!/bin/bash # Per-branch ProvinceSystem website previews at https://.tfminecraft.net. -# Runs as tfmc (docker group). Called by psdeploy (via ps-preview-ssh), ryan, and ps-preview-reap.timer. +# Installed as root's /usr/local/sbin/ps-preview and run as tfmc (docker group), by the +# preview key on psdeploy (through ps-preview-ssh), by admins with sudo -u tfmc, and by +# ps-preview-reap.timer. Preview containers run as tfmc too, so tfmc owns every file. set -euo pipefail umask 077 @@ -14,7 +16,6 @@ DOMAIN=tfminecraft.net MAX_PREVIEWS=4 TTL=5400 IMAGE_PREFIX=ghcr.io/tf-minecraft/provincesystem-preview -HELPER_IMAGE=alpine:3 RESERVED=(www dev api mail map admin staff play mc status preview t3 portainer) MOUNTS=(data input defines output) @@ -62,13 +63,8 @@ previews() { done } -# The backend runs as root in its container, so some files under a preview dir are root-owned. remove_dir() { - local slug="$1" - [[ -e "$ROOT/$slug" ]] || return 0 - rm -rf "${ROOT:?}/$slug" 2>/dev/null && return 0 - docker image inspect "$HELPER_IMAGE" >/dev/null 2>&1 || low_prio docker pull -q "$HELPER_IMAGE" >/dev/null - docker run --rm --network none -v "$ROOT:/previews" "$HELPER_IMAGE" rm -rf "/previews/$slug" + rm -rf "${ROOT:?}/$1" } staging_backend() { @@ -92,7 +88,7 @@ create_data() { case "$m" in # Copied separately below via sqlite's online backup; the other .db files are old backups. data) low_prio rsync -a --exclude='/*.db' --exclude='/*.db-*' --exclude='/maintenance-backups/' "$src/" "$tmp/$m/" ;; - # Regenerable tile/webp cache, written root-only by the dev backend. + # Regenerable tile/webp cache. output) low_prio rsync -a --exclude='/_derived/' "$src/" "$tmp/$m/" ;; *) low_prio rsync -a "$src/" "$tmp/$m/" ;; esac @@ -109,8 +105,6 @@ DELETE FROM discord_oauth_states; DELETE FROM discord_link_codes; VACUUM; SQL - # Open up the copies so the container (root) and tfmc can both use them. - chmod -R u+rwX,go+rX "$tmp"/{data,input,defines,output} { echo "STAFF_KEY=$(openssl rand -hex 32)" echo "PLUGIN_KEY=$(openssl rand -hex 32)" @@ -129,6 +123,8 @@ name: pv-$slug services: backend: image: $IMAGE_PREFIX-backend:$slug + user: "1001:1001" + init: true restart: unless-stopped security_opt: - no-new-privileges:true @@ -151,6 +147,7 @@ services: frontend: image: $IMAGE_PREFIX-frontend:$slug + init: true restart: unless-stopped security_opt: - no-new-privileges:true From baa1c98029edfcf8e2ac7d12ec828dc50d26e108 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:48:40 +0000 Subject: [PATCH 3/3] fix(host): allow ps-compose logs -f; previews' backends run as tfmc Co-Authored-By: Claude Opus 5.5 --- scripts/ps-compose.sh | 2 ++ scripts/ps-preview.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/ps-compose.sh b/scripts/ps-compose.sh index 2a68c067..97b3683d 100755 --- a/scripts/ps-compose.sh +++ b/scripts/ps-compose.sh @@ -38,6 +38,8 @@ case "$cmd" in esac for arg in "$@"; do + # After `logs`, -f is --follow; elsewhere it could select a compose file. + [[ $cmd == logs && $arg == -f ]] && continue case "$arg" in -f|--file|--file=*|-H|--host|--host=*|--context|--context=*|--project-directory|--project-directory=*|--project-name|--project-name=*|--env-file|--env-file=*|--privileged|--privileged=*|-v|--volume|--volume=*|--mount|--mount=*) echo "argument not allowed: $arg" >&2 diff --git a/scripts/ps-preview.sh b/scripts/ps-preview.sh index 82a30c73..2d01c998 100755 --- a/scripts/ps-preview.sh +++ b/scripts/ps-preview.sh @@ -2,7 +2,7 @@ # Per-branch ProvinceSystem website previews at https://.tfminecraft.net. # Installed as root's /usr/local/sbin/ps-preview and run as tfmc (docker group), by the # preview key on psdeploy (through ps-preview-ssh), by admins with sudo -u tfmc, and by -# ps-preview-reap.timer. Preview containers run as tfmc too, so tfmc owns every file. +# ps-preview-reap.timer. Preview backends run as tfmc too, so tfmc owns every file. set -euo pipefail umask 077