Skip to content

Commit a4e735f

Browse files
committed
feat(api,web): add deploy workflows, docker bootstrap, and vinted publishing
Made-with: Cursor
1 parent 0d9381a commit a4e735f

68 files changed

Lines changed: 4465 additions & 483 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-api.yml

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Déploie l’API FastAPI (dossier api/) sur le VPS — venv + systemd + Nginx reload.
2+
# La base MariaDB peut être distante (ex. o2switch) : DATABASE_URL dans les secrets.
3+
4+
name: Deploy API (FastAPI)
5+
6+
on:
7+
push:
8+
branches: [main]
9+
paths:
10+
- 'api/**'
11+
- '.github/workflows/deploy-api.yml'
12+
pull_request:
13+
branches: [main]
14+
paths:
15+
- 'api/**'
16+
workflow_dispatch:
17+
18+
concurrency:
19+
group: goupixdex-api-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
lint-pr:
24+
name: Sanity check (PR — sans déploiement)
25+
if: github.event_name == 'pull_request'
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
with:
31+
sparse-checkout: |
32+
api
33+
sparse-checkout-cone: true
34+
35+
- name: Setup Python
36+
uses: actions/setup-python@v5
37+
with:
38+
python-version: '3.12'
39+
40+
- name: Install deps & import check
41+
run: |
42+
python -m pip install --upgrade pip
43+
pip install -r api/requirements.txt
44+
cd api && PYTHONPATH=. python -c "import main; print('import ok')"
45+
46+
deploy:
47+
name: Deploy API
48+
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
49+
runs-on: ubuntu-latest
50+
steps:
51+
- name: Checkout (api only)
52+
uses: actions/checkout@v4
53+
with:
54+
fetch-depth: 1
55+
sparse-checkout: |
56+
api
57+
sparse-checkout-cone: true
58+
59+
- name: Write api/.env for upload
60+
env:
61+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
62+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
63+
SEED_USER_EMAIL: ${{ secrets.SEED_USER_EMAIL }}
64+
SEED_USER_PASSWORD: ${{ secrets.SEED_USER_PASSWORD }}
65+
SEED_MARGIN_PERCENT: ${{ secrets.SEED_MARGIN_PERCENT }}
66+
SEED_DEV_ARTICLES: ${{ secrets.SEED_DEV_ARTICLES }}
67+
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
68+
SUPABASE_API_KEY: ${{ secrets.SUPABASE_API_KEY }}
69+
SUPABASE_STORAGE_BUCKET: ${{ secrets.SUPABASE_STORAGE_BUCKET }}
70+
POKE_WALLET_API_KEY: ${{ secrets.POKE_WALLET_API_KEY }}
71+
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
72+
VINTED_EMAIL_OR_USERNAME: ${{ secrets.VINTED_EMAIL_OR_USERNAME }}
73+
VINTED_PASSWORD: ${{ secrets.VINTED_PASSWORD }}
74+
CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }}
75+
run: |
76+
set -euo pipefail
77+
f=api/.env
78+
{
79+
echo "DATABASE_URL=${DATABASE_URL}"
80+
echo "JWT_SECRET=${JWT_SECRET}"
81+
echo "SEED_USER_EMAIL=${SEED_USER_EMAIL:-}"
82+
echo "SEED_USER_PASSWORD=${SEED_USER_PASSWORD:-}"
83+
echo "SEED_MARGIN_PERCENT=${SEED_MARGIN_PERCENT:-60}"
84+
echo "SEED_DEV_ARTICLES=${SEED_DEV_ARTICLES:-0}"
85+
echo "SUPABASE_URL=${SUPABASE_URL:-}"
86+
echo "SUPABASE_API_KEY=${SUPABASE_API_KEY:-}"
87+
echo "SUPABASE_STORAGE_BUCKET=${SUPABASE_STORAGE_BUCKET:-GoupixDex}"
88+
echo "POKE_WALLET_API_KEY=${POKE_WALLET_API_KEY:-}"
89+
echo "GROQ_API_KEY=${GROQ_API_KEY:-}"
90+
echo "VINTED_EMAIL_OR_USERNAME=${VINTED_EMAIL_OR_USERNAME:-}"
91+
echo "VINTED_PASSWORD=${VINTED_PASSWORD:-}"
92+
echo "CORS_ORIGINS=${CORS_ORIGINS:-*}"
93+
} > "$f"
94+
95+
- name: Pack api tree
96+
run: tar -czf api-bundle.tar.gz api
97+
98+
- name: Upload to VPS
99+
uses: appleboy/scp-action@v0.1.7
100+
with:
101+
host: ${{ secrets.SSH_HOST }}
102+
port: ${{ secrets.SSH_PORT }}
103+
username: ${{ secrets.SSH_USERNAME }}
104+
key: ${{ secrets.SSH_PRIVATE_KEY }}
105+
source: api-bundle.tar.gz
106+
target: ${{ secrets.DEPLOY_API_DIR }}
107+
108+
- name: Extract, venv, systemd, nginx
109+
uses: appleboy/ssh-action@v1.0.3
110+
env:
111+
DEPLOY_API_DIR: ${{ secrets.DEPLOY_API_DIR }}
112+
API_SYSTEMD_SERVICE: ${{ secrets.API_SYSTEMD_SERVICE }}
113+
API_PORT: ${{ secrets.API_APP_PORT }}
114+
DEPLOY_USER: ${{ secrets.SSH_USERNAME }}
115+
with:
116+
host: ${{ secrets.SSH_HOST }}
117+
port: ${{ secrets.SSH_PORT }}
118+
username: ${{ secrets.SSH_USERNAME }}
119+
key: ${{ secrets.SSH_PRIVATE_KEY }}
120+
envs: DEPLOY_API_DIR,API_SYSTEMD_SERVICE,API_PORT,DEPLOY_USER
121+
script_stop: true
122+
script: |
123+
set -euo pipefail
124+
TARGET="${DEPLOY_API_DIR%/}"
125+
SVC="${API_SYSTEMD_SERVICE:-goupixdex-api.service}"
126+
PORT="${API_PORT:-8000}"
127+
mkdir -p "$TARGET"
128+
rm -rf "$TARGET/api"
129+
tar -xzf "$TARGET/api-bundle.tar.gz" -C "$TARGET"
130+
rm -f "$TARGET/api-bundle.tar.gz"
131+
APP_ROOT="$TARGET/api"
132+
sudo chown -R "$USER:$USER" "$APP_ROOT" || true
133+
sudo apt-get update -y
134+
sudo apt-get install -y python3-venv python3-pip
135+
if [ ! -x "$APP_ROOT/.venv/bin/python" ]; then
136+
python3 -m venv "$APP_ROOT/.venv"
137+
fi
138+
. "$APP_ROOT/.venv/bin/activate"
139+
python -m pip install --upgrade pip wheel
140+
pip install -r "$APP_ROOT/requirements.txt" --no-cache-dir
141+
deactivate
142+
# Migrations SQL (fichiers non déjà enregistrés dans schema_migrations)
143+
"$APP_ROOT/.venv/bin/python" "$APP_ROOT/migrations/run_migrations.py"
144+
# Bootstrap DB si tables vides (user / marges ; articles seulement si SEED_DEV_ARTICLES=1 sur le VPS)
145+
"$APP_ROOT/.venv/bin/python" "$APP_ROOT/seeders/conditional_seed.py"
146+
sudo tee "/etc/systemd/system/${SVC}" > /dev/null <<EOF
147+
[Unit]
148+
Description=GoupixDex FastAPI (Uvicorn)
149+
After=network.target
150+
151+
[Service]
152+
Type=simple
153+
User=${DEPLOY_USER}
154+
WorkingDirectory=${APP_ROOT}
155+
EnvironmentFile=${APP_ROOT}/.env
156+
ExecStart=${APP_ROOT}/.venv/bin/uvicorn main:app --host 127.0.0.1 --port ${PORT} --workers 2 --proxy-headers
157+
Restart=always
158+
RestartSec=5
159+
160+
[Install]
161+
WantedBy=multi-user.target
162+
EOF
163+
sudo systemctl daemon-reload
164+
sudo systemctl enable "$SVC"
165+
sudo systemctl restart "$SVC"
166+
sudo systemctl --no-pager --full status "$SVC" || true
167+
sudo nginx -t
168+
sudo systemctl reload nginx

.github/workflows/deploy-web.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Déploie le frontend Nuxt 4 (dossier web/) sur le VPS — Nitro node-server + PM2.
2+
# Secrets requis : voir README déploiement ou message de configuration du dépôt.
3+
4+
name: Deploy web (Nuxt)
5+
6+
on:
7+
push:
8+
branches: [main]
9+
paths:
10+
- 'web/**'
11+
- '.github/workflows/deploy-web.yml'
12+
pull_request:
13+
branches: [main]
14+
paths:
15+
- 'web/**'
16+
workflow_dispatch:
17+
18+
concurrency:
19+
group: goupixdex-web-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
build-pr:
24+
name: Build (PR — sans déploiement)
25+
if: github.event_name == 'pull_request'
26+
runs-on: ubuntu-latest
27+
defaults:
28+
run:
29+
working-directory: web
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: '22'
38+
cache: npm
39+
cache-dependency-path: web/package-lock.json
40+
41+
- name: Install & build
42+
env:
43+
NUXT_PUBLIC_API_BASE: https://example.com
44+
NUXT_PUBLIC_SITE_URL: https://example.com
45+
run: |
46+
npm ci
47+
npm run build
48+
49+
deploy:
50+
name: Build & deploy
51+
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
52+
runs-on: ubuntu-latest
53+
defaults:
54+
run:
55+
working-directory: web
56+
steps:
57+
- name: Checkout
58+
uses: actions/checkout@v4
59+
60+
- name: Setup Node.js
61+
uses: actions/setup-node@v4
62+
with:
63+
node-version: '22'
64+
cache: npm
65+
cache-dependency-path: web/package-lock.json
66+
67+
- name: Install dependencies
68+
run: npm ci
69+
70+
- name: Write production .env (build)
71+
env:
72+
NUXT_PUBLIC_API_BASE: ${{ secrets.NUXT_PUBLIC_API_BASE }}
73+
NUXT_PUBLIC_SITE_URL: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
74+
run: |
75+
{
76+
echo "NODE_ENV=production"
77+
echo "NUXT_PUBLIC_API_BASE=${NUXT_PUBLIC_API_BASE}"
78+
echo "NUXT_PUBLIC_SITE_URL=${NUXT_PUBLIC_SITE_URL}"
79+
} > .env
80+
81+
- name: Build Nuxt (Nitro)
82+
env:
83+
NUXT_BUILD_COMMIT: ${{ github.sha }}
84+
run: npm run build
85+
86+
- name: Pack .output
87+
working-directory: ${{ github.workspace }}
88+
run: tar -czf nuxt-output.tar.gz -C web .output
89+
90+
- name: Upload bundle to VPS
91+
uses: appleboy/scp-action@v0.1.7
92+
with:
93+
host: ${{ secrets.SSH_HOST }}
94+
port: ${{ secrets.SSH_PORT }}
95+
username: ${{ secrets.SSH_USERNAME }}
96+
key: ${{ secrets.SSH_PRIVATE_KEY }}
97+
source: nuxt-output.tar.gz
98+
target: ${{ secrets.DEPLOY_WEB_DIR }}
99+
100+
- name: Extract, PM2 & Nginx
101+
uses: appleboy/ssh-action@v1.0.3
102+
env:
103+
DEPLOY_WEB_DIR: ${{ secrets.DEPLOY_WEB_DIR }}
104+
WEB_PM2_APP_NAME: ${{ secrets.WEB_PM2_APP_NAME }}
105+
WEB_APP_PORT: ${{ secrets.WEB_APP_PORT }}
106+
with:
107+
host: ${{ secrets.SSH_HOST }}
108+
port: ${{ secrets.SSH_PORT }}
109+
username: ${{ secrets.SSH_USERNAME }}
110+
key: ${{ secrets.SSH_PRIVATE_KEY }}
111+
envs: DEPLOY_WEB_DIR,WEB_PM2_APP_NAME,WEB_APP_PORT
112+
script_stop: true
113+
script: |
114+
set -euo pipefail
115+
DEPLOY="${DEPLOY_WEB_DIR%/}"
116+
APP_NAME="${WEB_PM2_APP_NAME:-goupixdex-web}"
117+
WEB_PORT="${WEB_APP_PORT:-3000}"
118+
mkdir -p "$DEPLOY"
119+
tar -xzf "$DEPLOY/nuxt-output.tar.gz" -C "$DEPLOY"
120+
rm -f "$DEPLOY/nuxt-output.tar.gz"
121+
if ! command -v node >/dev/null 2>&1; then
122+
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
123+
sudo apt-get install -y nodejs
124+
fi
125+
if ! command -v pm2 >/dev/null 2>&1; then
126+
sudo npm install -g pm2
127+
fi
128+
cd "$DEPLOY/.output"
129+
pm2 describe "$APP_NAME" >/dev/null 2>&1 && pm2 delete "$APP_NAME" || true
130+
PORT="$WEB_PORT" HOST=127.0.0.1 NODE_ENV=production pm2 start server/index.mjs --name "$APP_NAME"
131+
pm2 save
132+
pm2 list
133+
sudo nginx -t
134+
sudo systemctl reload nginx

api/.env.example

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,14 @@
1-
# Database
2-
# Local dev (Python on host): point to localhost or Docker-published port.
31
DATABASE_URL=mysql+pymysql://goupix:goupix@127.0.0.1:3306/goupixdex
4-
# Docker Compose: the compose file sets DATABASE_URL to host "mariadb" for the api service
5-
# (overrides this line via process environment). Keep 127.0.0.1 for running uvicorn on the host.
6-
7-
# JWT
82
JWT_SECRET=change-me-to-a-long-random-string
9-
10-
# Optional: seed default user (python seeders/user_seeder.py)
113
SEED_USER_EMAIL=you@example.com
124
SEED_USER_PASSWORD=your-secure-password
13-
14-
# Dev: ensure margin row for users missing settings (default 20%)
15-
# SEED_MARGIN_PERCENT=20
16-
17-
# Dev: fake articles — uses SEED_USER_EMAIL if SEED_ARTICLE_USER_EMAIL is empty
18-
# SEED_ARTICLE_USER_EMAIL=you@example.com
19-
20-
# Run all seeders in order: python seeders/run_all.py
21-
22-
# APIs
5+
SEED_MARGIN_PERCENT=60
6+
SEED_DEV_ARTICLES=0
7+
SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
8+
SUPABASE_API_KEY=
9+
SUPABASE_STORAGE_BUCKET=GoupixDex
2310
POKE_WALLET_API_KEY=
2411
GROQ_API_KEY=
25-
26-
# Vinted: copied to users.vinted_email / users.vinted_password (Fernet-encrypted with JWT_SECRET) when you run user_seeder
27-
# (both keys required to sync). Used by cli_vinted_listings.py and publish fallback if DB fields are empty.
2812
VINTED_EMAIL_OR_USERNAME=
2913
VINTED_PASSWORD=
30-
31-
# When true, article creation skips real browser automation (recommended in dev)
32-
VINTED_PUBLISH_STUB=true
33-
34-
# CORS (comma-separated or *)
3514
CORS_ORIGINS=*

api/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ FROM python:3.12-slim-bookworm
33
WORKDIR /app
44

55
COPY requirements.txt .
6-
RUN pip install --no-cache-dir -r requirements.txt
6+
RUN pip install --no-cache-dir -r requirements.txt \
7+
&& python -c "from supabase import create_client"
78

89
COPY . .
910

0 commit comments

Comments
 (0)