diff --git a/.env.example b/.env.example index 916bc1e40..b457c162a 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,12 @@ # your shell before running docker compose (do NOT put it in .env, which # worktree-env regenerates per-worktree): # export NPM_CONFIG_REGISTRY=https://packagefeedproxy.microsoft.io/npm/ +# +# NPM_CONFIG_REGISTRY configures ordinary npm/pnpm package resolution only. It +# is not automatically reused for Corepack's per-version metadata requests. +# If your network provides a separate Corepack-compatible metadata endpoint, +# opt into it explicitly before the same one-command build: +# export COREPACK_NPM_REGISTRY=https://registry.npmjs.org/ # --------------------------------------------------------------------------- # CLI Configuration diff --git a/ENV_VARIABLES.md b/ENV_VARIABLES.md index bc66a4316..63f72ee4b 100644 --- a/ENV_VARIABLES.md +++ b/ENV_VARIABLES.md @@ -16,6 +16,48 @@ derives a localhost URL from `SCOPE_API_PORT` or uses `SCOPE_DEFAULT_API_URL`. ## Docker Development +### NPM_CONFIG_REGISTRY +**Default:** host npm registry for `pnpm docker:*`; otherwise `https://registry.npmjs.org/` +**Type:** HTTP(S) URL + +Registry used for ordinary npm and pnpm package installation in Docker image +builds. `scripts/dev-compose.sh` reads the host setting with +`npm config get registry`, so a private package proxy remains a one-command +workflow: + +```bash +NPM_CONFIG_REGISTRY=https://packages.example.test/npm/ pnpm docker:dev:copilot +``` + +Trailing slashes are normalized during the pinned pnpm bootstrap. Registry URLs +may point at authenticated/private package services, but credentials must be +provided through the environment's standard npm authentication mechanism rather +than embedded in the URL or build args. Credential-bearing URLs are rejected, +and bootstrap diagnostics report only the endpoint origin. + +This setting is deliberately **not** copied to `COREPACK_NPM_REGISTRY`: package +proxies that serve npm packuments are not necessarily compatible with +Corepack's per-version metadata protocol. + +### COREPACK_NPM_REGISTRY +**Default:** unset (Corepack's public default) +**Type:** optional HTTP(S) URL + +Corepack-compatible metadata endpoint used only to bootstrap the pinned pnpm +version in Docker images. Set it explicitly only when the endpoint supports +Corepack metadata: + +```bash +COREPACK_NPM_REGISTRY=https://corepack.example.test/ \ + NPM_CONFIG_REGISTRY=https://packages.example.test/npm/ \ + pnpm docker:dev:copilot +``` + +If Corepack fails, the pinned npm fallback uses `NPM_CONFIG_REGISTRY` and clears +the Corepack override, keeping the two paths in separate failure domains. Normal +`pnpm install` keeps pnpm's built-in package-fetch retries; Scope does not wrap +it in an outer retry loop. + ### API_DEBUG_PORT **Default:** `9200` **Type:** integer (Docker Compose development only) diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 874efc590..50a1c0738 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim AS builder ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -16,7 +13,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/db-migrations/package.json ./packages/db-migrations/ COPY apps/api/package.json ./apps/api/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -47,12 +44,9 @@ CMD ["/app/scripts/dev-entrypoint.sh"] FROM node:22.22.0-slim ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -61,7 +55,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/db-migrations/package.json ./packages/db-migrations/ COPY apps/api/package.json ./apps/api/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/judge/Dockerfile b/apps/judge/Dockerfile index cb8782c92..c0190b043 100644 --- a/apps/judge/Dockerfile +++ b/apps/judge/Dockerfile @@ -7,12 +7,9 @@ FROM node:22.22.0-slim AS builder # "fetch failed" / "No CA certificates were loaded from the system". RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -21,7 +18,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/judge/package.json ./apps/judge/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -47,12 +44,9 @@ FROM node:22.22.0-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -60,7 +54,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/judge/package.json ./apps/judge/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/model-scanners/anthropic/Dockerfile b/apps/model-scanners/anthropic/Dockerfile index 8293f780e..49ffb9078 100644 --- a/apps/model-scanners/anthropic/Dockerfile +++ b/apps/model-scanners/anthropic/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim AS builder ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -16,7 +13,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/model-scanning/package.json ./packages/model-scanning/ COPY apps/model-scanners/anthropic/package.json ./apps/model-scanners/anthropic/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -34,12 +31,9 @@ FROM node:22.22.0-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -48,7 +42,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/model-scanning/package.json ./packages/model-scanning/ COPY apps/model-scanners/anthropic/package.json ./apps/model-scanners/anthropic/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/model-scanners/copilot/Dockerfile b/apps/model-scanners/copilot/Dockerfile index 9741a758e..23666be44 100644 --- a/apps/model-scanners/copilot/Dockerfile +++ b/apps/model-scanners/copilot/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim AS builder ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -16,7 +13,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/model-scanning/package.json ./packages/model-scanning/ COPY apps/model-scanners/copilot/package.json ./apps/model-scanners/copilot/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -34,12 +31,9 @@ FROM node:22.22.0-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -48,7 +42,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/model-scanning/package.json ./packages/model-scanning/ COPY apps/model-scanners/copilot/package.json ./apps/model-scanners/copilot/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/portal/Dockerfile b/apps/portal/Dockerfile index 35e8d91f8..5b09f1909 100644 --- a/apps/portal/Dockerfile +++ b/apps/portal/Dockerfile @@ -1,16 +1,13 @@ # Shared Portal dependency layer FROM node:20-alpine AS deps ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ COPY apps/portal/package.json apps/portal/ -RUN pnpm install --frozen-lockfile --filter portal --prod=false +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --filter portal --prod=false # --- Dev stage: Vite dev server with HMR --- # Built from `deps` (not `builder`) so the dev image skips the production build. diff --git a/apps/scheduler/Dockerfile b/apps/scheduler/Dockerfile index 75db754fc..840c1bcf9 100644 --- a/apps/scheduler/Dockerfile +++ b/apps/scheduler/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim AS builder ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -15,7 +12,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/scheduler/package.json ./apps/scheduler/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -43,12 +40,9 @@ CMD ["/app/scripts/dev-entrypoint.sh"] FROM node:22.22.0-slim ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -56,7 +50,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/scheduler/package.json ./apps/scheduler/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/token-manager/Dockerfile b/apps/token-manager/Dockerfile index 664b026cc..23dee1a7e 100644 --- a/apps/token-manager/Dockerfile +++ b/apps/token-manager/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim AS builder ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -15,7 +12,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/token-manager/package.json ./apps/token-manager/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -40,12 +37,9 @@ FROM node:22.22.0-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -53,7 +47,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/token-manager/package.json ./apps/token-manager/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/workers/coder-acp-claude-code/Dockerfile b/apps/workers/coder-acp-claude-code/Dockerfile index 8639802bf..12d66efb9 100644 --- a/apps/workers/coder-acp-claude-code/Dockerfile +++ b/apps/workers/coder-acp-claude-code/Dockerfile @@ -2,12 +2,10 @@ FROM node:22.22.0-slim AS base ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +ENV npm_config_registry=${NPM_CONFIG_REGISTRY} +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm # Install system tools for coding agents: Python, uv, git, PowerShell, Go, .NET, Rust, Java, Maven, Gradle RUN apt-get update && apt-get install -y \ python3 \ @@ -89,7 +87,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/test-utils/package.json ./packages/test-utils/ COPY apps/workers/coder-acp-claude-code/package.json ./apps/workers/coder-acp-claude-code/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -110,7 +108,7 @@ RUN node -e "const{parse}=require(require.resolve('yaml',{paths:['/app/packages/ FROM builder AS dev ARG CLAUDE_CODE_ACP_VERSION RUN test -n "$CLAUDE_CODE_ACP_VERSION" || (echo 'CLAUDE_CODE_ACP_VERSION build-arg is required' && exit 1) -RUN npm install -g @agentclientprotocol/claude-agent-acp@${CLAUDE_CODE_ACP_VERSION} +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" npm install -g @agentclientprotocol/claude-agent-acp@${CLAUDE_CODE_ACP_VERSION} # Use existing node user (UID 1000) so claude-code-acp allows bypassPermissions mode RUN mkdir -p /workspace && chown node:node /workspace @@ -131,7 +129,7 @@ FROM base # Install Claude Code ACP globally ARG CLAUDE_CODE_ACP_VERSION RUN test -n "$CLAUDE_CODE_ACP_VERSION" || (echo 'CLAUDE_CODE_ACP_VERSION build-arg is required' && exit 1) -RUN npm install -g @agentclientprotocol/claude-agent-acp@${CLAUDE_CODE_ACP_VERSION} +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" npm install -g @agentclientprotocol/claude-agent-acp@${CLAUDE_CODE_ACP_VERSION} WORKDIR /app @@ -140,7 +138,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/workers/coder-acp-claude-code/package.json ./apps/workers/coder-acp-claude-code/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/workers/coder-acp-copilot/Dockerfile b/apps/workers/coder-acp-copilot/Dockerfile index ec8f708d5..251b8a23c 100644 --- a/apps/workers/coder-acp-copilot/Dockerfile +++ b/apps/workers/coder-acp-copilot/Dockerfile @@ -4,12 +4,10 @@ ARG COPILOT_CLI_VERSION FROM node:24-slim AS base ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +ENV npm_config_registry=${NPM_CONFIG_REGISTRY} +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm # Install system tools for coding agents: Python, uv, git, PowerShell, Go, .NET, Rust, Java, Maven, Gradle RUN apt-get update && apt-get install -y \ python3 \ @@ -84,7 +82,7 @@ COPY packages/telemetry/package.json ./packages/telemetry/ COPY packages/test-utils/package.json ./packages/test-utils/ COPY apps/workers/coder-acp-copilot/package.json ./apps/workers/coder-acp-copilot/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -104,7 +102,7 @@ RUN node -e "const{parse}=require(require.resolve('yaml',{paths:['/app/packages/ # --- Dev stage: hot reload with tsx watch --- FROM builder AS dev ARG COPILOT_CLI_VERSION -RUN npm install -g @github/copilot@${COPILOT_CLI_VERSION} +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" npm install -g @github/copilot@${COPILOT_CLI_VERSION} # Use existing node user (UID 1000) so coding agents allow bypassPermissions mode RUN mkdir -p /workspace && chown node:node /workspace @@ -125,7 +123,7 @@ FROM base # Install GitHub Copilot CLI globally ARG COPILOT_CLI_VERSION -RUN npm install -g @github/copilot@${COPILOT_CLI_VERSION} +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" npm install -g @github/copilot@${COPILOT_CLI_VERSION} WORKDIR /app @@ -134,7 +132,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/workers/coder-acp-copilot/package.json ./apps/workers/coder-acp-copilot/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/workers/post-processor/Dockerfile b/apps/workers/post-processor/Dockerfile index f339e9b81..0f789c8f9 100644 --- a/apps/workers/post-processor/Dockerfile +++ b/apps/workers/post-processor/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim AS builder ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -15,7 +12,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/workers/post-processor/package.json ./apps/workers/post-processor/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -40,12 +37,9 @@ CMD ["/app/scripts/dev-entrypoint.sh"] FROM node:22.22.0-slim ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -53,7 +47,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/workers/post-processor/package.json ./apps/workers/post-processor/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/apps/workers/report-generator/Dockerfile b/apps/workers/report-generator/Dockerfile index 8ff340526..ff298d7d8 100644 --- a/apps/workers/report-generator/Dockerfile +++ b/apps/workers/report-generator/Dockerfile @@ -6,12 +6,9 @@ FROM node:22.22.0-slim AS builder # opaque "fetch failed" / "No CA certificates were loaded from the system". RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -20,7 +17,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/workers/report-generator/package.json ./apps/workers/report-generator/ -RUN pnpm install --frozen-lockfile +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ @@ -46,12 +43,9 @@ FROM node:22.22.0-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ @@ -59,7 +53,7 @@ COPY packages/shared/package.json ./packages/shared/ COPY packages/telemetry/package.json ./packages/telemetry/ COPY apps/workers/report-generator/package.json ./apps/workers/report-generator/ -RUN pnpm install --frozen-lockfile --prod +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ COPY --from=builder /app/packages/telemetry/dist ./packages/telemetry/dist/ diff --git a/docker-compose.yml b/docker-compose.yml index bf65dc69c..0d72c6aad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,13 +50,15 @@ x-worker-deps: &worker-deps judge: condition: service_started -# npm/pnpm registry for the image builds. Defaults to the public registry, so +# npm/pnpm registry for package installation. Defaults to the public registry, so # external contributors and CI need no configuration. scripts/dev-compose.sh # auto-detects the host's registry (npm config get registry, which reads ~/.npmrc) # and forwards it here, so engineers behind a private proxy (e.g. Microsoft's npm -# proxy) build with no setup. Export NPM_CONFIG_REGISTRY to override the detected value. +# proxy) build with no setup. Corepack metadata resolution stays on its default +# registry unless COREPACK_NPM_REGISTRY explicitly selects a compatible endpoint. x-npm-build-args: &npm-build-args NPM_CONFIG_REGISTRY: ${NPM_CONFIG_REGISTRY:-https://registry.npmjs.org/} + COREPACK_NPM_REGISTRY: ${COREPACK_NPM_REGISTRY:-} services: # =========================================================================== diff --git a/packages/db-migrations/Dockerfile b/packages/db-migrations/Dockerfile index 25bda03c9..dbbb3fcf5 100644 --- a/packages/db-migrations/Dockerfile +++ b/packages/db-migrations/Dockerfile @@ -1,12 +1,9 @@ FROM node:22.22.0-slim ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ -ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ - COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} -# corepack bootstraps pnpm from the registry's per-version metadata endpoint, -# which some private registry proxies don't serve; fall back to a pinned global -# install (honors npm_config_registry) so restricted networks can still build. -RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) +ARG COREPACK_NPM_REGISTRY= +COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" COREPACK_NPM_REGISTRY="${COREPACK_NPM_REGISTRY}" sh /usr/local/bin/bootstrap-pnpm WORKDIR /app # Copy workspace root files @@ -15,7 +12,7 @@ COPY tsconfig.base.json ./ COPY packages/db-migrations/package.json ./packages/db-migrations/ # Install dependencies (production + dev for tsx) -RUN pnpm install --frozen-lockfile --filter db-migrations +RUN NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}" pnpm install --frozen-lockfile --filter db-migrations # Copy migration source files COPY packages/db-migrations/tsconfig.json ./packages/db-migrations/ diff --git a/scripts/bootstrap-pnpm.sh b/scripts/bootstrap-pnpm.sh new file mode 100644 index 000000000..c523e2cc3 --- /dev/null +++ b/scripts/bootstrap-pnpm.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +set -eu + +PNPM_VERSION=10.29.1 +DEFAULT_NPM_REGISTRY=https://registry.npmjs.org + +# Normalize only syntax that is safe for both npm and Corepack while keeping the +# complete URL out of diagnostics because it may contain authentication data. +normalize_registry() { + registry=$1 + setting_name=$2 + + case "$registry" in + http://*|https://*) ;; + *) + printf 'Invalid %s: expected an HTTP(S) URL.\n' "$setting_name" >&2 + return 1 + ;; + esac + + while [ "${registry%/}" != "$registry" ]; do + registry=${registry%/} + done + + REGISTRY_URL=$registry node --input-type=module -e ' + const url = new URL(process.env.REGISTRY_URL); + if ( + (url.protocol !== "http:" && url.protocol !== "https:") || + url.username || + url.password || + url.search || + url.hash + ) { + process.exit(1); + } + ' >/dev/null 2>&1 || { + printf 'Invalid %s: expected a credential-free HTTP(S) URL.\n' "$setting_name" >&2 + return 1 + } + + printf '%s\n' "$registry" +} + +# Report only the origin so private endpoint paths stay out of build output. +registry_origin() { + REGISTRY_URL=$1 node --input-type=module -e ' + const url = new URL(process.env.REGISTRY_URL); + process.stdout.write(url.origin); + ' +} + +npm_registry=$(normalize_registry "${NPM_CONFIG_REGISTRY:-$DEFAULT_NPM_REGISTRY}" NPM_CONFIG_REGISTRY) +npm_registry_origin=$(registry_origin "$npm_registry") +export NPM_CONFIG_REGISTRY=$npm_registry +export npm_config_registry=$npm_registry + +if [ -n "${COREPACK_NPM_REGISTRY:-}" ]; then + corepack_registry=$(normalize_registry "$COREPACK_NPM_REGISTRY" COREPACK_NPM_REGISTRY) + corepack_registry_origin=$(registry_origin "$corepack_registry") + export COREPACK_NPM_REGISTRY=$corepack_registry + printf 'Bootstrapping pnpm %s with explicit Corepack metadata registry origin %s.\n' \ + "$PNPM_VERSION" "$corepack_registry_origin" +else + unset COREPACK_NPM_REGISTRY + printf 'Bootstrapping pnpm %s with Corepack default registry; package registry origin is %s.\n' \ + "$PNPM_VERSION" "$npm_registry_origin" +fi + +corepack enable +if corepack prepare "pnpm@$PNPM_VERSION" --activate; then + exit 0 +fi + +# npm intentionally receives only the ordinary package registry. An explicit +# Corepack metadata override must not share the fallback's failure domain. +printf 'Corepack bootstrap failed; installing pinned pnpm %s from npm package registry origin %s.\n' \ + "$PNPM_VERSION" "$npm_registry_origin" >&2 +COREPACK_NPM_REGISTRY= npm install --global "pnpm@$PNPM_VERSION" --force diff --git a/scripts/bootstrap-pnpm.test.ts b/scripts/bootstrap-pnpm.test.ts new file mode 100644 index 000000000..f0307618f --- /dev/null +++ b/scripts/bootstrap-pnpm.test.ts @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { spawnSync } from "node:child_process"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +const script = resolve("scripts/bootstrap-pnpm.sh"); +const temporaryDirectories: string[] = []; +const nodeDockerfiles = [ + "apps/api/Dockerfile", + "apps/judge/Dockerfile", + "apps/model-scanners/anthropic/Dockerfile", + "apps/model-scanners/copilot/Dockerfile", + "apps/portal/Dockerfile", + "apps/scheduler/Dockerfile", + "apps/token-manager/Dockerfile", + "apps/workers/coder-acp-claude-code/Dockerfile", + "apps/workers/coder-acp-copilot/Dockerfile", + "apps/workers/post-processor/Dockerfile", + "apps/workers/report-generator/Dockerfile", + "packages/db-migrations/Dockerfile", +]; +const runtimeRegistryDockerfiles = new Set([ + "apps/workers/coder-acp-claude-code/Dockerfile", + "apps/workers/coder-acp-copilot/Dockerfile", +]); + +interface BootstrapFixture { + callsFile: string; + env: NodeJS.ProcessEnv; +} + +/** + * Creates fake Corepack and npm executables that capture registry configuration. + */ +function createFixture(corepackPrepareExitCode = 0): BootstrapFixture { + const directory = mkdtempSync(join(tmpdir(), "scope-bootstrap-pnpm-")); + temporaryDirectories.push(directory); + const bin = join(directory, "bin"); + const callsFile = join(directory, "calls"); + mkdirSync(bin); + writeFileSync(callsFile, ""); + + const corepack = join(bin, "corepack"); + writeFileSync( + corepack, + `#!/bin/sh +set -eu +printf 'corepack|%s|%s|%s\\n' "$*" "\${NPM_CONFIG_REGISTRY:-}" "\${COREPACK_NPM_REGISTRY:-}" >> "$BOOTSTRAP_CALLS_FILE" +if [ "\${1:-}" = "prepare" ]; then + exit "$COREPACK_PREPARE_EXIT_CODE" +fi +`, + ); + chmodSync(corepack, 0o755); + + const npm = join(bin, "npm"); + writeFileSync( + npm, + `#!/bin/sh +set -eu +printf 'npm|%s|%s|%s\\n' "$*" "\${NPM_CONFIG_REGISTRY:-}" "\${COREPACK_NPM_REGISTRY:-}" >> "$BOOTSTRAP_CALLS_FILE" +`, + ); + chmodSync(npm, 0o755); + + return { + callsFile, + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH ?? ""}`, + BOOTSTRAP_CALLS_FILE: callsFile, + COREPACK_PREPARE_EXIT_CODE: String(corepackPrepareExitCode), + }, + }; +} + +/** + * Runs the bootstrap script with deterministic fake package-manager commands. + */ +function runBootstrap( + fixture: BootstrapFixture, + registryEnvironment: NodeJS.ProcessEnv = {}, +) { + return spawnSync("sh", [script], { + encoding: "utf8", + env: { + ...fixture.env, + NPM_CONFIG_REGISTRY: "", + COREPACK_NPM_REGISTRY: "", + ...registryEnvironment, + }, + }); +} + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +describe("pnpm Docker bootstrap", () => { + it("uses public npm defaults without forcing a Corepack registry", () => { + const fixture = createFixture(); + + const result = runBootstrap(fixture); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(fixture.callsFile, "utf8")).toContain( + "corepack|prepare pnpm@10.29.1 --activate|https://registry.npmjs.org|", + ); + }); + + it("keeps an ordinary package proxy out of Corepack metadata resolution", () => { + const fixture = createFixture(); + + const result = runBootstrap(fixture, { + NPM_CONFIG_REGISTRY: "https://packages.example.test/npm", + }); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(fixture.callsFile, "utf8")).toContain( + "corepack|prepare pnpm@10.29.1 --activate|https://packages.example.test/npm|", + ); + }); + + it("normalizes trailing slashes for both registry roles", () => { + const fixture = createFixture(); + + const result = runBootstrap(fixture, { + NPM_CONFIG_REGISTRY: "https://packages.example.test/npm///", + COREPACK_NPM_REGISTRY: "https://corepack.example.test/metadata//", + }); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(fixture.callsFile, "utf8")).toContain( + "corepack|prepare pnpm@10.29.1 --activate|https://packages.example.test/npm|https://corepack.example.test/metadata", + ); + }); + + it("uses an explicit Corepack-compatible metadata registry", () => { + const fixture = createFixture(); + + const result = runBootstrap(fixture, { + COREPACK_NPM_REGISTRY: "https://corepack.example.test", + }); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(fixture.callsFile, "utf8")).toContain( + "corepack|prepare pnpm@10.29.1 --activate|https://registry.npmjs.org|https://corepack.example.test", + ); + }); + + it("keeps the npm fallback independent from the Corepack override", () => { + const fixture = createFixture(1); + + const result = runBootstrap(fixture, { + NPM_CONFIG_REGISTRY: "https://packages.example.test/npm/", + COREPACK_NPM_REGISTRY: "https://broken-corepack.example.test/", + }); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(fixture.callsFile, "utf8")).toContain( + "npm|install --global pnpm@10.29.1 --force|https://packages.example.test/npm|", + ); + }); + + it("rejects secret-bearing URLs without printing their contents", () => { + const npmFixture = createFixture(); + const secretNpmRegistry = + "https://build-user:super-secret@packages.example.test/private/token-123/"; + + const npmResult = runBootstrap(npmFixture, { + NPM_CONFIG_REGISTRY: secretNpmRegistry, + }); + + expect(npmResult.status).toBe(1); + const npmDiagnostics = `${npmResult.stdout}\n${npmResult.stderr}`; + expect(npmDiagnostics).toContain("credential-free HTTP(S) URL"); + expect(npmDiagnostics).not.toContain("build-user"); + expect(npmDiagnostics).not.toContain("super-secret"); + expect(npmDiagnostics).not.toContain("token-123"); + + const corepackFixture = createFixture(); + const corepackResult = runBootstrap(corepackFixture, { + COREPACK_NPM_REGISTRY: + "https://corepack-user:corepack-secret@corepack.example.test/private/", + }); + + expect(corepackResult.status).toBe(1); + const diagnostics = `${corepackResult.stdout}\n${corepackResult.stderr}`; + expect(diagnostics).toContain("credential-free HTTP(S) URL"); + expect(diagnostics).not.toContain("corepack-user"); + expect(diagnostics).not.toContain("corepack-secret"); + }); + + it("uses the shared bootstrap contract in every Node image", () => { + for (const dockerfile of nodeDockerfiles) { + const contents = readFileSync(dockerfile, "utf8"); + const packageInstallLines = contents + .split("\n") + .filter((line) => /RUN .*p?npm install/.test(line)); + + expect(contents, dockerfile).toContain( + "COPY scripts/bootstrap-pnpm.sh /usr/local/bin/bootstrap-pnpm", + ); + expect(contents, dockerfile).toContain("ARG COREPACK_NPM_REGISTRY="); + expect(contents, dockerfile).not.toContain( + "COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY}", + ); + expect(contents, dockerfile).not.toMatch( + /^ENV .*COREPACK_NPM_REGISTRY/m, + ); + if (runtimeRegistryDockerfiles.has(dockerfile)) { + expect(contents, dockerfile).toContain( + "ENV npm_config_registry=${NPM_CONFIG_REGISTRY}", + ); + } else { + expect(contents, dockerfile).not.toMatch(/^ENV .*npm_config_registry/m); + } + expect(contents, dockerfile).not.toMatch( + /fetch[-_]retr(?:y|ies)|network-concurrency/i, + ); + expect(packageInstallLines.length, dockerfile).toBeGreaterThan(0); + for (const line of packageInstallLines) { + expect(line, dockerfile).toContain( + 'NPM_CONFIG_REGISTRY="${NPM_CONFIG_REGISTRY}"', + ); + } + } + }); +}); diff --git a/scripts/dev-compose.sh b/scripts/dev-compose.sh index cbaeaf8e6..abf3e53e4 100755 --- a/scripts/dev-compose.sh +++ b/scripts/dev-compose.sh @@ -19,7 +19,7 @@ for p in /usr/local/bin /opt/homebrew/bin "$HOME/.docker/bin"; do done # --------------------------------------------------------------------------- -# Forward the host's npm registry into the image builds. +# Forward package and package-manager bootstrap registries into image builds. # --------------------------------------------------------------------------- # docker-compose.yml passes ${NPM_CONFIG_REGISTRY} to every image build as a # build arg (default: the public registry). Detect the registry configured on @@ -27,6 +27,10 @@ done # cannot reach registry.npmjs.org directly — e.g. behind Microsoft's npm proxy — # build out of the box. External contributors detect the public registry and are # unaffected. An explicitly exported NPM_CONFIG_REGISTRY always wins. +# +# COREPACK_NPM_REGISTRY is intentionally not inferred from the package registry: +# only forward it when the user explicitly selected a Corepack-compatible +# metadata endpoint. if [ -z "${NPM_CONFIG_REGISTRY:-}" ]; then detected_registry="$(npm config get registry 2>/dev/null || pnpm config get registry 2>/dev/null || true)" case "$detected_registry" in diff --git a/scripts/docker-compose.test.ts b/scripts/docker-compose.test.ts index 096b29c5f..034de36d5 100644 --- a/scripts/docker-compose.test.ts +++ b/scripts/docker-compose.test.ts @@ -5,6 +5,9 @@ import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; interface ComposeService { + build?: { + args?: Record; + }; cpus?: number; depends_on?: Record; healthcheck?: { @@ -49,16 +52,30 @@ describe("development Docker Compose configuration", () => { ], { encoding: "utf8", - env: { ...process.env, GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "" }, + env: { + ...process.env, + GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "", + NPM_CONFIG_REGISTRY: "https://packages.example.test/npm/", + COREPACK_NPM_REGISTRY: "https://corepack.example.test/", + }, }, ); expect(result.status, result.stderr).toBe(0); const config = JSON.parse(result.stdout) as ComposeConfig; + const api = config.services.api; const gateway = config.services.gateway; const copilotWorker = config.services["coder-acp-copilot"]; + expect(api.build?.args).toMatchObject({ + NPM_CONFIG_REGISTRY: "https://packages.example.test/npm/", + COREPACK_NPM_REGISTRY: "https://corepack.example.test/", + }); + expect(copilotWorker.build?.args).toMatchObject({ + NPM_CONFIG_REGISTRY: "https://packages.example.test/npm/", + COREPACK_NPM_REGISTRY: "https://corepack.example.test/", + }); expect(gateway.cpus).toBeUndefined(); expect(gateway.mem_limit).toBe(String(2 * 1024 ** 3)); expect(gateway.healthcheck?.start_period).toBe("10m0s");