# ERPFlow CI/CD

GitHub Actions validates every pull request and every push to `development`. A push to `development` deploys **only after** backend tests, frontend lint/build, and the deploy-script check succeed. `main` is **not** a deploy branch.

This does not change the production topology: Linux VPS, Docker Compose (`docker-compose.prod.yml`), git checkout on the server. Production secrets stay in `backend/.env` on the server.

## CI

Workflow: `.github/workflows/ci.yml`

| Job | What it runs | Required |
| --- | --- | --- |
| `backend-tests` | PHP 8.4, `composer validate`, `composer install` from `composer.lock`, `php artisan test` | Yes |
| `frontend-quality` | Node 22, `npm ci`, `npm run lint`, `npm run build` | Yes |
| `deploy-script-check` | `bash -n` + `scripts/deploy-production.sh --dry-run` | Yes |
| `production-deploy` | SSH to the VPS and deploy `github.sha` | Yes, on `development` only |

Pint is **not** a CI gate.

CI does not use developer `.env`, local Docker, MySQL, Redis, or production secrets. PHPUnit uses SQLite `:memory:` from `backend/phpunit.xml`. `APP_KEY` / `JWT_SECRET` in CI are dummy testing values (≥256-bit JWT secret; same values as `backend/.env.testing` / `phpunit.xml`).

Frontend CI build compiles the SPA only. It is **not** the production image. Production bakes `VITE_API_URL` from the **server** `backend/.env` during `docker compose build`.

## CD

1. Push (or merge) to `development`.
2. CI jobs above must pass.
3. `production-deploy` SSHes to the VPS.
4. The server materializes `scripts/deploy-production.sh` from the **exact SHA that passed CI** into `$PRODUCTION_DEPLOY_PATH/scripts/` (not a temp file), then runs `./scripts/deploy-production.sh <sha>` with `PRODUCTION_DEPLOY_PATH` set so `REPO_ROOT` is the clone.
5. The script: backup database → `git checkout --force -B development <sha>` → rebuild Compose stack → `composer install --no-dev` → `php artisan migrate --force` → cache / Horizon reload → health checks.
6. If a step after checkout fails, the script restores the **previous Git SHA**, rebuilds, and health-checks again. It does **not** roll back migrations.

Do not use `git pull` for production. The deployed revision is the 40-character SHA.

Manual equivalent on the server (after CI is green):

```bash
make prod-deploy SHA=<40-char-sha>
```

### One-time server setup

The VPS must already have the existing production clone, Docker Compose stack, and `backend/.env`. Also:

1. `git` remote `origin` can fetch this GitHub repository.
2. Deploy user can run `docker` / `docker compose`.
3. Backup directory is writable:

   ```bash
   sudo mkdir -p /var/backups/erpflow
   sudo chown "$USER":"$USER" /var/backups/erpflow
   chmod 700 /var/backups/erpflow
   ```

4. `backend/.env` has `APP_ENV=production` and a **public** `VITE_API_URL` on edge port **8088** (not `localhost`, not `:8010`). Example: `VITE_API_URL=http://YOUR_PUBLIC_HOST:8088/api/v1` (match `docker/nginx/host-edge.conf`).
5. Create a GitHub Environment named `production` (optional protection rules: required reviewers).
6. Add the GitHub Secrets below.
7. In branch protection on `development`, require `backend-tests`, `frontend-quality`, and `deploy-script-check`. Do **not** require `production-deploy` on pull requests (that job is skipped except on `development`).

The first automated deploy can run as soon as a `development` commit contains `scripts/deploy-production.sh`. The workflow writes that blob into `$PRODUCTION_DEPLOY_PATH/scripts/deploy-production.sh` and executes it from the clone (so `REPO_ROOT` is never a temp directory).

### Rollback

Application rollback is automatic when deploy/health fails after the revision switch.

Manual application rollback to a known-good SHA (still no migration rollback):

```bash
make prod-deploy SHA=<previous-good-sha>
```

Database restore is a **separate** operator action from a file under `/var/backups/erpflow/`. Do not automate `migrate:rollback` on production.

## Required GitHub Secrets

Set these on the repository (and/or the `production` environment). Values are never committed.

| Secret | Purpose |
| --- | --- |
| `PRODUCTION_SSH_HOST` | VPS hostname or IP |
| `PRODUCTION_SSH_USER` | SSH user that can deploy |
| `PRODUCTION_SSH_KEY` | Private key for that user (deploy-only key) |
| `PRODUCTION_SSH_FINGERPRINT` | SHA256 host key fingerprint (`ssh-keygen -lf <(ssh-keyscan -t ed25519 HOST)`) |
| `PRODUCTION_DEPLOY_PATH` | Absolute path to the git clone on the server |
| `PRODUCTION_SSH_PORT` | Optional. Defaults to `22`. Use `2222` when the VPS SSH daemon is not on 22 (e.g. InMotion). |

Do not put `backend/.env`, `APP_KEY`, database passwords, JWT secrets, or `VITE_API_URL` in GitHub Secrets.

## Health checks

After deploy the script verifies:

- required Compose containers are `running`
- MySQL ping inside `erpflow-mysql`
- Redis ping inside `erpflow-redis`
- HTTP `GET /up` on loopback `:8010` (Laravel health)
- HTTP `GET /up` on edge `:8088` (`http://127.0.0.1:8088/up`)
- HTTP `GET /` on frontend `:3010`
- `php artisan horizon:status` in `erpflow-queue`
- CORS preflight: `OPTIONS /api/v1/auth/login` with `Origin` derived from `VITE_API_URL` host + `FRONTEND_PROD_PORT` (must return `Access-Control-Allow-Origin`)

Module APIs such as `/api/v1/attendance/health` require auth and are not used as deploy probes.

`backend/.env` is **never** regenerated by CI/CD (gitignored; no `git clean`). Deploy only **reads** it for `VITE_API_URL` / compose and re-runs `config:cache`. CORS allow-list is built by `App\Support\CorsOriginResolver` for **local and server**: `CORS_ALLOWED_ORIGINS` + `FRONTEND_URL` + `VITE_API_URL` host on `FRONTEND_PROD_PORT` / `FRONTEND_DEV_PORT` (loopback expands `localhost` ↔ `127.0.0.1`).

Deploy also fixes a bind-mount permission leak: checkout under umask `077` left PHP files `0600` / dirs `0700`, so php-fpm (`www-data`) could not read the tree, Laravel never booted, and the browser reported CORS. `fix_bindmount_permissions` (and a post-deploy `chmod` in CI) restores world-readable source without touching `.env`.

## Safety rules

- Failed database backup stops the deploy.
- Failed migration stops the deploy (then application SHA rollback).
- Failed health check (including CORS preflight) stops the deploy (then application SHA rollback).
- No `migrate:fresh`, `migrate:refresh`, or data-drop flags.
- No `git clean` (that would delete gitignored `backend/.env`).
- Database dumps are written outside the git clone (`/var/backups/erpflow` by default).
- Backup dump uses umask `077` only for the dump file, then restores the previous umask so checkout does not inherit it.
- Prod `backend` has a php-fpm healthcheck so nginx does not 502 (browsers misreport that as CORS) during restart.