Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Dependency directories
node_modules/

# Build outputs
dist/
coverage/

# Environment files and credentials
.env
.env.*
*.local
*.pem

# Git files
.git/
.github/

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Docker files
Dockerfile
docker-compose.yml
.dockerignore

# OS metadata
.DS_Store
Thumbs.db

# Documentation and configs not needed in production container
README.md
nest-cli.json
tsconfig.json
tsconfig.build.json
eslint.config.mjs
.prettierrc
test/
106 changes: 91 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,100 @@
name: CI
name: CI Pipeline

on:
push:
branches: [main]
pull_request:
branches: [main]
branches:
- main

jobs:
build-lint-test:
ci:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mergefi
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

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

- uses: actions/setup-node@v4
- name: Setup Node.js v24
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm test
- run: npm audit --audit-level=high
node-version: 24
cache: 'npm'

- name: Scan for Hardcoded Secrets
run: |
echo "Scanning codebase for potential hardcoded secrets..."

# Detect Stellar Secret Keys (starts with S, 56 characters, uppercase A-Z, digits 2-7)
if grep -r -E "\bS[A-D][A-Z2-7]{54}\b" --exclude-dir={node_modules,dist,coverage,.git} .; then
echo "::error::Potential Stellar Secret Key detected in codebase!"
exit 1
fi

# Detect PEM Private Keys
if grep -r -E "(-+BEGIN [A-Z ]+ PRIVATE KEY-+)" --exclude-dir={node_modules,dist,coverage,.git} .; then
echo "::error::Potential PEM Private Key detected in codebase!"
exit 1
fi

# Detect GitHub Personal Access Tokens (Legacy and Fine-grained)
if grep -r -E "(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{82})" --exclude-dir={node_modules,dist,coverage,.git} .; then
echo "::error::Potential GitHub Token detected in codebase!"
exit 1
fi

echo "Secret scan completed. No secrets detected."
shell: bash

- name: Install Dependencies
run: npm ci

- name: Lint Code
run: npm run lint

- name: Build Application
run: npm run build

- name: Run Unit & Integration Tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mergefi
NODE_ENV: test
run: npm run test

- name: Run Coverage Tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mergefi
NODE_ENV: test
run: npm run test:cov

- name: Run E2E Tests (Conditional)
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mergefi
NODE_ENV: test
# Map GitHub Secrets if configured in the repository
GITHUB_CLIENT_ID: ${{ secrets.GITHUB_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}
run: |
if [ -z "$GITHUB_CLIENT_ID" ] || [ -z "$GITHUB_CLIENT_SECRET" ]; then
echo "=========================================================================="
echo "WARNING: Skipping E2E tests because external GitHub OAuth secrets are not"
echo "configured in the repository settings (GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET)."
echo "To enable E2E tests in CI, please set these secrets in GitHub."
echo "=========================================================================="
else
npm run test:e2e
fi
shell: bash
79 changes: 79 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ==========================================
# Stage 1: Base image
# ==========================================
FROM node:24-alpine AS base

# Install libc6-compat for compatibility with native modules on Alpine
RUN apk add --no-cache libc6-compat

WORKDIR /usr/src/app

# ==========================================
# Stage 2: Development dependencies and environment
# ==========================================
FROM base AS development

# Copy configuration files for dependency installation
COPY package*.json ./

# Install ALL dependencies (including devDependencies)
RUN npm ci

# Copy the rest of the application files
COPY . .

# Set default port and run watch mode for development
EXPOSE 3000
CMD ["npm", "run", "start:dev"]

# ==========================================
# Stage 3: Build the application
# ==========================================
FROM base AS builder

COPY package*.json ./

# Copy all node_modules from development stage to compile typescript
COPY --from=development /usr/src/app/node_modules ./node_modules
COPY . .

# Build the production files (calls nest build)
RUN npm run build

# ==========================================
# Stage 4: Production dependencies
# ==========================================
FROM base AS prod-deps

COPY package*.json ./

# Clean installation of production-only dependencies
RUN npm ci --omit=dev

# ==========================================
# Stage 5: Production runner
# ==========================================
FROM base AS runner

# Configure environment variables
ENV NODE_ENV=production
ENV PORT=3000

# Set ownership of the work directory to the non-root 'node' user
RUN chown -R node:node /usr/src/app

# Use the non-root node user for security compliance
USER node

# Copy package files and production node_modules
COPY --chown=node:node package*.json ./
COPY --chown=node:node --from=prod-deps /usr/src/app/node_modules ./node_modules

# Copy the built main application bundle
COPY --chown=node:node --from=builder /usr/src/app/dist ./dist

# Expose the default NestJS port
EXPOSE 3000

# Run the production bundle
CMD ["node", "dist/main.js"]
85 changes: 66 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,39 +184,86 @@ Route groups: `/api/auth`, `/api/users`, `/api/github`,

## Setup, run, test

You can run this project either natively on your host machine or completely containerized using Docker.

### 1. Local Development via Docker Compose (Recommended)

This is the easiest way to get up and running with a pre-configured, isolated PostgreSQL database.

```bash
# A. Configure Environment Variables
cp .env.example .env

# B. Start App and Database services
docker compose up --build
```

- **Hot Reloading**: The codebase is mounted into the container using a bind mount. File changes on the host will automatically trigger application restarts.
- **Node Modules Isolation**: The container uses an anonymous volume for `/usr/src/app/node_modules`. This prevents Windows/Host compiled node packages from contaminating the Linux-native container.
- **Services**:
- The API is served at `http://localhost:3000/api` (Swagger docs at `http://localhost:3000/api/docs`).
- PostgreSQL is mapped to port `5432` on your localhost with credentials `postgres:postgres` and database name `mergefi`.

### 2. Local Development Natively on Host

If you prefer to run NestJS directly on your host machine:

```bash
# 1. install
# A. Install Dependencies
npm install

# 2. configure
# B. Spin up only the Database service in Docker
docker compose up -d db

# C. Configure Environment Variables
cp .env.example .env
# fill in DATABASE_URL at minimum to run against a real Postgres instance
# Set DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mergefi

# D. Start NestJS in development mode
npm run start:dev
```

# 3. run
npm run start:dev # watch mode
npm run start:prod # after `npm run build`
### 3. Production Builds & Security

# 4. build
npm run build
The Dockerfile is structured as a secure, multi-stage build running on Alpine Linux.

# 5. test
npm test # unit tests (Jest, src/**/*.spec.ts)
npm run test:cov # with coverage
npm run test:e2e # e2e (requires a running Postgres per DATABASE_URL)
#### Build the production image:
```bash
docker build --target runner -t mergefi-backend:latest .
```

Unit tests cover, among other things:
#### Production Guardrails (Important):
- **Least Privilege**: The container runs under the non-root `node` user (`USER node`).
- **Production Mode**: The final image forces `NODE_ENV=production`.
- **JWT Secret Enforcer**: If the application boots in production with the default `JWT_SECRET=insecure-dev-secret` (or is missing entirely), the startup hook in `src/main.ts` will crash the container. You **must** provide a secure custom `JWT_SECRET` when running the production container:
```bash
docker run -p 3000:3000 -e JWT_SECRET="your-highly-secure-random-jwt-key" mergefi-backend:latest
```

### 4. Running Tests

Unit tests and end-to-end tests are fully supported:

```bash
# Run unit tests natively
npm test

# Run unit tests with code coverage
npm run test:cov

# Run End-to-End (E2E) tests (requires PostgreSQL to be running on DATABASE_URL)
npm run test:e2e
```

Unit tests cover critical domains including:
- `src/bounties/bounty-state-machine.spec.ts` — the bounty lifecycle state machine.
- `src/teams/team-split.util.spec.ts` — team payout split percentage math.
- `src/escrow/escrow.service.spec.ts` — escrow fund/release/split-release/refund orchestration, with the Soroban client mocked.
- `src/escrow/escrow.service.spec.ts` — escrow fund/release/split-release/refund orchestration (Soroban client mocked).
- `src/github/webhook-signature.util.spec.ts` — GitHub webhook HMAC-SHA256 signature verification.
- `src/github/github-webhooks.service.spec.ts` — end-to-end webhook → bounty release wiring.
- `src/bounties/bounties.service.spec.ts` — bounty service against mocked repositories/escrow.
- `src/github/github-webhooks.service.spec.ts` — webhook-to-escrow release logic.
- `src/bounties/bounties.service.spec.ts` — bounty core management.

`DATABASE_SYNCHRONIZE=true` will auto-create tables from entities for quick
local iteration; a real migration workflow (`typeorm migration:generate`) is
recommended before this touches any shared/staging database — see Roadmap.
`DATABASE_SYNCHRONIZE=true` (set in development) will auto-create tables from entities for fast local iteration. A real migration workflow (`typeorm migration:generate`) is recommended before deploying to production (see Roadmap).

## Roadmap

Expand Down
45 changes: 45 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: '3.8'

services:
app:
build:
context: .
target: development
ports:
- "3000:3000"
volumes:
# Bind mount host files for hot-reloading (development watch mode)
- .:/usr/src/app
# Anonymous volume prevents host's node_modules from overriding container's
- /usr/src/app/node_modules
# Load all environment variables from local .env if it exists
env_file:
- .env
environment:
# Override DATABASE_URL to target the postgres container inside the docker network
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mergefi
- PORT=3000
- NODE_ENV=development
depends_on:
db:
condition: service_healthy

db:
image: postgres:16-alpine
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=mergefi
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d mergefi"]
interval: 5s
timeout: 5s
retries: 5

volumes:
postgres_data:
Loading