Files
Christian Gick 6105040538
All checks were successful
Deploy Internet for Kids / Build & Push (push) Successful in 10s
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 / IndexNow Ping (push) Successful in 8s
Deploy Internet for Kids / Promote to Latest (push) Successful in 1s
Deploy Internet for Kids / Rollback (push) Has been skipped
Deploy Internet for Kids / Audit (push) Successful in 2s
fix CI: deploy on .md changes (content is the product)
Removed **.md from paths-ignore -- content changes must deploy since
Hugo builds markdown into the site. Only docs/ and .session/ ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:04:10 +03:00

259 lines
8.8 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:
- 'docs/**'
- '.session/**'
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.org"
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 }} \
.
docker push ${{ env.IMAGE }}:${{ github.sha }}
- 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 ${{ env.IMAGE }}:${{ github.sha }}
docker tag ${{ env.IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}: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/ /fr/; 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"
indexnow:
name: IndexNow Ping
runs-on: ubuntu-latest
needs: [smoke-test]
steps:
- name: Notify search engines
run: |
KEY="40101d97ec848f6ea016fac347b1a5bc"
for url in \
"https://internetforkids.org/" \
"https://internetforkids.org/en/" \
"https://internetforkids.org/de/" \
"https://internetforkids.org/fr/" \
"https://internetforkids.org/sitemap.xml"; do
curl -sf "https://api.indexnow.org/indexnow?url=${url}&key=${KEY}" || true
done
echo "IndexNow pings sent"
promote:
name: Promote to Latest
runs-on: ubuntu-latest
needs: [smoke-test]
steps:
- name: Login to registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u christian --password-stdin
- name: Tag and push latest
run: |
docker pull ${{ env.IMAGE }}:${{ github.sha }}
docker tag ${{ env.IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:latest
docker push ${{ env.IMAGE }}:latest
echo "Promoted ${{ github.sha }} to :latest"
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
# Push rolled-back image as :latest so Watchtower doesn't re-pull broken
docker push ${{ env.IMAGE }}:latest
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, promote, 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