Skip to content

Commit 5b585a5

Browse files
docs: restructure documenttation (#16)
1 parent e000196 commit 5b585a5

14 files changed

+721
-536
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Two separate Node.js processes:
115115

116116
## Per-repo configuration (`config/layne.json`)
117117

118-
See [docs/configuration.md](docs/configuration.md) for the full schema and examples.
118+
See [docs/2-configuration.md](docs/2-configuration.md) for the full schema and examples.
119119

120120
Key points for code navigation:
121121
- Read once per process startup — **restart both server and worker to pick up changes**

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Contributing to Layne
22

3-
Thanks for your interest in contributing. This document covers the branch model, PR workflow, and release process. For setting up a local development environment, see [docs/local-development.md](docs/local-development.md).
3+
Thanks for your interest in contributing. This document covers the branch model, PR workflow, and release process. For setting up a local development environment, see [docs/3-local-development.md](docs/3-local-development.md).
44

55
---
66

@@ -98,8 +98,8 @@ All four must pass before a PR can be merged.
9898

9999
## Extending Layne
100100

101-
- **Adding a new scanner:** see [docs/extending.md — Adding a New Scanner](docs/extending.md#adding-a-new-scanner)
102-
- **Adding a notification provider:** see [docs/extending.md — Adding a New Notification Provider](docs/extending.md#adding-a-new-notification-provider)
101+
- **Adding a new scanner:** see [docs/6-extending.md — Adding a New Scanner](docs/6-extending.md#adding-a-new-scanner)
102+
- **Adding a notification provider:** see [docs/6-extending.md — Adding a New Notifier](docs/6-extending.md#adding-a-new-notifier)
103103

104104
---
105105

README.md

Lines changed: 27 additions & 376 deletions
Large diffs are not rendered by default.
97.7 KB
Loading
23.6 KB
Loading
124 KB
Loading
106 KB
Loading

docs/1-deployment.md

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
# Deployment
2+
3+
This guide covers deploying Layne to a production EC2 instance with Docker Compose, TLS via Let's Encrypt, and an automated CI/CD pipeline. You can adapt it to any host that can run Docker Compose.
4+
5+
6+
## Table of Contents
7+
8+
- [Prerequisites](#prerequisites)
9+
- [Step 1 — Create the GitHub App](#step-1--create-the-github-app)
10+
- [Step 2 — Provision the EC2 Instance](#step-2--provision-the-ec2-instance)
11+
- [Step 3 — Install Docker](#step-3--install-docker)
12+
- [Step 4 — Deploy Layne](#step-4--deploy-layne)
13+
- [Step 5 — Verify](#step-5--verify)
14+
- [Operations](#operations)
15+
- [Automated Deployment](#automated-deployment)
16+
- [Scaling Workers](#scaling-workers)
17+
- [Renewing TLS Certificates](#renewing-tls-certificates)
18+
- [Updating Tool Versions](#updating-tool-versions)
19+
- [Debugging](#debugging)
20+
21+
22+
## Prerequisites
23+
24+
You can deploy Layne anywhere you want. The way we deploy it requires:
25+
26+
- An AWS account with EC2 access
27+
- A domain name you control (for TLS)
28+
- Docker and Docker Compose installed on the EC2 instance
29+
- GitHub organisation admin access (to create and install the GitHub App)
30+
31+
Everything runs inside Docker Compose — nginx, Certbot, Redis, the server, and the worker. No manual nginx install on the host is required.
32+
33+
34+
## Step 1 — Create the GitHub App
35+
36+
1. Go to **GitHub → Settings → Developer settings → GitHub Apps → New GitHub App**.
37+
38+
2. Fill in the form:
39+
- **GitHub App name:** `Layne` (or any name you prefer)
40+
- **Homepage URL:** `https://your-domain.com`
41+
- **Webhook URL:** `https://your-domain.com/webhook`
42+
- **Webhook secret:** generate one with `openssl rand -hex 32` and save it — you will need it later.
43+
44+
3. Under **Repository permissions**, set:
45+
| Permission | Access |
46+
|---|---|
47+
| Checks | Read & write |
48+
| Contents | Read-only |
49+
| Pull requests | Read-only |
50+
| Issues | Read & write (required for label management) |
51+
52+
4. Under **Subscribe to events**, check **Pull request**.
53+
54+
5. Set **Where can this GitHub App be installed?** to **Only on this account** (or Any account if you plan to share it).
55+
56+
6. Click **Create GitHub App**. Note the **App ID** shown at the top of the page.
57+
58+
7. Scroll down to **Private keys** and click **Generate a private key**. A `.pem` file will be downloaded — keep it safe.
59+
60+
8. On the left sidebar, click **Install App** and install it on the repositories you want Layne to scan.
61+
62+
63+
## Step 2 — Provision the EC2 Instance
64+
65+
1. Launch an EC2 instance. Recommended spec:
66+
- **Instance type:** `t3.medium` or larger (Semgrep is CPU-intensive)
67+
- **AMI:** Ubuntu 22.04 LTS or Amazon Linux 2023
68+
- **Storage:** 20 GB gp3 (scan workspaces are ephemeral but the Docker image is ~2 GB)
69+
70+
2. In the **Security Group**, open:
71+
- Port `443` (HTTPS) — inbound from `0.0.0.0/0`
72+
- Port `80` (HTTP) — inbound from `0.0.0.0/0` (needed for ACME challenges during cert issuance)
73+
- Port `22` (SSH) — inbound from your IP only
74+
75+
3. Assign an **Elastic IP** to the instance so the address is stable.
76+
77+
4. Point your domain's **A record** to the Elastic IP. Wait for DNS to propagate before continuing.
78+
79+
80+
## Step 3 — Install Docker
81+
82+
SSH into the instance and run:
83+
84+
```bash
85+
# Ubuntu 22.04
86+
sudo apt-get update
87+
sudo apt-get install -y ca-certificates curl gnupg
88+
sudo install -m 0755 -d /etc/apt/keyrings
89+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
90+
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
91+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
92+
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
93+
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
94+
sudo apt-get update
95+
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
96+
sudo usermod -aG docker $USER
97+
newgrp docker
98+
```
99+
100+
101+
## Step 4 — Deploy Layne
102+
103+
1. Clone the repository onto the instance:
104+
105+
```bash
106+
git clone https://github.com/your-org/layne.git
107+
cd layne
108+
```
109+
110+
2. Create your environment file:
111+
112+
```bash
113+
cp .env.example .env
114+
```
115+
116+
3. Edit `.env` and fill in your values:
117+
118+
```bash
119+
# The numeric App ID from the GitHub App settings page
120+
GITHUB_APP_ID=123456
121+
122+
# The private key from the downloaded .pem file — paste the full content
123+
# on a single line, replacing literal newlines with \n
124+
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAA...\n-----END RSA PRIVATE KEY-----"
125+
126+
# The webhook secret you generated in Step 1
127+
GITHUB_WEBHOOK_SECRET=your-32-char-hex-secret
128+
129+
# Your domain and email for Let's Encrypt
130+
DOMAIN=your-domain.com
131+
LETSENCRYPT_EMAIL=you@example.com
132+
```
133+
134+
To convert the private key to a single line:
135+
```bash
136+
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' layne.pem
137+
```
138+
139+
4. Run the one-time TLS setup. This uses Certbot inside Docker to obtain a Let's Encrypt certificate — no host-level nginx install needed:
140+
141+
```bash
142+
chmod +x scripts/init-tls.sh
143+
./scripts/init-tls.sh
144+
```
145+
146+
The script creates a dummy self-signed cert, starts nginx, requests the real certificate from Let's Encrypt, then reloads nginx with the real cert.
147+
148+
5. Build and start all services:
149+
150+
```bash
151+
docker compose up --build -d
152+
```
153+
154+
6. Check that everything is running:
155+
156+
```bash
157+
docker compose ps
158+
docker compose logs -f
159+
```
160+
161+
7. Verify the health endpoint:
162+
163+
```bash
164+
curl https://your-domain.com/health
165+
# → {"status":"ok"}
166+
```
167+
168+
169+
## Step 5 — Verify
170+
171+
Open a pull request on one of the repos where Layne is installed. Within a few seconds you should see a **Layne** check appear on the PR in `queued` status, then `in progress`, then `success` or `failure` with inline annotations if issues were found.
172+
173+
174+
## Operations
175+
176+
### Automated Deployment
177+
178+
Here is the GitHub Actions workflow we use internally to deploy Layne to an EC2 instance on every push to `develop`. Copy it into your own repository's `.github/workflows/deploy.yml` and configure the secrets below.
179+
180+
The workflow:
181+
182+
1. Runs the full test suite — the deploy step is skipped if tests fail
183+
2. Rsyncs the repository to `/home/ubuntu/layne/layne/` on the server, preserving `data/` (certbot certificates) and never touching `.env`
184+
3. Writes a fresh `.env` file from GitHub secrets
185+
4. Runs `docker compose up --build --no-deps -d server worker` — rebuilds and restarts only the server and worker, leaving Redis (and the BullMQ queue) untouched
186+
187+
```yaml
188+
name: Deploy
189+
190+
on:
191+
push:
192+
branches: [develop]
193+
workflow_dispatch:
194+
195+
jobs:
196+
test:
197+
runs-on: ubuntu-latest
198+
steps:
199+
- uses: actions/checkout@v4
200+
- uses: actions/setup-node@v4
201+
with:
202+
node-version: '22'
203+
cache: 'npm'
204+
- run: npm ci
205+
- run: npm run lint
206+
- run: npm run validate-config
207+
- run: npm test
208+
209+
deploy:
210+
needs: test
211+
runs-on: ubuntu-latest
212+
environment: production
213+
214+
steps:
215+
- uses: actions/checkout@v4
216+
217+
- name: Set up SSH
218+
run: |
219+
mkdir -p ~/.ssh
220+
echo "${{ secrets.EC2_SSH_KEY }}" | tr -d '\r' > ~/.ssh/deploy_key
221+
chmod 600 ~/.ssh/deploy_key
222+
echo "Host deploy-target" >> ~/.ssh/config
223+
echo " HostName ${{ secrets.EC2_HOST }}" >> ~/.ssh/config
224+
echo " User ubuntu" >> ~/.ssh/config
225+
echo " IdentityFile ~/.ssh/deploy_key" >> ~/.ssh/config
226+
echo " StrictHostKeyChecking no" >> ~/.ssh/config
227+
echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config
228+
229+
- name: Sync code to server
230+
run: |
231+
rsync -az --delete \
232+
--exclude='.git' \
233+
--exclude='node_modules' \
234+
--exclude='.env' \
235+
--exclude='data' \
236+
--exclude='coverage' \
237+
-e "ssh -F $HOME/.ssh/config" \
238+
./ deploy-target:/home/ubuntu/layne/layne/
239+
240+
- name: Write .env from secrets
241+
env:
242+
GH_APP_ID: ${{ secrets.GH_APP_ID }}
243+
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
244+
GH_WEBHOOK_SECRET: ${{ secrets.GH_WEBHOOK_SECRET }}
245+
DOMAIN: ${{ secrets.DOMAIN }}
246+
LETSENCRYPT_EMAIL: ${{ secrets.LETSENCRYPT_EMAIL }}
247+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
248+
ROCKETCHAT_WEBHOOK_URL: ${{ secrets.ROCKETCHAT_WEBHOOK_URL }}
249+
METRICS_ENABLED: ${{ vars.METRICS_ENABLED }}
250+
METRICS_PORT: ${{ vars.METRICS_PORT }}
251+
run: |
252+
{
253+
printf 'GITHUB_APP_ID=%s\n' "$GH_APP_ID"
254+
printf 'GITHUB_APP_PRIVATE_KEY=%s\n' "$GH_APP_PRIVATE_KEY"
255+
printf 'GITHUB_WEBHOOK_SECRET=%s\n' "$GH_WEBHOOK_SECRET"
256+
printf 'DOMAIN=%s\n' "$DOMAIN"
257+
printf 'LETSENCRYPT_EMAIL=%s\n' "$LETSENCRYPT_EMAIL"
258+
printf 'ANTHROPIC_API_KEY=%s\n' "$ANTHROPIC_API_KEY"
259+
printf 'ROCKETCHAT_WEBHOOK_URL=%s\n' "$ROCKETCHAT_WEBHOOK_URL"
260+
printf 'METRICS_ENABLED=%s\n' "${METRICS_ENABLED:-false}"
261+
printf 'METRICS_PORT=%s\n' "${METRICS_PORT:-9091}"
262+
} | ssh -F "$HOME/.ssh/config" deploy-target \
263+
'cat > /home/ubuntu/layne/layne/.env'
264+
265+
- name: Rebuild and restart server and worker
266+
run: |
267+
ssh -F "$HOME/.ssh/config" deploy-target \
268+
'cd /home/ubuntu/layne/layne &&
269+
docker compose up --build --no-deps -d server worker &&
270+
docker compose exec nginx nginx -s reload'
271+
```
272+
273+
**Required GitHub secrets:**
274+
275+
Go to your repository → **Settings → Secrets and variables → Actions** and add:
276+
277+
| Secret | Description |
278+
|---|---|
279+
| `EC2_HOST` | Public IP or hostname of the EC2 instance |
280+
| `EC2_SSH_KEY` | Contents of the SSH private key (`.pem`) used to connect to the instance |
281+
| `GH_APP_ID` | GitHub App ID (maps to `GITHUB_APP_ID` in `.env`) |
282+
| `GH_APP_PRIVATE_KEY` | RSA private key, single line with `\n`-escaped newlines (maps to `GITHUB_APP_PRIVATE_KEY` in `.env`) |
283+
| `GH_WEBHOOK_SECRET` | Webhook HMAC secret (maps to `GITHUB_WEBHOOK_SECRET` in `.env`) |
284+
| `DOMAIN` | Domain name for TLS (e.g. `layne.example.com`) |
285+
| `LETSENCRYPT_EMAIL` | Email for Let's Encrypt expiry notifications |
286+
| `ANTHROPIC_API_KEY` | Anthropic API key for Claude scanning (required when any repo has `claude.enabled: true`) |
287+
| `ROCKETCHAT_WEBHOOK_URL` | Global Rocket.Chat incoming webhook URL (required when `$global.notifications.rocketchat.webhookUrl` is `"$ROCKETCHAT_WEBHOOK_URL"`) |
288+
289+
**Optional GitHub Actions variables** (Settings → Secrets and variables → Actions → Variables):
290+
291+
| Variable | Default | Description |
292+
|---|---|---|
293+
| `METRICS_ENABLED` | `false` | Set to `true` to enable Prometheus metrics on the deployed instance |
294+
| `METRICS_PORT` | `9091` | Port for the worker metrics HTTP server |
295+
296+
GitHub reserves the `GITHUB_` prefix for its own built-in variables, so the three app secrets use a `GH_` prefix here. The workflow maps them to the correct `GITHUB_`-prefixed names when writing `.env`.
297+
298+
The workflow uses a GitHub [**environment**](https://docs.github.com/en/actions/deployment/targeting-different-deployment-environments) named `production`. You can configure deployment protection rules on that environment (e.g. require a manual approval before deploying to production).
299+
300+
301+
### Scaling Workers
302+
303+
The worker runs with `concurrency: 5` by default (5 jobs per process). To handle more simultaneous PRs, run additional worker containers:
304+
305+
```bash
306+
docker compose up --scale worker=3 -d
307+
```
308+
309+
310+
### Renewing TLS Certificates
311+
312+
Let's Encrypt certificates expire after 90 days. Renew with:
313+
314+
```bash
315+
docker compose run --rm certbot renew
316+
docker compose exec nginx nginx -s reload
317+
```
318+
319+
Add this to a monthly cron job on the host to automate renewal.
320+
321+
322+
### Updating Tool Versions
323+
324+
Trufflehog and Semgrep versions are pinned directly in the `Dockerfile`. To update them, edit the version strings in that file, then rebuild and restart:
325+
326+
```bash
327+
docker compose build
328+
docker compose up -d
329+
```
330+
331+
Test the new versions in a staging environment before deploying to production.
332+
333+
334+
### Debugging
335+
336+
Set `DEBUG_MODE=true` in your `.env` file (or as a Docker environment variable) to enable verbose logging across all Layne components. When active, you will see:
337+
338+
- Every git command executed during the clone and diff phases (with tokens redacted)
339+
- The exact files passed to each scanner
340+
- Trufflehog batch progress (useful for large PRs)
341+
- Every GitHub API call (createCheckRun, startCheckRun, completeCheckRun) and annotation chunk counts
342+
- Installation token generation events
343+
- Webhook event details (action, repo, PR number, commit SHA)
344+
345+
Stderr from subprocesses (git, semgrep, trufflehog) is always logged when non-empty, regardless of `DEBUG_MODE`. This is intentional — stderr from these tools almost always indicates a misconfiguration or tool error worth knowing about.
346+
347+
To enable on a running stack without a full rebuild:
348+
349+
```bash
350+
# Add to .env
351+
DEBUG_MODE=true
352+
353+
# Restart only the affected containers
354+
docker compose up --no-deps -d server worker
355+
356+
# Follow logs
357+
docker compose logs -f worker
358+
```
359+
360+
To disable, remove or set `DEBUG_MODE=false` and restart.

0 commit comments

Comments
 (0)