Skip to content
Open
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
11 changes: 11 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Dev container image for sync-engine
FROM mcr.microsoft.com/devcontainers/javascript-node:24

# Enable corepack so pnpm is available at the version specified in package.json
RUN corepack enable

# Install CLI tools useful for development and service health checks
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
56 changes: 56 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "Sync Engine",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/sync-engine",

"postCreateCommand": "corepack enable && pnpm install && pnpm build",
"postStartCommand": "echo 'Dev container ready. Services: postgres, stripe-mock, temporal.'",

"forwardPorts": [55432, 12111, 12112, 7233, 8080, 3000, 4010, 4020, 5173],
"portsAttributes": {
"55432": { "label": "Postgres", "onAutoForward": "silent" },
"12111": { "label": "Stripe Mock (HTTP)", "onAutoForward": "silent" },
"12112": { "label": "Stripe Mock (HTTPS)", "onAutoForward": "silent" },
"7233": { "label": "Temporal gRPC", "onAutoForward": "silent" },
"8080": { "label": "Temporal UI", "onAutoForward": "openBrowser" },
"3000": { "label": "Engine API", "onAutoForward": "notify" },
"4010": { "label": "Engine API (Docker)", "onAutoForward": "silent" },
"4020": { "label": "Service API", "onAutoForward": "notify" },
"5173": { "label": "Dashboard", "onAutoForward": "openBrowser" }
},

"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"vitest.explorer",
"ckolkman.vscode-postgres",
"ms-azuretools.vscode-docker",
"ms-vscode.vscode-typescript-next"
],
"settings": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"vitest.commandLine": "pnpm exec vitest",
"terminal.integrated.defaultProfile.linux": "bash",
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/.tsbuildinfo": true
}
}
}
},

"remoteUser": "node"
}
41 changes: 41 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Dev container compose overlay — imports the repo's compose.yml for
# infrastructure services and adds the development container alongside them.
# All services share the same Docker network, so the app container can
# reach postgres, stripe-mock, and temporal by service name.

include:
- path: ../compose.yml

services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
# Mount the workspace source code
- ..:/workspaces/sync-engine:cached
# Persist pnpm store across rebuilds
- pnpm-store:/home/node/.local/share/pnpm/store
# Keep the container running for VS Code to attach
command: sleep infinity
environment:
# Database — uses Docker service hostname, internal port
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable&search_path=stripe
POSTGRES_URL: postgres://postgres:postgres@postgres:5432/postgres
# Stripe mock — uses Docker service hostname
STRIPE_MOCK_URL: http://stripe-mock:12111
STRIPE_API_KEY: sk_test_fake123
# Temporal — uses Docker service hostname
TEMPORAL_ADDRESS: temporal:7233
# Node
NODE_ENV: development
depends_on:
postgres:
condition: service_healthy
stripe-mock:
condition: service_healthy
temporal:
condition: service_healthy

volumes:
pnpm-store:
73 changes: 73 additions & 0 deletions .devcontainer/test-devcontainer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Validation script for the dev container setup.
# Run inside the dev container to verify all services are reachable
# and the development toolchain is functional.
#
# Usage: bash .devcontainer/test-devcontainer.sh

set -euo pipefail

PASS=0
FAIL=0

check() {
local label="$1"
shift
if "$@" >/dev/null 2>&1; then
echo " PASS $label"
((PASS++))
else
echo " FAIL $label"
((FAIL++))
fi
}

echo "=== Dev Container Validation ==="
echo ""

# --- Toolchain ---
echo "Toolchain:"
check "Node.js >= 24" node -e "assert(parseInt(process.versions.node) >= 24)"
check "pnpm available" pnpm --version
check "TypeScript available" pnpm exec tsc --version
check "corepack enabled" corepack --version

echo ""

# --- Service connectivity ---
echo "Services:"
check "Postgres (postgres:5432)" pg_isready -h postgres -p 5432 -U postgres
check "stripe-mock (stripe-mock:12111)" nc -z stripe-mock 12111
check "Temporal gRPC (temporal:7233)" nc -z temporal 7233

echo ""

# --- Database ---
echo "Database:"
check "Postgres connection" psql "$DATABASE_URL" -c "SELECT 1"
check "stripe schema" psql "$POSTGRES_URL" -c "CREATE SCHEMA IF NOT EXISTS stripe"

echo ""

# --- Stripe mock ---
echo "Stripe Mock:"
check "GET /v1/customers" curl -sf -H "Authorization: Bearer sk_test_fake123" http://stripe-mock:12111/v1/customers

echo ""

# --- Build artifacts ---
echo "Build:"
check "node_modules exists" test -d node_modules
check "Build output exists" test -d packages/protocol/dist

echo ""

# --- Environment variables ---
echo "Environment:"
check "DATABASE_URL set" test -n "${DATABASE_URL:-}"
check "STRIPE_MOCK_URL set" test -n "${STRIPE_MOCK_URL:-}"
check "TEMPORAL_ADDRESS set" test -n "${TEMPORAL_ADDRESS:-}"

echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
[ "$FAIL" -eq 0 ] || exit 1
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ jobs:
# ---------------------------------------------------------------------------
e2e_docker:
name: E2E Docker
needs: build
needs: [build, publish_npm]
runs-on: ubuntu-24.04-arm

steps:
Expand Down Expand Up @@ -381,6 +381,11 @@ jobs:
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
ENGINE_IMAGE: 'ghcr.io/${{ github.repository }}:${{ github.sha }}-arm64'

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version-file: ./.nvmrc

- name: Publish test
run: |
if [ -z "${STRIPE_NPM_REGISTRY:-}" ]; then
Expand All @@ -390,6 +395,7 @@ jobs:
bash e2e/publish.test.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SKIP_PUBLISH: '1'

# ---------------------------------------------------------------------------
# E2E Stripe — Stripe API + Temporal integration tests (runs on every push/PR)
Expand Down
90 changes: 90 additions & 0 deletions e2e/devcontainer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { readFileSync, existsSync } from 'node:fs'
import { join } from 'node:path'
import { describe, it, expect } from 'vitest'

const ROOT = join(import.meta.dirname, '..')
const DEVCONTAINER_DIR = join(ROOT, '.devcontainer')

describe('devcontainer configuration', () => {
it('devcontainer.json is valid JSON with required fields', () => {
const path = join(DEVCONTAINER_DIR, 'devcontainer.json')
expect(existsSync(path), '.devcontainer/devcontainer.json must exist').toBe(true)

const config = JSON.parse(readFileSync(path, 'utf-8'))

expect(config.name).toBeDefined()
expect(config.dockerComposeFile).toBe('docker-compose.yml')
expect(config.service).toBe('app')
expect(config.workspaceFolder).toBeDefined()
expect(config.postCreateCommand).toContain('pnpm install')
expect(config.postCreateCommand).toContain('pnpm build')
expect(config.remoteUser).toBe('node')
})

it('devcontainer.json forwards required ports', () => {
const config = JSON.parse(readFileSync(join(DEVCONTAINER_DIR, 'devcontainer.json'), 'utf-8'))

const ports = config.forwardPorts as number[]
expect(ports).toContain(55432) // Postgres
expect(ports).toContain(12111) // stripe-mock HTTP
expect(ports).toContain(7233) // Temporal gRPC
})

it('devcontainer.json includes required VS Code extensions', () => {
const config = JSON.parse(readFileSync(join(DEVCONTAINER_DIR, 'devcontainer.json'), 'utf-8'))

const extensions = config.customizations?.vscode?.extensions as string[]
expect(extensions).toContain('dbaeumer.vscode-eslint')
expect(extensions).toContain('esbenp.prettier-vscode')
expect(extensions).toContain('vitest.explorer')
})

it('docker-compose.yml exists and references compose.yml', () => {
const path = join(DEVCONTAINER_DIR, 'docker-compose.yml')
expect(existsSync(path), '.devcontainer/docker-compose.yml must exist').toBe(true)

const content = readFileSync(path, 'utf-8')
expect(content).toContain('compose.yml')
expect(content).toContain('DATABASE_URL')
expect(content).toContain('STRIPE_MOCK_URL')
expect(content).toContain('TEMPORAL_ADDRESS')
})

it('docker-compose.yml app service depends on infrastructure', () => {
const content = readFileSync(join(DEVCONTAINER_DIR, 'docker-compose.yml'), 'utf-8')

expect(content).toContain('postgres')
expect(content).toContain('stripe-mock')
expect(content).toContain('temporal')
expect(content).toContain('service_healthy')
})

it('Dockerfile exists and sets up Node toolchain', () => {
const path = join(DEVCONTAINER_DIR, 'Dockerfile')
expect(existsSync(path), '.devcontainer/Dockerfile must exist').toBe(true)

const content = readFileSync(path, 'utf-8')
expect(content).toContain('javascript-node:24')
expect(content).toContain('corepack enable')
})

it('test-devcontainer.sh exists and is executable-ready', () => {
const path = join(DEVCONTAINER_DIR, 'test-devcontainer.sh')
expect(existsSync(path), '.devcontainer/test-devcontainer.sh must exist').toBe(true)

const content = readFileSync(path, 'utf-8')
expect(content).toContain('#!/usr/bin/env bash')
expect(content).toContain('set -euo pipefail')
expect(content).toContain('PASS')
expect(content).toContain('FAIL')
})

it('environment variables use Docker service hostnames, not localhost', () => {
const content = readFileSync(join(DEVCONTAINER_DIR, 'docker-compose.yml'), 'utf-8')

// Inside the container, services are reached by hostname, not localhost
expect(content).toMatch(/DATABASE_URL.*@postgres:/)
expect(content).toMatch(/STRIPE_MOCK_URL.*stripe-mock:/)
expect(content).toMatch(/TEMPORAL_ADDRESS.*temporal:/)
})
})
Loading
Loading