ci: use dind container for build, alpine+ssh for deploy, fix runner label
This commit is contained in:
parent
8a29858285
commit
caf541d750
1 changed files with 113 additions and 122 deletions
|
|
@ -12,55 +12,55 @@ env:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
# 1. BUILD & PUSH IMAGES
|
# 1. BUILD & PUSH IMAGES (Docker-in-Docker)
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
build:
|
build:
|
||||||
name: Build & Push Docker Images
|
name: Build & Push Docker Images
|
||||||
runs-on: self-hosted
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker:27-dind
|
||||||
|
env:
|
||||||
|
DOCKER_TLS_CERTDIR: ""
|
||||||
|
options: --privileged
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install git
|
||||||
|
run: apk add --no-cache git
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set image tag
|
- name: Set image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: echo "IMAGE_TAG=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
run: echo "IMAGE_TAG=$(echo $GITHUB_SHA | cut -c1-8)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Log in to Forgejo Container Registry
|
- name: Log in to registry
|
||||||
uses: docker/login-action@v3
|
run: |
|
||||||
with:
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||||||
registry: ${{ vars.REGISTRY }}
|
docker login ${{ vars.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||||
username: ${{ secrets.REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build & push backend
|
- name: Build & push backend
|
||||||
uses: docker/build-push-action@v5
|
run: |
|
||||||
with:
|
TAG=${{ steps.tag.outputs.IMAGE_TAG }}
|
||||||
context: ./backend
|
docker build \
|
||||||
file: ./backend/Dockerfile.prod
|
-f backend/Dockerfile.prod \
|
||||||
push: true
|
-t ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${TAG} \
|
||||||
tags: |
|
-t ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest \
|
||||||
${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${{ steps.tag.outputs.IMAGE_TAG }}
|
./backend
|
||||||
${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest
|
docker push ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${TAG}
|
||||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:cache
|
docker push ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest
|
||||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:cache,mode=max
|
|
||||||
|
|
||||||
- name: Build & push frontend
|
- name: Build & push frontend
|
||||||
uses: docker/build-push-action@v5
|
run: |
|
||||||
with:
|
TAG=${{ steps.tag.outputs.IMAGE_TAG }}
|
||||||
context: ./frontend
|
docker build \
|
||||||
file: ./frontend/Dockerfile.prod
|
-f frontend/Dockerfile.prod \
|
||||||
push: true
|
--build-arg VITE_API_URL=https://${{ vars.DOMAIN }}/api \
|
||||||
build-args: |
|
-t ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${TAG} \
|
||||||
VITE_API_URL=https://${{ vars.DOMAIN }}/api
|
-t ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest \
|
||||||
tags: |
|
./frontend
|
||||||
${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${{ steps.tag.outputs.IMAGE_TAG }}
|
docker push ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${TAG}
|
||||||
${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest
|
docker push ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest
|
||||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:cache
|
|
||||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:cache,mode=max
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
image_tag: ${{ steps.tag.outputs.IMAGE_TAG }}
|
image_tag: ${{ steps.tag.outputs.IMAGE_TAG }}
|
||||||
|
|
@ -70,52 +70,48 @@ jobs:
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy via SSH
|
name: Deploy via SSH
|
||||||
runs-on: self-hosted
|
runs-on: docker
|
||||||
needs: build
|
needs: build
|
||||||
|
container:
|
||||||
|
image: alpine:3.19
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install SSH tools
|
||||||
|
run: apk add --no-cache openssh-client
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup SSH key
|
- name: Setup SSH key
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
ssh-keyscan -p ${{ vars.SSH_PORT || '22' }} ${{ vars.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan -p ${{ vars.SSH_PORT }} ${{ vars.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
- name: Copy docker-compose.prod.yml to server
|
- name: Copy compose file to server
|
||||||
run: |
|
run: |
|
||||||
scp -i ~/.ssh/deploy_key \
|
scp -i ~/.ssh/deploy_key -P ${{ vars.SSH_PORT }} \
|
||||||
-P ${{ vars.SSH_PORT || '22' }} \
|
|
||||||
docker-compose.prod.yml \
|
docker-compose.prod.yml \
|
||||||
${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:/opt/saas-imobiliaria/docker-compose.prod.yml
|
${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:/opt/saas-imobiliaria/docker-compose.prod.yml
|
||||||
|
|
||||||
- name: Deploy on server (Docker Swarm stack)
|
- name: Deploy Swarm stack on server
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
|
||||||
run: |
|
run: |
|
||||||
ssh -i ~/.ssh/deploy_key \
|
ssh -i ~/.ssh/deploy_key -p ${{ vars.SSH_PORT }} \
|
||||||
-p ${{ vars.SSH_PORT || '22' }} \
|
|
||||||
${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} \
|
${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} \
|
||||||
bash -s << ENDSSH
|
"set -e
|
||||||
|
mkdir -p /opt/saas-imobiliaria
|
||||||
|
cd /opt/saas-imobiliaria
|
||||||
|
|
||||||
set -e
|
echo '${{ secrets.REGISTRY_PASSWORD }}' | \
|
||||||
|
docker login ${{ vars.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||||||
|
|
||||||
DEPLOY_DIR="/opt/saas-imobiliaria"
|
docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-backend:${IMAGE_TAG}
|
||||||
mkdir -p "\$DEPLOY_DIR"
|
docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-frontend:${IMAGE_TAG}
|
||||||
cd "\$DEPLOY_DIR"
|
|
||||||
|
|
||||||
# Log in to registry
|
IMAGE_TAG=${IMAGE_TAG} \
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
|
||||||
docker login ${{ vars.REGISTRY }} \
|
|
||||||
-u ${{ secrets.REGISTRY_USER }} \
|
|
||||||
--password-stdin
|
|
||||||
|
|
||||||
# Pull images explicitly so swarm has them cached
|
|
||||||
docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-backend:${{ needs.build.outputs.image_tag }}
|
|
||||||
docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-frontend:${{ needs.build.outputs.image_tag }}
|
|
||||||
|
|
||||||
# Deploy stack with env vars inline
|
|
||||||
IMAGE_TAG=${{ needs.build.outputs.image_tag }} \
|
|
||||||
REGISTRY=${{ vars.REGISTRY }} \
|
REGISTRY=${{ vars.REGISTRY }} \
|
||||||
DOMAIN=${{ vars.DOMAIN }} \
|
DOMAIN=${{ vars.DOMAIN }} \
|
||||||
POSTGRES_DB=${{ secrets.POSTGRES_DB }} \
|
POSTGRES_DB=${{ secrets.POSTGRES_DB }} \
|
||||||
|
|
@ -129,75 +125,70 @@ jobs:
|
||||||
--prune \
|
--prune \
|
||||||
saas-imobiliaria
|
saas-imobiliaria
|
||||||
|
|
||||||
echo "Stack deployed: ${{ needs.build.outputs.image_tag }}"
|
sleep 15
|
||||||
|
docker stack services saas-imobiliaria"
|
||||||
# Wait for services to converge
|
|
||||||
sleep 10
|
|
||||||
docker stack services saas-imobiliaria
|
|
||||||
|
|
||||||
ENDSSH
|
|
||||||
|
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
# 3. HEALTH CHECK — valida HTTPS + endpoints críticos
|
# 3. HEALTH CHECK
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
healthcheck:
|
healthcheck:
|
||||||
name: Validate HTTPS & Endpoints
|
name: Validate HTTPS & Endpoints
|
||||||
runs-on: self-hosted
|
runs-on: docker
|
||||||
needs: deploy
|
needs: [build, deploy]
|
||||||
|
container:
|
||||||
|
image: alpine:3.19
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for containers to stabilize
|
- name: Install tools
|
||||||
run: sleep 30
|
run: apk add --no-cache curl openssl
|
||||||
|
|
||||||
- name: Check frontend HTTPS
|
- name: Wait for stack to stabilize
|
||||||
run: |
|
run: sleep 40
|
||||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
||||||
--max-time 15 "https://${{ vars.DOMAIN }}")
|
|
||||||
echo "Frontend: $STATUS"
|
|
||||||
[ "$STATUS" = "200" ] || (echo "❌ Frontend HTTPS falhou ($STATUS)" && exit 1)
|
|
||||||
|
|
||||||
- name: Check HTTP → HTTPS redirect
|
- name: Frontend HTTPS
|
||||||
run: |
|
run: |
|
||||||
LOCATION=$(curl -s -o /dev/null -w "%{redirect_url}" \
|
S=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "https://${{ vars.DOMAIN }}")
|
||||||
--max-time 10 "http://${{ vars.DOMAIN }}")
|
echo "Frontend: $S"
|
||||||
echo "Redirect: $LOCATION"
|
[ "$S" = "200" ] || (echo "❌ Frontend falhou ($S)" && exit 1)
|
||||||
echo "$LOCATION" | grep -q "https://" || (echo "❌ Redirect HTTP→HTTPS ausente" && exit 1)
|
|
||||||
|
|
||||||
- name: Check backend /health
|
- name: HTTP → HTTPS redirect
|
||||||
run: |
|
run: |
|
||||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
L=$(curl -s -o /dev/null -w "%{redirect_url}" --max-time 10 "http://${{ vars.DOMAIN }}")
|
||||||
--max-time 15 "https://${{ vars.DOMAIN }}/api/health")
|
echo "Redirect: $L"
|
||||||
echo "Backend health: $STATUS"
|
echo "$L" | grep -q "https://" || (echo "❌ Redirect ausente" && exit 1)
|
||||||
[ "$STATUS" = "200" ] || (echo "❌ Backend health check falhou ($STATUS)" && exit 1)
|
|
||||||
|
|
||||||
- name: Check TLS certificate validity
|
- name: Backend /api/health
|
||||||
run: |
|
run: |
|
||||||
EXPIRY=$(echo | openssl s_client \
|
R=$(curl -s --max-time 15 "https://${{ vars.DOMAIN }}/api/health")
|
||||||
-connect ${{ vars.DOMAIN }}:443 \
|
S=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "https://${{ vars.DOMAIN }}/api/health")
|
||||||
|
echo "Health: $S → $R"
|
||||||
|
[ "$S" = "200" ] || (echo "❌ Health falhou ($S)" && exit 1)
|
||||||
|
echo "$R" | grep -q '"db": "ok"' || (echo "❌ DB não conectado" && exit 1)
|
||||||
|
|
||||||
|
- name: TLS certificate validity
|
||||||
|
run: |
|
||||||
|
EXPIRY=$(echo | openssl s_client -connect ${{ vars.DOMAIN }}:443 \
|
||||||
-servername ${{ vars.DOMAIN }} 2>/dev/null \
|
-servername ${{ vars.DOMAIN }} 2>/dev/null \
|
||||||
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||||
echo "Certificado expira: $EXPIRY"
|
echo "Expira: $EXPIRY"
|
||||||
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
|
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
|
||||||
NOW_EPOCH=$(date +%s)
|
echo "Dias restantes: $DAYS"
|
||||||
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
|
[ "$DAYS" -gt 7 ] || (echo "❌ Cert expira em $DAYS dias" && exit 1)
|
||||||
echo "Dias restantes: $DAYS_LEFT"
|
|
||||||
[ "$DAYS_LEFT" -gt 7 ] || (echo "❌ Certificado expira em $DAYS_LEFT dias!" && exit 1)
|
|
||||||
|
|
||||||
- name: Check API properties endpoint
|
- name: GET /api/properties
|
||||||
run: |
|
run: |
|
||||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
S=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "https://${{ vars.DOMAIN }}/api/properties?limit=1")
|
||||||
--max-time 15 "https://${{ vars.DOMAIN }}/api/properties?limit=1")
|
echo "Properties: $S"
|
||||||
echo "Properties API: $STATUS"
|
[ "$S" = "200" ] || (echo "❌ Properties falhou ($S)" && exit 1)
|
||||||
[ "$STATUS" = "200" ] || (echo "❌ Properties API falhou ($STATUS)" && exit 1)
|
|
||||||
|
|
||||||
- name: Check deployed version matches image tag
|
- name: Version tag matches
|
||||||
run: |
|
run: |
|
||||||
RESPONSE=$(curl -s --max-time 10 "https://${{ vars.DOMAIN }}/api/version")
|
R=$(curl -s --max-time 10 "https://${{ vars.DOMAIN }}/api/version")
|
||||||
echo "Version response: $RESPONSE"
|
echo "Version: $R"
|
||||||
echo "$RESPONSE" | grep -q "${{ needs.build.outputs.image_tag }}" \
|
echo "$R" | grep -q "${{ needs.build.outputs.image_tag }}" \
|
||||||
|| (echo "❌ Version tag não confere — esperado: ${{ needs.build.outputs.image_tag }}" && exit 1)
|
|| (echo "❌ Tag não confere — esperado ${{ needs.build.outputs.image_tag }}" && exit 1)
|
||||||
|
|
||||||
- name: All checks passed
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Deploy validado com sucesso!"
|
echo "✅ Todos os checks passaram!"
|
||||||
echo " → https://${{ vars.DOMAIN }} está no ar"
|
echo " → https://${{ vars.DOMAIN }} está no ar com tag ${{ needs.build.outputs.image_tag }}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue