CD #1
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: | |
| workflow_run: | |
| workflows: | |
| - CI | |
| types: | |
| - completed | |
| branches: | |
| - master | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| deploy_mode: | |
| description: "Deployment mode" | |
| required: true | |
| default: deploy | |
| type: choice | |
| options: | |
| - deploy | |
| - rollback | |
| rollback_tag: | |
| description: "Required when deploy_mode=rollback (for example, 1a2b3c4d5e6f)" | |
| required: false | |
| type: string | |
| permissions: | |
| contents: read | |
| packages: write | |
| concurrency: | |
| group: production-deploy | |
| cancel-in-progress: true | |
| jobs: | |
| prepare: | |
| name: Prepare Deployment | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mode: ${{ steps.vars.outputs.mode }} | |
| should_build: ${{ steps.vars.outputs.should_build }} | |
| image_tag: ${{ steps.vars.outputs.image_tag }} | |
| ghcr_owner: ${{ steps.vars.outputs.ghcr_owner }} | |
| source_ref: ${{ steps.vars.outputs.source_ref }} | |
| steps: | |
| - name: Resolve deployment variables | |
| id: vars | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ghcr_owner="${GITHUB_REPOSITORY_OWNER,,}" | |
| if [ "${{ github.event_name }}" = "workflow_run" ]; then | |
| mode="deploy" | |
| should_build="true" | |
| source_ref="${{ github.event.workflow_run.head_sha }}" | |
| image_tag="${source_ref:0:12}" | |
| else | |
| mode="${{ inputs.deploy_mode }}" | |
| source_ref="${GITHUB_SHA}" | |
| if [ "$mode" = "rollback" ]; then | |
| if [ -z "${{ inputs.rollback_tag }}" ]; then | |
| echo "rollback_tag is required when deploy_mode=rollback" >&2 | |
| exit 1 | |
| fi | |
| should_build="false" | |
| image_tag="${{ inputs.rollback_tag }}" | |
| else | |
| should_build="true" | |
| image_tag="${GITHUB_SHA::12}" | |
| fi | |
| fi | |
| if ! [[ "$image_tag" =~ ^[A-Za-z0-9._-]+$ ]]; then | |
| echo "Invalid image tag: $image_tag" >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "mode=$mode" | |
| echo "should_build=$should_build" | |
| echo "image_tag=$image_tag" | |
| echo "ghcr_owner=$ghcr_owner" | |
| echo "source_ref=$source_ref" | |
| } >> "$GITHUB_OUTPUT" | |
| build-and-push: | |
| name: Build and Push Images | |
| if: github.repository == 'Binary-ShadowMonarch/bookapp' | |
| needs: prepare | |
| runs-on: ubuntu-latest | |
| outputs: | |
| image_tag: ${{ needs.prepare.outputs.image_tag }} | |
| ghcr_owner: ${{ needs.prepare.outputs.ghcr_owner }} | |
| steps: | |
| - name: Checkout code | |
| if: needs.prepare.outputs.should_build == 'true' | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.source_ref }} | |
| - name: Setup Docker Buildx | |
| if: needs.prepare.outputs.should_build == 'true' | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f | |
| - name: Login to GHCR | |
| if: needs.prepare.outputs.should_build == 'true' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push frontend image | |
| if: needs.prepare.outputs.should_build == 'true' | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 | |
| with: | |
| context: ./frontend | |
| file: ./frontend/Dockerfile | |
| push: true | |
| provenance: true | |
| sbom: true | |
| tags: | | |
| ghcr.io/${{ needs.prepare.outputs.ghcr_owner }}/bookapp-frontend:${{ needs.prepare.outputs.image_tag }} | |
| ghcr.io/${{ needs.prepare.outputs.ghcr_owner }}/bookapp-frontend:latest | |
| - name: Build and push backend image | |
| if: needs.prepare.outputs.should_build == 'true' | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 | |
| with: | |
| context: ./backend | |
| file: ./backend/Dockerfile | |
| push: true | |
| provenance: true | |
| sbom: true | |
| tags: | | |
| ghcr.io/${{ needs.prepare.outputs.ghcr_owner }}/bookapp-backend:${{ needs.prepare.outputs.image_tag }} | |
| ghcr.io/${{ needs.prepare.outputs.ghcr_owner }}/bookapp-backend:latest | |
| - name: Skip image build for rollback | |
| if: needs.prepare.outputs.should_build != 'true' | |
| run: echo "Rollback mode selected, skipping image build and push." | |
| deploy: | |
| name: Deploy to Production | |
| if: github.repository == 'Binary-ShadowMonarch/bookapp' | |
| runs-on: | |
| - self-hosted | |
| - linux | |
| - bookapp-prod | |
| needs: | |
| - prepare | |
| - build-and-push | |
| env: | |
| COMPOSE_ENV_FILE: ${{ vars.BOOKAPP_ENV_FILE }} | |
| FRONTEND_SMOKE_URL: ${{ vars.BOOKAPP_SMOKE_URL }} | |
| BACKEND_SMOKE_URL: ${{ vars.BOOKAPP_API_SMOKE_URL }} | |
| GHCR_OWNER: ${{ needs.prepare.outputs.ghcr_owner }} | |
| IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} | |
| DEPLOY_MODE: ${{ needs.prepare.outputs.mode }} | |
| SOURCE_REF: ${{ needs.prepare.outputs.source_ref }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| ref: ${{ env.SOURCE_REF }} | |
| - name: Validate deployment variables | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$COMPOSE_ENV_FILE" ]; then | |
| echo "Repository variable BOOKAPP_ENV_FILE is required (absolute path to production .env on the runner host)." >&2 | |
| exit 1 | |
| fi | |
| if [ ! -f "$COMPOSE_ENV_FILE" ]; then | |
| echo "No env file found at $COMPOSE_ENV_FILE" >&2 | |
| exit 1 | |
| fi | |
| - name: Login to GHCR | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pull deployment images | |
| shell: bash | |
| run: | | |
| docker compose \ | |
| --env-file "$COMPOSE_ENV_FILE" \ | |
| -f docker-compose.yml \ | |
| -f docker-compose.prod.yml \ | |
| pull frontend backend | |
| - name: Deploy stack | |
| shell: bash | |
| run: | | |
| docker compose \ | |
| --env-file "$COMPOSE_ENV_FILE" \ | |
| -f docker-compose.yml \ | |
| -f docker-compose.prod.yml \ | |
| up -d --no-build --remove-orphans | |
| - name: Smoke checks | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| frontend_url="${FRONTEND_SMOKE_URL:-http://127.0.0.1:4353/}" | |
| backend_url="${BACKEND_SMOKE_URL:-http://127.0.0.1:4353/api/healthz}" | |
| curl --fail --silent --show-error "$frontend_url" > /dev/null | |
| echo "Frontend smoke check passed: $frontend_url" | |
| curl --fail --silent --show-error "$backend_url" > /dev/null | |
| echo "Backend smoke check passed: $backend_url" | |
| - name: Cleanup dangling images | |
| run: docker image prune -f | |
| - name: Deployment summary | |
| shell: bash | |
| run: | | |
| { | |
| echo "## Deployment Completed" | |
| echo "- Mode: $DEPLOY_MODE" | |
| echo "- Tag: $IMAGE_TAG" | |
| echo "- GHCR owner: $GHCR_OWNER" | |
| echo "- Runner: $RUNNER_NAME" | |
| } >> "$GITHUB_STEP_SUMMARY" |