Repository navigation
174 lines (154 loc) · 8.98 KB
/
Copy pathdeploy.yml
File metadata and controls
174 lines (154 loc) · 8.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
name: Deploy to VPS
on:
push:
branches:
- main
# Never run two deploys at once (a second push while a build is running would race on the VPS).
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
# Fast gate — a broken push must not reach the VPS. Full test suites
# (vitest, pytest) run separately in ci.yml and don't block the deploy.
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Frontend — type-check + build
working-directory: frontend
run: |
npm ci
npm run type-check
npm run build
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Backend — compile + error-lint
run: |
pip install ruff
python -m compileall -q backend/app backend/scripts
ruff check --select E9,F63,F7,F82 backend/app
deploy:
needs: checks
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH to VPS
# v1.0.0 exited 255 within seconds of `docker compose up --build`
# starting — the old drone-ssh choked on BuildKit's TTY progress
# stream (rapid carriage-return redraws). Fixed by pinning a newer
# action AND forcing plain build progress in the script below.
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
port: ${{ secrets.VPS_PORT }}
key: ${{ secrets.VPS_SSH_KEY }}
# v1.2.x dropped the `script_stop` input; `set -e` in the script
# below is what aborts on the first failed command now.
# Default command_timeout is 10m; a cold `docker compose up --build`
# (npm + vite + pip, no layer cache) runs several minutes even after
# trimming the CUDA wheels from requirements.txt. Headroom, not tight.
command_timeout: 30m
script: |
set -euo pipefail
cd /srv/hbrl/Metadata-Engine
# Plain progress: BuildKit's default TTY renderer streams
# carriage-return redraws that break the SSH action mid-build.
export BUILDKIT_PROGRESS=plain
export DOCKER_BUILDKIT=1
# Production compose file: Postgres (hbrl-postgres / metadata_engine) over the
# external hbrl-db network. vps.docker-compose.yml is only a deprecated alias now.
COMPOSE_FILE="docker-compose.yml"
DB_RE='^DATABASE_URL=postgres(ql)?(\+[a-z0-9]+)?://[^@[:space:]]+@hbrl-postgres:5432/metadata_engine'
# Guard 1 (fail closed, before anything is touched): the VPS .env OWNS DATABASE_URL and
# must already point at production Postgres. The GitHub secret of the same name is stale
# (SQLite) and is deliberately NOT written below - on 2026-09-21 it switched prod to SQLite.
test -f .env || { echo "FAIL: .env is missing on the VPS - refusing to deploy"; exit 1; }
OLD_DB_LINE="$(grep -m1 '^DATABASE_URL=' .env || true)"
printf '%s\n' "$OLD_DB_LINE" | grep -Eq "$DB_RE" || { echo "FAIL: DATABASE_URL in the VPS .env is not production Postgres (hbrl-postgres/metadata_engine) - refusing to deploy"; exit 1; }
# Same "VPS owns this" treatment for the R2 backup credentials: set once by hand on
# 2026-09-24, not mirrored to a GitHub Secret, so they must survive the rewrite below
# exactly like DATABASE_URL does.
OLD_R2_LINES="$(grep -E '^R2_(ACCOUNT_ID|ACCESS_KEY_ID|SECRET_ACCESS_KEY|BUCKET)=' .env || true)"
# Same for the transactional-mail credentials (the no-reply@ mailbox shared with the other
# HRL apps). They live only on the VPS - never in GitHub Secrets - so a deploy that rewrote
# them from non-existent secrets silently turned verification/reset e-mails off (2026-10-22).
OLD_SMTP_LINES="$(grep -E '^SMTP_(HOST|PORT|USER|PASS|FROM)=' .env || true)"
# Backup existing env (docker-compose reads root .env)
cp -p .env ".env.backup.$(date +%F-%H%M%S)"
# Write the env from GitHub Secrets into .env.new (no secrets in repo).
# NOTE: every heredoc body line below must stay indented to match
# this YAML block scalar (12 spaces) or the workflow file fails
# to parse entirely (GitHub reports it as a run with zero jobs).
# The sed strips that indentation back out before it reaches .env.new.
cat <<'EOF' | sed -e 's/^[[:space:]]*//' > .env.new
GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}
GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}
MISTRAL_API_KEY=${{ secrets.MISTRAL_API_KEY }}
DEEPSEEK_API_KEY=${{ secrets.DEEPSEEK_API_KEY }}
XAI_API_KEY=${{ secrets.XAI_API_KEY }}
OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}
OPENROUTER_ALLOW_PAID=${{ vars.OPENROUTER_ALLOW_PAID }}
PINATA_JWT=${{ secrets.PINATA_JWT }}
PINATA_GATEWAY=${{ secrets.PINATA_GATEWAY }}
SPOTIFY_CLIENT_ID=${{ secrets.SPOTIFY_CLIENT_ID }}
SPOTIFY_CLIENT_SECRET=${{ secrets.SPOTIFY_CLIENT_SECRET }}
LASTFM_API_KEY=${{ secrets.LASTFM_API_KEY }}
DISCOGS_API_KEY=${{ secrets.DISCOGS_API_KEY }}
DISCOGS_SECRETS=${{ secrets.DISCOGS_SECRETS }}
ACOUSTID_API=${{ secrets.ACOUSTID_API }}
ACOUSTID_API_TOKEN=${{ secrets.ACOUSTID_API_TOKEN }}
STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET=${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_PRICE_PACK_STARTER=${{ secrets.STRIPE_PRICE_PACK_STARTER }}
STRIPE_PRICE_PACK_PRODUCER=${{ secrets.STRIPE_PRICE_PACK_PRODUCER }}
STRIPE_PRICE_PACK_LABEL=${{ secrets.STRIPE_PRICE_PACK_LABEL }}
STRIPE_PRICE_PACK_STUDIO=${{ secrets.STRIPE_PRICE_PACK_STUDIO }}
CORS_ORIGINS=https://app-metadata.hardbanrecordslab.online
SECRET_KEY=${{ secrets.SECRET_KEY }}
API_CLIENT_ID=${{ secrets.API_CLIENT_ID }}
API_CLIENT_SECRET=${{ secrets.API_CLIENT_SECRET }}
APP_BASE_URL=https://app-metadata.hardbanrecordslab.online
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
EOF
# Re-attach the production DATABASE_URL and the R2 credentials, re-check the DB line,
# then swap the file in atomically.
printf '%s\n' "$OLD_DB_LINE" >> .env.new
printf '%s\n' "$OLD_R2_LINES" >> .env.new
printf '%s\n' "$OLD_SMTP_LINES" >> .env.new
grep -Eq "$DB_RE" .env.new || { echo "FAIL: assembled .env lost the Postgres DATABASE_URL - keeping the old .env"; rm -f .env.new; exit 1; }
chmod --reference=.env .env.new
mv .env.new .env
# Code: fast-forward only. Fails (instead of silently losing or mixing commits) if the
# VPS checkout ever diverges from origin/main again.
git fetch origin main
git checkout main
git merge --ff-only origin/main
# Keep the running image as a rollback target, then build + swap. No `compose down`:
# `up -d --build` builds first and only then replaces the container, so a failed build
# leaves the old container serving.
docker tag music-metadata-engine:latest music-metadata-engine:previous 2>/dev/null || true
docker compose -f "$COMPOSE_FILE" up -d --build
# Health-gated: container healthy AND still on production Postgres, else roll back.
healthy=0
status=unknown
for i in $(seq 1 30); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' metadata-backend 2>/dev/null || echo missing)"
if [ "$status" = "healthy" ]; then healthy=1; break; fi
sleep 10
done
CONTAINER_DB="$( (docker exec metadata-backend printenv DATABASE_URL 2>/dev/null | sed -E 's#(://[^:]+:)[^@]+@#\1***@#') || true )"
echo "container health: $status | DATABASE_URL: ${CONTAINER_DB:-<none>}"
if [ "$healthy" != "1" ] || ! printf '%s' "$CONTAINER_DB" | grep -Eq '@hbrl-postgres:5432/metadata_engine'; then
echo "FAIL: post-deploy verification failed - rolling back to the previous image"
docker tag music-metadata-engine:previous music-metadata-engine:latest
docker compose -f "$COMPOSE_FILE" up -d --no-build
exit 1
fi
curl -fsS http://127.0.0.1:8888/api/health/diagnose | grep -q '"llm"' || echo "WARN: /api/health/diagnose has no llm block"
echo "Deploy OK"