535 lines
23 KiB
YAML
535 lines
23 KiB
YAML
# Heartbeat to trigger fresh CI run after stall
|
|
name: Build & Deploy
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- "**"
|
|
tags:
|
|
- "v*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
skip_checks:
|
|
description: "Skip tests? (true/false)"
|
|
required: false
|
|
default: "false"
|
|
|
|
env:
|
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_name == 'main' && 'testing' || github.ref_name) }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 1: Prepare Environment
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
prepare:
|
|
name: 🔍 Prepare
|
|
runs-on: docker
|
|
outputs:
|
|
target: ${{ steps.determine.outputs.target }}
|
|
image_tag: ${{ steps.determine.outputs.image_tag }}
|
|
env_file: ${{ steps.determine.outputs.env_file }}
|
|
traefik_host: ${{ steps.determine.outputs.traefik_host }}
|
|
traefik_rule: ${{ steps.determine.outputs.traefik_rule }}
|
|
next_public_url: ${{ steps.determine.outputs.next_public_url }}
|
|
directus_url: ${{ steps.determine.outputs.directus_url }}
|
|
project_name: ${{ steps.determine.outputs.project_name }}
|
|
short_sha: ${{ steps.determine.outputs.short_sha }}
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
steps:
|
|
- name: 🧹 Maintenance (High Density Cleanup)
|
|
shell: bash
|
|
run: |
|
|
echo "Purging old build layers and dangling images..."
|
|
docker image prune -f
|
|
docker builder prune -f --filter "until=6h"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 2
|
|
|
|
- name: 🔍 Environment ermitteln
|
|
id: determine
|
|
shell: bash
|
|
run: |
|
|
REF="${{ github.ref_name }}"
|
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
|
DOMAIN="mintel.me"
|
|
PRJ="mintel-me"
|
|
|
|
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
|
|
TARGET="testing"
|
|
IMAGE_TAG="main-${SHORT_SHA}"
|
|
ENV_FILE=".env.testing"
|
|
TRAEFIK_HOST="testing.${DOMAIN}"
|
|
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
|
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
|
|
TARGET="production"
|
|
IMAGE_TAG="$REF"
|
|
ENV_FILE=".env.prod"
|
|
TRAEFIK_HOST="${DOMAIN}, www.${DOMAIN}"
|
|
else
|
|
TARGET="staging"
|
|
IMAGE_TAG="$REF"
|
|
ENV_FILE=".env.staging"
|
|
TRAEFIK_HOST="staging.${DOMAIN}"
|
|
fi
|
|
else
|
|
TARGET="branch"
|
|
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
|
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
|
|
ENV_FILE=".env.branch-${SLUG}"
|
|
TRAEFIK_HOST="${SLUG}.branch.${DOMAIN}"
|
|
fi
|
|
|
|
if [[ "$TARGET" != "skip" ]]; then
|
|
# Standardize Traefik Rule
|
|
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
|
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\`%s\`)%s", $i, (i==NF?"":" || ")}')
|
|
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
|
else
|
|
TRAEFIK_RULE="Host(\`$TRAEFIK_HOST\`)"
|
|
PRIMARY_HOST="$TRAEFIK_HOST"
|
|
fi
|
|
|
|
{
|
|
echo "target=$TARGET"
|
|
echo "image_tag=$IMAGE_TAG"
|
|
echo "env_file=$ENV_FILE"
|
|
echo "traefik_host=$PRIMARY_HOST"
|
|
echo "traefik_rule=$TRAEFIK_RULE"
|
|
echo "next_public_url=https://$PRIMARY_HOST"
|
|
echo "directus_url=https://cms.$PRIMARY_HOST"
|
|
if [[ "$TARGET" == "branch" ]]; then
|
|
echo "project_name=$PRJ-branch-$SLUG"
|
|
else
|
|
echo "project_name=$PRJ-$TARGET"
|
|
fi
|
|
echo "short_sha=$SHORT_SHA"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
|
|
else
|
|
echo "target=skip" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# (JOB 2: QA was removed to reduce pipeline noise)
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 3: Build & Push
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
build:
|
|
name: 🏗️ Build
|
|
needs: [prepare]
|
|
if: needs.prepare.outputs.target != 'skip'
|
|
runs-on: docker
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
- name: Provide sibling monorepo (context)
|
|
run: |
|
|
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
|
# Force ALL @mintel packages to use the local clone instead of the registry
|
|
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
|
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
|
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
|
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
|
|
|
- name: 🧹 Free Disk Space
|
|
run: |
|
|
docker builder prune -af || true
|
|
docker image prune -af || true
|
|
|
|
- name: 🐳 Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: 🔐 Registry Login
|
|
run: |
|
|
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
|
|
|
- name: 🧹 Nuclear Runner Cleanup
|
|
run: |
|
|
echo "Disk space before nuclear prune:"
|
|
df -h
|
|
|
|
# Massive cleanup of pre-installed runner software to free up ~5GB+
|
|
# These are standard on VM-based runners and often unnecessary
|
|
sudo rm -rf /usr/share/dotnet || true
|
|
sudo rm -rf /usr/local/lib/android || true
|
|
sudo rm -rf /opt/ghc || true
|
|
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
|
|
|
# Comprehensive Docker purge
|
|
docker system prune -af --volumes
|
|
docker builder prune -af
|
|
docker buildx prune -af
|
|
|
|
# Clean temp build artifacts
|
|
rm -rf /tmp/docker-actions-toolkit-* || true
|
|
|
|
echo "Disk space after nuclear prune:"
|
|
df -h
|
|
continue-on-error: true
|
|
|
|
- name: 🏗️ Build and Push
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
push: true
|
|
provenance: false
|
|
platforms: linux/amd64
|
|
build-args: |
|
|
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
|
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
|
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
|
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
|
S3_ENDPOINT=${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
|
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
|
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
|
S3_BUCKET=${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
|
S3_REGION=${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
|
S3_PREFIX=${{ secrets.S3_PREFIX || vars.S3_PREFIX || 'mintel.me' }}
|
|
|
|
BUILD_ID=${{ github.sha }}
|
|
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
|
# Temporarily disable registry cache export to save runner disk/bandwidth
|
|
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
|
# cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
|
secrets: |
|
|
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
|
|
|
- name: 🚨 Extract Build Error Logs
|
|
if: failure()
|
|
run: |
|
|
set +e
|
|
mkdir -p ~/.ssh
|
|
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
|
echo "Re-running docker build with plain progress to capture exact logs..."
|
|
echo "${{ secrets.NPM_TOKEN }}" > /tmp/npm_token.txt
|
|
docker build \
|
|
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} \
|
|
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
|
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
|
--build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \
|
|
--secret id=NPM_TOKEN,src=/tmp/npm_token.txt \
|
|
--progress plain \
|
|
-t temp-image . > docker_build_failed.log 2>&1
|
|
cat login.log >> docker_build_failed.log || true
|
|
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
|
# JOB 4: Deploy
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
deploy:
|
|
name: 🚀 Deploy
|
|
needs: [prepare, build]
|
|
runs-on: docker
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
env:
|
|
TARGET: ${{ needs.prepare.outputs.target }}
|
|
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
|
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
|
|
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
|
|
DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }}
|
|
|
|
|
|
|
|
# Mail
|
|
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
|
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
|
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
|
|
MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
|
|
MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
|
|
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
|
|
|
|
# Authentication
|
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
|
AUTH_COOKIE_NAME: ${{ secrets.AUTH_COOKIE_NAME || vars.AUTH_COOKIE_NAME || 'mintel_gatekeeper_session' }}
|
|
COOKIE_DOMAIN: ${{ secrets.COOKIE_DOMAIN || vars.COOKIE_DOMAIN || '.mintel.me' }}
|
|
|
|
# Monitoring & Services
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
|
PROJECT_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#ff00ff' }}
|
|
|
|
# S3 Object Storage
|
|
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
|
S3_BUCKET: ${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
|
S3_REGION: ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
|
S3_PREFIX: ${{ secrets.S3_PREFIX || vars.S3_PREFIX || github.event.repository.name }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
- name: 📝 Generate Environment
|
|
shell: bash
|
|
env:
|
|
TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }}
|
|
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
|
|
GATEKEEPER_HOST: gatekeeper.${{ needs.prepare.outputs.traefik_host }}
|
|
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
|
run: |
|
|
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
|
|
STD_MW="${PROJECT_NAME}-forward,compress"
|
|
|
|
if [[ "$TARGET" == "production" ]]; then
|
|
AUTH_MIDDLEWARE="$STD_MW"
|
|
COMPOSE_PROFILES=""
|
|
else
|
|
AUTH_MIDDLEWARE="${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,compress"
|
|
COMPOSE_PROFILES="gatekeeper"
|
|
fi
|
|
|
|
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
|
|
|
if [[ "$UMAMI_API_ENDPOINT" != http* ]]; then
|
|
UMAMI_API_ENDPOINT="https://$UMAMI_API_ENDPOINT"
|
|
fi
|
|
|
|
cat > .env.deploy << EOF
|
|
# Generated by CI - $TARGET
|
|
IMAGE_TAG=$IMAGE_TAG
|
|
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
|
GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN
|
|
SENTRY_DSN=$SENTRY_DSN
|
|
PROJECT_COLOR=$PROJECT_COLOR
|
|
LOG_LEVEL=$LOG_LEVEL
|
|
|
|
MAIL_HOST=$MAIL_HOST
|
|
MAIL_PORT=$MAIL_PORT
|
|
MAIL_USERNAME=$MAIL_USERNAME
|
|
MAIL_PASSWORD=$MAIL_PASSWORD
|
|
MAIL_FROM=$MAIL_FROM
|
|
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
|
|
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
|
|
AUTH_COOKIE_NAME=$AUTH_COOKIE_NAME
|
|
COOKIE_DOMAIN=$COOKIE_DOMAIN
|
|
UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
|
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
|
S3_ENDPOINT=$S3_ENDPOINT
|
|
S3_ACCESS_KEY=$S3_ACCESS_KEY
|
|
S3_SECRET_KEY=$S3_SECRET_KEY
|
|
S3_BUCKET=$S3_BUCKET
|
|
S3_REGION=$S3_REGION
|
|
S3_PREFIX=$S3_PREFIX
|
|
TARGET=$TARGET
|
|
SKIP_RUNTIME_ENV_VALIDATION=true
|
|
SENTRY_ENVIRONMENT=$TARGET
|
|
PROJECT_NAME=$PROJECT_NAME
|
|
ENV_FILE=$ENV_FILE
|
|
TRAEFIK_RULE='$TRAEFIK_RULE'
|
|
TRAEFIK_HOST='$TRAEFIK_HOST'
|
|
COMPOSE_PROFILES=$COMPOSE_PROFILES
|
|
TRAEFIK_MIDDLEWARES=$AUTH_MIDDLEWARE
|
|
TRAEFIK_ENTRYPOINT=websecure
|
|
TRAEFIK_TLS=true
|
|
TRAEFIK_CERT_RESOLVER=le
|
|
EOF
|
|
|
|
- name: 🚀 SSH Deploy
|
|
shell: bash
|
|
env:
|
|
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
|
run: |
|
|
mkdir -p ~/.ssh
|
|
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
|
|
|
# SSH keepalive to prevent timeout during long docker pull
|
|
cat > ~/.ssh/config <<SSHCFG
|
|
Host alpha.mintel.me
|
|
ServerAliveInterval 15
|
|
ServerAliveCountMax 20
|
|
ConnectTimeout 30
|
|
SSHCFG
|
|
chmod 600 ~/.ssh/config
|
|
|
|
if [[ "$TARGET" == "production" ]]; then
|
|
SITE_DIR="/home/deploy/sites/mintel.me"
|
|
elif [[ "$TARGET" == "testing" ]]; then
|
|
SITE_DIR="/home/deploy/sites/testing.mintel.me"
|
|
elif [[ "$TARGET" == "staging" ]]; then
|
|
SITE_DIR="/home/deploy/sites/staging.mintel.me"
|
|
else
|
|
SITE_DIR="/home/deploy/sites/branch.mintel.me/${SLUG:-unknown}"
|
|
fi
|
|
|
|
# Upload files
|
|
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
|
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
|
|
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
|
|
|
# Deploy
|
|
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
|
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
|
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
|
|
|
VALID_TOKEN=""
|
|
VALID_USER=""
|
|
for T_RAW in $TOKENS; do
|
|
if [ -n "$T_RAW" ]; then
|
|
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
|
for U in $USERS; do
|
|
if [ -n "$U" ]; then
|
|
echo "Attempting docker login for a token with user $U..."
|
|
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
|
echo "✅ Successfully authenticated with a token."
|
|
VALID_TOKEN="$T"
|
|
VALID_USER="$U"
|
|
break 2
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
|
TOKEN="$VALID_TOKEN"
|
|
|
|
# Deploy — alpha is pre-logged into registry.infra.mintel.me, no credential passing needed
|
|
ssh root@alpha.mintel.me "
|
|
docker network create '${{ needs.prepare.outputs.project_name }}-internal' || true
|
|
docker volume create 'mintel-me_payload-db-data' || true
|
|
cd $SITE_DIR
|
|
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull
|
|
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --wait --remove-orphans
|
|
"
|
|
|
|
- name: 🧹 Purge S3 Cache
|
|
shell: bash
|
|
run: |
|
|
echo "Installing rclone..."
|
|
curl -s -O https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
|
sudo dpkg -i rclone-current-linux-amd64.deb > /dev/null 2>&1
|
|
|
|
echo "Configuring rclone..."
|
|
cat > rclone.conf <<EOF
|
|
[mintel-s3]
|
|
type = s3
|
|
provider = Other
|
|
access_key_id = ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
|
secret_access_key = ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
|
endpoint = ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
|
region = ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
|
EOF
|
|
|
|
echo "Purging S3 cache for ${{ env.S3_PREFIX }} ..."
|
|
rclone --config rclone.conf delete mintel-s3:${{ env.S3_BUCKET }}/${{ env.S3_PREFIX }}/cache/ --include "*" || true
|
|
|
|
rm rclone.conf rclone-current-linux-amd64.deb
|
|
|
|
- name: 🧹 Post-Deploy Cleanup (Runner)
|
|
if: always()
|
|
run: docker builder prune -f --filter "until=1h"
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 5: Post-Deploy Verification
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
post_deploy_checks:
|
|
name: 🩺 Smoke Test
|
|
needs: [prepare, deploy]
|
|
runs-on: docker
|
|
if: needs.deploy.result == 'success'
|
|
container:
|
|
image: alpine:latest
|
|
steps:
|
|
- name: 🌐 Check Production URL
|
|
shell: sh
|
|
run: |
|
|
apk add --no-cache curl
|
|
# Wait longer (up to 2 minutes) for Next.js to fully prime
|
|
COUNT=0
|
|
MAX=24
|
|
URL="${{ needs.prepare.outputs.next_public_url }}"
|
|
|
|
echo "Verifying $URL is responsive..."
|
|
while [ $COUNT -lt $MAX ]; do
|
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/" || echo "000")
|
|
if [ "$STATUS" = "200" ]; then
|
|
echo "✅ Site is UP (200 OK)"
|
|
break
|
|
fi
|
|
echo "⏳ Wait for 200 OK (Status: $STATUS)..."
|
|
sleep 5
|
|
COUNT=$((COUNT + 1))
|
|
done
|
|
|
|
if [ "$STATUS" != "200" ]; then
|
|
echo "❌ Site failed smoke test after 2 minutes! (Status: $STATUS)"
|
|
exit 1
|
|
fi
|
|
|
|
- name: 🌐 Check Case Study Assets and Rewrites
|
|
shell: sh
|
|
run: |
|
|
BASE_URL="${{ needs.prepare.outputs.next_public_url }}"
|
|
|
|
echo "Verifying case study page..."
|
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/case-studies/klz-cables")
|
|
if [ "$STATUS" != "200" ]; then
|
|
echo "❌ Case study page failed! (Status: $STATUS)"
|
|
exit 1
|
|
fi
|
|
echo "✅ Case study page UP"
|
|
|
|
echo "Verifying breeze CSS (root path)..."
|
|
CSS_PATH="/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$CSS_PATH")
|
|
if [ "$STATUS" != "200" ]; then
|
|
echo "❌ Root asset path failed! (Status: $STATUS)"
|
|
exit 1
|
|
fi
|
|
echo "✅ Root asset path OK"
|
|
|
|
echo "Verifying breeze CSS (relative path from case-study)..."
|
|
REL_CSS_PATH="/case-studies/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$REL_CSS_PATH")
|
|
if [ "$STATUS" != "200" ]; then
|
|
echo "❌ Relative asset path failed! (Status: $STATUS)"
|
|
exit 1
|
|
fi
|
|
echo "✅ Relative asset path OK"
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 6: Notifications
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
notifications:
|
|
name: 🔔 Notify
|
|
needs: [prepare, deploy, post_deploy_checks]
|
|
if: always()
|
|
runs-on: docker
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
steps:
|
|
- name: 🔔 Gotify
|
|
run: |
|
|
DEPLOY="${{ needs.deploy.result }}"
|
|
SMOKE="${{ needs.post_deploy_checks.result }}"
|
|
TARGET="${{ needs.prepare.outputs.target }}"
|
|
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
|
|
|
if [[ "$DEPLOY" == "success" ]]; then
|
|
PRIORITY=5
|
|
EMOJI="✅"
|
|
else
|
|
PRIORITY=8
|
|
EMOJI="🚨"
|
|
fi
|
|
|
|
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
|
-F "title=$EMOJI mintel.me $VERSION -> $TARGET" \
|
|
-F "message=Deploy: $DEPLOY" \
|
|
-F "priority=$PRIORITY" || true
|