From e015f01cd65a999e6e97dca2554acf875d10df4f Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Thu, 14 May 2026 18:18:26 +0330 Subject: [PATCH] feat(deploy): add gitea actions deployment pipeline --- .gitea/workflows/deploy.yml | 85 ++++++++++++++++++++ README.md | 155 ++++++++++++++++++++++++++++++++++++ scripts/deploy.sh | 92 +++++++++++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 scripts/deploy.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..4bec60c --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -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" diff --git a/README.md b/README.md index 8745e2f..63776ec 100644 --- a/README.md +++ b/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 \ + --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 +``` + +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: diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..ea05c06 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +usage() { + cat <<'EOF' +Usage: + deploy.sh + +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