# Deployment (CI/CD) Guide

This project deploys automatically to our VPS through **GitHub Actions**. Push to `main` and the changed app is deployed within a couple of minutes. This guide explains how the pipeline works, how it was set up, and how to fix it when something breaks.

---

## 1. Overview

| | Admin Frontend | Backend API |
|---|---|---|
| Folder in repo | `admin-frontend/` | `backend/` |
| Folder on server | `/var/www/speaking-apk/admin-frontend` | `/var/www/speaking-apk/backend` |
| Port | `3063` | `3064` |
| URL | http://82.25.108.195:3063 | http://82.25.108.195:3064/api |
| PM2 process name | `speaking-app-admin` | `speaking-api` |
| Workflow file | `.github/workflows/deploy-frontend.yml` | `.github/workflows/deploy-backend.yml` |

The server (`srv1036398`) hosts many other projects under PM2. **Never restart, stop or edit any PM2 process other than the two above.**

---

## 2. How the flow works

```
Developer pushes / merges into main
            │
            ▼
GitHub Actions checks which folder changed
            │
   ┌────────┴─────────┐
   ▼                  ▼
backend/**        admin-frontend/**
changed?           changed?
   │                  │
   ▼                  ▼
Deploy Backend    Deploy Admin Frontend
   │                  │
   └──── SSH into VPS (82.25.108.195) ────┐
                                          ▼
                  cd /var/www/speaking-apk
                  git fetch origin main
                  git reset --hard origin/main
                                          │
          ┌───────────────────────────────┴──────────────┐
          ▼                                              ▼
  cd backend                                 cd admin-frontend
  npm ci --omit=dev                          write .env (VITE_API_URL)
  pm2 restart speaking-api --update-env      npm ci && npm run build
                                             pm2 restart speaking-app-admin
```

Key points:

- **Only `main` deploys.** Pushing to feature branches does nothing. Merge into `main` to release.
- **Only the changed app deploys.** A change in `backend/` deploys only the backend, and a change in `admin-frontend/` deploys only the frontend. Editing root files such as `PLAN.md` or this guide deploys nothing.
- **Deploys never run in parallel.** Both workflows share one checkout on the server, so they run one after the other.
- **The code is built on the server**, not on GitHub. The runner only opens an SSH session and runs commands.
- **Server-only files are never touched:** `backend/.env`, the Firebase service-account JSON, `backend/uploads/` and `node_modules/`. They are gitignored, and `git reset --hard` leaves untracked files alone.

---

## 3. Day-to-day usage

### Release a change
1. Work on your own branch.
2. Open a Pull Request into `main` and get it reviewed.
3. Merge it. The deploy starts automatically.
4. Watch it under GitHub → **Actions**. A green ✅ means it is live.

### Deploy manually (without a new commit)
GitHub → **Actions** → pick **Deploy Backend** or **Deploy Admin Frontend** → **Run workflow** → branch `main` → **Run workflow**.

### Skip a deploy for a commit
Add `[skip ci]` to the commit message.

### Check the apps on the server
```bash
pm2 list | grep speaking
pm2 logs speaking-api --lines 50 --nostream
pm2 logs speaking-app-admin --lines 50 --nostream
```

---

## 4. What the pipeline does NOT do

Do these by hand on the server:

| Task | How |
|---|---|
| Change backend env variables | Edit `/var/www/speaking-apk/backend/.env`, then run `pm2 restart speaking-api --update-env` |
| Replace the Firebase key | Upload the JSON into `/var/www/speaking-apk/backend/` and update `FIREBASE_SERVICE_ACCOUNT_PATH` in `.env` |
| Run DB migrations | Run the new SQL file from `backend/src/config/migrations/` against the server MySQL **before** merging code that needs it |
| Change the frontend API URL | Update the `VITE_API_URL` GitHub secret, then re-run **Deploy Admin Frontend** |

---

## 5. GitHub secrets

GitHub → repo → **Settings → Secrets and variables → Actions**

| Secret | Value | Used for |
|---|---|---|
| `VPS_HOST` | `82.25.108.195` | Server to SSH into |
| `VPS_USER` | `root` | SSH user |
| `VPS_SSH_PORT` | `22` | SSH port |
| `VPS_SSH_KEY` | Private key from `/root/.ssh/gh_speaking` | Lets GitHub Actions log in to the server |
| `VITE_API_URL` | `http://82.25.108.195:3064/api` | Baked into the frontend build |

Never paste private keys into code, chat or tickets. Store them only in GitHub secrets.

---

## 6. One-time setup (already done, kept for reference)

Follow these steps only when setting up a new server or rebuilding this one.

### 6.1 Key for GitHub Actions to log in to the server
```bash
ssh-keygen -t ed25519 -f ~/.ssh/gh_speaking -N "" -C "gh-speaking-apk"
cat ~/.ssh/gh_speaking.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/gh_speaking          # paste the output into the VPS_SSH_KEY secret
```

### 6.2 Key for the server to pull from GitHub (read-only deploy key)
```bash
ssh-keygen -t ed25519 -f ~/.ssh/speaking_repo -N "" -C "speaking-repo-read"
cat ~/.ssh/speaking_repo.pub
```
Add the public key under GitHub → repo → **Settings → Deploy keys**. Leave **Allow write access** unchecked.

### 6.3 Turn the server folder into a git checkout
```bash
cd /var/www/speaking-apk
git init -b main
git config core.sshCommand "ssh -i ~/.ssh/speaking_repo -o IdentitiesOnly=yes"
git remote add origin git@github.com:gtdteam32-coder/english_speaking_portal.git
git fetch origin main
git reset --hard origin/main
```
`core.sshCommand` is set only in this repo's `.git/config`, so other projects on the server are not affected.

### 6.4 Server-only files
- `backend/.env`: set `PORT=3064`, `NODE_ENV=production`, the DB credentials, the JWT secrets and `ADMIN_FRONTEND_URL=http://82.25.108.195:3063`. See `backend/.env.example`.
- `backend/btalk-eng-firebase-adminsdk-*.json`: the Firebase service-account key.

### 6.5 PM2 processes (created once, only restarted by the pipeline)
```bash
cd /var/www/speaking-apk/backend && pm2 start src/server.js --name speaking-api
cd /var/www/speaking-apk/admin-frontend && pm2 serve dist 3063 --name speaking-app-admin --spa
pm2 save
```

---

## 7. Troubleshooting

To read the error, open GitHub → **Actions** → the red run → **deploy** → expand **Deploy ... over SSH**.

| Symptom in the log | Cause | Fix |
|---|---|---|
| `Process exited with status 1` with no other output | A script line failed before printing anything | Check the workflow script. Every line must succeed. |
| `ssh: handshake failed` / `unable to authenticate` | `VPS_SSH_KEY` is wrong, or the public key is missing from `authorized_keys` | Re-paste the full key including the `BEGIN`/`END` lines, and check it with `grep gh-speaking-apk ~/.ssh/authorized_keys` |
| `dial tcp ... i/o timeout` | Wrong `VPS_HOST` or `VPS_SSH_PORT` | Fix the secret |
| `Permission denied (publickey)` during `git fetch` | Deploy key removed or not configured | Redo step 6.2 and check `git config core.sshCommand` |
| `npm: command not found` / `pm2: command not found` | Node or PM2 is not on the non-interactive `PATH` | Tell the team lead. Do not change the server's global config. |
| `vite build` errors (`MISSING_EXPORT` etc.) | Broken code on `main` | Run `npm run build` locally before merging, and fix the code |
| Backend restarts but APIs fail | Missing DB migration or a wrong `.env` value | Check `pm2 logs speaking-api`, then run the migration or fix `.env` |

**Lesson learned:** during a merge conflict we once lost an exported function (`assetUrl`), and the frontend build failed on the server. **Always run `npm run build` in `admin-frontend/` and start the backend locally before merging into `main`.**

---

## 8. Rolling back

Revert the bad commit on `main`. The revert deploys automatically:
```bash
git checkout main
git pull
git revert <bad-commit-hash>
git push origin main
```
Do not run `git reset` on the server by hand to roll back. The next deploy would overwrite it anyway.
