name: Build & Deploy on: push: branches: [fix_branch] 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: Checkout repository shell: bash env: GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_SHA: ${{ gitea.sha }} CHECKOUT_TOKEN: ${{ gitea.token }} run: | set -euo pipefail if ! command -v git >/dev/null 2>&1; then echo "::error::git must be available in the runner image; this workflow cannot install git while runner DNS is unavailable." exit 1 fi WORKSPACE="${GITHUB_WORKSPACE:-$PWD}" mkdir -p "$WORKSPACE" cd "$WORKSPACE" SERVER_URL="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" REPOSITORY="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" SHA="${GITEA_SHA:-${GITHUB_SHA:-}}" if [ -z "$SERVER_URL" ] || [ -z "$REPOSITORY" ] || [ -z "$SHA" ]; then echo "::error::Missing repository checkout context" exit 1 fi REPOSITORY_URL="${SERVER_URL}/${REPOSITORY}.git" if [ ! -d .git ]; then git init git remote add origin "$REPOSITORY_URL" fi git config --global --add safe.directory "$WORKSPACE" if [ -n "${CHECKOUT_TOKEN:-}" ]; then git -c "http.extraHeader=Authorization: token ${CHECKOUT_TOKEN}" fetch --no-tags --depth=1 origin "$SHA" else git fetch --no-tags --depth=1 origin "$SHA" fi git checkout --force FETCH_HEAD - 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_STOREFRONT_URL: ${{ secrets.NEXT_PUBLIC_STOREFRONT_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: | validate_url() { name="$1" value="$2" if [ -z "$value" ]; then echo "::error::$name is missing — add it to Gitea Actions secrets" exit 1 fi case "$value" in http://*|https://*) ;; *) echo "::error::$name must be an absolute URL (got: $value)" exit 1 ;; esac } validate_url NEXT_PUBLIC_API_URL "$NEXT_PUBLIC_API_URL" validate_url NEXT_PUBLIC_HOMEPAGE_URL "$NEXT_PUBLIC_HOMEPAGE_URL" validate_url NEXT_PUBLIC_STOREFRONT_URL "$NEXT_PUBLIC_STOREFRONT_URL" validate_url NEXT_PUBLIC_DASHBOARD_URL "$NEXT_PUBLIC_DASHBOARD_URL" validate_url NEXT_PUBLIC_ADMIN_URL "$NEXT_PUBLIC_ADMIN_URL" validate_url SITE_ORIGIN "$SITE_ORIGIN" - name: Check remote build credentials id: check-build-host env: VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" if [ -z "${{ secrets.VPS_SSH_KEY }}" ] || \ [ -z "$VPS_HOST" ] || \ [ -z "${{ secrets.VPS_USER }}" ]; then echo "::error::VPS_SSH_KEY, VPS_USER, and either VPS_IP or DEPLOY_REGISTRY_HOST are required to build on the remote Docker host" exit 1 fi - name: Check SSH tools run: | if ! command -v ssh >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1; then echo "::error::ssh and tar must be available in the runner image; this workflow cannot install packages while runner DNS is unavailable." exit 1 fi - name: Set up SSH key env: VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null chmod 644 ~/.ssh/known_hosts - name: Sync build context to VPS env: REMOTE_BUILD_DIR: ${{ env.DEPLOY_ROOT }}/build-context VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" ssh "${{ secrets.VPS_USER }}@$VPS_HOST" \ "rm -rf '$REMOTE_BUILD_DIR' && mkdir -p '$REMOTE_BUILD_DIR'" tar \ --exclude='.git' \ --exclude='node_modules' \ --exclude='.next' \ --exclude='dist' \ --exclude='coverage' \ -czf - . | ssh "${{ secrets.VPS_USER }}@$VPS_HOST" \ "tar -xzf - -C '$REMOTE_BUILD_DIR'" - name: Build and push on VPS env: PUSH_IMAGE: ${{ steps.registry-check.outputs.available == 'true' }} IMAGE_FULL: ${{ steps.image-meta.outputs.full }} IMAGE_LATEST: ${{ steps.image-meta.outputs.latest }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_HOMEPAGE_URL: ${{ secrets.NEXT_PUBLIC_HOMEPAGE_URL }} NEXT_PUBLIC_STOREFRONT_URL: ${{ secrets.NEXT_PUBLIC_STOREFRONT_URL }} NEXT_PUBLIC_DASHBOARD_URL: ${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }} NEXT_PUBLIC_ADMIN_URL: ${{ secrets.NEXT_PUBLIC_ADMIN_URL }} SITE_ORIGIN: ${{ secrets.SITE_ORIGIN }} REMOTE_BUILD_DIR: ${{ env.DEPLOY_ROOT }}/build-context VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" REGISTRY_PASSWORD_B64="$(printf '%s' "$REGISTRY_PASSWORD" | base64 | tr -d '\n')" ssh "${{ secrets.VPS_USER }}@$VPS_HOST" " set -e cd '$REMOTE_BUILD_DIR' if ! command -v docker >/dev/null 2>&1; then echo 'Docker must be installed on the VPS build host' >&2 exit 1 fi if [ '$PUSH_IMAGE' = 'true' ]; then printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d | docker login '$REGISTRY_HOST' \ --username '$REGISTRY_USERNAME' \ --password-stdin fi docker build \ --file '$DOCKERFILE_PATH' \ --platform '$DOCKER_PLATFORM' \ --tag '$IMAGE_FULL' \ --tag '$IMAGE_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='$NEXT_PUBLIC_API_URL' \ --build-arg NEXT_PUBLIC_HOMEPAGE_URL='$NEXT_PUBLIC_HOMEPAGE_URL' \ --build-arg NEXT_PUBLIC_STOREFRONT_URL='$NEXT_PUBLIC_STOREFRONT_URL' \ --build-arg NEXT_PUBLIC_DASHBOARD_URL='$NEXT_PUBLIC_DASHBOARD_URL' \ --build-arg NEXT_PUBLIC_ADMIN_URL='$NEXT_PUBLIC_ADMIN_URL' \ --build-arg SITE_ORIGIN='$SITE_ORIGIN' \ . if [ '$PUSH_IMAGE' = 'true' ]; then docker push '$IMAGE_FULL' docker push '$IMAGE_LATEST' fi " 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: Checkout repository shell: bash env: GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_SHA: ${{ gitea.sha }} CHECKOUT_TOKEN: ${{ gitea.token }} run: | set -euo pipefail if ! command -v git >/dev/null 2>&1; then echo "::error::git must be available in the runner image; this workflow cannot install git while runner DNS is unavailable." exit 1 fi WORKSPACE="${GITHUB_WORKSPACE:-$PWD}" mkdir -p "$WORKSPACE" cd "$WORKSPACE" SERVER_URL="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" REPOSITORY="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" SHA="${GITEA_SHA:-${GITHUB_SHA:-}}" if [ -z "$SERVER_URL" ] || [ -z "$REPOSITORY" ] || [ -z "$SHA" ]; then echo "::error::Missing repository checkout context" exit 1 fi REPOSITORY_URL="${SERVER_URL}/${REPOSITORY}.git" if [ ! -d .git ]; then git init git remote add origin "$REPOSITORY_URL" fi git config --global --add safe.directory "$WORKSPACE" if [ -n "${CHECKOUT_TOKEN:-}" ]; then git -c "http.extraHeader=Authorization: token ${CHECKOUT_TOKEN}" fetch --no-tags --depth=1 origin "$SHA" else git fetch --no-tags --depth=1 origin "$SHA" fi git checkout --force FETCH_HEAD - name: Check deploy credentials id: check-creds env: VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" if [ -z "${{ secrets.VPS_SSH_KEY }}" ] || \ [ -z "$VPS_HOST" ] || \ [ -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::ssh and scp must be available in the runner image; this workflow cannot install openssh-client while runner DNS is unavailable." exit 1 fi - name: Set up SSH key if: steps.check-creds.outputs.skip == 'false' env: VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null chmod 644 ~/.ssh/known_hosts - name: Sync deployment assets if: steps.check-creds.outputs.skip == 'false' env: VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" ssh "${{ secrets.VPS_USER }}@$VPS_HOST" \ "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 }}@$VPS_HOST:$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 }}@$VPS_HOST:$DEPLOY_ROOT/scripts/" scp docker/pgmanage/override.py \ "${{ secrets.VPS_USER }}@$VPS_HOST:$DEPLOY_ROOT/docker/pgmanage/" - name: Run deploy script on VPS if: steps.check-creds.outputs.skip == 'false' env: VPS_HOST: ${{ secrets.VPS_IP }} run: | VPS_HOST="${VPS_HOST:-$DEPLOY_REGISTRY_HOST}" REGISTRY_PASSWORD_B64="$(printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" | base64 | tr -d '\n')" ssh "${{ secrets.VPS_USER }}@$VPS_HOST" " 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 "