feat(deploy): add gitea actions deployment pipeline
This commit is contained in:
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
|
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
|
## Scope Boundary
|
||||||
|
|
||||||
This repo should document:
|
This repo should document:
|
||||||
|
|||||||
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