Fix Docker dependency installation for Copilot lifecycle hooks (#16983)

## Thinking Path

> - Paperclip manages AI agents and their work.
> - The standard Docker image contains the server and Runner.
> - Its dependency stage installs packages before copying application
source.
> - New Copilot postinstall hooks need files absent from that stage.
> - Both image architectures stop before compilation.
> - This change supplies the lifecycle inputs and tests both
entrypoints.

## Linked Issues or Issue Description

**What happened?**

Master Docker builds fail with a missing
`packages/paperclip-runner/scripts/install-copilot-assets.mjs`. The
server postinstall also needs its bootstrap script and source marker.
See the failed public [Docker
build](https://github.com/paperclipai/paperclip/actions/runs/39450988012).

**Expected behavior**

Dependency installation recognizes the existing source workspace. The
later build compiles and verifies provider assets through the existing
path.

**Steps to reproduce**

1. Build the standard Docker image from master.
2. Reach `pnpm install` in the `deps` stage.
3. Observe the missing lifecycle-script error before compilation.

**Paperclip version or commit**

Reproduced in the Docker build for
`24758b9da129a5e424fb7ba6b423d748ce23f3bc`. This PR starts at
`33cc567f75ae8a3fecf4442d20772e62944f1552`.

**Deployment mode**

Docker, Linux amd64 and arm64. No database is involved.

Searched open PRs and issues for Docker, Copilot, installer and the
exact missing script. No matching fix was found.

## What Changed

- Copy the Runner installer and its two helper modules into the
dependency layer.
- Copy the server installer and both existing source-workspace markers.
- Execute both real postinstall entrypoints in a temporary fixture
populated from the relevant Docker COPY inputs.
- Run the fixture in the existing trusted PR dependency-stage check.

## Verification

- `node --test scripts/docker-deps-postinstall.test.mjs` passed.
- The same test fails with the original Dockerfile because the installer
is missing. The fixed Dockerfile was restored and the test passed again.
- `node scripts/check-docker-deps-stage.mjs` passed.
- The fixture passed through the existing timing wrapper in default and
`PAPERCLIP_CI_PROFILE=true` modes. The latter wrote test timing records.
- `pnpm install --frozen-lockfile` passed.
- All three `node --test
packages/paperclip-runner/scripts/install-copilot-assets.test.mjs` cases
passed. These test fresh Runner/server assets, source bootstrap and
published-package failure.
- Full workspace `pnpm -r typecheck` and `pnpm build` passed. The full
local test command is running. All 55 checks passed on exact head
`8ab57339cb9a0dcefa3ae9c2c0f92dc4f6210a23`, with one neutral security
scan and four conditional skips. This includes full server, browser,
Runner, typecheck, build, package-smoke and dependency-stage checks.
Apex reviewed the same head at 5/5 with its prior timing finding
resolved. No review thread remains unresolved.
- The local Docker daemon does not respond to an info probe. A complete
local image build has not been run. The post-merge canonical image build
must pass before any deployment uses this change.

## Risks

The dependency cache now includes these small lifecycle inputs. Source
edits to the two marker files invalidate that cache. The existing
source-workspace detection and compiled asset verification remain in
place. This does not add an installer bypass, change provider pins or
alter runtime behavior. No schema change is included.

## Model Used

OpenAI GPT-6 through Codex, with source inspection, code execution and
GitHub tools. The exact served model ID and context-window size are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run focused tests locally and recorded the results above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-20 09:46:48 -07:00
1 parent 2c96f76780
commit 6b804bfeba
3 files changed
+52 -1

No files matched your search

+3 -1
View File
@@ -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
+4
View File
@@ -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.
+45
View File
@@ -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 });
}
});