- Consolidate all DB schema definitions into packages/shared as single source of truth - Migrate backend from raw SQL to Drizzle ORM across all modules - Extract frontend inline UI into separate component files - Refactor discord-gateway circuitBreaker into conversationState + moderationState - Convert messageStore to Proxy singleton pattern - Add validateBody/validateQuery middleware + Zod schemas for API endpoints - Modernize Docker builds with multi-stage + pnpm deploy - Migrate CI/CD from deployment to image-based pipeline - Remove 60+ unused/dead files (~15K lines) - Update color scheme from sky-blue to teal-cyan - Move DB connection management to @bete/shared/database Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
16 KiB
CI/CD Overhaul Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Migrate from hybrid CI/CD (GitHub Actions + GitLab CI + hot-deploy) to single Gitea CI pipeline with container registry — VPS pulls only.
Architecture: Three Docker images (backend, discord-gateway, proxy) built in Gitea CI, pushed to git.imrnes.team/MythEclipse/GMW/*, VPS pulls and restarts via SSH. No more hot-deploy bind-mounts.
Tech Stack: Gitea CI (Act Runner, GitHub Actions-compatible syntax), Docker Buildx, Gitea Container Registry, appleboy/ssh-action
Global Constraints
- Docker images must be self-contained (no bind-mount overlay at runtime)
- All three images must be built from monorepo root using
infra/docker/Dockerfile.* - Frontend static export built inside proxy Dockerfile (multi-stage, Next.js → Nginx)
- Gitea CI variables: GITEA_REGISTRY_TOKEN (secret), VPS_HOST (secret), VPS_USER (secret), VPS_SSH_KEY (secret), ENV_FILE (secret), GITEA_REGISTRY (variable)
- Registry URL:
git.imrnes.team/MythEclipse/GMW/ - Work on
mainbranch only - Must preserve voice recordings volume persistence across container restarts
Task 1: Create Gitea CI workflow
Files:
- Create:
.gitea/workflows/deploy.yml
Interfaces:
-
Consumes: Dockerfiles at
infra/docker/Dockerfile.{backend,discord-gateway,proxy} -
Produces: Docker images pushed to
git.imrnes.team/MythEclipse/GMW/bete-*:latestand:{sha} -
Depends on: Task 2 (proxy Dockerfile), Task 3 (backend Dockerfile) — but workflow can reference files that are being written in the same commit
-
Step 1: Create
.gitea/workflows/directory anddeploy.yml
mkdir -p .gitea/workflows
- Step 2: Write the workflow file
Create .gitea/workflows/deploy.yml:
name: Build & Deploy
run-name: "Build & Deploy ${{ gitea.sha }}"
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 2
matrix:
service: [backend, discord-gateway, proxy]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Gitea Registry
uses: docker/login-action@v4
with:
registry: ${{ vars.GITEA_REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_REGISTRY_TOKEN }}
- name: Build & Push ${{ matrix.service }}
uses: docker/build-push-action@v7
with:
context: .
file: infra/docker/Dockerfile.${{ matrix.service }}
push: true
tags: |
${{ vars.GITEA_REGISTRY }}/MythEclipse/GMW/bete-${{ matrix.service }}:${{ gitea.sha }}
${{ vars.GITEA_REGISTRY }}/MythEclipse/GMW/bete-${{ matrix.service }}:latest
cache-from: type=gha,scope=bete-${{ matrix.service }}
cache-to: type=gha,mode=max,scope=bete-${{ matrix.service }}
build-args: |
VITE_BE_API_URL=https://imphnen.asepharyana.my.id
VITE_BE_WS_URL=wss://imphnen.asepharyana.my.id
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: gitea.ref == 'refs/heads/main'
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@v1.2.5
env:
ENV_FILE: ${{ secrets.ENV_FILE }}
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
envs: ENV_FILE
script: |
set -eu
APP_DIR=/opt/imphenbot
cd "$APP_DIR/infra/docker"
printf '%s\n' "$ENV_FILE" | tr -d '\r' > .env
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
Note: Gitea's Act Runner supports gitea.* context variables (gitea.sha, gitea.actor, gitea.ref). If gitea.* vars don't resolve, fall back to github.* equivalents (Act Runner emulates GitHub context).
- Step 3: Commit
git add .gitea/workflows/deploy.yml
git commit -m "ci: add Gitea CI workflow for build & deploy
Gitea CI builds three Docker images (backend, discord-gateway, proxy),
pushes to Gitea Container Registry, then deploys to VPS via SSH pull.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 2: Rewrite Dockerfile.proxy for Next.js static export
Files:
- Rewrite:
infra/docker/Dockerfile.proxy
Interfaces:
-
Consumes:
services/frontend/(Next.js app),packages/shared/(workspace dep),infra/docker/nginx/nginx.conf -
Produces: Nginx image serving Next.js static export at
/usr/share/nginx/html/ -
Step 1: Rewrite Dockerfile.proxy
Replace entire content with:
# ---- Stage 1: Build Next.js static export ----
FROM node:22-slim AS frontend-builder
WORKDIR /app
# Install pnpm
RUN corepack enable
# Install build essentials for native deps
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
python3 make g++ && rm -rf /var/lib/apt/lists/*
# Copy dependency manifests first for layer caching
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/shared/package.json ./packages/shared/package.json
COPY services/frontend/package.json ./services/frontend/package.json
COPY services/frontend/tsconfig.json ./services/frontend/tsconfig.json
# Install dependencies (frontend + shared)
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --filter './packages/shared' --filter './services/frontend'
# Copy source code
COPY packages/shared/ ./packages/shared/
COPY services/frontend/ ./services/frontend/
# Pass API/WS URLs as build args for the frontend
ARG VITE_BE_API_URL
ARG VITE_BE_WS_URL
ENV VITE_BE_API_URL=${VITE_BE_API_URL}
ENV VITE_BE_WS_URL=${VITE_BE_WS_URL}
# Build shared lib first, then frontend static export
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm --filter './packages/shared' run build
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm --filter frontend run build
# ---- Stage 2: Nginx ----
FROM nginx:alpine
# Nginx config (API/WS proxy + static file serving)
COPY infra/docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Static export from frontend builder
COPY --from=frontend-builder /app/services/frontend/out/ /usr/share/nginx/html/
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:80/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
- Step 2: Validate nginx.conf handles static files correctly
Read and confirm infra/docker/nginx/nginx.conf.
cat infra/docker/nginx/nginx.conf
Verify it has:
-
Static file location with
try_files $uri /index.html(SPA fallback) -
/apiand/wsproxied tohttp://backend:3000 -
Step 3: Commit
git add infra/docker/Dockerfile.proxy
git commit -m "docker(proxy): rewrite for Next.js static export
Replaced stale Rust WASM build with multi-stage Docker build:
stage 1 builds Next.js static export, stage 2 serves via Nginx.
Includes VITE_BE_API_URL/VITE_BE_WS_URL build args.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 3: Add build args to Dockerfile.backend
Files:
-
Modify:
infra/docker/Dockerfile.backend -
Step 1: Add VITE build args to Dockerfile.backend
Insert after WORKDIR /app:
# Build args for frontend API URLs (passed through for future use)
ARG VITE_BE_API_URL
ARG VITE_BE_WS_URL
ENV VITE_BE_API_URL=${VITE_BE_API_URL}
ENV VITE_BE_WS_URL=${VITE_BE_WS_URL}
Note: These are consumed by the proxy Dockerfile (Task 2), not needed by backend itself but passed through the CI workflow to all three images for consistency.
- Step 2: Commit
git add infra/docker/Dockerfile.backend
git commit -m "docker(backend): add VITE_BE_API_URL and VITE_BE_WS_URL build args
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 4: Rewrite docker-compose.yml for Gitea registry + no bind-mounts
Files:
-
Rewrite:
infra/docker/docker-compose.yml -
Step 1: Write new docker-compose.yml
Replace entire content:
version: '3.8'
services:
proxy:
image: ${GITEA_REGISTRY}/MythEclipse/GMW/bete-proxy:${IMAGE_TAG:-latest}
container_name: imphenbot-proxy
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.imphenbot.rule=Host(`imphnen.asepharyana.my.id`)"
- "traefik.http.routers.imphenbot.entrypoints=websecure"
- "traefik.http.routers.imphenbot.tls=true"
- "traefik.http.services.imphenbot.loadbalancer.server.port=80"
depends_on:
- backend
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
memory: 64M
networks:
- app-shared-net
backend:
image: ${GITEA_REGISTRY}/MythEclipse/GMW/bete-backend:${IMAGE_TAG:-latest}
container_name: imphenbot-backend
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
WEBSERVER_PORT: 3000
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
start_period: 15s
retries: 3
deploy:
resources:
limits:
memory: 256M
networks:
- app-shared-net
discord-gateway:
image: ${GITEA_REGISTRY}/MythEclipse/GMW/bete-discord-gateway:${IMAGE_TAG:-latest}
container_name: imphenbot-discord-gateway
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
volumes:
- recordings:/app/recordings
healthcheck:
test: ["CMD-SHELL", "kill -0 1 || exit 1"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3
deploy:
resources:
limits:
memory: 512M
networks:
- app-shared-net
volumes:
recordings:
networks:
app-shared-net:
name: app-shared-net
external: true
Key changes:
-
Image refs:
registry.gitlab.com/...→${GITEA_REGISTRY}/MythEclipse/GMW/... -
Removed all bind-mount volumes:
./backend-dist,./gateway-dist,./frontend-dist,./shared-dist -
Changed
./recordingsbind-mount → named volumerecordings:(persists across restarts) -
Added
depends_on: backendto proxy (proxy needs backend for API/WS, though Nginx handles startup gracefully) -
Step 2: Commit
git add infra/docker/docker-compose.yml
git commit -m "docker(compose): switch to Gitea registry, remove bind-mounts
Images now come from git.imrnes.team/MythEclipse/GMW. All hot-deploy
bind-mounts removed — containers are fully self-contained. Voice
recordings use a named volume instead of bind-mount.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 5: Create lightweight deploy.sh
Files:
-
Create:
deploy.sh -
Step 1: Write deploy.sh
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INFRA_DIR="$SCRIPT_DIR/infra/docker"
: "${VPS_HOST:?required}"
: "${VPS_USER:?required}"
: "${VPS_SSH_KEY:?required}"
echo "=== Deploy to $VPS_HOST ==="
# Copy local .env if it exists (overrides CI env)
if [ -f "$INFRA_DIR/.env" ]; then
scp -i "$VPS_SSH_KEY" "$INFRA_DIR/.env" "$VPS_USER@$VPS_HOST:/opt/imphenbot/infra/docker/.env"
fi
ssh -i "$VPS_SSH_KEY" "$VPS_USER@$VPS_HOST" << 'REMOTESCRIPT'
set -eu
cd /opt/imphenbot/infra/docker
echo "=== Pulling images ==="
docker compose pull
echo "=== Restarting containers ==="
docker compose up -d --remove-orphans
echo "=== Cleaning up ==="
docker image prune -f
echo "=== Active containers ==="
docker ps --filter "name=imphenbot" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
REMOTESCRIPT
echo "=== Deploy complete ==="
- Step 2: Make executable
chmod +x deploy.sh
- Step 3: Commit
git add deploy.sh
git commit -m "chore: rewrite deploy.sh as lightweight SSH pull script
Replaced hot-deploy tar-pipe script with simple SSH-based deploy
that pulls latest images from Gitea registry and restarts containers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 6: Disable old CI files
Files:
-
Disable:
.github/workflows/deploy-docker.yml -
Keep:
.gitlab-ci.ymlif exists (already may have been removed) -
Step 1: Rename GitHub Actions workflow to .disabled
mv .github/workflows/deploy-docker.yml .github/workflows/deploy-docker.yml.disabled
- Step 2: Remove docker compose file's old frontend-dist directory from git (if tracked)
# Check if frontend-dist is tracked (it should be gitignored, but check)
git ls-files infra/docker/frontend-dist 2>/dev/null || echo "Not tracked — OK"
- Step 3: Commit
git add .github/workflows/deploy-docker.yml.disabled
git rm --cached .github/workflows/deploy-docker.yml 2>/dev/null || true
git commit -m "ci: disable GitHub Actions workflow
Renamed to .disabled. All CI now goes through Gitea CI (.gitea/workflows/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 7: Update .gitignore
Files:
-
Modify:
.gitignore -
Step 1: Add .gitea exclusion note and any missing entries
Read current .gitignore:
cat .gitignore
Then append (only if not already present):
# Gitea workflow logs (local runners)
.gitea/workflows/*.log
The .gitea/workflows/ YAML files themselves should be tracked in git.
- Step 2: Commit
git add .gitignore
git commit -m "chore: update gitignore for Gitea CI artifacts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 8: Push and verify CI pipeline
- Step 1: Verify all changes
git status
git log --oneline -10
Expected: clean working tree, all 7 commits ready to push.
- Step 2: Push to main
git push origin main
- Step 3: Monitor CI run
Watch Gitea CI at https://git.imrnes.team/MythEclipse/GMW/actions.
Expected outcome:
build-and-pushjob runs 3 matrix builds (backend, discord-gateway, proxy) in parallel (max 2)- Each image is pushed to
git.imrnes.team/MythEclipse/GMW/bete-*with both:latestand:{sha}tags deployjob SSHes into VPS, pulls images, restarts containers- All 3 containers
imphenbot-proxy,imphenbot-backend,imphenbot-discord-gatewayare running
- Step 4: Verify containers on VPS
# SSH into VPS and check
ssh -i "$VPS_SSH_KEY" "$VPS_USER@$VPS_HOST" "
docker ps --filter 'name=imphenbot' --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker compose -f /opt/imphenbot/infra/docker/docker-compose.yml ps
"
- Step 5: Verify no hot-deploy artifacts remain
ssh -i "$VPS_SSH_KEY" "$VPS_USER@$VPS_HOST" "
ls -la /opt/imphenbot/infra/docker/ | grep -E 'dist$' || echo 'No dist dirs — clean'
"
Rollback
If the pipeline fails at any point:
- Fix and re-push: Edit the broken file, commit, push to main — CI re-runs automatically
- Emergency rollback: SSH to VPS, run
docker compose up -dwith a known-good IMAGE_TAG:IMAGE_TAG=<last-working-sha> docker compose up -d - Restore old CI: Move
.github/workflows/deploy-docker.yml.disabledback and push