diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 8e3235b..154291e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -8,9 +8,6 @@ on: branches: ["main"] workflow_dispatch: -env: - SERVICE_NAME: tracking-api - permissions: contents: read @@ -33,12 +30,18 @@ jobs: - name: Security Audit (non-blocking) run: npm audit --audit-level=high || echo "Audit found vulnerabilities, please review." - - name: Tests + - name: Test tracking-api run: TEST_DATABASE_URL= npm run -w @tracking-base/tracking-api test - - name: Build API + - name: Build tracking-api run: npm run -w @tracking-base/tracking-api build + - name: Test router-worker + run: npm run -w @tracking-base/router-worker test + + - name: Build router-worker + run: npm run -w @tracking-base/router-worker build + deploy-gate: name: Deploy Gate runs-on: ubuntu-latest @@ -55,9 +58,14 @@ jobs: permissions: contents: read packages: write - outputs: - version: ${{ steps.version.outputs.version }} - image: ${{ steps.version.outputs.image }} + strategy: + fail-fast: false + matrix: + include: + - service: tracking-api + dockerfile: Dockerfile + - service: router-worker + dockerfile: apps/router-worker/Dockerfile steps: - name: Checkout repository @@ -68,10 +76,11 @@ jobs: run: | SHORT_SHA=$(echo ${{ github.event.pull_request.merge_commit_sha || github.sha }} | cut -c1-7) VERSION="staging-${SHORT_SHA}" - REGISTRY_IMAGE="ghcr.io/$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')/${{ env.SERVICE_NAME }}" + REGISTRY_IMAGE="ghcr.io/$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')/${{ matrix.service }}" echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "image=${REGISTRY_IMAGE}:${VERSION}" >> $GITHUB_OUTPUT - echo "REGISTRY_IMAGE=${REGISTRY_IMAGE}" >> $GITHUB_ENV + echo "registry_image=${REGISTRY_IMAGE}" >> $GITHUB_OUTPUT + echo "📦 Service: ${{ matrix.service }}" echo "📦 Version: ${VERSION}" echo "🐳 Image: ${REGISTRY_IMAGE}:${VERSION}" @@ -89,15 +98,16 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY_IMAGE }} + images: ${{ steps.version.outputs.registry_image }} tags: | type=raw,value=${{ steps.version.outputs.version }} + type=raw,value=latest - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . - file: Dockerfile + file: ${{ matrix.dockerfile }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -110,6 +120,6 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Service | ${{ env.SERVICE_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "| Service | ${{ matrix.service }} |" >> $GITHUB_STEP_SUMMARY echo "| Version | ${{ steps.version.outputs.version }} |" >> $GITHUB_STEP_SUMMARY echo "| Image | ${{ steps.version.outputs.image }} |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 222465c..bc6aa6b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ coverage *.tsbuildinfo .vite /docs/13-prompts +.DS_Store +fe-prank/ diff --git a/apps/router-worker/Dockerfile b/apps/router-worker/Dockerfile new file mode 100644 index 0000000..a611607 --- /dev/null +++ b/apps/router-worker/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY . . +RUN npm ci +RUN npm run -w @tracking-base/router-worker build +RUN npm prune --omit=dev + +FROM node:20-alpine AS runtime + +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/apps/router-worker/package.json ./apps/router-worker/package.json +COPY --from=builder /app/apps/router-worker/dist ./apps/router-worker/dist + +CMD ["node", "apps/router-worker/dist/worker.js"] diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..0608f64 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,67 @@ +# Deploy Layout + +This folder separates deploy concerns: + +- `deploy/app`: application containers (`tracking-api`, `router-worker`) pulled from GHCR. +- `deploy/infra`: shared infrastructure containers (`redis`). + +## Is this model correct for current repo flow? + +Yes, this matches the current architecture: + +1. `tracking-api` receives `POST /track`. +2. API enqueues delivery jobs to Redis/BullMQ. +3. `router-worker` consumes jobs and pushes to destinations. +4. Postgres remains the canonical source of truth. + +## Prerequisites + +1. GHCR images exist for both services: + - `tracking-api` + - `router-worker` +2. Postgres is reachable (Railway/Supabase/self-hosted). +3. `DATABASE_URL` and `REDIS_URL` are set consistently for both services. + +## Run + +### 1) Start infra (creates shared Docker network `tracking-base-net`) + +```bash +cd deploy/infra +cp .env.example .env +docker compose up -d +``` + +### 2) Start app + +```bash +cd deploy/app +cp .env.example .env +# edit .env values first +docker compose up -d +``` + +### 3) Smoke check + +```bash +curl -fsS http://127.0.0.1:3000/health +curl -fsS http://127.0.0.1:3000/ready +``` + +## Notes + +- Keep `router-worker` private (do not expose ports). +- Expose public domain only for `tracking-api` through reverse proxy (Nginx/Caddy) with TLS. +- If GHCR images are private, `docker login ghcr.io` is required on the VPS. + +## GHCR image naming from CI + +This repository workflow publishes images to: + +- `ghcr.io///tracking-api:{latest|staging-}` +- `ghcr.io///router-worker:{latest|staging-}` + +Example if your repo is `lecongthien/Tracking_Base_System`: + +- `ghcr.io/lecongthien/tracking_base_system/tracking-api:latest` +- `ghcr.io/lecongthien/tracking_base_system/router-worker:latest` diff --git a/deploy/app/docker-compose.yml b/deploy/app/docker-compose.yml new file mode 100644 index 0000000..81573bd --- /dev/null +++ b/deploy/app/docker-compose.yml @@ -0,0 +1,41 @@ +name: tracking-base-app + +services: + tracking-api: + image: ${TRACKING_API_IMAGE:-ghcr.io/your-org/your-repo/tracking-api:latest} + container_name: tracking-api + restart: unless-stopped + env_file: + - .env + environment: + NODE_ENV: production + PORT: ${TRACKING_API_PORT:-3000} + ports: + - "${TRACKING_API_PORT:-3000}:3000" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + networks: + - tracking-base-net + + router-worker: + image: ${ROUTER_WORKER_IMAGE:-ghcr.io/your-org/your-repo/router-worker:latest} + container_name: router-worker + restart: unless-stopped + env_file: + - .env + environment: + NODE_ENV: production + depends_on: + tracking-api: + condition: service_started + networks: + - tracking-base-net + +networks: + tracking-base-net: + external: true + name: tracking-base-net diff --git a/deploy/infra/docker-compose.yml b/deploy/infra/docker-compose.yml new file mode 100644 index 0000000..5513057 --- /dev/null +++ b/deploy/infra/docker-compose.yml @@ -0,0 +1,26 @@ +name: tracking-base-infra + +services: + redis: + image: redis:7-alpine + container_name: tracking-redis + restart: unless-stopped + command: ["redis-server", "--appendonly", "yes"] + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 5s + retries: 5 + networks: + - tracking-base-net + +volumes: + redis_data: + +networks: + tracking-base-net: + name: tracking-base-net