Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
799c3ed
chore: add prisma and SQLite deps
hmd-ali Aug 17, 2026
d70b32b
feat: add prisma schema
hmd-ali Aug 17, 2026
d07be56
ci: enhance CI/CD workflows with rollback support and Prisma integration
hmd-ali Aug 17, 2026
f4248db
chore: add lru-cache package
hmd-ali Aug 17, 2026
ccd4e7c
feat: add TAG_ACCESS_ROLE_ID env
hmd-ali Aug 17, 2026
631cd30
feat: add prisma to tsconfig
hmd-ali Aug 17, 2026
51d9003
feat: add utils
hmd-ali Aug 17, 2026
25628ae
feat: export prisma singleton
hmd-ali Aug 17, 2026
31e6196
feat: add ErrorMessages class
hmd-ali Aug 17, 2026
0b2e912
feat: add bot options
hmd-ali Aug 17, 2026
6c4b6bd
feat: add tags feature
hmd-ali Aug 17, 2026
20033be
fix: add missing test env
hmd-ali Aug 17, 2026
978bf69
fix: expiresAt column definition
hmd-ali Aug 17, 2026
1398aa0
feat: add `createdAt` and `lastUsedAt` column indices
hmd-ali Aug 18, 2026
a3fc77e
feat: implement select menu interaction handling
hmd-ali Aug 18, 2026
de72d3c
feat: add methods for retrieving unused and prunable tags, and implem…
hmd-ali Aug 18, 2026
ac70fda
feat: make `/tags list` ephemeral
hmd-ali Aug 18, 2026
b9d0296
feat: add prune command for managing unused
hmd-ali Aug 18, 2026
e9c7dd7
test: add prune-tags tests
hmd-ali Aug 19, 2026
3b1c609
feat: move prisma generated to src/ folder
hmd-ali Aug 19, 2026
9584e16
ci: switch to building on the vps instead of in ci
hmd-ali Aug 19, 2026
5ae4a86
feat: add pnpm `allowBuilds` list
hmd-ali Aug 19, 2026
80b095a
chore: remove unused build entry
hmd-ali Aug 19, 2026
8b9bb73
Merge branch 'main' of github.com:r-webdev/webdev-bot into feat/tags
hmd-ali Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ARCHIVE_CATEGORY_ID=archive_category_id_here
MODERATORS_ROLE_IDS=role_id_1,role_id_2,role_id_3
REPEL_ROLE_ID=your_repel_role_id_here
ONBOARDING_ROLE_ID=onboarding_role_id_here
TAG_ACCESS_ROLE_ID=tag_access_role_id_here

# Optional Role IDs
# ROLE_A_ID=optional_role_a_id
Expand All @@ -29,4 +30,7 @@ GUIDES_TRACKER_PATH=guides-tracker.json
ADVENT_OF_CODE_TRACKER_PATH=advent-of-code-tracker.json


DATABASE_URL=file:./data/webdev.db



1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ARCHIVE_CATEGORY_ID=837507969859977258
REPEL_ROLE_ID=1002411741776461844
MODERATORS_ROLE_IDS=849481536654803004
REGULAR_ROLE_ID=1383170370634514544
TAG_ACCESS_ROLE_ID=1502128862224711805

# Other
GUIDES_TRACKER_PATH=/app/data/guides-tracker.json
Expand Down
5 changes: 4 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ ARCHIVE_CATEGORY_ID=your-archived-category-id
REPEL_ROLE_ID=your-repel-role-id
MODERATORS_ROLE_IDS=your-moderator-role-id
REGULAR_ROLE_ID=your-regular-role-id
TAG_ACCESS_ROLE_ID=your-tag-access-role-id

# Other
GUIDES_TRACKER_PATH=guides-tracker.json
ADVENT_OF_CODE_TRACKER_PATH=test-advent-tracker.json
ADVENT_OF_CODE_TRACKER_PATH=test-advent-tracker.json

DATABASE_URL=test.db
132 changes: 100 additions & 32 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,58 +1,126 @@
name: Build, Test and Deploy Discord Bot to VPS

name: Deploy
on:
push:
branches: ['main']
workflow_dispatch: # Allow manual deployment
branches: [main]
workflow_dispatch:
inputs:
ref:
description: 'Git ref (commit SHA or tag) to deploy. Leave blank for latest main.'
required: false
default: ''

concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false

env:
MAX_BACKUPS: 4
DIR: /opt/webdev-bot

jobs:
deploy:
lint:
name: Lint & Format Check
runs-on: ubuntu-latest

steps:
- name: Checkout code
- name: Checkout
uses: actions/checkout@v4

- name: Read Node version
run: |
NODE_VERSION=$(cat .nvmrc | sed 's/v//')
echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Prisma generate (sanity check schema)
run: pnpm run prisma:generate
env:
DATABASE_URL: file:/tmp/ci.db

- name: Deploy to VPS
uses: appleboy/ssh-action@v1.0.3
- name: Lint
run: pnpm lint

- name: Format
run: pnpm fmt:check

- name: Typecheck
run: pnpm typecheck

deploy:
name: Deploy to VPS
runs-on: ubuntu-latest
needs: [lint]
permissions:
contents: read
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
envs: REPO_URL,REF
script: |
cd /home/${{ secrets.VPS_USER }}/webdev-bot-deploy
set -eu
mkdir -p ${{ env.DIR }}
cd ${{ env.DIR }}

# Stash any local changes
git stash push -m "Auto-deploy $(date)" 2>/dev/null || true
if [ -d .git ]; then
git stash push -m "Auto-deploy $(date)" 2>/dev/null || true
git fetch origin
git checkout "$REF"
git reset --hard "origin/$REF" 2>/dev/null || git reset --hard "$REF"
else
git clone "$REPO_URL" .
git checkout "$REF"
fi

# Pull latest changes
git checkout main
git pull origin main
mkdir -p ${{ env.DIR }}/backups

# Read NODE_VERSION from .nvmrc
export NODE_VERSION=$(cat .nvmrc | sed 's/v//')
export NODE_VERSION=$(cat .nvmrc | tr -d 'v')
echo "Using Node version: $NODE_VERSION"

# Create .env file with secrets
# Public config comes from .env.production (committed to repo)
# NODE_ENV=production is set in docker-compose.yml
cat > .env << EOF
cat > .env <<'EOF'
DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}
CLIENT_ID=${{ secrets.CLIENT_ID }}
EOF
chmod 600 .env

# Backup the SQLite DB before touching anything
if docker volume inspect webdev-bot_guides-data >/dev/null 2>&1; then
docker run --rm \
-v webdev-bot_guides-data:/data \
-v ${{ env.DIR }}/backups:/backup \
alpine sh -c "if [ -f /data/db.sqlite ]; then cp /data/db.sqlite /backup/db-\$(date +%Y%m%d%H%M%S).sqlite; fi"
ls -1t ${{ env.DIR }}/backups/*.sqlite 2>/dev/null \
| tail -n +$((env.MAX_BACKUPS+1)) \
| xargs -r rm --
fi

docker compose config >/dev/null
docker compose build bot

if ! docker compose run --rm migrate; then
echo "Migration failed — leaving existing bot container untouched."
exit 1
fi

# Stop any existing containers
docker compose down || true
docker compose up -d --remove-orphans bot

# Build and start production container with profile
# NODE_ENV=production is explicitly set in docker-compose.yml bot-prod service
docker compose --profile prod up -d --build
sleep 8
if ! docker compose ps bot --status running | grep -q bot; then
echo "Bot container is not running after deploy!"
docker compose logs --tail=50 bot
exit 1
fi

# Check status
echo "Deployment completed. Container status:"
docker compose ps
docker image prune -f
env:
REPO_URL: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
REF: ${{ github.event.inputs.ref != '' && github.event.inputs.ref || 'main' }}
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Prisma generate (sanity check schema)
run: pnpm run prisma:generate
env:
DATABASE_URL: file:/tmp/ci.db

- name: Lint
run: pnpm lint

Expand Down
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ docker-compose.yml
# terraform
**/.terraform/
*.tfstate
*.tfstate.*
*.tfstate.*

# prisma
src/generated

# Database files
*.db
*.sqlite
*.sqlite3
68 changes: 45 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,72 +1,94 @@
# Base stage - Setup Node.js and pnpm
# Node version is read from .nvmrc at build time via NODE_VERSION build arg
ARG NODE_VERSION=22.20.0
FROM node:${NODE_VERSION}-alpine AS base

# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.17.1 --activate
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0

RUN corepack enable

WORKDIR /app

# Dependencies stage - Install production dependencies only
# --- Production dependencies only ---
FROM base AS deps

RUN apk add --no-cache python3 make g++

ENV PRISMA_SKIP_POSTINSTALL_GENERATE=1

COPY package.json pnpm-lock.yaml ./

RUN pnpm install --frozen-lockfile --production --ignore-scripts
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --production

# Dev dependencies stage - Install all dependencies
# --- Full dependencies ---
FROM base AS deps-dev

RUN apk add --no-cache python3 make g++

ENV PRISMA_SKIP_POSTINSTALL_GENERATE=1

COPY package.json pnpm-lock.yaml ./

RUN pnpm install --frozen-lockfile --ignore-scripts
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile

# Build stage - Compile TypeScript
# --- Build ---
FROM deps-dev AS build

# These files are required by prisma.config.ts.
# Keeping them separate allows Prisma generate to be cached
# when only application source code changes.
COPY prisma ./prisma
COPY prisma.config.ts ./
COPY src/loadEnvFile.ts ./src/loadEnvFile.ts

ENV DATABASE_URL="file:/tmp/build.db"

RUN pnpm exec prisma generate

COPY . .

RUN pnpm run build

# Production stage - Minimal runtime image
# --- Production runtime ---
FROM base AS production

ENV NODE_ENV=production

# Copy production dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
# Production dependencies
COPY --from=deps --chown=node:node \
/app/node_modules ./node_modules

COPY --from=build --chown=node:node \
/app/src/generated/prisma ./src/generated/prisma

# Copy built application
COPY --from=build /app/dist ./dist
COPY package.json ./

# Copy environment config file (public, non-secret)
COPY .env.production ./

# Copy static assets (guides, tips, etc.) from the build stage
COPY --from=build /app/assets ./assets
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
COPY --from=build /app/src/loadEnvFile.ts ./src/loadEnvFile.ts

# Create data directory and set permissions for node user
RUN mkdir -p /app/data && chown -R node:node /app/data

# Run as non-root user for security
USER node

CMD ["node", "dist/index.js"]

# Development stage - Full dev environment with hot reload
# --- Development ---
FROM deps-dev AS development

ENV NODE_ENV=development

COPY . .

# Create data directory and set permissions for node user
RUN pnpm exec prisma generate

RUN mkdir -p /app/data && chown -R node:node /app/data

# Run as non-root user for security
USER node

CMD ["pnpm", "run", "dev"]

CMD ["pnpm", "run", "dev"]
29 changes: 29 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
services:
bot-dev:
build:
context: .
target: development
args:
NODE_VERSION: ${NODE_VERSION}
container_name: webdev-bot-dev
restart: unless-stopped
env_file:
- .env
environment:
- NODE_ENV=development
- GUIDES_TRACKER_PATH=/app/data/guides-tracker.json
- DATABASE_URL=file:/app/data/db.sqlite
volumes:
- ./src:/app/src:ro
- ./scripts:/app/scripts:ro
- ./prisma:/app/prisma:ro
- ./prisma.config.ts:/app/prisma.config.ts:ro
- ./tsconfig.json:/app/tsconfig.json:ro
- ./package.json:/app/package.json:ro
- ./pnpm-lock.yaml:/app/pnpm-lock.yaml:ro
- ./.env.production:/app/.env.production:ro
- guides-data:/app/data

volumes:
guides-data:
driver: local
Loading
Loading