feat: Add deployment guide and update README for Coolify setup; include dotenv for environment variable management
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
# Deployment Guide
|
||||
|
||||
This document covers production deployment for the `omo-bot` Coolify project with **two separate resources**:
|
||||
|
||||
- `omo-bot-backend` (Node.js bot + Admin API)
|
||||
- `omo-bot-dashboard` (React/Vite static dashboard from `admin-dashboard/`)
|
||||
|
||||
## Deployment Topology
|
||||
|
||||
Recommended domains:
|
||||
|
||||
- `api.yourdomain.com` -> `omo-bot-backend`
|
||||
- `admin.yourdomain.com` -> `omo-bot-dashboard`
|
||||
|
||||
Optional same-origin pattern:
|
||||
|
||||
- `admin.yourdomain.com` serves dashboard and reverse-proxies `/admin/*` + `/health` to backend.
|
||||
|
||||
## 1. Create Coolify Project and Resources
|
||||
|
||||
1. In Coolify, create (or open) project `omo-bot`.
|
||||
2. Add resource `omo-bot-backend` from this repository:
|
||||
- Build context: repository root
|
||||
- Start command: `npm start`
|
||||
- Port: `8787` (Admin API) if exposed
|
||||
3. Add resource `omo-bot-dashboard` from this repository:
|
||||
- Base directory: `admin-dashboard`
|
||||
- Build command: `npm run build`
|
||||
- Publish directory: `dist`
|
||||
4. Configure domains for each resource and enable TLS certificates.
|
||||
|
||||
## 2. Configure Runtime Variables
|
||||
|
||||
Backend (`omo-bot-backend`) minimum:
|
||||
|
||||
- `DATABASE_URL`
|
||||
- `CONFIG_DB_ENABLED=true`
|
||||
- Any temporary legacy vars needed for first-time `db:seed`
|
||||
|
||||
Dashboard (`omo-bot-dashboard`) minimum:
|
||||
|
||||
- `VITE_ADMIN_API_BASE_URL` (for split-domain deployment, set to `https://api.yourdomain.com`)
|
||||
- `VITE_DISCORD_CLIENT_ID`
|
||||
- `VITE_DISCORD_REDIRECT_URI` (must match Discord OAuth2 redirect list)
|
||||
|
||||
OAuth alignment:
|
||||
|
||||
- Discord Developer Portal redirect URI must match dashboard redirect domain.
|
||||
- Config DB key `OAUTH_BRIDGE_REDIRECT_URI` must match the same URI.
|
||||
|
||||
## 3. Get Coolify Deployment Hooks and Tokens
|
||||
|
||||
For each resource (`omo-bot-backend` and `omo-bot-dashboard`):
|
||||
|
||||
1. Open resource in Coolify.
|
||||
2. Go to Deployments/Webhooks (name may vary by Coolify version).
|
||||
3. Copy the Deploy Webhook URL.
|
||||
4. If webhook auth token is enabled, copy the token.
|
||||
|
||||
Store both resources separately:
|
||||
|
||||
- Backend deploy hook URL/token
|
||||
- Dashboard deploy hook URL/token
|
||||
|
||||
## 4. Configure Gitea Action Secrets/Variables
|
||||
|
||||
In Gitea repository settings, add Actions secrets:
|
||||
|
||||
- `COOLIFY_DEPLOY_HOOK_URL_BOT`
|
||||
- `COOLIFY_DEPLOY_TOKEN_BOT` (optional)
|
||||
- `COOLIFY_DEPLOY_HOOK_URL_DASHBOARD`
|
||||
- `COOLIFY_DEPLOY_TOKEN_DASHBOARD` (optional)
|
||||
|
||||
Current workflow in `.gitea/workflows/ci-cd.yml` uses a single pair:
|
||||
|
||||
- `COOLIFY_DEPLOY_HOOK_URL`
|
||||
- `COOLIFY_DEPLOY_TOKEN`
|
||||
|
||||
If you use separate resources with separate hooks, update workflow deploy step to call both hooks.
|
||||
|
||||
## 5. Example Deploy Step for Two Coolify Resources
|
||||
|
||||
```yaml
|
||||
deploy-coolify:
|
||||
name: Deploy to Coolify
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- bot-checks
|
||||
- dashboard-checks
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- name: Trigger backend deploy
|
||||
env:
|
||||
HOOK_URL: ${{ secrets.COOLIFY_DEPLOY_HOOK_URL_BOT }}
|
||||
HOOK_TOKEN: ${{ secrets.COOLIFY_DEPLOY_TOKEN_BOT }}
|
||||
run: |
|
||||
if [ -z "$HOOK_URL" ]; then
|
||||
echo "Missing COOLIFY_DEPLOY_HOOK_URL_BOT"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$HOOK_TOKEN" ]; then
|
||||
curl --fail --show-error --silent -X POST -H "Authorization: Bearer $HOOK_TOKEN" "$HOOK_URL"
|
||||
else
|
||||
curl --fail --show-error --silent -X POST "$HOOK_URL"
|
||||
fi
|
||||
|
||||
- name: Trigger dashboard deploy
|
||||
env:
|
||||
HOOK_URL: ${{ secrets.COOLIFY_DEPLOY_HOOK_URL_DASHBOARD }}
|
||||
HOOK_TOKEN: ${{ secrets.COOLIFY_DEPLOY_TOKEN_DASHBOARD }}
|
||||
run: |
|
||||
if [ -z "$HOOK_URL" ]; then
|
||||
echo "Missing COOLIFY_DEPLOY_HOOK_URL_DASHBOARD"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$HOOK_TOKEN" ]; then
|
||||
curl --fail --show-error --silent -X POST -H "Authorization: Bearer $HOOK_TOKEN" "$HOOK_URL"
|
||||
else
|
||||
curl --fail --show-error --silent -X POST "$HOOK_URL"
|
||||
fi
|
||||
```
|
||||
|
||||
## 6. Domain and DNS Checklist
|
||||
|
||||
1. Create DNS records to Coolify host:
|
||||
- `A` record for root/subdomain targets
|
||||
- `CNAME` where appropriate
|
||||
2. Verify certificates issued for both domains.
|
||||
3. Verify dashboard can call API at configured `VITE_ADMIN_API_BASE_URL`.
|
||||
4. Verify API health endpoint over TLS:
|
||||
- `GET https://api.yourdomain.com/health`
|
||||
|
||||
## 7. Post-Deploy Verification
|
||||
|
||||
1. `npm run register:commands` (if bot app/guild IDs changed).
|
||||
2. Confirm bot online in Discord and slash commands visible.
|
||||
3. Open dashboard URL and run OAuth login flow.
|
||||
4. Validate Admin API auth behavior with and without bearer token.
|
||||
@@ -83,6 +83,41 @@ This project uses the arc42 template. Chapters are in `docs/`.
|
||||
- npm 10+
|
||||
- Discord Developer Application (bot token + OAuth2 app)
|
||||
- PostgreSQL instance
|
||||
- Discord server where you can install bots with role/channel permissions
|
||||
|
||||
### Discord Bot Setup (Portal)
|
||||
|
||||
1. Open Discord Developer Portal and create a new application.
|
||||
2. In `Bot` tab, click `Add Bot`.
|
||||
3. Copy these values for runtime config (`bot_settings`):
|
||||
- `DISCORD_TOKEN` from Bot tab (Reset Token if needed)
|
||||
- `DISCORD_CLIENT_ID` from OAuth2 > General
|
||||
4. In `Bot` > `Privileged Gateway Intents`, enable only:
|
||||
- `Server Members Intent` (required by current code)
|
||||
5. Keep these disabled unless code changes require them:
|
||||
- `Message Content Intent`
|
||||
- `Presence Intent`
|
||||
|
||||
Current gateway intents used by this project (`src/bot.ts`):
|
||||
|
||||
- `Guilds`
|
||||
- `GuildMembers` (privileged)
|
||||
- `GuildMessageReactions`
|
||||
|
||||
### Discord Bot Install (OAuth2 URL)
|
||||
|
||||
1. Open `OAuth2` > `URL Generator`.
|
||||
2. Select scopes:
|
||||
- `bot`
|
||||
- `applications.commands`
|
||||
3. Select bot permissions (minimum recommended for current features):
|
||||
- `View Channels`
|
||||
- `Send Messages`
|
||||
- `Read Message History`
|
||||
- `Add Reactions`
|
||||
- `Manage Roles` (call-sheet role assignment)
|
||||
- `Manage Channels` (queue moderation paths)
|
||||
4. Use generated URL to add bot to your server.
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -155,7 +190,7 @@ npm run db:setup
|
||||
```
|
||||
|
||||
- `db:migrate`: applies schema migrations tracked in `schema_migrations`
|
||||
- `db:seed`: inserts initial config values from environment into `bot_settings` when missing
|
||||
- `db:seed`: ensures migrations are applied, then inserts initial config values from environment into `bot_settings` when missing
|
||||
- `db:setup`: runs migrate then seed
|
||||
|
||||
### Runtime Config Source
|
||||
@@ -173,6 +208,16 @@ Recommended flow:
|
||||
3. Run `npm run db:seed` once while legacy env vars are still present.
|
||||
4. Remove legacy runtime vars from `.env` after confirming `bot_settings` contains required keys.
|
||||
|
||||
### Deployment
|
||||
|
||||
Deployment setup and domain configuration moved to `DEPLOYMENT.md`.
|
||||
|
||||
- Coolify project/resource setup
|
||||
- Real domain DNS/TLS and routing patterns
|
||||
- Discord OAuth production redirect alignment
|
||||
- Gitea Actions secret configuration for deploy hooks
|
||||
- Two-resource deploy flow (`omo-bot-backend` + `omo-bot-dashboard`)
|
||||
|
||||
Dashboard quality check:
|
||||
|
||||
```bash
|
||||
@@ -201,16 +246,7 @@ npm run build
|
||||
|
||||
Pipeline file: `.gitea/workflows/ci-cd.yml`
|
||||
|
||||
Flow:
|
||||
|
||||
- bot checks: `npm ci -> npm run lint -> npm run build -> npm run test`
|
||||
- dashboard checks: `admin-dashboard/npm ci -> npm run lint -> npm run build`
|
||||
- deploy: on push to `main`, trigger Coolify deploy webhook
|
||||
|
||||
Required Gitea secrets:
|
||||
|
||||
- `COOLIFY_DEPLOY_HOOK_URL` (required)
|
||||
- `COOLIFY_DEPLOY_TOKEN` (optional bearer token)
|
||||
For full deployment flow, webhook secret strategy, and two-resource Coolify deployment, see `DEPLOYMENT.md`.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
Generated
+13
@@ -10,6 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.26.4",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"pg": "^8.20.0"
|
||||
},
|
||||
@@ -3131,6 +3132,18 @@
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.26.4",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"pg": "^8.20.0"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "dotenv/config";
|
||||
import { migrateAndSeed, runMigrations, seedInitialConfig } from "./migrations";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -10,6 +11,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (command === "seed") {
|
||||
await runMigrations();
|
||||
const inserted = await seedInitialConfig();
|
||||
console.log(`Database seed complete. Inserted ${inserted} config entries.`);
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "dotenv/config";
|
||||
import { REST, Routes } from "discord.js";
|
||||
import { commands } from "./commands";
|
||||
import { loadRuntimeConfig } from "./config";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "dotenv/config";
|
||||
import { startBot } from "./bot";
|
||||
import { loadRuntimeConfig } from "./config";
|
||||
import { runMigrations } from "./db/migrations";
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ChatInputCommandInteraction } from "discord.js";
|
||||
import { pingCommand } from "../../src/commands/ping";
|
||||
|
||||
describe("pingCommand", () => {
|
||||
it("replies with Pong", async () => {
|
||||
const reply = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await pingCommand.execute({ reply } as any);
|
||||
await pingCommand.execute({
|
||||
reply,
|
||||
} as unknown as ChatInputCommandInteraction);
|
||||
|
||||
expect(reply).toHaveBeenCalledWith("Pong!");
|
||||
});
|
||||
|
||||
@@ -20,14 +20,15 @@ describe("signUpCommand", () => {
|
||||
(getTourSchedule as jest.Mock).mockReturnValue(queue);
|
||||
});
|
||||
|
||||
function createInteraction(action: string): any {
|
||||
function createInteraction(action: string) {
|
||||
return {
|
||||
inGuild: () => true,
|
||||
guildId: "guild-1",
|
||||
user: { id: "user-1" },
|
||||
guild: { id: "guild-1" },
|
||||
memberPermissions: {
|
||||
has: jest.fn((permission: bigint) =>
|
||||
has: jest.fn(
|
||||
(permission: bigint) =>
|
||||
permission === PermissionFlagsBits.ManageChannels,
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GuildMember } from "discord.js";
|
||||
|
||||
const mockClient = {
|
||||
query: jest.fn(),
|
||||
release: jest.fn(),
|
||||
@@ -53,7 +55,7 @@ describe("MileageEngine flow", () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as unknown as GuildMember;
|
||||
|
||||
const engine = new MileageEngine({
|
||||
databaseUrl: "postgres://test",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChannelType } from "discord.js";
|
||||
import { ChannelType, Guild } from "discord.js";
|
||||
import { TourScheduleEngine } from "../../src/tour-schedule";
|
||||
|
||||
describe("TourScheduleEngine flow", () => {
|
||||
@@ -15,9 +15,7 @@ describe("TourScheduleEngine flow", () => {
|
||||
}),
|
||||
},
|
||||
channels: {
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockImplementation(async (channelId: string) => {
|
||||
fetch: jest.fn().mockImplementation(async (channelId: string) => {
|
||||
if (channelId === "stage-1") {
|
||||
return {
|
||||
type: ChannelType.GuildStageVoice,
|
||||
@@ -34,7 +32,7 @@ describe("TourScheduleEngine flow", () => {
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
} as any;
|
||||
} as unknown as Guild;
|
||||
|
||||
const engine = new TourScheduleEngine({
|
||||
stageChannelId: "stage-1",
|
||||
|
||||
Reference in New Issue
Block a user