feat(cd): add continuous deployment on push to master
Some checks failed
CI / ruff (push) Successful in 8s
CI / pytest (push) Successful in 36m19s
CI / architecture (push) Successful in 11s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 9s
CI / docs (push) Failing after 59s
CI / deploy (push) Failing after 3s
Some checks failed
CI / ruff (push) Successful in 8s
CI / pytest (push) Successful in 36m19s
CI / architecture (push) Successful in 11s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 9s
CI / docs (push) Failing after 59s
CI / deploy (push) Failing after 3s
Deploy job SSHes to production after ruff/pytest/architecture pass, running scripts/deploy.sh (stash, pull, docker rebuild, migrate, health check). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -205,10 +205,10 @@ Configure these in your Gitea repository under **Settings > Actions > Secrets**:
|
||||
|
||||
| Secret | Description | Used by |
|
||||
|--------|-------------|---------|
|
||||
| `SSH_PRIVATE_KEY` | Private key for deployment server | Deploy job (if added) |
|
||||
| `SERVER_HOST` | Production server IP/hostname | Deploy job |
|
||||
| `SERVER_USER` | SSH user on production server | Deploy job |
|
||||
| `SERVER_PATH` | App directory on server | Deploy job |
|
||||
| `DEPLOY_SSH_KEY` | Ed25519 private key for deployment | Deploy job |
|
||||
| `DEPLOY_HOST` | Production server IP (e.g. `127.0.0.1`) | Deploy job |
|
||||
| `DEPLOY_USER` | SSH user on production server (e.g. `samir`) | Deploy job |
|
||||
| `DEPLOY_PATH` | App directory on server (e.g. `/home/samir/apps/orion`) | Deploy job |
|
||||
|
||||
---
|
||||
|
||||
@@ -223,16 +223,17 @@ push/PR to master
|
||||
├── architecture (architecture validation)
|
||||
├── dependency-scanning (pip-audit, non-blocking)
|
||||
├── audit (custom audit, non-blocking)
|
||||
└── docs (mkdocs build, master-only, after lint+test pass)
|
||||
├── docs (mkdocs build, master-only, after lint+test pass)
|
||||
└── deploy (SSH deploy, master-only, after lint+test+arch pass)
|
||||
```
|
||||
|
||||
All jobs run in parallel except `docs`, which waits for `ruff`, `pytest`, and `architecture` to pass.
|
||||
All jobs run in parallel except `docs` and `deploy`, which wait for `ruff`, `pytest`, and `architecture` to pass. The `deploy` job only runs on push (not PRs).
|
||||
|
||||
---
|
||||
|
||||
## 9. Adding a Deploy Job (Optional)
|
||||
## 9. Deploy Job (Continuous Deployment)
|
||||
|
||||
To add automated deployment via SSH (similar to the GitLab deploy stage), add this job to `.gitea/workflows/ci.yml`:
|
||||
The CI pipeline includes an automated deploy job that runs on every successful push to master. It SSHes to the production server and runs a version-controlled deploy script:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
@@ -240,23 +241,28 @@ To add automated deployment via SSH (similar to the GitLab deploy stage), add th
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: [ruff, pytest, architecture]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy via SSH
|
||||
- name: Deploy to production
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd ${{ secrets.SERVER_PATH }}
|
||||
git pull origin master
|
||||
source .venv/bin/activate
|
||||
uv sync --frozen
|
||||
python -m alembic upgrade head
|
||||
sudo systemctl restart wizamart
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
port: 22
|
||||
command_timeout: 10m
|
||||
script: cd ${{ secrets.DEPLOY_PATH }} && bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
The `scripts/deploy.sh` script handles the full deploy lifecycle:
|
||||
|
||||
1. Stash local changes (preserves `.env` and other server-side edits)
|
||||
2. Pull latest code (`--ff-only`)
|
||||
3. Pop stash to restore local changes
|
||||
4. Rebuild and restart Docker containers (`docker compose --profile full up -d --build`)
|
||||
5. Run database migrations (`alembic upgrade heads`)
|
||||
6. Health check `http://localhost:8001/health` with retries
|
||||
|
||||
See [Hetzner Server Setup — Step 16](hetzner-server-setup.md#step-16-continuous-deployment) for the full setup guide including SSH key generation and Gitea secrets configuration.
|
||||
|
||||
---
|
||||
|
||||
## 10. Firewall Configuration
|
||||
|
||||
@@ -14,7 +14,7 @@ Complete step-by-step guide for deploying Wizamart on a Hetzner Cloud VPS.
|
||||
- **Setup date**: 2026-02-11
|
||||
|
||||
!!! success "Progress — 2026-02-12"
|
||||
**Completed (Steps 1–15):**
|
||||
**Completed (Steps 1–16):**
|
||||
|
||||
- Non-root user `samir` with SSH key
|
||||
- Server hardened (UFW firewall, SSH root login disabled, fail2ban)
|
||||
@@ -45,10 +45,10 @@ Complete step-by-step guide for deploying Wizamart on a Hetzner Cloud VPS.
|
||||
- AAAA (IPv6) records added for all wizard.lu domains
|
||||
- mkdocs build clean (zero warnings) — all 32 orphan pages added to nav
|
||||
- Pre-commit documented in `docs/development/code-quality.md`
|
||||
- **Step 16: Continuous deployment** — auto-deploy on push to master via `scripts/deploy.sh` + Gitea Actions
|
||||
|
||||
**Next steps:**
|
||||
|
||||
- [ ] Step 16: Continuous deployment — auto-deploy on push to master
|
||||
- [ ] Step 17: Backups — verify Hetzner backup scope, add PostgreSQL pg_dump
|
||||
- [ ] Step 18: Monitoring & observability — Prometheus, Grafana, uptime checks, alerting
|
||||
|
||||
@@ -670,7 +670,83 @@ sudo systemctl status gitea-runner
|
||||
|
||||
Verify the runner shows as **Online** in Gitea: **Site Administration > Actions > Runners**.
|
||||
|
||||
## Step 16: Verify Full Deployment
|
||||
## Step 16: Continuous Deployment
|
||||
|
||||
Automate deployment on every successful push to master. The Gitea Actions runner and the app both run on the same server, so the deploy job SSHes from the CI Docker container to `127.0.0.1`.
|
||||
|
||||
```
|
||||
push to master
|
||||
├── ruff ──────┐
|
||||
├── pytest ────┤
|
||||
└── architecture ─┤
|
||||
└── deploy (SSH → scripts/deploy.sh)
|
||||
├── git stash / pull / pop
|
||||
├── docker compose up -d --build
|
||||
├── alembic upgrade heads
|
||||
└── health check (retries)
|
||||
```
|
||||
|
||||
### 16.1 Generate Deploy SSH Key (on server)
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "gitea-deploy@wizard.lu" -f ~/.ssh/deploy_ed25519 -N ""
|
||||
cat ~/.ssh/deploy_ed25519.pub >> ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
### 16.2 Add Gitea Secrets
|
||||
|
||||
In **Repository Settings > Actions > Secrets**, add:
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| `DEPLOY_SSH_KEY` | Contents of `~/.ssh/deploy_ed25519` (private key) |
|
||||
| `DEPLOY_HOST` | `127.0.0.1` |
|
||||
| `DEPLOY_USER` | `samir` |
|
||||
| `DEPLOY_PATH` | `/home/samir/apps/orion` |
|
||||
|
||||
### 16.3 Deploy Script
|
||||
|
||||
The deploy script lives at `scripts/deploy.sh` in the repository. It:
|
||||
|
||||
1. Stashes local changes (preserves `.env`)
|
||||
2. Pulls latest code (`--ff-only`)
|
||||
3. Pops stash to restore local changes
|
||||
4. Rebuilds and restarts Docker containers (`docker compose --profile full up -d --build`)
|
||||
5. Runs database migrations (`alembic upgrade heads`)
|
||||
6. Health checks `http://localhost:8001/health` with 12 retries (60s total)
|
||||
|
||||
Exit codes: `0` success, `1` git pull failed, `2` docker compose failed, `3` migration failed, `4` health check failed.
|
||||
|
||||
### 16.4 CI Workflow
|
||||
|
||||
The deploy job in `.gitea/workflows/ci.yml` runs only on master push, after `ruff`, `pytest`, and `architecture` pass:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: [ruff, pytest, architecture]
|
||||
steps:
|
||||
- name: Deploy to production
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
port: 22
|
||||
command_timeout: 10m
|
||||
script: cd ${{ secrets.DEPLOY_PATH }} && bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
### 16.5 Manual Fallback
|
||||
|
||||
If CI is down, deploy manually:
|
||||
|
||||
```bash
|
||||
cd ~/apps/orion && bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
### 16.6 Verify
|
||||
|
||||
```bash
|
||||
# All app containers running
|
||||
@@ -771,29 +847,15 @@ git stash pop
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Deploy updates (pull & rebuild)
|
||||
### Deploy updates
|
||||
|
||||
After pushing code to Gitea from local:
|
||||
Deployments happen automatically when pushing to master (see [Step 16](#step-16-continuous-deployment)). For manual deploys:
|
||||
|
||||
```bash
|
||||
cd ~/apps/orion && git pull && docker compose --profile full up -d --build
|
||||
cd ~/apps/orion && bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
If there are local changes on the server (e.g. `.env` edits), stash first:
|
||||
|
||||
```bash
|
||||
cd ~/apps/orion
|
||||
git stash # Save local config changes
|
||||
git pull
|
||||
git stash pop # Re-apply local config
|
||||
docker compose --profile full up -d --build
|
||||
```
|
||||
|
||||
If the update includes database migrations:
|
||||
|
||||
```bash
|
||||
docker compose --profile full exec -e PYTHONPATH=/app api python -m alembic upgrade heads
|
||||
```
|
||||
The script handles stashing local changes, pulling, rebuilding containers, running migrations, and health checks.
|
||||
|
||||
### View logs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user