Fix release packaging for standalone public packages (#4494)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies, and the sandbox-provider work just moved E2B into a standalone publishable plugin package. > - That plugin is intentionally excluded from the root pnpm workspace so it can model third-party install behavior without forcing lockfile churn in the main repo. > - The merged architecture change exposed a follow-up release problem: the canary publish workflow tried to publish `@paperclipai/plugin-e2b`, but the tarball had no `dist/` payload because standalone public packages were not being built in the release path. > - That means the release pipeline needed a packaging fix in core release tooling, not another architectural change in the sandbox provider itself. > - This pull request adds a generic release step for public packages that live outside the pnpm workspace, instead of hardcoding E2B-specific behavior into the release script. > - The benefit is that standalone publishable packages can be built and packed correctly during release, including future sandbox-provider plugins that follow the same pattern. ## What Changed - Added `scripts/build-standalone-public-packages.mjs` to discover public packages outside the pnpm workspace, run a clean package-local install, and build them before publish. - Updated `scripts/release.sh` to invoke that helper immediately after the normal workspace build step. - Kept the behavior generic by driving off the existing public package map and pnpm workspace patterns rather than special-casing `@paperclipai/plugin-e2b`. ## Verification - `rm -rf packages/plugins/sandbox-providers/e2b/dist` - `node ./scripts/build-standalone-public-packages.mjs` - `cd packages/plugins/sandbox-providers/e2b && npm pack --dry-run` - Confirm the tarball now includes the rebuilt `dist/` files instead of only `README.md` / `package.json` ## Risks - Low risk: this only changes the release build path for public packages outside the pnpm workspace. - The helper performs a clean package-local install for each standalone public package, so release time may increase slightly as more such packages are added. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex via `codex_local` - Model ID: `gpt-5.4` - Reasoning effort: `high` - Context window observed in runtime session metadata: `258400` tokens - Capabilities used: terminal tool execution, git, GitHub CLI, and local build/test inspection ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import path, { dirname } from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const workspacePath = path.join(repoRoot, "pnpm-workspace.yaml");
|
||||
const releasePackageMapPath = path.join(repoRoot, "scripts", "release-package-map.mjs");
|
||||
|
||||
function parseWorkspaceEntries(workspaceText) {
|
||||
// Keep this aligned with the repo's block-sequence `packages:` format in
|
||||
// pnpm-workspace.yaml. If that file moves to a more complex YAML shape,
|
||||
// switch this parser to a real YAML parser instead of line matching.
|
||||
return workspaceText
|
||||
.split("\n")
|
||||
.map((line) => line.match(/^\s*-\s+(.+)\s*$/)?.[1]?.trim() ?? null)
|
||||
.map((entry) => {
|
||||
if (!entry) return entry;
|
||||
return entry.replace(/^(['"])(.*)\1$/, "$2");
|
||||
})
|
||||
.filter(Boolean)
|
||||
.map((entry) => ({
|
||||
pattern: entry.startsWith("!") ? entry.slice(1) : entry,
|
||||
negated: entry.startsWith("!"),
|
||||
}));
|
||||
}
|
||||
|
||||
function globToRegExp(pattern) {
|
||||
let regex = "";
|
||||
|
||||
for (let index = 0; index < pattern.length; index += 1) {
|
||||
const char = pattern[index];
|
||||
const next = pattern[index + 1];
|
||||
|
||||
if (char === "*" && next === "*") {
|
||||
regex += ".*";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === "*") {
|
||||
regex += "[^/]*";
|
||||
continue;
|
||||
}
|
||||
if (char === "?") {
|
||||
regex += "[^/]";
|
||||
continue;
|
||||
}
|
||||
regex += /[|\\{}()[\]^$+?.]/.test(char) ? `\\${char}` : char;
|
||||
}
|
||||
|
||||
return new RegExp(`^${regex}$`);
|
||||
}
|
||||
|
||||
function isWorkspacePackage(pkgDir, workspaceEntries) {
|
||||
let included = false;
|
||||
|
||||
for (const entry of workspaceEntries) {
|
||||
if (globToRegExp(entry.pattern).test(pkgDir)) {
|
||||
included = !entry.negated;
|
||||
}
|
||||
}
|
||||
|
||||
return included;
|
||||
}
|
||||
|
||||
function listPublicPackages() {
|
||||
const output = execFileSync(
|
||||
process.execPath,
|
||||
[releasePackageMapPath, "list"],
|
||||
{ cwd: repoRoot, encoding: "utf8" },
|
||||
);
|
||||
|
||||
return output
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [dir, name] = line.split("\t");
|
||||
return { dir, name };
|
||||
});
|
||||
}
|
||||
|
||||
function readPackageJson(pkgDir) {
|
||||
return JSON.parse(
|
||||
readFileSync(path.join(repoRoot, pkgDir, "package.json"), "utf8"),
|
||||
);
|
||||
}
|
||||
|
||||
function run(command, args, cwd) {
|
||||
execFileSync(command, args, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
CI: "true",
|
||||
},
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
const workspaceEntries = parseWorkspaceEntries(readFileSync(workspacePath, "utf8"));
|
||||
const standalonePackages = listPublicPackages()
|
||||
.filter(({ dir }) => !isWorkspacePackage(dir, workspaceEntries));
|
||||
|
||||
if (standalonePackages.length === 0) {
|
||||
console.log(" i No standalone public packages detected outside the pnpm workspace");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const pkg of standalonePackages) {
|
||||
const pkgDir = path.join(repoRoot, pkg.dir);
|
||||
const pkgJson = readPackageJson(pkg.dir);
|
||||
const nodeModulesDir = path.join(pkgDir, "node_modules");
|
||||
const packageLockfilePath = path.join(pkgDir, "pnpm-lock.yaml");
|
||||
|
||||
console.log(` Preparing standalone package ${pkg.name} (${pkg.dir})`);
|
||||
if (existsSync(nodeModulesDir)) {
|
||||
rmSync(nodeModulesDir, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
const installArgs = existsSync(packageLockfilePath)
|
||||
? ["install", "--ignore-workspace", "--frozen-lockfile"]
|
||||
: [
|
||||
"install",
|
||||
"--ignore-workspace",
|
||||
"--no-lockfile",
|
||||
// Standalone packages intentionally avoid committed lockfile churn in the repo.
|
||||
];
|
||||
|
||||
run("pnpm", installArgs, pkgDir);
|
||||
|
||||
if (pkgJson.scripts?.build) {
|
||||
run("pnpm", ["run", "build"], pkgDir);
|
||||
} else {
|
||||
console.log(" i No build script; skipped build");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -214,6 +214,7 @@ release_info ""
|
||||
release_info "==> Step 2/7: Building workspace artifacts..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm build
|
||||
node "$REPO_ROOT/scripts/build-standalone-public-packages.mjs"
|
||||
bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh"
|
||||
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
|
||||
rm -rf "$REPO_ROOT/$pkg_dir/skills"
|
||||
|
||||
Reference in New Issue
Block a user