Skip to content

CD

CD #1

Workflow file for this run

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"