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
20 changes: 20 additions & 0 deletions .github/workflows/broken-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ on:
- 'server/src/addie/rules/**'
- 'scripts/check-owned-links.js'
- 'scripts/smoke-docs-index-runtime.mjs'
- 'scripts/smoke-hosted-compliance-runtime.mjs'
- 'scripts/prune-runtime-artifacts.cjs'
- 'static/compliance/published-versions.json'
- 'server/src/training-agent/schema-compat.ts'
- 'server/src/services/hosted-compliance-version.ts'
- 'tests/runtime-artifact-pruning.test.cjs'
- 'scripts/update-release-docs-nav.mjs'
- 'tests/docs-nav-validation.test.cjs'
- '.markdown-link-check.json'
Expand All @@ -41,6 +47,12 @@ on:
- 'server/src/addie/rules/**'
- 'scripts/check-owned-links.js'
- 'scripts/smoke-docs-index-runtime.mjs'
- 'scripts/smoke-hosted-compliance-runtime.mjs'
- 'scripts/prune-runtime-artifacts.cjs'
- 'static/compliance/published-versions.json'
- 'server/src/training-agent/schema-compat.ts'
- 'server/src/services/hosted-compliance-version.ts'
- 'tests/runtime-artifact-pruning.test.cjs'
- 'scripts/update-release-docs-nav.mjs'
- 'tests/docs-nav-validation.test.cjs'
- '.markdown-link-check.json'
Expand Down Expand Up @@ -141,3 +153,11 @@ jobs:
adcp-docs-index-smoke
--input-type=module
< scripts/smoke-docs-index-runtime.mjs

- name: Load every hosted bundle with the production SDK
run: >-
docker run --rm --interactive
--entrypoint node
adcp-docs-index-smoke
--input-type=module
< scripts/smoke-hosted-compliance-runtime.mjs
3 changes: 3 additions & 0 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ jobs:
- name: Assert dist/ contains every server/src runtime asset
run: node scripts/copy-server-assets.cjs --check

- name: Check Fly runtime artifact closure
run: npm run test:runtime-artifacts

- name: Routed tenant smoke tests
run: npm run test:tenant-routing

Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/runtime-default-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Default runtime image footprint

on:
pull_request:
branches: [main]
paths:
- '.github/workflows/runtime-default-image.yml'
workflow_dispatch:
inputs:
expected_sha:
description: Exact reviewed source commit to measure
required: true
type: string

permissions:
contents: read

jobs:
footprint:
name: Default image with repository cache
runs-on: ubuntu-latest
timeout-minutes: 60
env:
SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
IMAGE: adcp-default-runtime-footprint
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- name: Verify the exact source commit
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
test "$EXPECTED_SHA" = "$SOURCE_SHA"
fi
OUTPUT_DIR="$RUNNER_TEMP/default-runtime-footprint"
printf 'OUTPUT_DIR=%s\n' "$OUTPUT_DIR" >> "$GITHUB_ENV"
mkdir -p "$OUTPUT_DIR"
printf '%s\n' "$SOURCE_SHA" > "$OUTPUT_DIR/source-sha.txt"

- name: Build with default repository cloning and size ceiling
run: |
set -euo pipefail
# No build-argument overrides: the production cache and 7424 MiB
# application ceiling are enforced by the unchanged Dockerfile.
docker build --progress=plain --tag "$IMAGE" . 2>&1 | tee "$OUTPUT_DIR/build.log"

- name: Measure the image and verify every configured repository cache
run: |
set -euo pipefail
docker image inspect "$IMAGE" > "$OUTPUT_DIR/image.json"
docker run --rm --interactive --user 0 --entrypoint node "$IMAGE" --input-type=module > "$OUTPUT_DIR/runtime.json" <<'JS'
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
const mib = path => Number(execFileSync('du', ['-smx', path], { encoding: 'utf8' }).split(/\s+/)[0]);
const root = '/app/.addie-repos';
const repositories = fs.readdirSync(root).filter(name => fs.statSync(`${root}/${name}`).isDirectory()).sort();
console.log(JSON.stringify({ rootFilesystemMiB: mib('/'), applicationMiB: mib('/app'), cacheMiB: mib(root), repositories }, null, 2));
JS
python3 - <<'PY'
import json, os, pathlib, re
output = pathlib.Path(os.environ['OUTPUT_DIR'])
runtime = json.loads((output / 'runtime.json').read_text())
dockerfile = pathlib.Path('Dockerfile').read_text()
clone_lines = re.findall(r'^run_clone\s+.*$', dockerfile, re.M)
configured = set(re.findall(r'^run_clone\s+\S+\s+\S+\s+([\w-]+)\s*$', dockerfile, re.M))
assert configured, 'No configured repository clones found'
assert len(configured) == len(clone_lines), 'Repository clone parser did not match every configured clone'
assert configured <= set(runtime['repositories']), sorted(configured - set(runtime['repositories']))
assert runtime['cacheMiB'] > 0
assert runtime['applicationMiB'] <= 7424
runtime.update(sourceSha=os.environ['SOURCE_SHA'], defaultBuildArguments=True, configuredRepositories=sorted(configured))
(output / 'runtime.json').write_text(json.dumps(runtime, indent=2) + '\n')
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as summary:
summary.write(f"Source `{runtime['sourceSha']}`: visible unpacked root {runtime['rootFilesystemMiB']} MiB; application {runtime['applicationMiB']} MiB / 7424 MiB; repository cache {runtime['cacheMiB']} MiB, {len(configured)} configured repositories.\n")
PY

- name: Initialize every configured docs snapshot
run: |
set -euo pipefail
docker run --rm --interactive --entrypoint node "$IMAGE" --input-type=module < scripts/smoke-docs-index-runtime.mjs 2>&1 | tee "$OUTPUT_DIR/docs-smoke.log"

- name: Load every hosted bundle with the production SDK
run: |
set -euo pipefail
docker run --rm --interactive --entrypoint node "$IMAGE" --input-type=module < scripts/smoke-hosted-compliance-runtime.mjs 2>&1 | tee "$OUTPUT_DIR/sdk-smoke.log"

- name: Retain footprint and smoke receipts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: default-runtime-footprint-${{ github.run_id }}
path: ${{ runner.temp }}/default-runtime-footprint
if-no-files-found: warn
retention-days: 7
15 changes: 12 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ COPY . .
# Build the TypeScript server (increase heap for large tsc compilation)
RUN NODE_OPTIONS=--max-old-space-size=4096 npm run build

# Historical releases remain immutable in git and in the public R2 artifact
# CDN. Fly still carries every compliance, docs, and protocol artifact, but its
# local schema tree only needs hosted-compliance pairs, current docs, aliases,
# and the training-agent checkpoint. Prune the builder copy so repository
# release records are never deleted or rewritten.
FROM builder AS runtime-artifacts
RUN node scripts/prune-runtime-artifacts.cjs \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MUST FIX: Pruning the 35 omitted schema versions from the Fly image changes the wire for pinned immutable URLs if the Node server still serves /schemas/. schemas-middleware.ts:462-493 does not 404 a missing pinned version — resolvePinnedFallback silently rewrites /schemas/3.0.5/... to the nearest lower kept version on the same major and serves different bytes with Cache-Control: public, no-cache, must-revalidate instead of the exact immutable bytes. On main the image carries all 86 versions, so these are exact hits. After this prune, every adopter who pinned one of the 35 omitted paths — exactly the population the immutability rule protects — gets silently substituted schema content. The PR body's "confirm routing before deploying" is procedural, not a guard. Fail-closed: do not deploy this image as the /schemas/ origin unless the public route is bound to the artifact-cdn worker (which carries all versions); the discovery surfaces already advertise only the kept subset, so the exposure is direct pinned requests.

&& DOCS_SMOKE_APP_ROOT=/app node scripts/smoke-docs-index-runtime.mjs \
&& HOSTED_COMPLIANCE_SMOKE_APP_ROOT=/app node scripts/smoke-hosted-compliance-runtime.mjs

# Pre-clone external repos in parallel, then strip .git metadata.
# Only markdown files are indexed at runtime so .git dirs are dead weight.
# IMPORTANT: Keep in sync with EXTERNAL_REPOS in server/src/addie/mcp/external-repos.ts
Expand Down Expand Up @@ -165,7 +175,7 @@ RUN npm ci --omit=dev --ignore-scripts \
# 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=runtime-artifacts /app/dist ./dist
COPY --from=builder /app/server/public ./server/public
COPY --from=builder /app/static ./static
COPY --from=builder /app/docs ./docs
Expand All @@ -191,8 +201,7 @@ RUN npm run verify:training-agent-runtime
COPY --from=repos /repos ./.addie-repos

# Fly unpacks images onto an 8 GiB root filesystem. Reserve space for the
# base image and runtime writes; fail the build before the release migration
# if growing artifacts or dependencies consume that headroom again.
# base image and runtime writes; preserve the current application ceiling.
RUN app_mib=$(du -sm /app | cut -f1) \
&& echo "Runtime application size: ${app_mib} MiB (limit: 7424 MiB)" \
&& test "$app_mib" -le 7424
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"test:release-workflow": "node tests/release-workflow-immutability.test.cjs && node --test tests/release-publication-ordering.test.cjs tests/maintenance-release-publication.test.cjs tests/changesets-fence.test.cjs tests/release-supersession.test.cjs tests/skip-withdrawn-release.test.cjs && node tests/promote-release-candidate.test.cjs && node tests/published-compliance-versions.test.cjs && node --test --test-timeout=30000 tests/cdn-artifacts.test.cjs tests/artifact-cdn-worker.test.cjs",
"promote:rc": "node scripts/promote-release-candidate.mjs",
"test:immutable-release-artifacts": "node tests/immutable-release-artifacts.test.cjs",
"test:runtime-artifacts": "node --test --test-force-exit --test-timeout=30000 tests/runtime-artifact-pruning.test.cjs",
"test:patch-3-0-compat-bundle": "node --test --test-force-exit --test-timeout=30000 tests/patch-3-0-compat-bundle.test.cjs",
"test:error-codes": "node scripts/lint-error-codes.cjs && npm run test:seller-governance-policy-surfaces && npm run test:seller-policy-decline-details",
"test:seller-policy-decline-details": "node --test --test-force-exit tests/seller-policy-decline-details.test.cjs",
Expand Down
Loading
Loading