ci(cd): fix invalid job-level if; move secret/PR checks into steps fo… #11
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 ] | |
| pull_request_target: | |
| 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 }} | |
| # Only run on PRs originating from this repo to avoid exposing secrets to forks | |
| if: ${{ github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository }} | |
| steps: | |
| - name: Select ref | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request_target" ]; then | |
| echo "REF=${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV | |
| else | |
| echo "REF=${GITHUB_SHA}" >> $GITHUB_ENV | |
| fi | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.REF }} | |
| - 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: | |
| - name: Select ref | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request_target" ]; then | |
| echo "REF=${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV | |
| else | |
| echo "REF=${GITHUB_SHA}" >> $GITHUB_ENV | |
| fi | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.REF }} | |
| - 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: Verify PR origin (internal only) | |
| if: ${{ github.event_name == 'pull_request_target' }} | |
| run: | | |
| if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then | |
| echo "Refusing to deploy from external fork PR"; exit 1; fi | |
| - 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) |