docs(spec): add specification for Docker packaging and GitHub Actions CI pipeline

This commit is contained in:
Lucas Winther
2026-09-06 20:36:43 +02:00
parent 815741b45b
commit 7ce86012d0
@@ -0,0 +1,209 @@
# Specification: Docker Packaging & GitHub Actions CI/CD Pipeline
## 1. Overview & Objectives
This specification defines the Docker containerization and continuous integration (CI/CD) pipeline for `openproject-mcp`. The goal is to produce an official, multi-platform Docker container image published to GitHub Container Registry (`ghcr.io`) whenever changes land on `main` or semantic release tags are pushed, while validating code health and container build integrity on every pull request.
### Primary Objectives
1. **Container Packaging**: Provide a lean, secure, multi-stage `Dockerfile` and `.dockerignore` targeting the official `oven/bun:1-slim` runtime.
2. **Multi-Architecture Support**: Build for both `linux/amd64` and `linux/arm64` using Docker Buildx and QEMU.
3. **Least Privilege Security**: Run container processes under the unprivileged `bun` user (UID/GID 1000) and ensure credentials (`.env*`, tokens) are never included in the image layers.
4. **CI/CD Automation**: Create a unified GitHub Actions workflow (`.github/workflows/ci.yml`) that runs type checking (`tsc --noEmit`), test suites (`bun test`), and multi-arch Docker image compilation.
5. **Publishing Strategy**: Publish images to GitHub Container Registry (`ghcr.io/<owner>/<repo>`) with `latest`, commit SHA (`sha-<hash>`), and semantic version tags (`vX.Y.Z`, `X.Y`), while verifying pull requests with build-only checks without pushing.
6. **Documentation**: Update `README.md` with Docker execution instructions and record an Architectural Decision Record in `docs/DECISIONS.md`.
---
## 2. Container Architecture & Packaging
### 2.1 Base Image & Runtime Strategy
- **Base Image**: `oven/bun:1-slim` (Debian Bookworm slim base with Bun v1.x).
- **Rationale**:
- Official Bun Docker image maintained by Oven.
- Includes glibc and essential CA certificates needed for HTTPS communication with OpenProject instances.
- Slim footprint (~80-90 MB) compared to full OS images.
- Provides a built-in unprivileged user `bun` (UID 1000, GID 1000).
### 2.2 Multi-Stage Dockerfile (`Dockerfile`)
```dockerfile
# Stage 1: Dependency resolution and production prune
FROM oven/bun:1-slim AS builder
WORKDIR /app
# Copy dependency manifests
COPY package.json bun.lock ./
# Install production dependencies only with strict lockfile
RUN bun install --frozen-lockfile --production
# Stage 2: Minimal runtime image
FROM oven/bun:1-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copy installed production node_modules from builder
COPY --from=builder /app/node_modules ./node_modules
# Copy application configuration and source code
COPY package.json tsconfig.json ./
COPY src ./src
# Set ownership and drop root privileges
RUN chown -R bun:bun /app
USER bun
# Standard MCP stdio communication
ENTRYPOINT ["bun", "run", "src/index.ts"]
```
### 2.3 Context Hygiene (`.dockerignore`)
The `.dockerignore` file prevents build context bloat and guarantees sensitive local files are excluded:
```
node_modules
.git
.github
.env*
coverage
tests
docs
scripts
.superpowers
*.log
*.md
docker-compose.yml
```
---
## 3. GitHub Actions CI/CD Pipeline (`.github/workflows/ci.yml`)
### 3.1 Pipeline Triggers & Permissions
- **Triggers**:
- `push`: branches `[main]`, tags `['v*.*.*']`
- `pull_request`: branches `[main]`
- `workflow_dispatch`: manual execution
- **Permissions**:
- `contents: read`
- `packages: write` (for publishing container images to GHCR)
### 3.2 Pipeline Jobs
```mermaid
graph TD
A[Trigger: Push / PR / Tag] --> B[Job 1: test]
B --> C[Setup Bun 1.3.14]
C --> D[bun install --frozen-lockfile]
D --> E[bun run typecheck]
E --> F[bun test]
F --> G{All Tests Passed?}
G -- Yes --> H[Job 2: docker]
G -- No --> Z[Fail Pipeline]
H --> I[Setup QEMU & Buildx]
H --> J[Compute Docker Metadata & Tags]
H --> K{Is PR?}
K -- Yes --> L[Build Multi-Arch (amd64, arm64) with GHA Cache - Push=False]
K -- No --> M[Login to GHCR with GITHUB_TOKEN]
M --> N[Build & Push Multi-Arch (amd64, arm64) to GHCR]
```
#### Job 1: `test`
- **Environment**: `ubuntu-latest`
- **Steps**:
1. `actions/checkout@v4`
2. `oven-sh/setup-bun@v2` with `bun-version: 1.3.14`
3. `bun install --frozen-lockfile`
4. `bun run typecheck` (`tsc --noEmit`)
5. `bun test` (all mock unit and integration tests execute; live container tests gracefully skip if environment variables are unset)
#### Job 2: `docker`
- **Depends on**: `test` (will not run if tests or typecheck fail)
- **Environment**: `ubuntu-latest`
- **Steps**:
1. `actions/checkout@v4`
2. `docker/setup-qemu-action@v3` (enables multi-arch emulation for `linux/amd64` and `linux/arm64`)
3. `docker/setup-buildx-action@v3`
4. `docker/metadata-action@v5`:
- Image base: `ghcr.io/${{ github.repository }}`
- Flavor: `latest=auto`
- Tags:
- `type=ref,event=branch`
- `type=ref,event=pr`
- `type=semver,pattern={{version}}`
- `type=semver,pattern={{major}}.{{minor}}`
- `type=sha,prefix=sha-`
- `type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}`
5. `docker/login-action@v3` (run only if `github.event_name != 'pull_request'`):
- Registry: `ghcr.io`
- Username: `${{ github.actor }}`
- Password: `${{ secrets.GITHUB_TOKEN }}`
6. `docker/build-push-action@v6`:
- Context: `.`
- File: `./Dockerfile`
- Platforms: `linux/amd64,linux/arm64`
- Push: `${{ github.event_name != 'pull_request' }}`
- Tags: `${{ steps.meta.outputs.tags }}`
- Labels: `${{ steps.meta.outputs.labels }}`
- Cache from: `type=gha`
- Cache to: `type=gha,mode=max`
---
## 4. Operational Usage
### 4.1 Running the Docker Image
Users can run the container image via interactive stdio transport:
```bash
docker run -i --rm \
-e OPENPROJECT_BASE_URL="https://openproject.example.com" \
-e OPENPROJECT_API_KEY="your-api-key" \
ghcr.io/<owner>/openproject-mcp:latest
```
Running in read-only mode:
```bash
docker run -i --rm \
-e OPENPROJECT_BASE_URL="https://openproject.example.com" \
-e OPENPROJECT_API_KEY="your-api-key" \
ghcr.io/<owner>/openproject-mcp:latest --read-only
```
### 4.2 MCP Client Configuration Example (Claude Desktop / Cursor)
```json
{
"mcpServers": {
"openproject": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e", "OPENPROJECT_BASE_URL=https://openproject.example.com",
"-e", "OPENPROJECT_API_KEY=your-api-key",
"ghcr.io/<owner>/openproject-mcp:latest"
]
}
}
}
```
---
## 5. Verification & Testing Plan
1. **Local Docker Build**: Run `docker build -t openproject-mcp:test .` to verify multi-stage build finishes with zero errors.
2. **Local Execution & Stdio Verification**:
- Run container with test environment variables:
```bash
docker run -i --rm \
-e OPENPROJECT_BASE_URL="http://localhost:8080" \
-e OPENPROJECT_API_KEY="test-token" \
openproject-mcp:test --read-only
```
- Verify that container starts as non-root user `bun`, logs `[openproject-mcp] Server started (readOnly=true)` to stderr, and keeps stdout clean for JSON-RPC messages.
3. **Workflow Syntax & Linting**:
- Validate `.github/workflows/ci.yml` YAML syntax and structure.
4. **Documentation Check**:
- Record ADR-015 in `docs/DECISIONS.md`.
- Update `README.md` with Docker usage guide.
- Update `docs/TODO.md` tracking the CI/CD pipeline completion.