feat(deploy): add gitea actions deployment pipeline
Some checks failed
Deployment CI/CD / validate (push) Has been cancelled
Deployment CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-14 18:18:26 +03:30
parent e190825135
commit e015f01cd6
3 changed files with 332 additions and 0 deletions

View 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
View File

@@ -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:

92
scripts/deploy.sh Normal file
View 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