Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 52s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
623 lines
30 KiB
YAML
623 lines
30 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"
|
|
|
|
# ⏳ Wait for Upstream Packages/Images if Tagged
|
|
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
|
echo "🔎 Checking for @mintel dependencies in package.json..."
|
|
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | head -1 | cut -d'"' -f4 | sed 's/\^//; s/\~//')
|
|
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
|
|
|
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
|
echo "⏳ This release depends on @mintel v$UPSTREAM_VERSION. Waiting for upstream build..."
|
|
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
|
|
chmod +x wait-for-upstream.sh
|
|
|
|
GITEA_TOKEN=${{ secrets.GITHUB_TOKEN }} ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
|
fi
|
|
fi
|
|
else
|
|
echo "target=skip" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 2: QA (Lint, Typecheck, Test)
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
qa:
|
|
name: 🧪 QA
|
|
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: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v3
|
|
with:
|
|
version: 10
|
|
- name: Provide sibling monorepo
|
|
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
|
|
# This handles root package.json
|
|
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
|
# Special case for pdf -> pdf-library
|
|
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
|
|
|
# Handle apps/web/package.json
|
|
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
|
# Special case for pdf -> pdf-library
|
|
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
|
|
|
# Fix tsconfig paths if they exist
|
|
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
|
|
|
# Fix tsconfig paths if they exist
|
|
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
|
- name: 🔐 Registry Auth
|
|
run: |
|
|
TOKEN="${{ secrets.NPM_TOKEN }}"
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.MINTEL_PRIVATE_TOKEN }}"; fi
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITEA_PAT }}"; fi
|
|
if [ -z "$TOKEN" ]; then echo "❌ Missing NPM_TOKEN / MINTEL_PRIVATE_TOKEN / GITEA_PAT secret!"; exit 1; fi
|
|
|
|
# Mask token in logs (just in case, but Gitea usually does this automatically)
|
|
echo "::add-mask::$TOKEN"
|
|
|
|
echo "Configuring .npmrc for git.infra.mintel.me..."
|
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
|
echo "always-auth=true" >> .npmrc
|
|
|
|
# Also export for pnpm to pick it up from env if needed
|
|
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
|
- name: 🏗️ Compile Sibling Monorepo
|
|
timeout-minutes: 15
|
|
run: |
|
|
mkdir -p ci-logs
|
|
echo "=== Compile Sibling Monorepo ===" >> ci-logs/summary.txt
|
|
cp .npmrc _at-mintel/
|
|
cd _at-mintel
|
|
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
|
pnpm --filter "...@mintel/payload-ai" \
|
|
--filter @mintel/pdf... \
|
|
--filter @mintel/concept-engine... \
|
|
--filter @mintel/estimation-engine... \
|
|
--filter @mintel/meme-generator... \
|
|
build --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
|
- name: Install dependencies
|
|
timeout-minutes: 10
|
|
run: |
|
|
echo "=== Install dependencies (Root) ===" >> ci-logs/summary.txt
|
|
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
|
- name: 🧪 Test
|
|
if: github.event.inputs.skip_checks != 'true'
|
|
timeout-minutes: 10
|
|
run: |
|
|
echo "=== Test (@mintel/web) ===" >> ci-logs/summary.txt
|
|
pnpm --filter @mintel/web test --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
|
- name: Inspect on Failure
|
|
if: failure()
|
|
run: |
|
|
echo "==== runner state ===="
|
|
ls -la
|
|
echo "==== _at-mintel state ===="
|
|
ls -la _at-mintel || true
|
|
echo "==== .npmrc check ===="
|
|
cat .npmrc | sed -E 's/authToken=[a-f0-9]{5}.*/authToken=REDACTED/'
|
|
echo "==== pnpm debug logs ===="
|
|
[ -f pnpm-debug.log ] && tail -n 100 pnpm-debug.log || echo "No root pnpm-debug.log"
|
|
[ -f _at-mintel/pnpm-debug.log ] && tail -n 100 _at-mintel/pnpm-debug.log || echo "No sibling pnpm-debug.log"
|
|
- name: Extract QA Error Logs
|
|
if: failure()
|
|
run: |
|
|
mkdir -p ci-logs
|
|
echo "QA Failure Report" > ci-logs/summary.txt
|
|
ls -R >> ci-logs/summary.txt
|
|
[ -f pnpm-debug.log ] && cp pnpm-debug.log ci-logs/ || true
|
|
[ -f _at-mintel/pnpm-debug.log ] && cp _at-mintel/pnpm-debug.log ci-logs/at-mintel-pnpm-debug.log || true
|
|
|
|
SSH_KEY_FILE=$(mktemp)
|
|
echo "${{ secrets.ALPHA_SSH_KEY }}" > "$SSH_KEY_FILE"
|
|
chmod 600 "$SSH_KEY_FILE"
|
|
|
|
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" root@alpha.mintel.me "mkdir -p ~/logs"
|
|
scp -r -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" ci-logs/* root@alpha.mintel.me:~/logs/ || true
|
|
rm "$SSH_KEY_FILE"
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 3: Build & Push
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
build:
|
|
name: 🏗️ Build
|
|
needs: [prepare, qa]
|
|
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: 🐳 Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: 🔐 Prepare Registry Token
|
|
id: prep_token
|
|
run: |
|
|
TOKEN="${{ secrets.NPM_TOKEN }}"
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.MINTEL_PRIVATE_TOKEN }}"; fi
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITEA_PAT }}"; fi
|
|
if [ -z "$TOKEN" ]; then echo "Missing NPM_TOKEN secret! Add it to Gitea repo settings."; exit 1; fi
|
|
echo "token=$TOKEN" >> $GITHUB_OUTPUT
|
|
|
|
- name: 🔐 Registry Login
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: git.infra.mintel.me
|
|
username: mmintel
|
|
password: ${{ secrets.NPM_TOKEN }}
|
|
|
|
- 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 }}
|
|
tags: git.infra.mintel.me/mmintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
|
cache-from: type=registry,ref=git.infra.mintel.me/mmintel/mintel.me:buildcache
|
|
cache-to: type=registry,ref=git.infra.mintel.me/mmintel/mintel.me:buildcache,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 }}" | docker login git.infra.mintel.me -u "mmintel" --password-stdin > login.log 2>&1
|
|
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
|
|
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
|
# JOB 4: Deploy
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
deploy:
|
|
name: 🚀 Deploy
|
|
needs: [prepare, build, qa]
|
|
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 }}
|
|
|
|
# Database configuration
|
|
postgres_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
|
postgres_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
|
postgres_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
|
DATABASE_URI: postgres://${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}:${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}@postgres-db:5432/${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
|
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'secret' }}
|
|
|
|
# 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
|
|
postgres_DB_NAME=$postgres_DB_NAME
|
|
postgres_DB_USER=$postgres_DB_USER
|
|
postgres_DB_PASSWORD=$postgres_DB_PASSWORD
|
|
DATABASE_URI=$DATABASE_URI
|
|
PAYLOAD_SECRET=$PAYLOAD_SECRET
|
|
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
|
|
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
|
|
TOKEN="${{ secrets.NPM_TOKEN }}"
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.MINTEL_PRIVATE_TOKEN }}"; fi
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITEA_PAT }}"; fi
|
|
if [ -z "$TOKEN" ]; then echo "Missing NPM_TOKEN secret! Add it to Gitea repo settings."; exit 1; fi
|
|
|
|
DB_CONTAINER="${{ needs.prepare.outputs.project_name }}-postgres-db-1"
|
|
ssh root@alpha.mintel.me bash <<DEPLOYEOF
|
|
set -e
|
|
docker network create '${{ needs.prepare.outputs.project_name }}-internal' || true
|
|
docker volume create 'mintel-me_payload-db-data' || true
|
|
echo '$TOKEN' | docker login git.infra.mintel.me -u 'mmintel' --password-stdin
|
|
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 --remove-orphans
|
|
DEPLOYEOF
|
|
|
|
- name: 🧹 Post-Deploy Cleanup (Runner)
|
|
if: always()
|
|
run: docker builder prune -f --filter "until=1h"
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# JOB 5: Post-Deploy Verification
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
post_deploy_checks:
|
|
name: 🧪 Post-Deploy Verification
|
|
needs: [prepare, deploy]
|
|
if: needs.deploy.result == 'success'
|
|
runs-on: docker
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v3
|
|
with:
|
|
version: 10
|
|
- name: Provide sibling monorepo
|
|
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
|
|
|
|
# Fix tsconfig paths if they exist
|
|
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
|
- name: 🔐 Registry Auth
|
|
run: |
|
|
TOKEN="${{ secrets.NPM_TOKEN }}"
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.MINTEL_PRIVATE_TOKEN }}"; fi
|
|
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITEA_PAT }}"; fi
|
|
if [ -z "$TOKEN" ]; then echo "❌ Missing NPM_TOKEN / MINTEL_PRIVATE_TOKEN / GITEA_PAT secret!"; exit 1; fi
|
|
echo "Configuring .npmrc for git.infra.mintel.me..."
|
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
|
echo "always-auth=true" >> .npmrc
|
|
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
- name: 🏥 App Health Check
|
|
shell: bash
|
|
env:
|
|
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
run: |
|
|
echo "Waiting for app to start at $DEPLOY_URL ..."
|
|
for i in {1..30}; do
|
|
HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' "$DEPLOY_URL" 2>&1) || true
|
|
echo "Attempt $i: HTTP $HTTP_CODE"
|
|
if [[ "$HTTP_CODE" =~ ^2 ]]; then
|
|
echo "✅ App is up (HTTP $HTTP_CODE)"
|
|
exit 0
|
|
fi
|
|
echo "⏳ Waiting... (got $HTTP_CODE)"
|
|
sleep 10
|
|
done
|
|
echo "❌ App health check failed after 30 attempts"
|
|
exit 1
|
|
- name: 🚀 OG Image Check
|
|
env:
|
|
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
run: pnpm --filter @mintel/web check:og
|
|
- name: 📝 E2E Smoke Test
|
|
env:
|
|
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
|
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
|
run: |
|
|
# Install system Chromium + dependencies (KLZ pattern)
|
|
# Ubuntu's default 'chromium' is a snap wrapper, so we use xtradeb PPA for native binary
|
|
sudo apt-get update && sudo apt-get install -y gnupg wget ca-certificates
|
|
|
|
# Setup xtradeb PPA for native chromium
|
|
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
|
sudo mkdir -p /etc/apt/keyrings
|
|
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | sudo gpg --dearmor -o /etc/apt/keyrings/xtradeb.gpg || true
|
|
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" | sudo tee /etc/apt/sources.list.d/xtradeb-ppa.list
|
|
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" | sudo tee /etc/apt/preferences.d/xtradeb
|
|
|
|
sudo apt-get update
|
|
sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64
|
|
|
|
[ -f /usr/bin/chromium ] && sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
|
pnpm --filter @mintel/web check:forms
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# 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" && "$SMOKE" == "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 | Smoke: $SMOKE" \
|
|
-F "priority=$PRIORITY" || true
|