CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | name: Vultr Deploy (gluecron.com)
# Triggered on every push to main and on manual dispatch.
# Steps:
# 1. Capture the current SHA on the box (for rollback)
# 2. SSH in, git pull, run deploy-crontech.sh (which restarts systemd + reloads Caddy)
# 3. Smoke-test https://gluecron.com/healthz with retries
# 4. On smoke failure: roll back to the previous SHA and restart
# 5. On any failure: have Claude read the last 100 journal lines and post a one-paragraph
# root-cause analysis to the workflow summary (and optionally a webhook)
#
# Secrets required:
# VULTR_HOST — public IP/hostname of the box (e.g. 45.76.171.37)
# VULTR_USER — ssh user (e.g. root)
# VULTR_SSH_KEY — private deploy key (PEM/OpenSSH format)
#
# Optional:
# ANTHROPIC_API_KEY — enables AI failure-diagnosis step
# DEPLOY_WEBHOOK_URL — POSTed with JSON deploy status (Slack/Discord/anything)
on:
push:
branches: [main]
workflow_dispatch: {}
concurrency:
group: vultr-deploy
cancel-in-progress: false
jobs:
deploy:
name: Deploy gluecron.com
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
# ─── 1. Capture pre-deploy SHA so we can rollback ───────────────────
- name: Capture pre-deploy SHA
id: prev
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VULTR_HOST }}
username: ${{ secrets.VULTR_USER }}
key: ${{ secrets.VULTR_SSH_KEY }}
script_stop: true
script: |
cd /opt/gluecron
sha=$(git rev-parse HEAD)
echo "Previous SHA: $sha"
echo "$sha" > /tmp/gluecron_prev_sha
cat /tmp/gluecron_prev_sha
# ─── 2. Deploy: pull main, run the production script ────────────────
- name: Deploy
id: deploy
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VULTR_HOST }}
username: ${{ secrets.VULTR_USER }}
key: ${{ secrets.VULTR_SSH_KEY }}
command_timeout: 8m
script_stop: true
script: |
set -euo pipefail
cd /opt/gluecron
git fetch --prune origin main
git reset --hard origin/main
new_sha=$(git rev-parse HEAD)
echo "Deploying SHA: $new_sha"
bash scripts/deploy-crontech.sh
# ─── 3. Smoke-test the live URL ─────────────────────────────────────
- name: Smoke test (https://gluecron.com/healthz)
id: smoke
run: |
set +e
for i in 1 2 3 4 5 6 7 8; do
code=$(curl -s -o /dev/null -w "%{http_code}" https://gluecron.com/healthz)
echo "Attempt $i: /healthz → $code"
if [ "$code" = "200" ]; then
echo "smoke_status=ok" >> $GITHUB_OUTPUT
echo "✓ gluecron.com is live and healthy."
exit 0
fi
sleep 8
done
echo "smoke_status=fail" >> $GITHUB_OUTPUT
echo "✗ /healthz did not return 200 after 64s. Triggering rollback."
exit 1
# ─── 4. Auto-rollback on smoke failure ──────────────────────────────
# Only rolls back if the workflow was triggered by a normal push.
# Manual workflow_dispatch runs SKIP rollback so the operator can
# diagnose the new code on the box before reverting. This stops the
# pathological case where rollback masks the real failure by reverting
# to an already-broken previous SHA.
- name: Rollback on failure
if: failure() && steps.smoke.conclusion == 'failure' && github.event_name == 'push'
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VULTR_HOST }}
username: ${{ secrets.VULTR_USER }}
key: ${{ secrets.VULTR_SSH_KEY }}
script_stop: false
script: |
cd /opt/gluecron
prev=$(cat /tmp/gluecron_prev_sha)
echo "Rolling back to $prev"
git reset --hard "$prev"
systemctl restart gluecron
sleep 5
systemctl status gluecron --no-pager | head -20 || true
# ─── 5. Failure diagnostics — captured into a file for summary + AI ──
- name: Capture failure context
if: failure()
id: ctx
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VULTR_HOST }}
username: ${{ secrets.VULTR_USER }}
key: ${{ secrets.VULTR_SSH_KEY }}
script_stop: false
script: |
echo "===== systemd status ====="
systemctl status gluecron --no-pager 2>&1 | head -40 || true
echo ""
echo "===== last 80 journal lines (gluecron) ====="
journalctl -u gluecron -n 80 --no-pager --output=cat 2>&1 || true
echo ""
echo "===== caddy validate ====="
caddy validate --config /etc/caddy/Caddyfile 2>&1 | head -20 || true
echo ""
echo "===== /healthz from inside box ====="
curl -s -w "\nHTTP %{http_code}\n" http://localhost:3000/healthz 2>&1 || true
echo ""
echo "===== port 3000 listener ====="
ss -tlnp 2>&1 | grep ':3000' || echo '(nothing listening on :3000)'
# Always post the captured diagnostics to the workflow summary so the
# owner can read what broke without SSH'ing or grepping log files.
- name: Post diagnostics to summary
if: failure() && steps.ctx.outputs.stdout != ''
env:
DIAG: ${{ steps.ctx.outputs.stdout }}
run: |
{
echo "## ❌ Deploy failed — diagnostics"
echo ""
echo "**Commit:** \`${GITHUB_SHA:0:7}\` — ${GITHUB_EVENT_HEAD_COMMIT_MESSAGE:-${GITHUB_SHA:0:7}}"
echo "**Run:** [#${GITHUB_RUN_ID}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
echo ""
echo '```'
echo "$DIAG"
echo '```'
} >> $GITHUB_STEP_SUMMARY
- name: AI root-cause analysis (Claude)
if: failure() && env.ANTHROPIC_API_KEY != ''
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
DEPLOY_LOGS: ${{ steps.ctx.outputs.stdout }}
COMMIT_SHA: ${{ github.sha }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
set +e
# Build the prompt
cat > /tmp/prompt.json <<EOF
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 600,
"system": "You are a senior SRE diagnosing a failed deploy. Read the systemd status, journal, and curl output. In 1 short paragraph (under 100 words), identify the most likely root cause and the single fastest fix. Be direct, no preamble.",
"messages": [{
"role": "user",
"content": "Commit: $COMMIT_SHA\nMessage: $COMMIT_MSG\n\nDeploy logs:\n$DEPLOY_LOGS"
}]
}
EOF
response=$(curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data @/tmp/prompt.json)
analysis=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('content',[{}])[0].get('text','(no analysis)'))" 2>/dev/null)
echo "## 🤖 AI Failure Analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${COMMIT_SHA:0:7}\` — $COMMIT_MSG" >> $GITHUB_STEP_SUMMARY
# ─── 6. Optional webhook notification ───────────────────────────────
- name: Notify webhook
if: always() && env.DEPLOY_WEBHOOK_URL != ''
env:
DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
STATUS: ${{ job.status }}
run: |
curl -s -X POST "$DEPLOY_WEBHOOK_URL" \
-H "content-type: application/json" \
--data "{\"status\":\"$STATUS\",\"target\":\"gluecron.com\",\"sha\":\"${{ github.sha }}\",\"run\":\"${{ github.run_id }}\"}" || true
# ─── 7. Workflow summary on success ─────────────────────────────────
- name: Success summary
if: success()
run: |
echo "## ✅ Deploy succeeded" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Target:** https://gluecron.com" >> $GITHUB_STEP_SUMMARY
echo "- **SHA:** \`${GITHUB_SHA:0:7}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Health:** /healthz → 200" >> $GITHUB_STEP_SUMMARY
|