CD Deploy to Server (SSH) #144
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CD Deploy to Server (SSH) | |
| on: | |
| workflow_run: | |
| workflows: | |
| - Build & Push Docker Images | |
| - Build & Push coding-service | |
| - Build & Push file-service | |
| types: [ completed ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: deploy-${{ github.event_name }}-${{ github.run_id }} | |
| cancel-in-progress: false | |
| jobs: | |
| ## | |
| deploy: | |
| if: > | |
| (github.event_name == 'workflow_dispatch') || | |
| ( | |
| github.event_name == 'workflow_run' && | |
| ( | |
| github.event.workflow_run.head_branch == 'main' || | |
| startsWith(github.event.workflow_run.head_branch, 'v') | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| environment: production | |
| env: | |
| # Truyền DEPLOY_DIR qua env (có thể rỗng); xử lý default ở shell | |
| DEPLOY_DIR_SECRET: ${{ secrets.DEPLOY_DIR }} | |
| steps: | |
| - name: Show trigger context (debug) | |
| run: | | |
| echo "event_name: ${{ github.event_name }}" | |
| echo "run.conclusion: ${{ github.event.workflow_run.conclusion }}" | |
| echo "run.head_branch: ${{ github.event.workflow_run.head_branch }}" | |
| echo "run.head_sha: ${{ github.event.workflow_run.head_sha }}" | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Derive IMAGE_TAG (from workflow_run or manual) | |
| shell: bash | |
| run: | | |
| echo "Resolved IMAGE_TAG=latest" | |
| echo "IMAGE_TAG=latest" >> "$GITHUB_ENV" | |
| - name: Compute .env SHA256 | |
| run: | | |
| if [ ! -f .env ]; then | |
| echo "::error::.env không tồn tại trong repo tại thời điểm deploy." | |
| exit 1 | |
| fi | |
| echo "ENV_SHA=$(sha256sum .env | awk '{print $1}')" >> "$GITHUB_ENV" | |
| - name: Prepare deploy bundle | |
| run: | | |
| set -euo pipefail | |
| mkdir -p deploy_bundle | |
| cp -v .env deploy_bundle/.env | |
| cp -v docker-compose.prod-infra.yml deploy_bundle/ | |
| mkdir -p deploy_bundle/init | |
| cp -vr init/postgres deploy_bundle/init/postgres | |
| cp -vr init/mongo deploy_bundle/init/mongo | |
| cp -v docker-compose.prod-services.yml deploy_bundle/ | |
| mkdir -p deploy_bundle/monitoring/grafana/provisioning | |
| cp -v monitoring/prometheus.yml deploy_bundle/monitoring/ | |
| cp -v monitoring/loki-config.yml deploy_bundle/monitoring/ | |
| cp -v monitoring/promtail-config.yml deploy_bundle/monitoring/ | |
| cp -vr monitoring/grafana/provisioning/* deploy_bundle/monitoring/grafana/provisioning/ 2>/dev/null || true | |
| mkdir -p ops | |
| cat > ops/deploy.sh <<'EOS' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| DEPLOY_DIR="${DEPLOY_DIR:-$HOME/codecampus}" | |
| mkdir -p "$DEPLOY_DIR" | |
| cd "$DEPLOY_DIR" | |
| # Backup .env | |
| cp -f .env ".env.bak.$(date +%Y%m%d-%H%M%S)" || true | |
| # Ghi IMAGE_TAG vào .env: | |
| # - nếu IMAGE_TAG=latest thì giữ latest (hoặc đặt latest nếu chưa có) | |
| # - nếu là version cụ thể/SHA thì cập nhật đúng giá trị | |
| if grep -q '^IMAGE_TAG=' .env; then | |
| if [ "${IMAGE_TAG}" = "latest" ]; then | |
| sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=latest/" .env | |
| else | |
| sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=${IMAGE_TAG}/" .env | |
| fi | |
| else | |
| echo "IMAGE_TAG=${IMAGE_TAG}" >> .env | |
| fi | |
| # Load env | |
| set -a | |
| source .env | |
| set +a | |
| # docker compose wrapper | |
| compose() { | |
| if docker compose version >/dev/null 2>&1; then | |
| docker compose "$@" | |
| else | |
| docker-compose "$@" | |
| fi | |
| } | |
| # docker login Docker Hub (nếu có) | |
| if [ -n "${DOCKERHUB_USER:-}" ] && [ -n "${DOCKERHUB_TOKEN:-}" ]; then | |
| echo "docker login Docker Hub..." | |
| echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin | |
| else | |
| echo "Thiếu DOCKERHUB_USER/DOCKERHUB_TOKEN trong .env (image public thì vẫn OK)." | |
| fi | |
| # docker login GHCR (nếu có) | |
| if [ -n "${GHCR_USERNAME:-}" ] && [ -n "${GHCR_TOKEN:-}" ]; then | |
| echo "docker login GHCR..." | |
| echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin | |
| fi | |
| echo "Hạ tầng (idempotent)..." | |
| compose -f docker-compose.prod-infra.yml --env-file .env up -d | |
| echo "Pull images tag ${IMAGE_TAG}..." | |
| compose -f docker-compose.prod-services.yml --env-file .env pull | |
| echo "Up services..." | |
| compose -f docker-compose.prod-services.yml --env-file .env up -d | |
| echo "Prune dangling images..." | |
| docker image prune -f || true | |
| echo "Deploy xong." | |
| EOS | |
| chmod +x ops/deploy.sh | |
| cp -v ops/deploy.sh deploy_bundle/ | |
| - name: Decide remote target dir | |
| id: targetdir | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${DEPLOY_DIR_SECRET:-}" ]; then | |
| echo "DEPLOY_DIR_FINAL=${DEPLOY_DIR_SECRET%/}" >> "$GITHUB_OUTPUT" | |
| else | |
| # Mặc định | |
| echo "DEPLOY_DIR_FINAL=~/codecampus" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Ensure remote dir exists | |
| uses: appleboy/[email protected] | |
| env: | |
| DEPLOY_DIR: ${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }} | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SSH_PORT }} | |
| script: | | |
| mkdir -p '${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }}' | |
| # Kiểm tra bundle có file hay không | |
| - name: Verify local deploy bundle | |
| run: | | |
| set -euo pipefail | |
| ls -la deploy_bundle | |
| test -f deploy_bundle/.env | |
| test -f deploy_bundle/docker-compose.prod-infra.yml | |
| test -f deploy_bundle/docker-compose.prod-services.yml | |
| test -f deploy_bundle/deploy.sh | |
| test -f deploy_bundle/monitoring/prometheus.yml | |
| test -f deploy_bundle/monitoring/loki-config.yml | |
| test -f deploy_bundle/monitoring/promtail-config.yml | |
| test -d deploy_bundle/init/postgres | |
| test -d deploy_bundle/init/mongo | |
| - name: Pre-clean monitoring paths on server | |
| uses: appleboy/[email protected] | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SSH_PORT }} | |
| script: | | |
| set -euo pipefail | |
| DEPLOY_DIR='${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }}' | |
| mkdir -p "$DEPLOY_DIR/monitoring/grafana/provisioning" | |
| mkdir -p "$DEPLOY_DIR/init/postgres" "$DEPLOY_DIR/init/mongo" | |
| # Nếu lỡ có THƯ MỤC trùng tên file, xóa đi | |
| for f in prometheus.yml loki-config.yml promtail-config.yml; do | |
| if [ -d "$DEPLOY_DIR/monitoring/$f" ]; then | |
| echo "Found directory at $DEPLOY_DIR/monitoring/$f -> removing" | |
| rm -rf "$DEPLOY_DIR/monitoring/$f" | |
| fi | |
| done | |
| - name: Upload bundle to server | |
| uses: appleboy/[email protected] | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SSH_PORT }} | |
| source: "deploy_bundle/.env,deploy_bundle/docker-compose.prod-infra.yml,deploy_bundle/docker-compose.prod-services.yml,deploy_bundle/deploy.sh,deploy_bundle/monitoring/**,deploy_bundle/init/**" | |
| target: "${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }}" | |
| overwrite: true | |
| strip_components: 1 | |
| - name: Verify files exist on server (debug) | |
| uses: appleboy/[email protected] | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SSH_PORT }} | |
| script: | | |
| ls -la '${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }}' | |
| - name: Verify .env identical & run deploy | |
| uses: appleboy/[email protected] | |
| env: | |
| IMAGE_TAG: ${{ env.IMAGE_TAG }} | |
| ENV_SHA: ${{ env.ENV_SHA }} | |
| DEPLOY_DIR: ${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }} | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SSH_PORT }} | |
| envs: IMAGE_TAG,ENV_SHA | |
| script_stop: true | |
| script: | | |
| set -euo pipefail | |
| DEPLOY_DIR='${{ steps.targetdir.outputs.DEPLOY_DIR_FINAL }}' | |
| mkdir -p "$DEPLOY_DIR" | |
| cd "$DEPLOY_DIR" | |
| if [ ! -f .env ]; then | |
| echo "::error::Không thấy $DEPLOY_DIR/.env trên server." | |
| exit 1 | |
| fi | |
| SERVER_SHA=$(sha256sum .env | awk '{print $1}') | |
| echo "Local .env sha: $ENV_SHA" | |
| echo "Server .env sha: $SERVER_SHA" | |
| if [ "$SERVER_SHA" != "$ENV_SHA" ]; then | |
| echo "::error::.env trên server KHÁC repo. Hủy deploy để tránh sai lệch." | |
| exit 1 | |
| fi | |
| IMAGE_TAG="latest" DEPLOY_DIR="$DEPLOY_DIR" bash ./deploy.sh |