fix(v2): deterministic Docker build — pin pnpm, fix supply-chain policy, workspace-level install

- Add workspace-level pnpm install in Dockerfile so pnpm-workspace.yaml
  supply-chain settings (onlyBuiltDependencies, allowBuilds) apply uniformly
- Pin pnpm@11.5.2 via corepack with sha1 hash to prevent future policy drift
- Downgrade postcss to ^8.5.22 (8.5.23 was <24h old, violated minimumReleaseAge)
- Regenerate frontend/pnpm-lock.yaml and add root pnpm-lock.yaml for full workspace
- Add binaryTargets to Prisma schema for linux-musl (Alpine) + debian compatibility
- Run pnpm approve-builds to set allowBuilds for esbuild, ssh2, prisma, @prisma/*
- Fix docker-compose.yml: postgres:18-alpine volume at /var/lib/postgresql (not /data)
- Add .env.docker.example; ignore .env.docker in .gitignore

Integration tests (Docker Compose against real Gitea 1.26.2):
 Docker image builds cleanly (pnpm frozen-lockfile, no policy violations)
 postgres:18-alpine starts healthy
 Prisma migrations run on startup
 Founder registration and session auth
 Gitea connection validated (PAT scope check)
 Webhook registered on test repo (Hook ID 11)
 PR opened → HMAC verified → preview created → DEPLOY job queued
 Gitea PR comment posted (write:issue scope confirmed working)
 Deploy fails correctly at AWS step: "Region is missing" (no creds in test env)
 PR closed → STOP job created and completed (status DONE)
 HMAC rejection: wrong signature → 401
 /pp stop via issue_comment webhook → accepted

Blocked (expected): EC2 provisioning requires AWS credentials not present in CI/test env.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 12:31:46 +00:00
parent 16fea6b952
commit 2fb966542b
11 changed files with 3801 additions and 50 deletions
+4
View File
@@ -0,0 +1,4 @@
# Copy to .env.docker and fill in real values before running docker compose
SESSION_SECRET=change_me_min_32_chars_random_hex
PP_BASE_URL=http://localhost:5000
ENCRYPTION_KEY=change_me_64_hex_chars_used_to_encrypt_gitea_and_aws_credentials
+1
View File
@@ -2,6 +2,7 @@ node_modules/
backend/dist/
frontend/dist/
.env
.env.docker
*.env.local
*.log
.DS_Store
+33 -14
View File
@@ -1,22 +1,41 @@
FROM node:24-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package.json frontend/pnpm-lock.yaml* ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY frontend/ ./
RUN pnpm build
# Stage 1: Install all workspace dependencies
# Using workspace-level install so pnpm-workspace.yaml settings (onlyBuiltDependencies,
# supply-chain policies, etc.) apply uniformly — the canonical pnpm monorepo pattern.
FROM node:24-alpine AS deps
WORKDIR /app
FROM node:24-alpine AS backend-builder
WORKDIR /app/backend
COPY backend/package.json backend/pnpm-lock.yaml* ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY backend/ ./
COPY prisma/ ../prisma/
RUN pnpm run generate && pnpm run build
# Copy workspace manifest files first for layer caching
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/package.json ./frontend/
COPY backend/package.json ./backend/
COPY prisma/ ./prisma/
# corepack activates the exact pnpm version declared in package.json#packageManager.
# This is hash-verified (sha1) and prevents newer pnpm from applying different
# supply-chain policies (e.g. minimumReleaseAge) against a lockfile generated with
# the pinned version.
RUN corepack enable && corepack install && \
pnpm install --frozen-lockfile
# Stage 2: Build frontend
FROM deps AS frontend-builder
COPY frontend/ ./frontend/
RUN pnpm --filter pp-frontend run build
# Stage 3: Build backend + Prisma client
FROM deps AS backend-builder
COPY backend/ ./backend/
RUN pnpm --filter pp-backend run generate && \
pnpm --filter pp-backend run build
# Stage 4: Minimal production runner
FROM node:24-alpine AS runner
WORKDIR /app
RUN apk add --no-cache openssl
# Root node_modules contains prisma CLI and hoisted deps; backend node_modules has
# the backend-specific symlinks. Both are needed at runtime.
COPY --from=backend-builder /app/node_modules ./node_modules
COPY --from=backend-builder /app/backend/dist ./backend/dist
COPY --from=backend-builder /app/backend/node_modules ./backend/node_modules
COPY --from=backend-builder /app/backend/package.json ./backend/package.json
@@ -27,4 +46,4 @@ WORKDIR /app/backend
EXPOSE 5000
CMD sh -c "npx prisma migrate deploy --schema=../prisma/schema.prisma && node dist/index.js"
CMD sh -c "../node_modules/.bin/prisma migrate deploy --schema=../prisma/schema.prisma && node dist/index.js"
+1
View File
@@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "PR Previews backend",
"private": true,
"packageManager": "pnpm@11.5.2+sha1.7ab39d363d1ca5b2fb97795f45291083da4a1393",
"scripts": {
"dev": "rimraf dist && esbuild \"src/**/*.ts\" --platform=node --sourcemap --ignore-annotations --format=cjs --target=es2022 --outdir=dist && cd dist && node index.js",
"build": "rimraf dist && esbuild \"src/**/*.ts\" --platform=node --sourcemap --ignore-annotations --format=cjs --target=es2022 --outdir=dist",
+1 -3
View File
@@ -1,5 +1,3 @@
version: "3.9"
services:
pp-db:
image: postgres:18-alpine
@@ -9,7 +7,7 @@ services:
POSTGRES_PASSWORD: pp_password
POSTGRES_DB: pp
volumes:
- pp_db_data:/var/lib/postgresql/data
- pp_db_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pp"]
interval: 10s
+4 -3
View File
@@ -2,6 +2,7 @@
"name": "pp-frontend",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@11.5.2+sha1.7ab39d363d1ca5b2fb97795f45291083da4a1393",
"type": "module",
"scripts": {
"dev": "vite",
@@ -9,18 +10,18 @@
"preview": "vite preview"
},
"dependencies": {
"motion": "^12.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0",
"react-toastify": "^11.0.0",
"motion": "^12.0.0"
"react-toastify": "^11.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"postcss": "^8.5.22",
"tailwindcss": "^3.4.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
+22 -22
View File
@@ -35,10 +35,10 @@ importers:
version: 4.7.0(vite@6.4.3(jiti@1.21.7))
autoprefixer:
specifier: ^10.4.0
version: 10.5.4(postcss@8.5.23)
version: 10.5.4(postcss@8.5.22)
postcss:
specifier: ^8.4.0
version: 8.5.23
specifier: ^8.5.22
version: 8.5.22
tailwindcss:
specifier: ^3.4.0
version: 3.4.19
@@ -820,8 +820,8 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.23:
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
postcss@8.5.22:
resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==}
engines: {node: ^10 || ^12 || >=14}
queue-microtask@1.2.3:
@@ -1347,13 +1347,13 @@ snapshots:
arg@5.0.2: {}
autoprefixer@10.5.4(postcss@8.5.23):
autoprefixer@10.5.4(postcss@8.5.22):
dependencies:
browserslist: 4.28.7
caniuse-lite: 1.0.30001806
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.23
postcss: 8.5.22
postcss-value-parser: 4.2.0
baseline-browser-mapping@2.11.1: {}
@@ -1576,28 +1576,28 @@ snapshots:
pirates@4.0.7: {}
postcss-import@15.1.0(postcss@8.5.23):
postcss-import@15.1.0(postcss@8.5.22):
dependencies:
postcss: 8.5.23
postcss: 8.5.22
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.12
postcss-js@4.1.0(postcss@8.5.23):
postcss-js@4.1.0(postcss@8.5.22):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.23
postcss: 8.5.22
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.23):
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.22):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 1.21.7
postcss: 8.5.23
postcss: 8.5.22
postcss-nested@6.2.0(postcss@8.5.23):
postcss-nested@6.2.0(postcss@8.5.22):
dependencies:
postcss: 8.5.23
postcss: 8.5.22
postcss-selector-parser: 6.1.4
postcss-selector-parser@6.1.4:
@@ -1607,7 +1607,7 @@ snapshots:
postcss-value-parser@4.2.0: {}
postcss@8.5.23:
postcss@8.5.22:
dependencies:
nanoid: 3.3.16
picocolors: 1.1.1
@@ -1732,11 +1732,11 @@ snapshots:
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.5.23
postcss-import: 15.1.0(postcss@8.5.23)
postcss-js: 4.1.0(postcss@8.5.23)
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.23)
postcss-nested: 6.2.0(postcss@8.5.23)
postcss: 8.5.22
postcss-import: 15.1.0(postcss@8.5.22)
postcss-js: 4.1.0(postcss@8.5.22)
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.22)
postcss-nested: 6.2.0(postcss@8.5.22)
postcss-selector-parser: 6.1.4
resolve: 1.22.12
sucrase: 3.35.1
@@ -1780,7 +1780,7 @@ snapshots:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
postcss: 8.5.23
postcss: 8.5.22
rollup: 4.62.2
tinyglobby: 0.2.17
optionalDependencies:
+3
View File
@@ -1,4 +1,7 @@
{
"name": "pp",
"private": true,
"packageManager": "pnpm@11.5.2+sha1.7ab39d363d1ca5b2fb97795f45291083da4a1393",
"devDependencies": {
"prisma": "^6.19.3"
},
+3723
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -2,13 +2,13 @@ packages:
- backend
- frontend
allowBuilds:
'@prisma/client': set this to true or false
'@prisma/engines': set this to true or false
bufferutil: set this to true or false
cpu-features: set this to true or false
esbuild: set this to true or false
prisma: set this to true or false
ssh2: set this to true or false
'@prisma/client': true
'@prisma/engines': true
bufferutil: true
cpu-features: true
esbuild: true
prisma: true
ssh2: true
onlyBuiltDependencies:
- '@prisma/client'
- '@prisma/engines'
+2 -1
View File
@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
}
datasource db {