Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e9905ca
Refactor code structure for improved readability and maintainability
rajashish147 Apr 3, 2026
d125282
feat(ci): add guard against forbidden docker exec curl usage and impr…
rajashish147 Apr 3, 2026
f264a82
fix(ci): enhance guard against forbidden docker exec curl usage in wo…
rajashish147 Apr 3, 2026
e409cac
fix(ci): refine guard against forbidden docker exec curl usage in wor…
rajashish147 Apr 3, 2026
f3200c9
feat: enhance CI/CD workflow with production simulation and infra con…
rajashish147 Apr 4, 2026
6f1110b
fix: enforce canonical redis URL in env.example + scope guard to prod…
rajashish147 Apr 4, 2026
9967c4c
fix(ci): docker-exec curl guard ignores path-prefixed lines and self-doc
rajashish147 Apr 4, 2026
46f2abd
fix(ci): remove api-ci-test container before docker rmi in bootstrap …
rajashish147 Apr 4, 2026
16d804e
fix(ci): infra leakage grep pipeline must not fail on zero matches (p…
rajashish147 Apr 4, 2026
b337e47
Merge branch 'master' into beta
rajashish147 Apr 4, 2026
13c6881
fix(ci): path-aware grep filters for -r output; harden .env API_BASE_…
rajashish147 Apr 4, 2026
7d76800
fix(vps): port 80/443 readiness use ss and allow docker-proxy (not ls…
rajashish147 Apr 4, 2026
5a225d1
fix(deploy): writable state dir for lock/slot; chown /var/lib/fieldtr…
rajashish147 Apr 4, 2026
36c592c
fix(deploy): sudo mkdir+chown for INFRA nginx live/backup; readiness …
rajashish147 Apr 4, 2026
8c39a1a
Merge branch 'master' into beta
rajashish147 Apr 4, 2026
f7e23e2
refactor: canonical INFRA_ROOT=/opt/infra for nginx paths and compose…
rajashish147 Apr 4, 2026
ca540f8
Merge branch 'master' into beta
rajashish147 Apr 4, 2026
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
8 changes: 8 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ jobs:
script: |
set -euo pipefail
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
[ -d "$DEPLOY_ROOT" ] || { echo "::error::DEPLOY_ROOT not found: $DEPLOY_ROOT"; exit 1; }
cd "$DEPLOY_ROOT"
# Pull latest scripts without full deploy
Expand Down Expand Up @@ -775,6 +776,7 @@ jobs:
set -euo pipefail
T0=$(date +%s)
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
[ -d "$DEPLOY_ROOT" ] || { echo "::error::DEPLOY_ROOT not found: $DEPLOY_ROOT"; exit 1; }
cd "$DEPLOY_ROOT"
# Enforce repo is at the exact SHA being deployed (issue 7 β€” prevents
Expand All @@ -794,6 +796,7 @@ jobs:
key: ${{ secrets.DO_SSH_KEY }}
script: |
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
ACTIVE_SLOT=$(cat "$DEPLOY_ROOT/.fieldtrack/active-slot" 2>/dev/null || cat /var/lib/fieldtrack/active-slot 2>/dev/null || echo "unknown")
ACTIVE_CONTAINER="api-${ACTIVE_SLOT}"
DEPLOY_STATUS="UNKNOWN"
Expand Down Expand Up @@ -836,6 +839,7 @@ jobs:
script: |
set -euo pipefail
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
[ -d "$DEPLOY_ROOT" ] || { echo "::error::DEPLOY_ROOT not found: $DEPLOY_ROOT"; exit 1; }
cd "$DEPLOY_ROOT"

Expand Down Expand Up @@ -914,6 +918,7 @@ jobs:
script: |
set -euo pipefail
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
[ -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 2>/dev/null | head -1 | cut -d'=' -f2- || true)
Expand Down Expand Up @@ -951,6 +956,7 @@ jobs:
script: |
set -euo pipefail
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
[ -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 2>/dev/null | head -1 | cut -d'=' -f2- || true)
Expand Down Expand Up @@ -992,6 +998,7 @@ jobs:
key: ${{ secrets.DO_SSH_KEY }}
script: |
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
ACTIVE_SLOT=$(cat "$DEPLOY_ROOT/.fieldtrack/active-slot" 2>/dev/null || cat /var/lib/fieldtrack/active-slot 2>/dev/null || echo "unknown")
ACTIVE_CONTAINER="api-${ACTIVE_SLOT}"
FT_CURL_IMG="curlimages/curl:8.7.1"
Expand Down Expand Up @@ -1062,6 +1069,7 @@ jobs:
script: |
set -euo pipefail
export DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/api}"
export INFRA_ROOT="${INFRA_ROOT:-/opt/infra}"
[ -d "$DEPLOY_ROOT" ] || { echo "::error::DEPLOY_ROOT not found: $DEPLOY_ROOT"; exit 1; }
cd "$DEPLOY_ROOT"
chmod +x scripts/*.sh
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,11 @@ All variables are validated at startup by `src/config/env.ts` (Zod schema, fail-

This API requires an external infra repository.

Expected on server:
- nginx (connected to `api_network`)
- Redis (`redis:6379`)

Default path:
- `INFRA_ROOT=/opt/infra`
Expected on server (all under **`INFRA_ROOT=/opt/infra`**):
- `$INFRA_ROOT/docker-compose.nginx.yml` β€” operator runs nginx from here
- `$INFRA_ROOT/docker-compose.redis.yml` β€” operator runs Redis from here
- `$INFRA_ROOT/nginx/live`, `nginx/backup`, `nginx/api.conf` β€” layout enforced by `deploy.sh` and readiness check
- nginx container on `api_network`; Redis at `redis:6379` on `api_network`

Deployments run automatically via GitHub Actions on every push to `master` (after CodeQL scan passes).

Expand Down
13 changes: 8 additions & 5 deletions docs/infra-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,14 @@ Default `INFRA_ROOT` on server: `/opt/infra`

## External Dependencies (infra repo)

Required paths under `INFRA_ROOT=/opt/infra`:
Required layout under **`INFRA_ROOT=/opt/infra`** (canonical; scripts default to this path):

```
$INFRA_ROOT/nginx/live/ (directory, created by infra)
$INFRA_ROOT/nginx/backup/ (directory, created by infra)
$INFRA_ROOT/nginx/api.conf (template, managed by infra)
$INFRA_ROOT/nginx/live/ (directory β€” deploy writes live api.conf)
$INFRA_ROOT/nginx/backup/ (directory β€” rolling backups)
$INFRA_ROOT/nginx/api.conf (template with placeholders β€” infra managed)
$INFRA_ROOT/docker-compose.nginx.yml (operator starts nginx from here)
$INFRA_ROOT/docker-compose.redis.yml (operator starts redis from here)
```

The API deploy script (`scripts/deploy.sh`) fails fast if these are absent.
The API deploy script (`scripts/deploy.sh`) and `scripts/vps-readiness-check.sh` fail fast if these paths are missing. Compose files are not executed by deploy; they must exist so operators (and checks) know the canonical layout.
2 changes: 2 additions & 0 deletions infra/docker-compose.nginx.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Canonical on server: INFRA_ROOT=/opt/infra β€” place this file at $INFRA_ROOT/docker-compose.nginx.yml
# Volume ./nginx/live is relative to this file (β†’ $INFRA_ROOT/nginx/live when deployed).
services:

nginx:
Expand Down
1 change: 1 addition & 0 deletions infra/docker-compose.redis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Canonical on server: INFRA_ROOT=/opt/infra β€” place this file at $INFRA_ROOT/docker-compose.redis.yml
services:

redis:
Expand Down
33 changes: 23 additions & 10 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
# - FIELDTRACK_STATE_DIR or /var/lib/fieldtrack when writable (sudo chown if needed)
# - Otherwise $DEPLOY_ROOT/.fieldtrack; existing /var/lib/fieldtrack/* is migrated once
#
# Nginx config paths (INFRA_ROOT, default /opt/infra):
# - deploy user must write $INFRA_ROOT/nginx/live and nginx/backup (sudo mkdir+chown if needed)
# Canonical infra root on VPS: INFRA_ROOT=/opt/infra (default). All paths below use $INFRA_ROOT.
# Nginx + compose files (all under INFRA_ROOT):
# - $INFRA_ROOT/nginx/live, nginx/backup (deploy user must be able to write live + backup)
# - $INFRA_ROOT/nginx/api.conf (template)
# - $INFRA_ROOT/docker-compose.nginx.yml, docker-compose.redis.yml (must exist; deploy does not start them)
# =============================================================================
set -euo pipefail
if [ "${DEBUG:-false}" = "true" ]; then set -x; fi
Expand Down Expand Up @@ -175,7 +178,7 @@ _ft_snapshot() {
"$(cat "${SLOT_BACKUP_FILE:-/var/lib/fieldtrack/active-slot.backup}" 2>/dev/null || echo 'MISSING')" >&2
printf '[DEPLOY] nginx_upstream = %s\n' \
"$(grep -oE 'http://(api-blue|api-green):3000' \
"${NGINX_CONF:-/opt/infra/nginx/live/api.conf}" 2>/dev/null \
"${NGINX_CONF:-${INFRA_ROOT:-/opt/infra}/nginx/live/api.conf}" 2>/dev/null \
| grep -oE 'api-blue|api-green' | head -1 || echo 'unreadable')" >&2
printf '[DEPLOY] containers =\n' >&2
docker ps --format '[DEPLOY] {{.Names}} -> {{.Status}} ({{.Ports}})' 1>&2 2>/dev/null \
Expand Down Expand Up @@ -499,21 +502,29 @@ ensure_network() {
# ensure_nginx β€” nginx MUST exist and be on api_network; hard fail otherwise
# ---------------------------------------------------------------------------
ensure_nginx() {
if [ ! -d "$INFRA_ROOT/nginx/live" ]; then
_ft_error "msg='infra not initialized at expected path' infra_root=$INFRA_ROOT required=$INFRA_ROOT/nginx/live"
if [ ! -d "$NGINX_LIVE_DIR" ]; then
_ft_error "msg='infra not initialized' infra_root=$INFRA_ROOT required=$NGINX_LIVE_DIR"
_ft_exit 1 "DEPLOY_FAILED_SAFE" "reason=infra_not_initialized"
fi
if [ ! -d "$INFRA_ROOT/nginx/backup" ]; then
_ft_error "msg='infra not initialized at expected path' infra_root=$INFRA_ROOT required=$INFRA_ROOT/nginx/backup"
if [ ! -d "$NGINX_BACKUP_DIR" ]; then
_ft_error "msg='infra not initialized' infra_root=$INFRA_ROOT required=$NGINX_BACKUP_DIR"
_ft_exit 1 "DEPLOY_FAILED_SAFE" "reason=infra_not_initialized"
fi
if [ ! -f "$INFRA_ROOT/nginx/api.conf" ]; then
_ft_error "msg='infra template missing' path=$INFRA_ROOT/nginx/api.conf"
if [ ! -f "$NGINX_TEMPLATE" ]; then
_ft_error "msg='infra template missing' path=$NGINX_TEMPLATE"
_ft_exit 1 "DEPLOY_FAILED_SAFE" "reason=infra_template_missing"
fi
if [ ! -f "$INFRA_COMPOSE_NGINX" ]; then
_ft_error "msg='infra compose file missing' path=$INFRA_COMPOSE_NGINX"
_ft_exit 1 "DEPLOY_FAILED_SAFE" "reason=infra_compose_nginx_missing"
fi
if [ ! -f "$INFRA_COMPOSE_REDIS" ]; then
_ft_error "msg='infra compose file missing' path=$INFRA_COMPOSE_REDIS"
_ft_exit 1 "DEPLOY_FAILED_SAFE" "reason=infra_compose_redis_missing"
fi

if ! docker inspect nginx >/dev/null 2>&1; then
_ft_error "msg='nginx container not found β€” nginx is managed by the infra repo' hint='docker compose -f docker-compose.nginx.yml up -d'"
_ft_error "msg='nginx container not found' infra_root=$INFRA_ROOT hint='docker compose -f $INFRA_COMPOSE_NGINX up -d'"
_ft_exit 1 "DEPLOY_FAILED_SAFE" "reason=nginx_missing"
fi
local net
Expand Down Expand Up @@ -1312,6 +1323,8 @@ NGINX_CONF="$INFRA_ROOT/nginx/live/api.conf"
NGINX_LIVE_DIR="$INFRA_ROOT/nginx/live"
NGINX_BACKUP_DIR="$INFRA_ROOT/nginx/backup"
NGINX_TEMPLATE="$INFRA_ROOT/nginx/api.conf"
INFRA_COMPOSE_NGINX="$INFRA_ROOT/docker-compose.nginx.yml"
INFRA_COMPOSE_REDIS="$INFRA_ROOT/docker-compose.redis.yml"
NGINX_BACKUP="" # set inside switch_nginx()

MAX_HISTORY=5
Expand Down
11 changes: 6 additions & 5 deletions scripts/verify-stabilization.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ check_not_exists "$SCRIPT_DIR/analytics-backfill.ts"
check_not_exists "$SCRIPT_DIR/load-testing"

# Infra coupling guard (deployment/runtime paths only)
# We only block local repo-relative infra paths (./infra or ../infra) in
# executable/deploy code paths. External absolute paths like /opt/infra are allowed.
# Block repo-relative ./infra/ or ../infra/ in scripts and src. Canonical server
# layout is INFRA_ROOT=/opt/infra (see docs/infra-contract.md). Workflows are
# not scanned here β€” they contain guard strings that mention ./infra/ by design.
if grep -R -E "\.\./infra/|\./infra/" \
"$REPO_ROOT/scripts" "$REPO_ROOT/src" "$REPO_ROOT/.github/workflows" \
"$REPO_ROOT/scripts" "$REPO_ROOT/src" \
--exclude="verify-stabilization.sh" \
--binary-files=without-match --exclude-dir=node_modules --exclude-dir=.git >/dev/null; then
fail "Found local repo-relative infra coupling in scripts/src/workflows"
fail "Found local repo-relative infra coupling in scripts/ or src/"
else
pass "No local repo-relative infra coupling found"
pass "No local repo-relative infra coupling in scripts/ or src/"
fi

# Deploy workflow guard
Expand Down
9 changes: 8 additions & 1 deletion scripts/vps-readiness-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ if [ ! -f "$INFRA_ROOT/nginx/api.conf" ]; then
else
ok "Nginx template present: $INFRA_ROOT/nginx/api.conf"
fi
for compose in "$INFRA_ROOT/docker-compose.nginx.yml" "$INFRA_ROOT/docker-compose.redis.yml"; do
if [ ! -f "$compose" ]; then
record_failure "Missing compose file: $compose (expected under canonical INFRA_ROOT=$INFRA_ROOT)"
else
ok "Compose file present: $compose"
fi
done

# ── CHECK 8: Network attachment enforcement ───────────────────────────────────
#
Expand Down Expand Up @@ -268,7 +275,7 @@ echo "--- CHECK 9: Nginx container ---"
if ! docker inspect nginx >/dev/null 2>&1; then
record_failure "nginx container not found β€” required for deployment routing."
echo " nginx must be running before deploy can proceed."
echo " Fix: docker compose -f docker-compose.nginx.yml up -d"
echo " Fix: cd \"$INFRA_ROOT\" && docker compose -f docker-compose.nginx.yml up -d"
else
ok "nginx container exists."
# Advisory in-network health probe β€” nginx may return non-200 before
Expand Down
Loading