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
14 changes: 9 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,10 @@ jobs:
# i.e., direct references to alertmanager, loki push clients, or
# docker-compose.monitoring in the application source.
# Exclude comment-only lines (-h suppresses filenames for grep -Ev).
# With bash -o pipefail (GHA default), grep exits 1 when there are zero matches;
# that must not fail the step β€” only non-empty LEAKS after filtering is an error.
LEAKS=$(grep -rhE "(alertmanager|docker-compose\.monitoring)" src/ tests/ 2>/dev/null \
| grep -Ev '^\s*(//|#|\*|/\*)')
| grep -Ev '^\s*(//|#|\*|/\*)' || true)
if [ -n "$LEAKS" ]; then
echo "::error::Infra client references found in src/ or tests/"
echo "$LEAKS"
Expand Down Expand Up @@ -278,7 +280,7 @@ jobs:
# Guard 1: no stale network name (fieldtrack_network is not the canonical name)
if grep -rE '\bfieldtrack_network\b' src/ scripts/ \
--include='*.ts' --include='*.sh' \
2>/dev/null | grep -Ev '^\s*#'; then
2>/dev/null | grep -Ev '^[^:]+:\s*(#|//)'; then
echo "::error::Forbidden network name 'fieldtrack_network' found β€” canonical name is 'api_network'"
FAIL=1
fi
Expand Down Expand Up @@ -896,7 +898,7 @@ jobs:
# Scope: scripts/ and src/ only (not workflows where guard steps live).
if grep -rE "\./infra/|\.\.\./infra/" scripts/ src/ \
--binary-files=without-match --exclude-dir=node_modules 2>/dev/null \
| grep -Ev '^\s*#'; then
| grep -Ev '^[^:]+:\s*(#|//)'; then
echo "::error::Local repo-relative infra coupling (./infra/ or ../infra/) detected in scripts/ or src/"
exit 1
fi
Expand All @@ -913,7 +915,8 @@ jobs:
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
[ -d "$DEPLOY_ROOT" ] || { echo "::error::DEPLOY_ROOT not found: $DEPLOY_ROOT"; exit 1; }
cd "$DEPLOY_ROOT"
API_BASE_URL=$(grep -E '^API_BASE_URL=' .env | head -1 | cut -d'=' -f2-)
API_BASE_URL=$(grep -E '^API_BASE_URL=' .env 2>/dev/null | head -1 | cut -d'=' -f2- || true)
[ -n "$API_BASE_URL" ] || { echo "::error::API_BASE_URL missing or empty in .env"; exit 1; }
API_HOSTNAME=$(echo "$API_BASE_URL" | sed -E 's|^https?://||' | cut -d'/' -f1)
for i in $(seq 1 30); do
# Phase 1: in-network (source of truth)
Expand Down Expand Up @@ -949,7 +952,8 @@ jobs:
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
[ -d "$DEPLOY_ROOT" ] || { echo "::error::DEPLOY_ROOT not found: $DEPLOY_ROOT"; exit 1; }
cd "$DEPLOY_ROOT"
API_BASE_URL=$(grep -E '^API_BASE_URL=' .env | head -1 | cut -d'=' -f2-)
API_BASE_URL=$(grep -E '^API_BASE_URL=' .env 2>/dev/null | head -1 | cut -d'=' -f2- || true)
[ -n "$API_BASE_URL" ] || { echo "::error::API_BASE_URL missing or empty in .env"; exit 1; }
API_HOSTNAME=$(echo "$API_BASE_URL" | sed -E 's|^https?://||' | cut -d'/' -f1)
for i in $(seq 1 10); do
# Phase 1: in-network (source of truth)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:

if grep -rE '\bfieldtrack_network\b' src/ scripts/ \
--include='*.ts' --include='*.sh' \
2>/dev/null | grep -Ev '^\s*#'; then
2>/dev/null | grep -Ev '^[^:]+:\s*(#|//)'; then
echo "::error::Forbidden network name 'fieldtrack_network' β€” canonical name is 'api_network'"
FAIL=1
fi
Expand Down
42 changes: 18 additions & 24 deletions scripts/vps-readiness-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#
# HARD FAILURES (exit 1):
# - Docker daemon not running
# - Ports 80 or 443 occupied by ANY non-docker-proxy, non-nginx process
# - Ports 80 or 443 occupied by processes other than docker-proxy / nginx
# - Any container has host port bindings (violates production architecture)
# - Required containers not attached to api_network
# - Required .env file missing
Expand Down Expand Up @@ -87,40 +87,34 @@ if ! docker network ls --format '{{.Name}}' | grep -Eq "^${NETWORK}$"; then
else
ok "Network '$NETWORK' exists."
fi
# ── CHECK 4: Ports 80 and 443 β€” no non-docker processes ──────────────────────
# ── CHECK 4: Ports 80 and 443 β€” expected listeners only ─────────────────────
#
# Design: we do NOT auto-kill unknown processes. If port 80 or 443 is held by
# a non-docker process (e.g., system nginx, apache, lighttpd), that is a VPS
# configuration error that requires operator action. Silently killing unknown
# processes risks breaking the system in unpredictable ways.
# Design: we do NOT auto-kill unknown processes. Published container ports show
# up as docker-proxy (full name in `ss -tlnp`; lsof often truncates COMMAND to
# 8 chars e.g. "docker-pr", which broke older allowlists).
#
# Allowed occupants (hard-coded safe list):
# - docker-proxy (managed by Docker / our nginx container)
# - nginx (running as Docker container β€” docker exec nginx)
# Use ss (same as elsewhere in this script) and allow:
# - docker-proxy / docker-pr (truncated) β€” Docker publishing nginx :80/:443
# - nginx β€” system or container worker name in ss output
#
# Everything else β†’ hard fail with diagnostics.
# Everything else on these ports β†’ hard fail (e.g. apache bind-mount).
echo ""
echo "--- CHECK 4: Port 80/443 β€” no non-docker processes ---"
echo "--- CHECK 4: Port 80/443 β€” docker-proxy / nginx only ---"
_check_port() {
local port="$1"
local listeners
listeners=$(sudo ss -tlnp "sport = :${port}" 2>/dev/null || ss -tlnp "sport = :${port}" 2>/dev/null || true)

# Check if anything is listening on the port at all
if ! ss -tlnp "sport = :${port}" 2>/dev/null | grep -q 'LISTEN'; then
if ! echo "$listeners" | grep -q 'LISTEN'; then
ok "Port $port is free."
return 0
fi

# Check for non-docker-proxy, non-nginx processes via lsof
# lsof -i :PORT lists ALL processes holding the port.
# We exclude docker-proxy and nginx (expected Docker-managed processes).
NON_DOCKER=$(sudo lsof -i ":${port}" -sTCP:LISTEN -P -n 2>/dev/null \
| awk 'NR>1 {print $1, $2}' \
| grep -vE '^(docker-pro|nginx)' || true)

if [ -n "$NON_DOCKER" ]; then
record_failure "Port $port is occupied by a non-docker process."
echo " Offending process(es):"
sudo lsof -i ":${port}" -sTCP:LISTEN -P -n 2>/dev/null | awk 'NR>1' | sed 's/^/ /'
# Any LISTEN line that does not reference an allowed process is a failure.
if echo "$listeners" | grep 'LISTEN' | grep -Ev 'docker-proxy|docker-pr|nginx' | grep -q .; then
record_failure "Port $port is occupied by an unexpected process (not docker-proxy/nginx)."
echo " Listeners (ss -tlnp sport = :${port}):"
echo "$listeners" | sed 's/^/ /'
echo " This is a VPS configuration error. Stop the conflicting service before deploying."
echo " Example: sudo systemctl stop nginx OR sudo systemctl stop apache2"
return 1
Expand Down
Loading