Skip to content

feat(landing): i18n via next-intl + Caddy locale root redirects (#99) #57

feat(landing): i18n via next-intl + Caddy locale root redirects (#99)

feat(landing): i18n via next-intl + Caddy locale root redirects (#99) #57

Workflow file for this run

name: Deploy to EC2
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
LANDING_IMAGE: ghcr.io/overthelex/merged-landing
PORTAL_IMAGE: ghcr.io/overthelex/merged-portal
EC2_HOST: 18.196.127.148
EC2_USER: ubuntu
SSH_KEY: ~/.ssh/merged-key.pem
jobs:
build:
if: "!contains(github.event.head_commit.message, 'chore: bump version')"
runs-on: [self-hosted, merged]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push landing image
run: |
docker build \
-t $LANDING_IMAGE:${{ github.sha }} \
-t $LANDING_IMAGE:latest \
-f apps/landing/Dockerfile .
docker push $LANDING_IMAGE:${{ github.sha }}
docker push $LANDING_IMAGE:latest
- name: Build and push portal image
run: |
docker build \
-t $PORTAL_IMAGE:${{ github.sha }} \
-t $PORTAL_IMAGE:latest \
-f apps/portal/Dockerfile .
docker push $PORTAL_IMAGE:${{ github.sha }}
docker push $PORTAL_IMAGE:latest
deploy:
needs: build
runs-on: [self-hosted, merged]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Sync config + migrations to EC2
run: |
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST \
'mkdir -p /home/ubuntu/merged /home/ubuntu/merged/secrets /home/ubuntu/merged/migrations'
scp -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no \
docker-compose.prod.yml $EC2_USER@$EC2_HOST:/home/ubuntu/merged/docker-compose.prod.yml
scp -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no \
infra/Caddyfile $EC2_USER@$EC2_HOST:/home/ubuntu/merged/Caddyfile
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST \
'rm -rf /home/ubuntu/merged/migrations/* /home/ubuntu/merged/migrations/.* 2>/dev/null; true'
scp -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no \
-r packages/db/migrations/. $EC2_USER@$EC2_HOST:/home/ubuntu/merged/migrations/
- name: Ensure secrets + env on EC2
run: |
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST << EOF
set -euo pipefail
cd /home/ubuntu/merged
if [ ! -f secrets/postgres_password ]; then
head -c 24 /dev/urandom | base64 | tr -d '=+/' > secrets/postgres_password
chmod 600 secrets/postgres_password
fi
PG_PW=\$(cat secrets/postgres_password)
cat > .env.portal <<ENV
NODE_ENV=production
DATABASE_URL=postgres://merged:\${PG_PW}@merged-postgres:5432/merged
PUBLIC_BASE_URL=https://merged.com.ua
PORTAL_URL=https://portal.merged.com.ua
APP_VERSION=${{ github.sha }}
AUTH_URL=https://portal.merged.com.ua
AUTH_TRUST_HOST=true
AUTH_SECRET=${{ secrets.AUTH_SECRET }}
AUTH_GOOGLE_ID=${{ secrets.AUTH_GOOGLE_ID }}
AUTH_GOOGLE_SECRET=${{ secrets.AUTH_GOOGLE_SECRET }}
GITHUB_APP_ID=${{ secrets.GH_APP_ID }}
GITHUB_APP_PRIVATE_KEY=${{ secrets.GH_APP_PRIVATE_KEY }}
GITHUB_APP_INSTALLATION_ID=${{ secrets.GH_APP_INSTALLATION_ID }}
GITHUB_WEBHOOK_SECRET=${{ secrets.GH_WEBHOOK_SECRET }}
TASKS_ORG=imerged
# mail.legal.org.ua and mail.merged.com.ua both resolve to the
# shared mail MTA (178.162.234.145). We use mail.legal.org.ua here
# because the MTA's TLS cert advertises it as a SAN while a
# merged-specific SAN isn't wired into postfix SNI yet. Relay
# still works because merged-prod is in the MTA's mynetworks and
# OpenDKIM signs by sender domain, not HELO.
SMTP_HOST=mail.legal.org.ua
SMTP_PORT=587
SMTP_SECURE=false
EMAIL_FROM=merged <no-reply@merged.com.ua>
EMAIL_BRAND_URL=https://merged.com.ua
EMAIL_LOGO_URL=https://portal.merged.com.ua/brand/logo-ink-128.png
ENV
chmod 600 .env.portal
EOF
- name: Start postgres + pull images
run: |
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST << 'EOF'
set -euo pipefail
cd /home/ubuntu/merged
if systemctl is-active --quiet caddy; then
sudo systemctl stop caddy
sudo systemctl disable caddy
fi
echo "${{ secrets.GITHUB_TOKEN }}" | sudo docker login ghcr.io -u ${{ github.actor }} --password-stdin
sudo docker pull ghcr.io/overthelex/merged-landing:latest
sudo docker pull ghcr.io/overthelex/merged-portal:latest
# Bring up postgres first so migrations can run against it
sudo docker compose -f docker-compose.prod.yml up -d postgres
# Wait for healthy (max 60s)
for i in $(seq 1 30); do
if sudo docker exec merged-postgres pg_isready -U merged -d merged >/dev/null 2>&1; then
echo "postgres ready"
break
fi
sleep 2
done
EOF
- name: Run drizzle migrations
run: |
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST << 'EOF'
set -euo pipefail
cd /home/ubuntu/merged
PG_PW=$(cat secrets/postgres_password)
# Use the merged_default network so the ephemeral node container can reach merged-postgres by name
sudo docker run --rm \
--network merged_default \
-v "$PWD/migrations":/migrations:ro \
-e DATABASE_URL="postgres://merged:${PG_PW}@merged-postgres:5432/merged" \
node:20-alpine sh -c '
cd /tmp && mkdir app && cd app
npm init -y >/dev/null
npm i --silent drizzle-orm@0.36.4 postgres@3.4.5 >/dev/null
node -e "
const postgres = require(\"postgres\");
const { drizzle } = require(\"drizzle-orm/postgres-js\");
const { migrate } = require(\"drizzle-orm/postgres-js/migrator\");
(async () => {
const client = postgres(process.env.DATABASE_URL, { max: 1 });
const db = drizzle(client);
await migrate(db, { migrationsFolder: \"/migrations\" });
await client.end();
console.log(\"migrations applied\");
})().catch(e => { console.error(e); process.exit(1); });
"
'
EOF
- name: Bring up full stack
run: |
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST << 'EOF'
set -euo pipefail
cd /home/ubuntu/merged
sudo docker compose -f docker-compose.prod.yml up -d
sudo docker image prune -f
EOF
- name: Health check
run: |
sleep 15
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST \
'sudo docker exec merged-landing wget -qO- http://127.0.0.1:8080/ | grep -qi "merged" && echo "landing healthy"'
ssh -o IdentitiesOnly=yes -i $SSH_KEY -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST \
'sudo docker exec merged-portal wget -qO- http://127.0.0.1:3000/api/health | grep -q "\"status\":\"ok\"" && echo "portal healthy"'
curl -sSf https://merged.com.ua/ | grep -qi "технічний скринінг" && echo "landing public endpoint healthy"
curl -sSf https://portal.merged.com.ua/api/health | grep -q '"status":"ok"' && echo "portal public endpoint healthy"