name: Build & Deploy on: push: branches: [develop] workflow_dispatch: concurrency: group: build-${{ gitea.ref }} cancel-in-progress: false env: NODE_VERSION: "20" # TODO: Remove after installing internal CA certificate on the runner GIT_SSL_NO_VERIFY: "true" REGISTRY_HOST: 192.168.3.80 DEPLOY_REGISTRY_HOST: 10.0.0.4 DOCKERFILE_PATH: Dockerfile.production DOCKER_PLATFORM: linux/amd64 DEPLOY_ROOT: /opt/rentaldrivego jobs: build-image: name: Build & Push Docker Image runs-on: ubuntu-latest outputs: image_repository: ${{ steps.image-meta.outputs.repository }} docker_image: ${{ steps.image-meta.outputs.full }} steps: - name: Check out repository shell: bash run: | set -euo pipefail if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" git init . git remote add origin "$REPOSITORY_URL" if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" \ fetch --depth 1 origin "${{ gitea.ref }}" else git fetch --depth 1 origin "${{ gitea.ref }}" fi git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD fi git rev-parse --short HEAD - name: Ensure Docker CLI is available run: | if ! command -v docker >/dev/null 2>&1; then echo "::error::Docker CLI is not available in this Gitea runner image." echo "::error::Configure the runner label used by this job to an image that already includes Docker CLI, for example ubuntu-latest:docker://catthehacker/ubuntu:act-latest." echo "::error::The current runner maps ubuntu-latest to node:20-bookworm, and this job cannot install docker.io because the job container has no external DNS." exit 1 fi docker --version docker info >/dev/null - name: Set up Docker Buildx shell: bash run: | set -euo pipefail docker buildx version printf '[registry."%s"]\n insecure = true\n' "$REGISTRY_HOST" > /tmp/buildkitd.toml if ! docker buildx inspect rentaldrivego-builder >/dev/null 2>&1; then docker buildx create \ --name rentaldrivego-builder \ --driver docker-container \ --config /tmp/buildkitd.toml \ --use else docker buildx use rentaldrivego-builder fi docker buildx inspect --bootstrap - name: Docker image metadata id: image-meta run: | REPO="${{ gitea.repository }}" TAG="${{ gitea.sha }}" echo "repository=${REPO}" >> "$GITHUB_OUTPUT" echo "full=${REGISTRY_HOST}/${REPO}:${TAG}" >> "$GITHUB_OUTPUT" echo "latest=${REGISTRY_HOST}/${REPO}:latest" >> "$GITHUB_OUTPUT" - name: Check Docker registry credentials id: registry-check run: | if [ -n "${{ secrets.REGISTRY_USERNAME }}" ] && [ -n "${{ secrets.REGISTRY_PASSWORD }}" ]; then echo "available=true" >> "$GITHUB_OUTPUT" else echo "::warning::REGISTRY_USERNAME or REGISTRY_PASSWORD secrets not set — push will be skipped" echo "available=false" >> "$GITHUB_OUTPUT" fi - name: Validate public URLs shell: bash env: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_HOMEPAGE_URL: ${{ secrets.NEXT_PUBLIC_HOMEPAGE_URL }} NEXT_PUBLIC_CARPLACE_URL: ${{ secrets.NEXT_PUBLIC_CARPLACE_URL }} NEXT_PUBLIC_DASHBOARD_URL: ${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }} NEXT_PUBLIC_ADMIN_URL: ${{ secrets.NEXT_PUBLIC_ADMIN_URL }} SITE_ORIGIN: ${{ secrets.SITE_ORIGIN }} run: | for variable in \ NEXT_PUBLIC_API_URL \ NEXT_PUBLIC_HOMEPAGE_URL \ NEXT_PUBLIC_CARPLACE_URL \ NEXT_PUBLIC_DASHBOARD_URL \ NEXT_PUBLIC_ADMIN_URL \ SITE_ORIGIN do value="${!variable}" if [ -z "$value" ]; then echo "::error::$variable is missing — add it to Gitea Actions secrets" exit 1 fi case "$value" in http://*|https://*) ;; *) echo "::error::$variable must be an absolute URL (got: $value)" exit 1 ;; esac done - name: Log in to Gitea Container Registry if: steps.registry-check.outputs.available == 'true' shell: bash run: | printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" | \ docker login "$REGISTRY_HOST" \ --username "${{ secrets.REGISTRY_USERNAME }}" \ --password-stdin - name: Build and push shell: bash env: BUILDKIT_NO_CLIENT_TOKEN: "true" run: | set -euo pipefail OUTPUT_ARG="--load" if [ "${{ steps.registry-check.outputs.available }}" = "true" ]; then OUTPUT_ARG="--push" fi docker buildx build \ --file "$DOCKERFILE_PATH" \ --platform "$DOCKER_PLATFORM" \ --tag "${{ steps.image-meta.outputs.full }}" \ --tag "${{ steps.image-meta.outputs.latest }}" \ --build-arg API_INTERNAL_URL=http://api:4000/api/v1 \ --build-arg DASHBOARD_INTERNAL_URL=http://dashboard:3001 \ --build-arg ADMIN_INTERNAL_URL=http://admin:3002 \ --build-arg NEXT_PUBLIC_API_URL="${{ secrets.NEXT_PUBLIC_API_URL }}" \ --build-arg NEXT_PUBLIC_HOMEPAGE_URL="${{ secrets.NEXT_PUBLIC_HOMEPAGE_URL }}" \ --build-arg NEXT_PUBLIC_CARPLACE_URL="${{ secrets.NEXT_PUBLIC_CARPLACE_URL }}" \ --build-arg NEXT_PUBLIC_DASHBOARD_URL="${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }}" \ --build-arg NEXT_PUBLIC_ADMIN_URL="${{ secrets.NEXT_PUBLIC_ADMIN_URL }}" \ --build-arg SITE_ORIGIN="${{ secrets.SITE_ORIGIN }}" \ "$OUTPUT_ARG" \ . deploy: name: Deploy to VPS runs-on: ubuntu-latest needs: [build-image] env: DOCKER_IMAGE: ${{ needs.build-image.outputs.docker_image }} IMAGE_REPOSITORY: ${{ needs.build-image.outputs.image_repository }} steps: - name: Check out repository shell: bash run: | set -euo pipefail if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" git init . git remote add origin "$REPOSITORY_URL" if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" \ fetch --depth 1 origin "${{ gitea.ref }}" else git fetch --depth 1 origin "${{ gitea.ref }}" fi git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD fi git rev-parse --short HEAD - name: Check deploy credentials id: check-creds run: | if [ -z "${{ secrets.VPS_SSH_KEY }}" ] || \ [ -z "${{ secrets.VPS_IP }}" ] || \ [ -z "${{ secrets.VPS_USER }}" ]; then echo "VPS secrets not fully configured — skipping deploy" echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Install SSH client if: steps.check-creds.outputs.skip == 'false' run: | if ! command -v ssh >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then echo "::error::OpenSSH client tools are not available in this Gitea runner image." echo "::error::Use a runner image that already includes ssh and scp; installing packages from apt is not reliable because the job container has no external DNS." exit 1 fi ssh -V - name: Set up SSH key if: steps.check-creds.outputs.skip == 'false' run: | mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H "${{ secrets.VPS_IP }}" >> ~/.ssh/known_hosts 2>/dev/null chmod 644 ~/.ssh/known_hosts - name: Sync deployment assets if: steps.check-creds.outputs.skip == 'false' run: | ssh "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}" \ "mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage' '$DEPLOY_ROOT/docker/registry/auth' '$DEPLOY_ROOT/dynamic'" scp docker-compose.production.yml \ docker-compose.portainer.production.yml \ docker-compose.registry.production.yml \ docker-compose.registry.local.yml \ traefik.yaml \ "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}:$DEPLOY_ROOT/" scp scripts/docker-prod-common.sh \ scripts/docker-prod-deploy.sh \ scripts/docker-prod-up-registry.sh \ scripts/docker-registry-local-up.sh \ "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}:$DEPLOY_ROOT/scripts/" scp docker/pgmanage/override.py \ "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}:$DEPLOY_ROOT/docker/pgmanage/" - name: Run deploy script on VPS if: steps.check-creds.outputs.skip == 'false' run: | REGISTRY_PASSWORD_B64="$(printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" | base64 | tr -d '\n')" ssh "${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }}" " set -e cd '$DEPLOY_ROOT' APP_IMAGE='$DEPLOY_REGISTRY_HOST/$IMAGE_REPOSITORY' \ APP_VERSION='${{ gitea.sha }}' \ IMAGE_TAG='${{ gitea.sha }}' \ REGISTRY_HOST='$DEPLOY_REGISTRY_HOST' \ REGISTRY_USER='${{ secrets.REGISTRY_USERNAME }}' \ REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \ bash scripts/docker-prod-deploy.sh "