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
5 changes: 5 additions & 0 deletions .changeset/dockerfile-asset-hygiene.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": patch
---

Build pipeline: prevent PR #4769-class outages where a runtime asset added to `server/src/**` is missing from the shipping image because the Dockerfile copied it by exact filename. `npm run build` now mirrors every allowlisted non-TypeScript file (`.json`, `.md`, `.sql`, `.txt`, `.csv`, `.yaml`/`.yml`, `.html`, `.xml`) from `server/src/**` into `dist/**` automatically (`scripts/copy-server-assets.cjs`); the Dockerfile drops three redundant per-directory `COPY` lines; and a new CI check (`scripts/copy-server-assets.cjs --check`) fails the build if `dist/` ever diverges from source. No protocol surface change.
9 changes: 9 additions & 0 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ jobs:
- name: Build
run: npm run build

- name: Assert dist/ contains every server/src runtime asset
# PR #4769 root cause: ui-element-formats.json was added to source
# and `require()`d by task-handlers.ts, but the build script didn't
# copy it into dist/, so the runtime image crashlooped with
# `Cannot find module './ui-element-formats.json'`. Prod was down
# ~75 min. This assertion fails CI the moment a referenced asset
# would be missing in the shipping artifact.
run: node scripts/copy-server-assets.cjs --check

- name: Server unit tests
run: npm run test:server-unit

Expand Down
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,13 @@ COPY package*.json ./
# dep tree; native deps are rebuilt explicitly per-package.
RUN npm ci --omit=dev --ignore-scripts && npm rebuild sharp

# Copy built files from builder
# Copy built files from builder. Runtime assets under server/src/** (JSON
# format catalogs, SQL migrations, Addie rule markdown, etc.) are mirrored
# into dist/ by scripts/copy-server-assets.cjs during `npm run build`, so
# `COPY ... /dist` is sufficient — no per-directory asset lines needed here.
# Adding an asset to server/src/** is a one-place change.
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server/public ./server/public
COPY --from=builder /app/server/src/db/migrations ./dist/db/migrations
COPY --from=builder /app/server/src/creative-agent/*.json ./dist/creative-agent/
COPY --from=builder /app/server/src/addie/rules/*.md ./dist/addie/rules/
COPY --from=builder /app/static ./static
COPY --from=builder /app/docs ./docs

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start:stripe": "bash -c 'source <(grep CONDUCTOR_PORT .env.local | sed \"s/^/export /\") && PORT=${CONDUCTOR_PORT:-3000} && stripe listen --forward-to localhost:$PORT/api/webhooks/stripe'",
"dev": "echo 'Use docker compose up --build for local development' && exit 1",
"db:migrate": "DOTENV_CONFIG_PATH=.env.local tsx --import dotenv/config server/src/db/migrate.ts",
"build": "tsc --project server/tsconfig.json && mkdir -p dist/addie/rules && cp server/src/addie/rules/*.md dist/addie/rules/ && npm run build:schemas && npm run build:compliance && npm run build:protocol-tarball",
"build": "tsc --project server/tsconfig.json && node scripts/copy-server-assets.cjs && npm run build:schemas && npm run build:compliance && npm run build:protocol-tarball",
"build:openapi": "tsx scripts/generate-openapi.ts",
"build:addie-tools": "tsx scripts/build-addie-tool-reference.ts",
"test:addie-tools": "tsx scripts/build-addie-tool-reference.ts --check",
Expand Down
60 changes: 60 additions & 0 deletions scripts/copy-server-assets.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node
// Mirror non-TypeScript runtime assets from server/src/** into dist/**.
// `tsc` only emits .js from .ts, so any module that does
// require('./foo.json') / readFile(__dirname + '/foo.sql')
// will ENOENT at runtime in the built image unless the asset is copied here.
// Establishing this in the npm build script (rather than in the Dockerfile)
// means `node dist/index.js` works locally too, and CI exercises the same
// asset graph the runtime image ships.
//
// Extensions are an explicit allowlist: any non-TS file type that needs to
// reach dist should be added here. The allowlist is intentionally permissive
// so contributors don't have to remember to opt-in when adding a new asset.
//
// Modes:
// (default) Copy every allowlisted src asset into the matching dist path.
// --check Don't copy. Exit non-zero if any src asset is missing in dist
// (verifies the build script ran and the contract holds — the
// CI assertion that catches PR #4769-class regressions).

const fs = require("node:fs");
const path = require("node:path");

const SRC = path.join(__dirname, "..", "server", "src");
const DST = path.join(__dirname, "..", "dist");
const EXTENSIONS = new Set([".json", ".md", ".sql", ".txt", ".csv", ".yaml", ".yml", ".html", ".xml"]);
const checkMode = process.argv.includes("--check");

const assets = [];

function walk(dir, relBase = "") {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const rel = path.join(relBase, entry.name);
const src = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(src, rel);
} else if (entry.isFile() && EXTENSIONS.has(path.extname(entry.name))) {
assets.push({ rel, src });
}
}
}

walk(SRC);

if (checkMode) {
const missing = assets.filter(({ rel }) => !fs.existsSync(path.join(DST, rel)));
if (missing.length > 0) {
console.error(`copy-server-assets --check: ${missing.length} asset(s) missing in dist/:`);
for (const { rel } of missing) console.error(` server/src/${rel} -> dist/${rel}`);
console.error("\nRun `npm run build` to refresh dist/, or update the EXTENSIONS allowlist if a new file type was introduced.");
process.exit(1);
}
console.log(`copy-server-assets --check: ${assets.length} asset(s) present in dist`);
} else {
for (const { rel, src } of assets) {
const dst = path.join(DST, rel);
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.copyFileSync(src, dst);
}
console.log(`copy-server-assets: ${assets.length} file(s) copied from server/src to dist`);
}
Loading