Compare commits
3 Commits
5ef7f18f77
...
e015f01cd6
| Author | SHA1 | Date | |
|---|---|---|---|
| e015f01cd6 | |||
| e190825135 | |||
| 9a764fafb4 |
85
.gitea/workflows/deploy.yml
Normal file
85
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Deployment CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: qlockify-deploy
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends bash ca-certificates git python3 python3-yaml
|
||||
|
||||
- name: Checkout repository
|
||||
env:
|
||||
REPO_URL: ${{ gitea.server_url }}/${{ gitea.repository }}.git
|
||||
REPO_SHA: ${{ gitea.sha }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
WORKSPACE: ${{ gitea.workspace }}
|
||||
run: |
|
||||
mkdir -p "$WORKSPACE"
|
||||
cd "$WORKSPACE"
|
||||
git init
|
||||
git remote add origin "$REPO_URL"
|
||||
git -c http.extraHeader="Authorization: Bearer $GITEA_TOKEN" fetch --depth 1 origin "$REPO_SHA"
|
||||
git checkout --detach FETCH_HEAD
|
||||
|
||||
- name: Validate deployment script
|
||||
working-directory: ${{ gitea.workspace }}
|
||||
run: bash -n scripts/deploy.sh
|
||||
|
||||
- name: Validate docker-compose.yml syntax
|
||||
working-directory: ${{ gitea.workspace }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
path = Path("docker-compose.yml")
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
assert isinstance(data, dict), "docker-compose.yml must contain a mapping at the top level"
|
||||
assert "services" in data, "docker-compose.yml must define services"
|
||||
PY
|
||||
|
||||
deploy:
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
needs:
|
||||
- validate
|
||||
runs-on: qlockify-deploy
|
||||
steps:
|
||||
- name: Install SSH client
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends bash openssh-client
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
|
||||
run: |
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy updated infrastructure
|
||||
env:
|
||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
|
||||
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
|
||||
DEPLOY_BRANCH: ${{ vars.DEPLOY_BRANCH }}
|
||||
BACKEND_BRANCH: ${{ vars.BACKEND_BRANCH }}
|
||||
FRONTEND_BRANCH: ${{ vars.FRONTEND_BRANCH }}
|
||||
run: |
|
||||
ssh -p "${DEPLOY_PORT:-22}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
"DEPLOY_ROOT='${DEPLOY_PATH}' DEPLOY_BRANCH='${DEPLOY_BRANCH}' BACKEND_BRANCH='${BACKEND_BRANCH}' FRONTEND_BRANCH='${FRONTEND_BRANCH}' bash '${DEPLOY_PATH}/scripts/deploy.sh' deployment"
|
||||
155
README.md
155
README.md
@@ -235,6 +235,161 @@ Stop everything:
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## CI/CD with Gitea Actions
|
||||
|
||||
This repository now ships with a Gitea Actions deployment workflow in:
|
||||
|
||||
- `.gitea/workflows/deploy.yml`
|
||||
|
||||
The backend and frontend repositories each ship with their own workflow files:
|
||||
|
||||
- backend: `.gitea/workflows/backend.yml`
|
||||
- frontend: `.gitea/workflows/frontend.yml`
|
||||
|
||||
Deployment behavior:
|
||||
|
||||
- backend repo push to `main`: runs backend CI, then updates the backend checkout on the server and rebuilds `backend`, `celery`, and `celery-beat`
|
||||
- frontend repo push to `main`: runs frontend lint/build, then updates the frontend checkout on the server and rebuilds `frontend`
|
||||
- deployment repo push to `main`: validates deployment files, then updates the deployment checkout on the server and rebuilds `nginx` plus the app services
|
||||
|
||||
The remote deploy entrypoint is:
|
||||
|
||||
- `./scripts/deploy.sh`
|
||||
|
||||
### One-Time Server Bootstrap
|
||||
|
||||
Before Actions can deploy automatically, make sure the server is prepared once.
|
||||
|
||||
1. Clone all three repositories on the server into the expected layout:
|
||||
|
||||
```text
|
||||
~/qlockify-deployment
|
||||
~/qlockify-deployment/backend/qlockify-backend-deployment
|
||||
~/qlockify-deployment/frontend/qlockify-frontend-deployment
|
||||
```
|
||||
|
||||
2. Make sure the server can `git fetch` all three repositories non-interactively.
|
||||
|
||||
Recommended approach:
|
||||
|
||||
- add a deploy SSH key on the server
|
||||
- add the public key to Gitea as a deploy key or a machine-user SSH key
|
||||
- switch the server-side git remotes to SSH URLs
|
||||
|
||||
3. Pull the latest deployment repo once so the server has `scripts/deploy.sh`.
|
||||
|
||||
4. Make the deploy script executable:
|
||||
|
||||
```bash
|
||||
chmod +x ~/qlockify-deployment/scripts/deploy.sh
|
||||
```
|
||||
|
||||
5. Make sure the deploy user can run Docker Compose on the server.
|
||||
|
||||
### Gitea Runner Setup
|
||||
|
||||
Gitea Actions requires a trusted runner. Gitea's official docs describe the runner and label model here:
|
||||
|
||||
- Actions overview: `https://docs.gitea.com/usage/actions/overview`
|
||||
- Act Runner: `https://docs.gitea.com/usage/actions/act-runner`
|
||||
|
||||
Recommended label setup for this project:
|
||||
|
||||
```text
|
||||
qlockify-python:docker://python:3.14-bookworm
|
||||
qlockify-node:docker://node:22-bookworm
|
||||
qlockify-deploy:docker://ubuntu:24.04
|
||||
```
|
||||
|
||||
Example non-interactive runner registration:
|
||||
|
||||
```bash
|
||||
./act_runner register \
|
||||
--no-interactive \
|
||||
--instance https://git.amiirkhl.ir \
|
||||
--token <runner_registration_token> \
|
||||
--name qlockify-runner \
|
||||
--labels "qlockify-python:docker://python:3.14-bookworm,qlockify-node:docker://node:22-bookworm,qlockify-deploy:docker://ubuntu:24.04"
|
||||
```
|
||||
|
||||
Then start the runner daemon:
|
||||
|
||||
```bash
|
||||
./act_runner daemon
|
||||
```
|
||||
|
||||
### Gitea Secrets
|
||||
|
||||
Create these Actions secrets either at the `Qlockify` organization level or per repository.
|
||||
|
||||
- `SSH_PRIVATE_KEY`
|
||||
- private key used by the workflow to SSH into the deployment server
|
||||
- `SSH_KNOWN_HOSTS`
|
||||
- output of:
|
||||
|
||||
```bash
|
||||
ssh-keyscan -H <your-server-hostname-or-ip>
|
||||
```
|
||||
|
||||
Do not create `GITEA_TOKEN` manually. Gitea provides a built-in job token and exposes it as `${{ secrets.GITEA_TOKEN }}`. See:
|
||||
|
||||
- `https://docs.gitea.com/usage/actions/token-permissions`
|
||||
|
||||
### Gitea Variables
|
||||
|
||||
Create these Actions variables in the Gitea UI:
|
||||
|
||||
- `DEPLOY_HOST`
|
||||
- `DEPLOY_PORT`
|
||||
- `DEPLOY_USER`
|
||||
- `DEPLOY_PATH`
|
||||
- `DEPLOY_BRANCH`
|
||||
- `BACKEND_BRANCH`
|
||||
- `FRONTEND_BRANCH`
|
||||
|
||||
Suggested values for your current server layout:
|
||||
|
||||
```text
|
||||
DEPLOY_HOST=h9arjloaye
|
||||
DEPLOY_PORT=22
|
||||
DEPLOY_USER=ubuntu
|
||||
DEPLOY_PATH=/home/ubuntu/qlockify-deployment
|
||||
DEPLOY_BRANCH=main
|
||||
BACKEND_BRANCH=main
|
||||
FRONTEND_BRANCH=main
|
||||
```
|
||||
|
||||
Gitea variables are available in workflows through `${{ vars.NAME }}`. Gitea documents that here:
|
||||
|
||||
- `https://docs.gitea.com/usage/actions/actions-variables`
|
||||
|
||||
### Recommended UI Setup
|
||||
|
||||
If all three repositories live under the same `Qlockify` organization, the cleanest setup is:
|
||||
|
||||
1. Add one organization-level runner to `Qlockify`
|
||||
2. Add organization-level variables for the shared deploy target
|
||||
3. Add organization-level secrets for the SSH key and known hosts
|
||||
|
||||
This keeps the three repositories consistent and avoids copying the same values three times.
|
||||
|
||||
### First Deploy Check
|
||||
|
||||
After creating the runner, variables, and secrets:
|
||||
|
||||
1. push the deployment repository first
|
||||
2. confirm the deployment workflow succeeds
|
||||
3. then push backend or frontend changes and confirm their repo-specific workflows deploy only the affected services
|
||||
|
||||
If a workflow fails on the server step, first check:
|
||||
|
||||
- runner logs
|
||||
- repository Actions logs
|
||||
- `docker compose logs -f backend`
|
||||
- `docker compose logs -f frontend`
|
||||
- `docker compose logs -f celery`
|
||||
- `docker compose logs -f celery-beat`
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
This repo should document:
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
ENVIRONMENT=development
|
||||
DEBUG=True
|
||||
|
||||
# Django Core
|
||||
DJANGO_SETTINGS_MODULE=config.settings
|
||||
DJANGO_SECRET_KEY=
|
||||
DJANGO_ALLOWED_HOSTS=api.qlockify.ir,qlockify.ir,www.qlockify.ir
|
||||
# Django Core
|
||||
DJANGO_SETTINGS_MODULE=config.settings
|
||||
DJANGO_SECRET_KEY=
|
||||
DJANGO_ALLOWED_HOSTS=api.qlockify.ir,qlockify.ir,www.qlockify.ir
|
||||
|
||||
# Database
|
||||
POSTGRES_DB=app_db
|
||||
POSTGRES_USER=app_user
|
||||
POSTGRES_PASSWORD=app_password
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# CORS / CSRF
|
||||
CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir
|
||||
CSRF_TRUSTED_ORIGINS=https://api.qlockify.ir,https://qlockify.ir,https://www.qlockify.ir
|
||||
# CORS / CSRF
|
||||
CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir
|
||||
CSRF_TRUSTED_ORIGINS=https://api.qlockify.ir,https://qlockify.ir,https://www.qlockify.ir
|
||||
|
||||
DJANGO_CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://api.qlockify.ir,https://qlockify.ir,https://www.qlockify.ir
|
||||
|
||||
# JWT
|
||||
ACCESS_TOKEN_LIFETIME=5
|
||||
@@ -30,15 +33,19 @@ JWT_ALGORITHM=HS256
|
||||
|
||||
# Redis / Celery
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
REDIS_HOST=redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
CELERY_BROKER_URL=redis://redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
CELERY_BROKER_URL=redis://redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
|
||||
# Timzone / Language
|
||||
LANGUAGE_CODE=en-us
|
||||
TIME_ZONE=Asia/Tehran
|
||||
|
||||
SMS_APIKEY=
|
||||
BASE_URL=https://api.qlockify.ir
|
||||
SMS_APIKEY=
|
||||
BASE_URL=https://api.qlockify.ir
|
||||
GOOGLE_OAUTH_CLIENT_ID=
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=
|
||||
GOOGLE_OAUTH_REDIRECT_URI=https://qlockify.ir/api/users/oauth/google/callback/
|
||||
GOOGLE_OAUTH_FRONTEND_CALLBACK_URL=https://qlockify.ir/auth/google/callback
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_API_BASE_URL=https://api.qlockify.ir
|
||||
VITE_API_BASE_URL=https://qlockify.ir
|
||||
|
||||
@@ -5,13 +5,6 @@ server {
|
||||
return 301 https://qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.qlockify.ir;
|
||||
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
@@ -42,58 +35,7 @@ server {
|
||||
client_max_body_size 100M;
|
||||
sendfile on;
|
||||
|
||||
location /api/ {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location /admin/ {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location /redoc {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location /openapi.json {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
return 301 https://api.qlockify.ir$request_uri;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name api.qlockify.ir;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
|
||||
client_max_body_size 100M;
|
||||
sendfile on;
|
||||
|
||||
# Static and Media files
|
||||
location /static/ {
|
||||
alias /usr/share/nginx/html/static/;
|
||||
expires 30d;
|
||||
@@ -106,6 +48,7 @@ server {
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Protect API Documentation with Basic Auth
|
||||
location ~ ^/(docs|redoc|openapi.json|api/docs|api/redoc|api/openapi.json|api/v1/docs) {
|
||||
auth_basic "Restricted API Documentation";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
@@ -147,4 +90,12 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
92
scripts/deploy.sh
Normal file
92
scripts/deploy.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
deploy.sh <component>
|
||||
|
||||
Components:
|
||||
deployment Update the deployment repo and rebuild nginx + app services
|
||||
backend Update the backend repo and rebuild backend + celery services
|
||||
frontend Update the frontend repo and rebuild frontend
|
||||
full Update all three repos and rebuild all app services
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[deploy] %s\n' "$*"
|
||||
}
|
||||
|
||||
require_git_repo() {
|
||||
local path="$1"
|
||||
if [[ ! -d "$path/.git" ]]; then
|
||||
printf 'Expected git repository at %s\n' "$path" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
sync_repo() {
|
||||
local path="$1"
|
||||
local branch="$2"
|
||||
|
||||
require_git_repo "$path"
|
||||
log "Syncing $path -> origin/$branch"
|
||||
git -C "$path" fetch --prune origin
|
||||
git -C "$path" checkout "$branch"
|
||||
git -C "$path" reset --hard "origin/$branch"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "$DEPLOY_ROOT/docker-compose.yml" "$@"
|
||||
}
|
||||
|
||||
COMPONENT="${1:-}"
|
||||
if [[ -z "$COMPONENT" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/qlockify-deployment}"
|
||||
DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
|
||||
BACKEND_BRANCH="${BACKEND_BRANCH:-main}"
|
||||
FRONTEND_BRANCH="${FRONTEND_BRANCH:-main}"
|
||||
|
||||
DEPLOY_REPO_PATH="$DEPLOY_ROOT"
|
||||
BACKEND_REPO_PATH="$DEPLOY_ROOT/backend/qlockify-backend-deployment"
|
||||
FRONTEND_REPO_PATH="$DEPLOY_ROOT/frontend/qlockify-frontend-deployment"
|
||||
|
||||
cd "$DEPLOY_ROOT"
|
||||
|
||||
case "$COMPONENT" in
|
||||
deployment)
|
||||
sync_repo "$DEPLOY_REPO_PATH" "$DEPLOY_BRANCH"
|
||||
compose config -q
|
||||
compose up -d --build nginx backend frontend celery celery-beat
|
||||
;;
|
||||
backend)
|
||||
sync_repo "$DEPLOY_REPO_PATH" "$DEPLOY_BRANCH"
|
||||
sync_repo "$BACKEND_REPO_PATH" "$BACKEND_BRANCH"
|
||||
compose config -q
|
||||
compose up -d --build backend celery celery-beat
|
||||
;;
|
||||
frontend)
|
||||
sync_repo "$DEPLOY_REPO_PATH" "$DEPLOY_BRANCH"
|
||||
sync_repo "$FRONTEND_REPO_PATH" "$FRONTEND_BRANCH"
|
||||
compose config -q
|
||||
compose up -d --build frontend
|
||||
;;
|
||||
full)
|
||||
sync_repo "$DEPLOY_REPO_PATH" "$DEPLOY_BRANCH"
|
||||
sync_repo "$BACKEND_REPO_PATH" "$BACKEND_BRANCH"
|
||||
sync_repo "$FRONTEND_REPO_PATH" "$FRONTEND_BRANCH"
|
||||
compose config -q
|
||||
compose up -d --build nginx backend frontend celery celery-beat
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
compose ps
|
||||
Reference in New Issue
Block a user