diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2b2a06e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,82 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build output +dist/ +build/ + +# Git +.git/ +.gitignore + +# Environment files +.env +.env.* +!.env.example + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage/ +*.lcov + +# Temporary folders +tmp/ +temp/ + +# Documentation +docs/ +ai_context/ +README.md +CONTRIBUTING.md +CODE_OF_CONDUCT.md +LICENSE +SECURITY.md + +# Package manager files +package-lock.json +.yarn-integrity +.yarn/ + +# TypeScript +*.tsbuildinfo + +# Cache +.cache/ +.parcel-cache/ +.eslintcache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Testing +test/ +tests/ +__tests__/ \ No newline at end of file diff --git a/.env.example b/.env.example index 9ba41f1..f014a85 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ NODE_ENV=development PORT=3000 -TARGET_PLATFORM=telegram +# For local development use: redis://localhost:6379 +# For Docker Compose, this gets overridden automatically to: redis://redis:6379 REDIS_URL=redis://localhost:6379 -UNTHREAD_WEBHOOK_SECRET=your_signing_secret_here \ No newline at end of file +TARGET_PLATFORM=telegram +UNTHREAD_WEBHOOK_SECRET=your_webhook_secret_here \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..91aa5d8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,74 @@ +name: Build + +on: + push: + branches: [dev] + +env: + REGISTRY_DOCKERHUB: wgtechlabs/unthread-webhook-server + REGISTRY_GHCR: ghcr.io/wgtechlabs/unthread-webhook-server + +jobs: + build-dev: + name: Build Development Images + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + run: | + echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + - name: Build and push development images + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64 + tags: | + ${{ env.REGISTRY_DOCKERHUB }}:dev + ${{ env.REGISTRY_DOCKERHUB }}:dev-${{ steps.meta.outputs.short_sha }} + ${{ env.REGISTRY_GHCR }}:dev + ${{ env.REGISTRY_GHCR }}:dev-${{ steps.meta.outputs.short_sha }} + labels: | + org.opencontainers.image.title=Unthread Webhook Server + org.opencontainers.image.description=Development build of Unthread Webhook Server + org.opencontainers.image.version=dev-${{ steps.meta.outputs.short_sha }} + org.opencontainers.image.created=${{ steps.meta.outputs.build_date }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Development build summary + run: | + echo "## 🔨 Development Build Complete" >> $GITHUB_STEP_SUMMARY + echo "**Images built and pushed:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:dev-${{ steps.meta.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:dev-${{ steps.meta.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Test the dev image:**" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "docker pull ${{ env.REGISTRY_DOCKERHUB }}:dev" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..625afd6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,135 @@ +name: Release + +on: + release: + types: [published] + +env: + REGISTRY_DOCKERHUB: wgtechlabs/unthread-webhook-server + REGISTRY_GHCR: ghcr.io/wgtechlabs/unthread-webhook-server + +jobs: + build-production: + name: Build Production Images + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: cloud + endpoint: "wgtechlabs/unthread-bot-builder" + install: true + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from package.json + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "major=$(echo $VERSION | cut -d. -f1)" >> $GITHUB_OUTPUT + echo "minor=$(echo $VERSION | cut -d. -f1-2)" >> $GITHUB_OUTPUT + echo "patch=$(echo $VERSION | cut -d. -f1-3)" >> $GITHUB_OUTPUT + echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + - name: Generate Docker tags + id: tags + run: | + VERSION="${{ steps.version.outputs.version }}" + MAJOR="${{ steps.version.outputs.major }}" + MINOR="${{ steps.version.outputs.minor }}" + PATCH="${{ steps.version.outputs.patch }}" + + # Docker Hub tags (no 'v' prefix) + DOCKERHUB_TAGS="${{ env.REGISTRY_DOCKERHUB }}:latest" + DOCKERHUB_TAGS="$DOCKERHUB_TAGS,${{ env.REGISTRY_DOCKERHUB }}:$VERSION" + DOCKERHUB_TAGS="$DOCKERHUB_TAGS,${{ env.REGISTRY_DOCKERHUB }}:$PATCH" + DOCKERHUB_TAGS="$DOCKERHUB_TAGS,${{ env.REGISTRY_DOCKERHUB }}:$MINOR" + DOCKERHUB_TAGS="$DOCKERHUB_TAGS,${{ env.REGISTRY_DOCKERHUB }}:$MAJOR" + + # GitHub Container Registry tags (with 'v' prefix) + GHCR_TAGS="${{ env.REGISTRY_GHCR }}:latest" + GHCR_TAGS="$GHCR_TAGS,${{ env.REGISTRY_GHCR }}:v$VERSION" + GHCR_TAGS="$GHCR_TAGS,${{ env.REGISTRY_GHCR }}:v$PATCH" + GHCR_TAGS="$GHCR_TAGS,${{ env.REGISTRY_GHCR }}:v$MINOR" + GHCR_TAGS="$GHCR_TAGS,${{ env.REGISTRY_GHCR }}:v$MAJOR" + + # Combine all tags + ALL_TAGS="$DOCKERHUB_TAGS,$GHCR_TAGS" + + echo "tags=$ALL_TAGS" >> $GITHUB_OUTPUT + + - name: Build and push production images + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.tags.outputs.tags }} + labels: | + org.opencontainers.image.title=Unthread Webhook Server + org.opencontainers.image.description=A Node.js server application that receives webhook events from Unthread.io + org.opencontainers.image.version=${{ steps.version.outputs.version }} + org.opencontainers.image.created=${{ steps.version.outputs.build_date }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.licenses=GPL-3.0 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ${{ env.REGISTRY_DOCKERHUB }}:${{ steps.version.outputs.version }} + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + - name: Production release summary + run: | + echo "## 🚀 Production Release Complete" >> $GITHUB_STEP_SUMMARY + echo "**Version:** \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Release:** \`${{ github.event.release.tag_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Docker Hub Images:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:latest\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:${{ steps.version.outputs.patch }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:${{ steps.version.outputs.minor }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_DOCKERHUB }}:${{ steps.version.outputs.major }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**GitHub Container Registry Images:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:latest\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:v${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:v${{ steps.version.outputs.patch }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:v${{ steps.version.outputs.minor }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY_GHCR }}:v${{ steps.version.outputs.major }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Deploy with:**" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "docker pull ${{ env.REGISTRY_DOCKERHUB }}:latest" >> $GITHUB_STEP_SUMMARY + echo "# OR" >> $GITHUB_STEP_SUMMARY + echo "docker pull ${{ env.REGISTRY_GHCR }}:latest" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..d9fdcab --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,37 @@ +name: Validate + +on: + pull_request: + branches: [dev, main] + +jobs: + validate: + name: Validate Changes + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Type checking + run: yarn type-check + + - name: Build TypeScript + run: yarn build + + - name: Test Docker build (no push) + run: | + echo "Testing Docker build..." + docker build -t test-build . + echo "Build successful, cleaning up..." + docker image rm test-build + echo "✅ Docker build test completed" diff --git a/.gitignore b/.gitignore index 39bc54b..c517f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,9 @@ ai_context/ # Enforce Yarn usage - ignore npm lockfile package-lock.json + +# Environment files with sensitive data +.env +.env.local +.env.*.local +.env.production diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f13570a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +# Multi-stage build for Unthread Webhook Server + +# Build stage +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package manager files +COPY package.json yarn.lock .yarnrc ./ + +# Install all dependencies (including devDependencies for building) +RUN yarn install --frozen-lockfile --ignore-scripts + +# Copy source code and build configuration +COPY src/ ./src/ +COPY tsconfig.json ./ + +# Build the TypeScript application +RUN yarn build + +# Production stage +FROM node:20-alpine AS production + +# Set working directory +WORKDIR /app + +# Copy package manager files +COPY package.json yarn.lock .yarnrc ./ + +# Set environment to production +ENV NODE_ENV=production +# Install only production dependencies +RUN yarn install --frozen-lockfile --production --ignore-scripts && \ + yarn cache clean + +# Copy built application from builder stage +COPY --from=builder /app/dist ./dist + +# Copy environment example (for reference) +COPY .env.example ./ + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Change ownership of the app directory to nodejs user +RUN chown -R nodejs:nodejs /app +USER nodejs + +# Expose the port the app runs on +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" + +# Start the application +CMD ["node", "dist/app.js"] \ No newline at end of file diff --git a/README.md b/README.md index 65d2166..1a5077f 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,31 @@ Server runs on `http://localhost:3000` with endpoints: - `GET /health` - Health check - `POST /unthread-webhook` - Webhook endpoint +## 🐳 Docker Setup + +```bash +# 1. Copy environment template +cp .env.example .env +# Edit .env with your webhook secret + +# 2. Start with Docker Compose +docker-compose up -d + +# 3. Check status +docker-compose ps + +# 4. View logs +docker-compose logs -f + +# 5. Stop services +docker-compose down +``` + +**Environment Files:** + +- `.env` - Single config file for both local development and Docker +- `.env.example` - Template (Redis URL gets overridden automatically for Docker) + ## ⚙️ Configuration ### Environment Variables diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..90ec322 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 30s + webhook-server: + image: wgtechlabs/unthread-webhook-server:latest + container_name: docker-unthread-webhook-server + ports: + - "3000:3000" + env_file: + - .env + environment: + - REDIS_URL=redis://redis:6379 + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + redis_data: + driver: local + +networks: + default: + name: unthread-integration-network diff --git a/package.json b/package.json index 3219a70..c6d0977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unthread-webhook-server", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "description": "A Node.js server application that receives webhook events from Unthread.io and queues them for processing.", "license": "GPL-3.0", "private": true,