All checks were successful
Deploy Internet for Kids / Build & Push (push) Successful in 33s
Deploy Internet for Kids / Deploy (push) Successful in 6s
Deploy Internet for Kids / Health Check (push) Successful in 2s
Deploy Internet for Kids / Smoke Tests (push) Successful in 3s
Deploy Internet for Kids / Rollback (push) Has been skipped
Deploy Internet for Kids / Audit (push) Successful in 2s
- docker-compose.yml in repo (replaces inline generation in CI) - /health nginx endpoint for container health checks - HEALTHCHECK directive in Dockerfile - CI pipeline: build → deploy → health check → smoke test → rollback on failure - Smoke tests verify /health, /en/, /de/ after deploy - Automatic rollback to previous image on health/smoke failure - workflow_dispatch with force_deploy option - Deploy-guard audit logging IFK-6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
224 lines
7.5 KiB
YAML
224 lines
7.5 KiB
YAML
# Internet for Kids - Production Deployment
|
|
# Deploys to infra VM at /opt/apps/internetforkids
|
|
#
|
|
# Features:
|
|
# - Docker registry-based deployment (build → push → pull)
|
|
# - Health check with automatic rollback on failure
|
|
# - Smoke tests against public URLs
|
|
# - Deploy-guard audit logging
|
|
|
|
name: Deploy Internet for Kids
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths-ignore:
|
|
- '**.md'
|
|
- 'docs/**'
|
|
workflow_dispatch:
|
|
inputs:
|
|
force_deploy:
|
|
description: 'Force deploy even if health checks fail'
|
|
required: false
|
|
default: 'false'
|
|
|
|
env:
|
|
PROJECT_NAME: "internetforkids"
|
|
REGISTRY: gitea.agiliton.internal:3000
|
|
IMAGE: gitea.agiliton.internal:3000/christian/internetforkids
|
|
REMOTE_HOST: infra.agiliton.internal
|
|
REMOTE_USER: root
|
|
DEPLOY_PATH: /opt/apps/internetforkids
|
|
HEALTH_ENDPOINT: "http://localhost:3006/health"
|
|
PUBLIC_URL: "https://internetforkids.ong"
|
|
HEALTH_TIMEOUT: 60
|
|
|
|
jobs:
|
|
build-and-push:
|
|
name: Build & Push
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: true
|
|
|
|
- name: Login to registry
|
|
run: |
|
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u christian --password-stdin
|
|
|
|
- name: Build and push
|
|
run: |
|
|
DOCKER_BUILDKIT=1 docker build --pull \
|
|
-t ${{ env.IMAGE }}:${{ github.sha }} \
|
|
-t ${{ env.IMAGE }}:latest \
|
|
.
|
|
docker push ${{ env.IMAGE }}:${{ github.sha }}
|
|
docker push ${{ env.IMAGE }}:latest
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
run: docker builder prune -f --filter "until=24h" 2>/dev/null || true
|
|
|
|
deploy:
|
|
name: Deploy
|
|
runs-on: ubuntu-latest
|
|
needs: build-and-push
|
|
outputs:
|
|
previous_image: ${{ steps.deploy.outputs.previous_image }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup SSH
|
|
run: |
|
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H ${{ env.REMOTE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
- name: Deploy
|
|
id: deploy
|
|
run: |
|
|
# Capture current image digest for rollback
|
|
PREVIOUS=$(ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} \
|
|
"docker inspect --format='{{.Image}}' internetforkids 2>/dev/null || echo 'none'")
|
|
echo "previous_image=$PREVIOUS" >> $GITHUB_OUTPUT
|
|
echo "Previous image: $PREVIOUS"
|
|
|
|
# Sync docker-compose.yml and deploy
|
|
scp docker-compose.yml ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }}:${{ env.DEPLOY_PATH }}/docker-compose.yml
|
|
|
|
ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} << 'EOF'
|
|
cd /opt/apps/internetforkids
|
|
docker pull gitea.agiliton.internal:3000/christian/internetforkids:latest
|
|
docker compose up -d --force-recreate --remove-orphans
|
|
EOF
|
|
|
|
echo "Deployment initiated"
|
|
|
|
health-check:
|
|
name: Health Check
|
|
runs-on: ubuntu-latest
|
|
needs: deploy
|
|
steps:
|
|
- name: Setup SSH
|
|
run: |
|
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H ${{ env.REMOTE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
- name: Wait for health
|
|
run: |
|
|
echo "Waiting for health (timeout: ${HEALTH_TIMEOUT}s)..."
|
|
ELAPSED=0
|
|
while [ $ELAPSED -lt $HEALTH_TIMEOUT ]; do
|
|
if ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} \
|
|
"curl -sf ${{ env.HEALTH_ENDPOINT }}" >/dev/null 2>&1; then
|
|
echo "Health check passed!"
|
|
exit 0
|
|
fi
|
|
echo "Waiting... ($ELAPSED/${HEALTH_TIMEOUT}s)"
|
|
sleep 10
|
|
ELAPSED=$((ELAPSED + 10))
|
|
done
|
|
|
|
echo "::error::Health check timeout after ${HEALTH_TIMEOUT}s"
|
|
ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} \
|
|
"cd ${{ env.DEPLOY_PATH }} && docker compose ps && docker compose logs --tail 20" || true
|
|
|
|
if [ "${{ github.event.inputs.force_deploy }}" = "true" ]; then
|
|
echo "::warning::Force deploy — continuing despite health failure"
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
|
|
smoke-test:
|
|
name: Smoke Tests
|
|
runs-on: ubuntu-latest
|
|
needs: health-check
|
|
steps:
|
|
- name: Setup SSH
|
|
run: |
|
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H ${{ env.REMOTE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
- name: Test key URLs
|
|
run: |
|
|
FAILED=0
|
|
for path in /health /en/ /de/; do
|
|
STATUS=$(ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} \
|
|
"curl -so /dev/null -w '%{http_code}' http://localhost:3006${path}" 2>/dev/null || echo "000")
|
|
if [ "$STATUS" = "200" ] || [ "$STATUS" = "302" ]; then
|
|
echo "OK $path → $STATUS"
|
|
else
|
|
echo "FAIL $path → $STATUS"
|
|
FAILED=$((FAILED + 1))
|
|
fi
|
|
done
|
|
|
|
if [ $FAILED -gt 0 ]; then
|
|
echo "::error::$FAILED smoke test(s) failed"
|
|
exit 1
|
|
fi
|
|
echo "All smoke tests passed"
|
|
|
|
rollback:
|
|
name: Rollback
|
|
runs-on: ubuntu-latest
|
|
needs: [deploy, health-check, smoke-test]
|
|
if: failure() && needs.deploy.outputs.previous_image != 'none' && needs.deploy.outputs.previous_image != ''
|
|
steps:
|
|
- name: Setup SSH
|
|
run: |
|
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H ${{ env.REMOTE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
- name: Rollback to previous image
|
|
run: |
|
|
PREVIOUS="${{ needs.deploy.outputs.previous_image }}"
|
|
echo "::warning::Rolling back to image $PREVIOUS"
|
|
|
|
ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} << EOF
|
|
cd ${{ env.DEPLOY_PATH }}
|
|
docker tag $PREVIOUS ${{ env.IMAGE }}:latest
|
|
docker compose up -d --force-recreate
|
|
EOF
|
|
|
|
- name: Verify rollback
|
|
run: |
|
|
sleep 10
|
|
if ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} \
|
|
"curl -sf ${{ env.HEALTH_ENDPOINT }}" >/dev/null 2>&1; then
|
|
echo "Rollback successful"
|
|
else
|
|
echo "::error::Rollback may have failed — check manually"
|
|
fi
|
|
|
|
audit:
|
|
name: Audit
|
|
runs-on: ubuntu-latest
|
|
needs: [deploy, health-check, smoke-test, rollback]
|
|
if: always()
|
|
steps:
|
|
- name: Setup SSH
|
|
run: |
|
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
ssh-keyscan -H ${{ env.REMOTE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
- name: Log deployment
|
|
run: |
|
|
STATUS="success"
|
|
[ "${{ needs.health-check.result }}" = "failure" ] && STATUS="failed"
|
|
[ "${{ needs.smoke-test.result }}" = "failure" ] && STATUS="failed"
|
|
[ "${{ needs.rollback.result }}" = "success" ] && STATUS="rolled_back"
|
|
|
|
echo "${{ env.PROJECT_NAME }} deployment: $STATUS (sha: ${{ github.sha }})"
|
|
ssh ${{ env.REMOTE_USER }}@${{ env.REMOTE_HOST }} \
|
|
"/opt/scripts/deploy-guard.sh check 'ci-deploy ${{ env.PROJECT_NAME }} $STATUS'" 2>/dev/null || true
|