diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index bd7b53e1238..c15a34085fa 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -90,7 +90,9 @@ jobs: "${{ github.event.pull_request.head.sha }}" - name: Validate Dockerfile deps stage - run: node ./scripts/check-docker-deps-stage.mjs + run: | + node ./scripts/check-docker-deps-stage.mjs + node "$CI_NODE_TEST" ./scripts/docker-deps-postinstall.test.mjs - name: Validate Node version policy run: pnpm check:node-version diff --git a/Dockerfile b/Dockerfile index 3736f1f4e82..4b87db9e82f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,10 @@ COPY packages/plugins/plugin-llm-wiki/package.json packages/plugins/plugin-llm-w COPY packages/plugins/plugin-workspace-diff/package.json packages/plugins/plugin-workspace-diff/ COPY patches/ patches/ COPY scripts/link-plugin-dev-sdk.mjs scripts/ +COPY packages/paperclip-runner/scripts/install-copilot-assets.mjs packages/paperclip-runner/scripts/materialize-copilot-binary.mjs packages/paperclip-runner/scripts/copilot-inner-distribution.mjs packages/paperclip-runner/scripts/ +COPY packages/paperclip-runner/src/drivers/acpx/copilot-installation.ts packages/paperclip-runner/src/drivers/acpx/ +COPY server/scripts/install-copilot-assets.mjs server/scripts/ +COPY server/src/services/copilot-connection-probe.ts server/src/services/ # CI owns repository lock updates. Resolve manifest changes in the image's # private build tree, then install from that exact generated lock. diff --git a/scripts/docker-deps-postinstall.test.mjs b/scripts/docker-deps-postinstall.test.mjs new file mode 100644 index 00000000000..fc048a3e715 --- /dev/null +++ b/scripts/docker-deps-postinstall.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { cp, mkdtemp, mkdir, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; + +const repo = fileURLToPath(new URL("../", import.meta.url)); + +test("dependency-layer inputs can run both Copilot postinstall entrypoints before compilation", async () => { + const dockerfile = await readFile(path.join(repo, "Dockerfile"), "utf8"); + const stage = dockerfile.split(/^FROM .* AS deps\s*$/im)[1]?.split(/^FROM /im)[0]; + assert.ok(stage, "dependency stage exists"); + const beforeInstall = stage.split(/^RUN pnpm install/m)[0]; + const fixture = await mkdtemp(path.join(tmpdir(), "paperclip-docker-postinstall-")); + try { + for (const line of beforeInstall.split("\n")) { + if (!line.startsWith("COPY ") || line.includes("--")) continue; + const args = line.trim().split(/\s+/).slice(1); + const destination = args.pop(); + for (const source of args) { + if (source !== "pnpm-workspace.yaml" + && !source.startsWith("packages/paperclip-runner/") + && !source.startsWith("server/")) continue; + assert.ok(destination.endsWith("/"), "fixture supports directory COPY destinations"); + const target = path.join(fixture, destination, path.basename(source)); + await mkdir(path.dirname(target), { recursive: true }); + await cp(path.join(repo, source), target, { recursive: true }); + } + } + for (const owner of ["packages/paperclip-runner", "server"]) { + const cwd = path.join(fixture, owner); + const manifest = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8")); + assert.equal(manifest.scripts.postinstall, "node scripts/install-copilot-assets.mjs"); + const output = execFileSync(process.execPath, ["scripts/install-copilot-assets.mjs"], { + cwd, encoding: "utf8", timeout: 10_000, + }); + assert.match(output, /Copilot runtime assets: (source-workspace-pending-build|unsupported-platform)/); + await assert.rejects(readFile(path.join(cwd, "provider-assets/copilot")), { code: "ENOENT" }); + } + } finally { + await rm(fixture, { recursive: true, force: true }); + } +});