···11+name: Deploy Control
22+33+on:
44+ push:
55+ branches:
66+ - main
77+ workflow_dispatch:
88+99+jobs:
1010+ deploy:
1111+ runs-on: ubuntu-latest
1212+ steps:
1313+ - uses: actions/checkout@v3
1414+1515+ - name: Setup Tailscale
1616+ uses: tailscale/github-action@v3
1717+ with:
1818+ oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
1919+ oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
2020+ tags: tag:ci
2121+ use-cache: "true"
2222+2323+ - name: Configure SSH
2424+ run: |
2525+ mkdir -p ~/.ssh
2626+ echo "StrictHostKeyChecking no" >> ~/.ssh/config
2727+2828+ - name: Deploy to server
2929+ run: |
3030+ ssh control@terebithia << 'EOF'
3131+ cd /var/lib/control/app
3232+ git fetch --all
3333+ git reset --hard origin/main
3434+ bun install
3535+ sudo /run/current-system/sw/bin/systemctl restart control.service
3636+ EOF
3737+3838+ - name: Health check
3939+ run: |
4040+ HEALTH_URL="https://control.dunkirk.sh/health"
4141+ MAX_RETRIES=6
4242+ RETRY_DELAY=5
4343+4444+ for i in $(seq 1 $MAX_RETRIES); do
4545+ echo "Health check attempt $i/$MAX_RETRIES..."
4646+4747+ RESPONSE=$(curl -s -w "\n%{http_code}" "$HEALTH_URL" || echo "000")
4848+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
4949+ BODY=$(echo "$RESPONSE" | head -n-1)
5050+5151+ if [ "$HTTP_CODE" = "200" ]; then
5252+ # Validate response contains "status":"ok"
5353+ if echo "$BODY" | grep -q '"status":"ok"'; then
5454+ echo "✅ Service is healthy (HTTP $HTTP_CODE)"
5555+ echo "Response: $BODY"
5656+ exit 0
5757+ else
5858+ echo "❌ Health check returned 200 but invalid body"
5959+ echo "Response: $BODY"
6060+ fi
6161+ else
6262+ echo "❌ Health check failed with HTTP $HTTP_CODE"
6363+ echo "Response: $BODY"
6464+ fi
6565+6666+ if [ $i -lt $MAX_RETRIES ]; then
6767+ echo "Retrying in ${RETRY_DELAY}s..."
6868+ sleep $RETRY_DELAY
6969+ fi
7070+ done
7171+7272+ echo "❌ Health check failed after $MAX_RETRIES attempts"
7373+ exit 1
+4
src/index.ts
···3939 });
4040});
41414242+app.get("/health", (c) => {
4343+ return c.json({ status: "ok" });
4444+});
4545+4246// Kill-check endpoint for Caddy to call before proxying protected routes
4347// Returns 200 to allow, 503 to block
4448// No auth required - this is called by Caddy internally