CD #13
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 | |
| on: | |
| push: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: ghcr.io | |
| jobs: | |
| build-and-push: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| backend_image: ${{ steps.tags.outputs.backend }} | |
| frontend_image: ${{ steps.tags.outputs.frontend }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Derive lowercase image repo | |
| run: | | |
| echo "IMAGE_REPO=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set image tags | |
| id: tags | |
| run: | | |
| SHA_TAG=sha-${GITHUB_SHA::7} | |
| echo "backend=${{ env.REGISTRY }}/${IMAGE_REPO}-backend:${SHA_TAG}" >> $GITHUB_OUTPUT | |
| echo "backend_latest=${{ env.REGISTRY }}/${IMAGE_REPO}-backend:latest" >> $GITHUB_OUTPUT | |
| echo "frontend=${{ env.REGISTRY }}/${IMAGE_REPO}-frontend:${SHA_TAG}" >> $GITHUB_OUTPUT | |
| echo "frontend_latest=${{ env.REGISTRY }}/${IMAGE_REPO}-frontend:latest" >> $GITHUB_OUTPUT | |
| - name: Build and push backend | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: backend/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ steps.tags.outputs.backend }} | |
| ${{ steps.tags.outputs.backend_latest }} | |
| - name: Build and push frontend | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: frontend | |
| file: frontend/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ steps.tags.outputs.frontend }} | |
| ${{ steps.tags.outputs.frontend_latest }} | |
| - name: Show image tags | |
| run: | | |
| echo "Backend: ${{ steps.tags.outputs.backend }}" | |
| echo "Frontend: ${{ steps.tags.outputs.frontend }}" | |
| deploy-compose: | |
| runs-on: ubuntu-latest | |
| needs: build-and-push | |
| environment: production | |
| concurrency: | |
| group: prod-deploy | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Verify required secrets are present | |
| env: | |
| SSH_HOST: ${{ secrets.SSH_HOST }} | |
| SSH_USER: ${{ secrets.SSH_USER }} | |
| SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} | |
| run: | | |
| for v in SSH_HOST SSH_USER SSH_PRIVATE_KEY; do | |
| if [ -z "${!v}" ]; then echo "Missing secret: $v"; exit 1; fi | |
| done | |
| - name: Upload docker-compose file | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| source: "docker-compose.prod.yml" | |
| target: "/opt/edgeflow/docker-compose.yml" | |
| strip_components: 0 | |
| - name: Deploy via SSH | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| script: | | |
| set -euo pipefail | |
| APP_DIR=/opt/edgeflow | |
| sudo mkdir -p ${APP_DIR} | |
| cd ${APP_DIR} | |
| # Write .env with image tags | |
| cat > .env << 'EOF' | |
| BACKEND_IMAGE=${{ needs.build-and-push.outputs.backend_image }} | |
| FRONTEND_IMAGE=${{ needs.build-and-push.outputs.frontend_image }} | |
| EOF | |
| # Login to GHCR if credentials provided | |
| if [ -n "${{ secrets.GHCR_USERNAME }}" ] && [ -n "${{ secrets.GHCR_TOKEN }}" ]; then | |
| echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin | |
| fi | |
| # Ensure docker compose is available | |
| (docker compose version || docker-compose version) | |
| # Pull and run | |
| (docker compose --env-file .env -f docker-compose.yml pull || docker-compose --env-file .env -f docker-compose.yml pull) | |
| (docker compose --env-file .env -f docker-compose.yml up -d || docker-compose --env-file .env -f docker-compose.yml up -d) | |
| docker image prune -f || true | |
| # Basic health checks | |
| curl -fsS http://localhost:8000/api/health || (echo 'Backend health failed' && exit 1) | |
| curl -fsS http://localhost:3000/ || (echo 'Frontend health failed' && exit 1) |