This commit replaces the previous deployment mechanism with a blue-green strategy to lay the groundwork for zero-downtime deployments. Key changes: Introduces a deploy-blue-green.sh script to manage "blue" and "green" container sets, creating versioned releases. Updates the Anubis gatekeeper template to dynamically route traffic based on the active deployment color, allowing for seamless traffic switching. Modifies Docker Compose files to include color-specific labels and environment variables. Adapts the GitHub Actions workflow to execute the new blue-green deployment process. Removes the old, now-obsolete deployment and health check scripts. Note: Automated rollback on health check failure is not yet implemented. Downgrades can be performed manually by switching the active color.
262 lines
No EOL
10 KiB
YAML
262 lines
No EOL
10 KiB
YAML
name: Build & Deploy
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
# Job 1: Build the Docker image
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
repo: ${{ steps.meta.outputs.REPO }}
|
|
repo-name: ${{ steps.meta.outputs.REPO_NAME_ONLY }}
|
|
repo-path: ${{ steps.meta.outputs.REPO_PROJECT_PATH }}
|
|
image-tar: ${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar
|
|
tags: ${{ steps.generated_docker_tags.outputs.tag }}
|
|
prod: ${{ steps.meta.outputs.prod }}
|
|
steps:
|
|
- name: Checkout the repo
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Ensure scripts are executable
|
|
run: chmod +x scripts/*.sh
|
|
|
|
- name: Setup build metadata and environment
|
|
id: meta
|
|
run: |
|
|
echo "✅ Exit script on any error"
|
|
set -eu -o pipefail
|
|
|
|
# Parse repository name and set outputs
|
|
eval "$(./scripts/parse-repository-name.sh '${{ github.repository }}')"
|
|
echo "REPO=$REPO" >> $GITHUB_OUTPUT
|
|
echo "REPO_NAME_ONLY=$REPO_NAME_ONLY" >> $GITHUB_OUTPUT
|
|
echo "REPO_PROJECT_PATH=$REPO_PROJECT_PATH" >> $GITHUB_OUTPUT
|
|
|
|
# Determine PROD environment
|
|
prod_value=""
|
|
echo "🔍 Check if PROD is set via vars; if not, determine from github.ref"
|
|
if [ -z "${{ vars.PROD }}" ]; then
|
|
prod_value="${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-prerelease') }}"
|
|
echo "📦 PROD mode unset, determined from github.ref (starts with v and does not end with -prerelease?): ${prod_value}"
|
|
else
|
|
prod_value="${{ vars.PROD }}"
|
|
echo "📦 PROD mode already set to: ${prod_value}"
|
|
fi
|
|
echo "prod=${prod_value}" >> $GITHUB_OUTPUT
|
|
|
|
# Set environment variables for subsequent steps
|
|
echo "🖊️ Writing determined values to GITHUB_ENV:"
|
|
echo "PROD=${prod_value}" >> $GITHUB_ENV
|
|
echo "PROD=${prod_value} -> GITHUB_ENV"
|
|
echo "IMAGE_TAR_NAME=${REPO_NAME_ONLY}-${{ github.ref_name }}_${{ github.sha }}.tar" >> $GITHUB_ENV
|
|
echo "IMAGE_TAR_NAME=${REPO_NAME_ONLY}-${{ github.ref_name }}_${{ github.sha }}.tar -> GITHUB_ENV"
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Generate tags
|
|
id: generated_docker_tags
|
|
run: |
|
|
echo "✅ Exit script on any error"
|
|
set -eu -o pipefail
|
|
|
|
# Use the script to generate tags
|
|
TAG_LIST=$(./scripts/generate-docker-tags.sh \
|
|
"${{ steps.meta.outputs.REPO }}" \
|
|
"${{ github.sha }}" \
|
|
"${{ github.ref }}" \
|
|
"$PROD")
|
|
|
|
echo "Final list of generated tags:"
|
|
echo "$TAG_LIST"
|
|
|
|
TAG_COUNT=$(echo "$TAG_LIST" | wc -l)
|
|
if [[ -z "${{ github.sha }}" || $TAG_COUNT -lt 4 ]]; then
|
|
echo "⚠️ No tags (or too few) were generated based on the logic. Need at least 4 tags. Generated: $TAG_COUNT"
|
|
exit 1
|
|
fi
|
|
|
|
# Output the tags for the docker build action
|
|
{
|
|
echo "tag<<EOF"
|
|
echo "$TAG_LIST"
|
|
echo "EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Run prebuild tasks
|
|
run: ./scripts/prebuild.sh
|
|
|
|
|
|
- name: Cache Docker layers
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: /tmp/.buildx-cache
|
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
restore-keys: |
|
|
${{ runner.os }}-buildx-
|
|
|
|
- name: Extract version for Docker build
|
|
id: extract_version
|
|
run: |
|
|
pip uninstall setuptools
|
|
pip install setuptools-scm
|
|
VERSION=$(python -c "from setuptools_scm import get_version; print(get_version())")
|
|
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
|
|
|
- name: Build container
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
outputs: type=docker,dest=${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar
|
|
tags: ${{ steps.generated_docker_tags.outputs.tag }}
|
|
build-args: |
|
|
VERSION=${{ env.VERSION }}
|
|
context: .
|
|
cache-from: type=local,src=/tmp/.buildx-cache
|
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
|
|
|
- name: Rotate cache
|
|
run: |
|
|
rm -rf /tmp/.buildx-cache
|
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
|
|
- name: Upload container as artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ env.IMAGE_TAR_NAME }}
|
|
path: ${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
# Job 2: Deploy (only runs on main branch or tags)
|
|
deploy:
|
|
#needs: build
|
|
runs-on: ubuntu-latest
|
|
#if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
|
# Determine environment based on ref
|
|
environment: ${{ (startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-prerelease')) && 'production' || 'staging' }}
|
|
steps:
|
|
- name: Checkout the repo
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Ensure scripts are executable
|
|
run: chmod +x scripts/*.sh
|
|
|
|
- name: Setup deployment metadata and environment
|
|
id: meta
|
|
run: |
|
|
echo "✅ Exit script on any error"
|
|
set -eu -o pipefail
|
|
|
|
# Parse repository name and set outputs
|
|
eval "$(./scripts/parse-repository-name.sh '${{ github.repository }}')"
|
|
echo "REPO=$REPO" >> $GITHUB_OUTPUT
|
|
echo "REPO_NAME_ONLY=$REPO_NAME_ONLY" >> $GITHUB_OUTPUT
|
|
echo "REPO_PROJECT_PATH=$REPO_PROJECT_PATH" >> $GITHUB_OUTPUT
|
|
|
|
# Determine PROD environment
|
|
prod_value=""
|
|
echo "🔍 Check if PROD is set via vars; if not, determine from github.ref"
|
|
if [ -z "${{ vars.PROD }}" ]; then
|
|
prod_value="${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-prerelease') }}"
|
|
echo "📦 PROD mode unset, determined from github.ref (starts with v and does not end with -prerelease?): ${prod_value}"
|
|
else
|
|
prod_value="${{ vars.PROD }}"
|
|
echo "📦 PROD mode already set to: ${prod_value}"
|
|
fi
|
|
echo "prod=${prod_value}" >> $GITHUB_OUTPUT
|
|
|
|
# Set all deployment environment variables
|
|
echo "📝 Setting deployment environment variables"
|
|
echo "REPO_PROJECT_PATH=${REPO_PROJECT_PATH}" >> $GITHUB_ENV
|
|
echo "REPO_NAME_ONLY=${REPO_NAME_ONLY}" >> $GITHUB_ENV
|
|
echo "REPO=${REPO}" >> $GITHUB_ENV
|
|
echo "IMAGE_TAR_NAME=${REPO_NAME_ONLY}-${{ github.ref_name }}_${{ github.sha }}.tar" >> $GITHUB_ENV
|
|
echo "PROD=${prod_value}" >> $GITHUB_ENV
|
|
echo "GIT_SHA=${{ github.sha }}" >> $GITHUB_ENV
|
|
echo "REPLICA_COUNT=${{ vars.REPLICA_COUNT }}" >> $GITHUB_ENV
|
|
echo "PRODUCTION_DOMAIN=${{ vars.PRODUCTION_DOMAIN }}" >> $GITHUB_ENV
|
|
echo "STAGING_DOMAIN=${{ vars.STAGING_DOMAIN }}" >> $GITHUB_ENV
|
|
|
|
- name: Download container artifact
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: ${{ env.IMAGE_TAR_NAME }}
|
|
path: ${{ runner.temp }}
|
|
|
|
- name: Get Deploy Secrets
|
|
uses: bitwarden/sm-action@v2
|
|
with:
|
|
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
|
|
secrets: |
|
|
27a8da8d-5fb4-4f58-baaf-b2dd0032eca8 > DEPLOY_HOST
|
|
4cf9ab8d-1772-4ab0-9219-b2dd003315d4 > DEPLOY_KEY
|
|
9aefe34e-c2cf-442e-973c-b2dd0032b6cf > ENV_FILE_BASE64
|
|
d3bb47f8-bfc0-4a61-9cee-b2df0147a02a > CF_PEM_CERT
|
|
5f658ddf-aadd-4464-b501-b2df0147c338 > CF_PEM_CA
|
|
|
|
- name: Set up SSH
|
|
run: |
|
|
mkdir -p $HOME/.ssh
|
|
echo -e "${DEPLOY_KEY}" > $HOME/.ssh/deploy.key
|
|
chmod 700 $HOME/.ssh
|
|
chmod 600 $HOME/.ssh/deploy.key
|
|
|
|
cat >>$HOME/.ssh/config <<END
|
|
Host deploy
|
|
HostName ${DEPLOY_HOST}
|
|
Port ${{ vars.DEPLOY_PORT }}
|
|
User ${{ vars.DEPLOY_USER }}
|
|
IdentityFile $HOME/.ssh/deploy.key
|
|
UserKnownHostsFile /dev/null
|
|
StrictHostKeyChecking no
|
|
ControlMaster auto
|
|
ControlPath $HOME/.ssh/control-%C
|
|
ControlPersist yes
|
|
END
|
|
|
|
- name: Deploy to Server
|
|
env:
|
|
DOCKER_HOST: ssh://deploy
|
|
REPO_PROJECT_PATH: ${{ env.REPO_PROJECT_PATH }}
|
|
REPO: ${{ env.REPO }}
|
|
REPO_NAME_ONLY: ${{ env.REPO_NAME_ONLY }}
|
|
IMAGE_TAR: ${{ runner.temp }}/${{ env.IMAGE_TAR_NAME }}
|
|
PRODrequire_var: ${{ env.PROD }}
|
|
GIT_SHA: ${{ github.sha }}
|
|
REPLICA_COUNT: ${{ env.REPLICA_COUNT }}
|
|
PRODUCTION_DOMAIN: ${{ vars.PRODUCTION_DOMAIN }}
|
|
STAGING_DOMAIN: ${{ vars.STAGING_DOMAIN }}
|
|
run: |
|
|
echo "✅ Exit script on any error"
|
|
set -eu -o pipefail
|
|
./scripts/deploy-blue-green.sh
|
|
|
|
# - name: Health Check and Rollback
|
|
# run: |
|
|
# # Determine the correct URL based on environment
|
|
# if [ "${{ env.PROD }}" = "true" ]; then
|
|
# # Ensure PRODUCTION_DOMAIN is set
|
|
# if [ -z "${{ vars.PRODUCTION_DOMAIN }}" ]; then
|
|
# echo "Error: PRODUCTION_DOMAIN is not set"
|
|
# exit 1
|
|
# fi
|
|
# HEALTH_CHECK_URL="https://${{ vars.PRODUCTION_DOMAIN }}/health/"
|
|
# else
|
|
# # Ensure STAGING_DOMAIN is set
|
|
# if [ -z "${{ vars.STAGING_DOMAIN }}" ]; then
|
|
# echo "Error: STAGING_DOMAIN is not set"
|
|
# exit 1
|
|
# fi
|
|
# HEALTH_CHECK_URL="https://${{ vars.STAGING_DOMAIN }}/health/"
|
|
# fi
|
|
|
|
# # Copy script to remote and execute
|
|
# scp scripts/health-check-and-rollback.sh deploy:/tmp/
|
|
# ssh deploy "chmod +x /tmp/health-check-and-rollback.sh"
|
|
# ssh deploy "/tmp/health-check-and-rollback.sh '${{ env.REPO_PROJECT_PATH }}' '$HEALTH_CHECK_URL' 30"
|
|
# ssh deploy "rm -f /tmp/health-check-and-rollback.sh" |