Skip to content

Commit 2483387

Browse files
committed
Merge remote-tracking branch 'upstream/master' into update/public-master
# Conflicts: # server/src/__tests__/auth-session-route.test.ts # server/src/middleware/auth.ts # server/src/types/express.d.ts
2 parents cd58607 + d6bee62 commit 2483387

51 files changed

Lines changed: 3070 additions & 119 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ permissions:
1414
jobs:
1515
build-and-push:
1616
runs-on: ubuntu-latest
17-
timeout-minutes: 30
17+
timeout-minutes: 60
1818
concurrency:
1919
group: docker-${{ github.ref }}
2020
cancel-in-progress: true

.github/workflows/pr.yml

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ jobs:
4545
- name: Validate Dockerfile deps stage
4646
run: node ./scripts/check-docker-deps-stage.mjs
4747

48+
- name: Validate release package manifest
49+
run: node ./scripts/release-package-map.mjs check
50+
51+
- name: Verify release package bootstrap for changed manifests
52+
run: |
53+
mapfile -t changed_paths < <(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")
54+
PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA="${{ github.event.pull_request.base.sha }}" \
55+
node ./scripts/check-release-package-bootstrap.mjs "${changed_paths[@]}"
56+
4857
- name: Validate dependency resolution when manifests change
4958
run: |
5059
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
@@ -76,19 +85,88 @@ jobs:
7685
- name: Install dependencies
7786
run: pnpm install --frozen-lockfile
7887

79-
- name: Typecheck
80-
run: pnpm -r typecheck
88+
- name: Typecheck workspaces whose build scripts skip TypeScript
89+
run: pnpm run typecheck:build-gaps
8190

82-
- name: Run tests
83-
run: pnpm test:run
91+
- name: Run general test suites
92+
run: pnpm test:run:general
8493

8594
- name: Verify release registry test coverage
8695
run: pnpm run test:release-registry
8796

8897
- name: Build
8998
run: pnpm build
9099

91-
- name: Release canary dry run
100+
verify_serialized_server:
101+
name: Verify serialized server suites (${{ matrix.shard_label }})
102+
needs: [policy]
103+
runs-on: ubuntu-latest
104+
timeout-minutes: 20
105+
strategy:
106+
fail-fast: false
107+
matrix:
108+
include:
109+
- shard_index: 0
110+
shard_count: 4
111+
shard_label: 1/4
112+
- shard_index: 1
113+
shard_count: 4
114+
shard_label: 2/4
115+
- shard_index: 2
116+
shard_count: 4
117+
shard_label: 3/4
118+
- shard_index: 3
119+
shard_count: 4
120+
shard_label: 4/4
121+
122+
steps:
123+
- name: Checkout repository
124+
uses: actions/checkout@v4
125+
126+
- name: Setup pnpm
127+
uses: pnpm/action-setup@v4
128+
with:
129+
version: 9.15.4
130+
131+
- name: Setup Node.js
132+
uses: actions/setup-node@v4
133+
with:
134+
node-version: 24
135+
cache: pnpm
136+
137+
- name: Install dependencies
138+
run: pnpm install --frozen-lockfile
139+
140+
- name: Run serialized server test shard
141+
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
142+
143+
canary_dry_run:
144+
name: Canary Dry Run
145+
needs: [policy]
146+
runs-on: ubuntu-latest
147+
timeout-minutes: 20
148+
149+
steps:
150+
- name: Checkout repository
151+
uses: actions/checkout@v4
152+
153+
- name: Setup pnpm
154+
uses: pnpm/action-setup@v4
155+
with:
156+
version: 9.15.4
157+
158+
- name: Setup Node.js
159+
uses: actions/setup-node@v4
160+
with:
161+
node-version: 24
162+
cache: pnpm
163+
164+
- name: Install dependencies
165+
run: pnpm install --frozen-lockfile
166+
167+
# `release.sh` always executes its Step 2/7 workspace build, even when
168+
# `--skip-verify` bypasses the initial verification gate.
169+
- name: Release canary dry run via release.sh internal build
92170
run: |
93171
git checkout -B master HEAD
94172
git checkout -- pnpm-lock.yaml
@@ -117,9 +195,6 @@ jobs:
117195
- name: Install dependencies
118196
run: pnpm install --frozen-lockfile
119197

120-
- name: Build
121-
run: pnpm build
122-
123198
- name: Install Playwright
124199
run: npx playwright install --with-deps chromium
125200

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ jobs:
5050
node-version: 24
5151
cache: pnpm
5252

53+
- name: Validate release package manifest
54+
run: node ./scripts/release-package-map.mjs check
55+
5356
- name: Install dependencies
5457
run: pnpm install --no-frozen-lockfile
5558

@@ -89,6 +92,9 @@ jobs:
8992
node-version: 24
9093
cache: pnpm
9194

95+
- name: Validate release package manifest
96+
run: node ./scripts/release-package-map.mjs check
97+
9298
- name: Install dependencies
9399
run: pnpm install --no-frozen-lockfile
94100

@@ -139,6 +145,9 @@ jobs:
139145
node-version: 24
140146
cache: pnpm
141147

148+
- name: Validate release package manifest
149+
run: node ./scripts/release-package-map.mjs check
150+
142151
- name: Install dependencies
143152
run: pnpm install --no-frozen-lockfile
144153

@@ -177,6 +186,9 @@ jobs:
177186
node-version: 24
178187
cache: pnpm
179188

189+
- name: Validate release package manifest
190+
run: node ./scripts/release-package-map.mjs check
191+
180192
- name: Install dependencies
181193
run: pnpm install --no-frozen-lockfile
182194

doc/PUBLISHING.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,58 @@ That means:
176176

177177
See [doc/RELEASE-AUTOMATION-SETUP.md](RELEASE-AUTOMATION-SETUP.md) for the GitHub/npm setup steps.
178178

179+
## Release enrollment for new public packages
180+
181+
Paperclip does not auto-publish every non-private workspace package anymore.
182+
CI publishing is controlled by [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json).
183+
184+
When you add a new public package:
185+
186+
1. add it to the manifest and decide whether CI should publish it immediately
187+
2. if CI should publish it, bootstrap the package on npm before merge
188+
3. if CI should not publish it yet, keep `"publishFromCi": false`
189+
4. only enable `"publishFromCi": true` after npm trusted publishing is configured for that package
190+
191+
PR CI now checks changed release-enabled package manifests against npm. That catches a missing first-publish bootstrap before the change reaches `master`.
192+
193+
### One-time bootstrap sequence for a new package
194+
195+
The first publish of a brand-new package still needs one human maintainer with npm write access.
196+
After that, trusted publishing can take over.
197+
198+
Example for `@paperclipai/adapter-acpx-local` from the repo root:
199+
200+
```bash
201+
# safe preview
202+
pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local
203+
204+
# one-time first publish from an authenticated maintainer machine
205+
pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local --publish --otp 123456
206+
```
207+
208+
The helper script:
209+
210+
- checks that the package does not already exist on npm
211+
- builds the target package unless `--skip-build` is passed
212+
- runs `npm pack --dry-run` in the package directory
213+
- only runs the real `npm publish --access public` when `--publish --otp <code>` is provided
214+
215+
For the real `--publish` step, the maintainer machine must already be authenticated to npm.
216+
If `npm whoami` returns `401`, first run `npm logout --registry=https://registry.npmjs.org/` to clear any stale local auth, then run `npm login` or `npm adduser` locally as an npm org member, and finally rerun the helper.
217+
That local human auth is fine for the one-time bootstrap publish; we just do not want the same auth model inside CI.
218+
The helper now requires `--otp <code>` up front for `--publish`, so it fails before the real publish attempt if the one-time password is missing.
219+
220+
After that first publish succeeds:
221+
222+
1. open `https://www.npmjs.com/package/@paperclipai/adapter-acpx-local`
223+
2. go to `Settings``Trusted publishing`
224+
3. add repository `paperclipai/paperclip`
225+
4. set workflow filename to `release.yml`
226+
5. optionally go to `Settings``Publishing access` and enable `Require two-factor authentication and disallow tokens`
227+
6. keep `publishFromCi: true` in [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json)
228+
229+
Once those steps are done, future canary and stable publishes for that package are automated through GitHub OIDC. The manual step is only the first package creation on npm.
230+
179231
## Rollback model
180232

181233
Rollback does not unpublish anything.

doc/RELEASE-AUTOMATION-SETUP.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ Why:
6767
- the single `release.yml` workflow handles both canary and stable publishing
6868
- GitHub environments `npm-canary` and `npm-stable` still enforce different approval rules on the GitHub side
6969

70+
### 2.2.1. Newly added public packages need a bootstrap phase
71+
72+
Trusted publishing is configured on the npm package itself, not at the repo scope.
73+
That means a brand-new public package must not be auto-enrolled into CI publishing until its npm package exists and its trusted publisher has been configured.
74+
75+
Repo policy:
76+
77+
1. add every non-private package to [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json)
78+
2. set `"publishFromCi": true` only when CI is expected to publish that package
79+
3. if the package is not ready for CI publishing yet, keep `"publishFromCi": false`
80+
4. complete the package bootstrap before merging any PR that changes a release-enabled new package
81+
82+
Bootstrap sequence for a new package:
83+
84+
1. publish the package once from a trusted maintainer machine using normal npm auth
85+
2. open that package on npm and add the `paperclipai/paperclip` trusted publisher for `.github/workflows/release.yml`
86+
3. rerun or dry-run the release flow as needed to confirm CI publishing now works
87+
4. only then enable `"publishFromCi": true`
88+
89+
PR CI enforces this by checking changed release-enabled package manifests against npm. That keeps `master` canary publishing healthy while preserving the no-long-lived-token model for normal CI releases.
90+
7091
### 2.3. Verify trusted publishing before removing old auth
7192

7293
After the workflows are live:

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
"build-storybook": "pnpm --filter @paperclipai/ui build-storybook",
1616
"build": "pnpm run preflight:workspace-links && pnpm -r build",
1717
"typecheck": "pnpm run preflight:workspace-links && pnpm -r typecheck",
18+
"typecheck:build-gaps": "pnpm run preflight:workspace-links && node scripts/run-typecheck-build-gaps.mjs",
1819
"test": "pnpm run test:run",
1920
"test:watch": "pnpm run preflight:workspace-links && vitest",
2021
"test:run": "pnpm run preflight:workspace-links && node scripts/run-vitest-stable.mjs",
22+
"test:run:general": "pnpm run preflight:workspace-links && pnpm --filter @paperclipai/plugin-sdk build && node scripts/run-vitest-stable.mjs --mode general",
23+
"test:run:serialized": "pnpm run preflight:workspace-links && pnpm --filter @paperclipai/plugin-sdk build && node scripts/run-vitest-stable.mjs --mode serialized",
2124
"db:generate": "pnpm --filter @paperclipai/db generate",
2225
"db:migrate": "pnpm --filter @paperclipai/db migrate",
2326
"issue-references:backfill": "pnpm run preflight:workspace-links && tsx scripts/backfill-issue-reference-mentions.ts",
@@ -30,13 +33,14 @@
3033
"release:stable": "./scripts/release.sh stable",
3134
"release:github": "./scripts/create-github-release.sh",
3235
"release:rollback": "./scripts/rollback-latest.sh",
36+
"release:bootstrap-package": "node scripts/bootstrap-npm-package.mjs",
3337
"check:tokens": "node scripts/check-forbidden-tokens.mjs",
3438
"docs:dev": "cd docs && npx mintlify dev",
3539
"smoke:openclaw-join": "./scripts/smoke/openclaw-join.sh",
3640
"smoke:openclaw-docker-ui": "./scripts/smoke/openclaw-docker-ui.sh",
3741
"smoke:openclaw-sse-standalone": "./scripts/smoke/openclaw-sse-standalone.sh",
3842
"smoke:terminal-bench-loop-skill": "node scripts/smoke/terminal-bench-loop-skill-smoke.mjs",
39-
"test:release-registry": "node --test scripts/verify-release-registry-state.test.mjs",
43+
"test:release-registry": "node --test scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs",
4044
"test:e2e": "npx playwright test --config tests/e2e/playwright.config.ts",
4145
"test:e2e:headed": "npx playwright test --config tests/e2e/playwright.config.ts --headed",
4246
"test:e2e:multiuser-authenticated": "npx playwright test --config tests/e2e/playwright-multiuser-authenticated.config.ts",

packages/adapter-utils/src/execution-target.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
22
import * as ssh from "./ssh.js";
33
import {
44
adapterExecutionTargetUsesManagedHome,
5+
ensureAdapterExecutionTargetRuntimeCommandInstalled,
56
resolveAdapterExecutionTargetCwd,
67
runAdapterExecutionTargetShellCommand,
78
} from "./execution-target.js";
@@ -161,6 +162,80 @@ describe("runAdapterExecutionTargetShellCommand", () => {
161162
});
162163
});
163164

165+
describe("ensureAdapterExecutionTargetRuntimeCommandInstalled", () => {
166+
afterEach(() => {
167+
vi.restoreAllMocks();
168+
});
169+
170+
it("runs install commands for sandbox targets", async () => {
171+
const runner = {
172+
execute: vi.fn(async () => ({
173+
exitCode: 0,
174+
signal: null,
175+
timedOut: false,
176+
stdout: "",
177+
stderr: "",
178+
pid: null,
179+
startedAt: new Date().toISOString(),
180+
})),
181+
};
182+
183+
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
184+
runId: "run-install",
185+
target: {
186+
kind: "remote",
187+
transport: "sandbox",
188+
providerKey: "e2b",
189+
remoteCwd: "/remote/workspace",
190+
runner,
191+
},
192+
installCommand: "npm install -g @google/gemini-cli",
193+
cwd: "/local/workspace",
194+
env: { PATH: "/usr/bin" },
195+
timeoutSec: 30,
196+
});
197+
198+
expect(runner.execute).toHaveBeenCalledWith(expect.objectContaining({
199+
command: "sh",
200+
args: ["-lc", "npm install -g @google/gemini-cli"],
201+
cwd: "/remote/workspace",
202+
env: { PATH: "/usr/bin" },
203+
timeoutMs: 30_000,
204+
}));
205+
});
206+
207+
it("skips install commands for SSH targets", async () => {
208+
const runSshCommandSpy = vi.spyOn(ssh, "runSshCommand").mockResolvedValue({
209+
stdout: "",
210+
stderr: "",
211+
});
212+
213+
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
214+
runId: "run-skip",
215+
target: {
216+
kind: "remote",
217+
transport: "ssh",
218+
remoteCwd: "/srv/paperclip/workspace",
219+
spec: {
220+
host: "ssh.example.test",
221+
port: 22,
222+
username: "ssh-user",
223+
remoteCwd: "/srv/paperclip/workspace",
224+
remoteWorkspacePath: "/srv/paperclip/workspace",
225+
privateKey: null,
226+
knownHosts: null,
227+
strictHostKeyChecking: true,
228+
},
229+
},
230+
installCommand: "npm install -g @google/gemini-cli",
231+
cwd: "/tmp/local",
232+
env: {},
233+
});
234+
235+
expect(runSshCommandSpy).not.toHaveBeenCalled();
236+
});
237+
});
238+
164239
describe("resolveAdapterExecutionTargetCwd", () => {
165240
const sshTarget = {
166241
kind: "remote" as const,

0 commit comments

Comments
 (0)