Skip to content

Commit b786feb

Browse files
feat: add GitHub Actions deployment pipeline
1 parent d15dd63 commit b786feb

4 files changed

Lines changed: 303 additions & 18 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Deploy Discord Bot to Hetzner
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'discord-server/**'
9+
- 'docker-compose.yml'
10+
- 'deploy.sh'
11+
- '.github/workflows/deploy-discord-bot.yml'
12+
13+
jobs:
14+
deploy:
15+
runs-on: ubuntu-latest
16+
name: Deploy to Hetzner VPS
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Setup Bun
22+
uses: oven-sh/setup-bun@v2
23+
with:
24+
bun-version: latest
25+
26+
- name: Install dependencies
27+
working-directory: discord-server
28+
run: bun install --frozen-lockfile
29+
30+
- name: Setup SSH
31+
run: |
32+
mkdir -p ~/.ssh
33+
echo "${{ secrets.HETZNER_SSH_KEY }}" > ~/.ssh/id_ed25519
34+
chmod 600 ~/.ssh/id_ed25519
35+
36+
# Configure SSH host alias
37+
cat >> ~/.ssh/config << EOF
38+
Host hetzner
39+
HostName ${{ secrets.HETZNER_HOST }}
40+
User ${{ secrets.HETZNER_USER }}
41+
IdentityFile ~/.ssh/id_ed25519
42+
StrictHostKeyChecking no
43+
EOF
44+
45+
# Add host to known_hosts
46+
ssh-keyscan -H ${{ secrets.HETZNER_HOST }} >> ~/.ssh/known_hosts
47+
48+
- name: Create .env from secrets
49+
working-directory: discord-server
50+
run: |
51+
cat << EOF > .env
52+
DISCORD_BOT_TOKEN=${{ secrets.DISCORD_BOT_TOKEN }}
53+
DISCORD_INBOX_CHANNEL_ID=${{ secrets.DISCORD_INBOX_CHANNEL_ID }}
54+
CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
55+
OBSIDIAN_VAULT_PATH=/srv/claude-jobs/obsidian-vault
56+
REDIS_HOST=obsidian-redis
57+
REDIS_PORT=6379
58+
PORT=3001
59+
NODE_ENV=production
60+
EOF
61+
62+
- name: Run deploy.sh
63+
run: |
64+
# Make deploy.sh executable
65+
chmod +x deploy.sh
66+
67+
# Run deploy script
68+
./deploy.sh
69+
env:
70+
CI: true
71+
72+
- name: Deployment summary
73+
if: success()
74+
run: |
75+
echo "✅ Discord Bot deployment complete!"
76+
echo "🤖 Bot: Internal Discord bot (no public endpoint)"
77+
echo "🏥 Health: https://obsidian.quietloop.dev/health"

CLAUDE.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,57 @@ curl http://localhost:3001/health # Via tunnel (direct to contai
158158
- Next.js chat interface with AI SDK hooks
159159
- Professional UI for web-based interaction
160160

161+
## Production Deployment
162+
163+
### Automated Deployment via GitHub Actions
164+
165+
**GitHub Actions Workflow:** `.github/workflows/deploy-discord-bot.yml`
166+
167+
**Trigger:** Push to `main` with changes to:
168+
- `discord-server/**`
169+
- `docker-compose.yml`
170+
- `deploy.sh`
171+
- `.github/workflows/deploy-discord-bot.yml`
172+
173+
**Deploy Flow:**
174+
1. Bun setup + dependency installation
175+
2. SSH setup via GitHub Secrets
176+
3. Create `.env` from GitHub Secrets
177+
4. Execute `deploy.sh` (CI mode)
178+
5. Health check + deployment summary
179+
180+
**Manual Deploy (fallback):**
181+
```bash
182+
./deploy.sh # Detects CI mode automatically
183+
```
184+
185+
### GitHub Secrets Setup (One-Time)
186+
187+
**Sync Secrets to GitHub:**
188+
```bash
189+
# One command syncs everything (reads discord-server/.env + SSH config)
190+
./scripts/sync-secrets-to-github.sh
191+
```
192+
193+
**What the script does:**
194+
- Reads all secrets from `discord-server/.env` and syncs to GitHub
195+
- Auto-detects Hetzner SSH config from `~/.ssh/config` (hetzner alias)
196+
- Extracts SSH key, host, and user automatically
197+
- Validates all required secrets are configured
198+
199+
**Required Secrets:**
200+
- `DISCORD_BOT_TOKEN` - Discord bot authentication
201+
- `DISCORD_INBOX_CHANNEL_ID` - Channel for bot messages
202+
- `CLAUDE_CODE_OAUTH_TOKEN` - Claude API authentication
203+
- `HETZNER_SSH_KEY` - SSH private key for deployment
204+
- `HETZNER_HOST` - VPS IP address
205+
- `HETZNER_USER` - SSH user (usually root)
206+
207+
**When to Re-run:**
208+
- Initial setup (one-time)
209+
- When secrets in `discord-server/.env` change
210+
- When rotating credentials
211+
161212
## Production Infrastructure
162213

163214
### Clean Architecture ✅

deploy.sh

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,48 @@ REMOTE_PATH="~/obsidian-bridge-server"
1010
PROJECT_NAME="quietloop-claude-obsidian-server"
1111
SERVICE_URL="https://obsidian.quietloop.dev"
1212

13-
# Load port validation from infra repo
14-
INFRA_SCRIPTS="../quietloop-hetzner-infra/scripts"
15-
if [ -f "$INFRA_SCRIPTS/validate-local-ports.sh" ]; then
16-
source "$INFRA_SCRIPTS/validate-local-ports.sh"
13+
# Load port validation from infra repo (skip in CI)
14+
if [ -z "$CI" ]; then
15+
INFRA_SCRIPTS="../quietloop-hetzner-infra/scripts"
16+
if [ -f "$INFRA_SCRIPTS/validate-local-ports.sh" ]; then
17+
source "$INFRA_SCRIPTS/validate-local-ports.sh"
18+
else
19+
echo "⚠️ Port validation script not found - skipping validation"
20+
validate_local_ports() { return 0; }
21+
fi
1722
else
18-
echo "⚠️ Port validation script not found - skipping validation"
23+
# CI mode: Skip port validation
1924
validate_local_ports() { return 0; }
2025
fi
2126

22-
# Load Claude token from local discord-server/.env
23-
if [ -f discord-server/.env ]; then
24-
source discord-server/.env
25-
echo "🔑 Loaded environment from discord-server/.env"
26-
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
27-
echo "🔑 Claude token found in discord-server/.env"
27+
# Load or verify environment variables
28+
if [ -z "$CI" ]; then
29+
# Local deployment: Load from .env file
30+
if [ -f discord-server/.env ]; then
31+
source discord-server/.env
32+
echo "🔑 Loaded environment from discord-server/.env"
33+
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
34+
echo "🔑 Claude token found in discord-server/.env"
35+
else
36+
echo "⚠️ WARNING: No CLAUDE_CODE_OAUTH_TOKEN in discord-server/.env. Bot will fail authentication."
37+
fi
38+
if [ -n "$DISCORD_BOT_TOKEN" ]; then
39+
echo "🤖 Discord bot token found in discord-server/.env"
40+
else
41+
echo "⚠️ WARNING: No DISCORD_BOT_TOKEN in discord-server/.env. Bot will fail to connect."
42+
fi
2843
else
29-
echo "⚠️ WARNING: No CLAUDE_CODE_OAUTH_TOKEN in discord-server/.env. Bot will fail authentication."
44+
echo "❌ ERROR: discord-server/.env file not found"
45+
exit 1
3046
fi
31-
if [ -n "$DISCORD_BOT_TOKEN" ]; then
32-
echo "🤖 Discord bot token found in discord-server/.env"
47+
else
48+
# CI deployment: .env file created by GitHub Actions
49+
if [ -f discord-server/.env ]; then
50+
echo "🔑 Using .env created by GitHub Actions"
3351
else
34-
echo "⚠️ WARNING: No DISCORD_BOT_TOKEN in discord-server/.env. Bot will fail to connect."
52+
echo "❌ ERROR: discord-server/.env not found (should be created by GitHub Actions)"
53+
exit 1
3554
fi
36-
else
37-
echo "❌ ERROR: discord-server/.env file not found"
38-
exit 1
3955
fi
4056

4157
# Pre-deployment validation

scripts/sync-secrets-to-github.sh

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Sync discord-server/.env secrets to GitHub repository
5+
# Requires: gh CLI (brew install gh)
6+
7+
echo "🔐 Syncing discord-server/.env to GitHub Secrets..."
8+
echo ""
9+
10+
# Check if gh is installed
11+
if ! command -v gh &> /dev/null; then
12+
echo "❌ gh CLI is not installed"
13+
echo "Install: brew install gh"
14+
exit 1
15+
fi
16+
17+
# Check if authenticated
18+
if ! gh auth status &> /dev/null; then
19+
echo "❌ Not authenticated with GitHub"
20+
echo "Run: gh auth login"
21+
exit 1
22+
fi
23+
24+
# Check if discord-server/.env exists
25+
if [ ! -f "discord-server/.env" ]; then
26+
echo "❌ discord-server/.env not found"
27+
echo "Create it first with your production secrets"
28+
exit 1
29+
fi
30+
31+
# Get repository (auto-detect from git remote)
32+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "")
33+
if [ -z "$REPO" ]; then
34+
echo "❌ Could not detect GitHub repository"
35+
echo "Make sure you're in a git repository with a GitHub remote"
36+
exit 1
37+
fi
38+
39+
echo "📦 Repository: $REPO"
40+
echo ""
41+
42+
# Parse discord-server/.env and set secrets
43+
while IFS='=' read -r key value; do
44+
# Skip empty lines and comments
45+
[[ -z "$key" || "$key" =~ ^# ]] && continue
46+
47+
# Remove quotes from value
48+
value=$(echo "$value" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")
49+
50+
echo "🔑 Setting secret: $key"
51+
echo "$value" | gh secret set "$key" --repo "$REPO"
52+
done < discord-server/.env
53+
54+
echo ""
55+
echo "✅ All discord-server/.env secrets synced to GitHub!"
56+
echo ""
57+
58+
# Auto-detect SSH config from 'hetzner' alias
59+
echo "🔍 Auto-detecting Hetzner SSH config..."
60+
if ssh -G hetzner &>/dev/null; then
61+
HETZNER_USER=$(ssh -G hetzner 2>/dev/null | grep "^user " | awk '{print $2}')
62+
HETZNER_HOST=$(ssh -G hetzner 2>/dev/null | grep "^hostname " | awk '{print $2}')
63+
SSH_KEY_PATH=$(ssh -G hetzner 2>/dev/null | grep "^identityfile " | head -1 | awk '{print $2}')
64+
65+
# Expand ~ in path
66+
SSH_KEY_PATH="${SSH_KEY_PATH/#\~/$HOME}"
67+
68+
echo "📦 Detected from SSH config:"
69+
echo " Host: $HETZNER_HOST"
70+
echo " User: $HETZNER_USER"
71+
echo " Key: $SSH_KEY_PATH"
72+
echo ""
73+
74+
# Set Hetzner secrets
75+
if [ -f "$SSH_KEY_PATH" ]; then
76+
echo "🔑 Setting HETZNER_SSH_KEY..."
77+
cat "$SSH_KEY_PATH" | gh secret set HETZNER_SSH_KEY --repo "$REPO"
78+
79+
echo "🔑 Setting HETZNER_HOST..."
80+
echo "$HETZNER_HOST" | gh secret set HETZNER_HOST --repo "$REPO"
81+
82+
echo "🔑 Setting HETZNER_USER..."
83+
echo "$HETZNER_USER" | gh secret set HETZNER_USER --repo "$REPO"
84+
85+
echo ""
86+
echo "✅ All Hetzner SSH secrets synced!"
87+
else
88+
echo "⚠️ SSH key not found at: $SSH_KEY_PATH"
89+
echo "Set manually:"
90+
echo " cat ~/.ssh/your_key | gh secret set HETZNER_SSH_KEY --repo $REPO"
91+
fi
92+
else
93+
echo "⚠️ 'hetzner' SSH alias not found in ~/.ssh/config"
94+
echo ""
95+
echo "📋 Set Hetzner secrets manually:"
96+
echo " cat ~/.ssh/your_deploy_key | gh secret set HETZNER_SSH_KEY --repo $REPO"
97+
echo " echo 'your.server.ip' | gh secret set HETZNER_HOST --repo $REPO"
98+
echo " echo 'root' | gh secret set HETZNER_USER --repo $REPO"
99+
fi
100+
101+
echo ""
102+
echo "🔍 Validating all required secrets..."
103+
echo ""
104+
105+
# Required secrets for Discord Bot deployment
106+
REQUIRED_SECRETS=(
107+
"DISCORD_BOT_TOKEN"
108+
"DISCORD_INBOX_CHANNEL_ID"
109+
"CLAUDE_CODE_OAUTH_TOKEN"
110+
"HETZNER_SSH_KEY"
111+
"HETZNER_HOST"
112+
"HETZNER_USER"
113+
)
114+
115+
# Get list of existing secrets
116+
EXISTING_SECRETS=$(gh secret list --repo "$REPO" 2>/dev/null | awk '{print $1}')
117+
118+
# Check each required secret
119+
MISSING_SECRETS=()
120+
for secret in "${REQUIRED_SECRETS[@]}"; do
121+
if echo "$EXISTING_SECRETS" | grep -q "^${secret}$"; then
122+
echo "$secret"
123+
else
124+
echo "$secret (MISSING)"
125+
MISSING_SECRETS+=("$secret")
126+
fi
127+
done
128+
129+
echo ""
130+
if [ ${#MISSING_SECRETS[@]} -eq 0 ]; then
131+
echo "🎉 All required secrets are configured!"
132+
echo "✅ Ready for automated deployment via GitHub Actions"
133+
else
134+
echo "⚠️ Missing ${#MISSING_SECRETS[@]} secret(s):"
135+
for secret in "${MISSING_SECRETS[@]}"; do
136+
echo " - $secret"
137+
done
138+
echo ""
139+
echo "Run this script again or set missing secrets manually"
140+
exit 1
141+
fi

0 commit comments

Comments
 (0)