Merge origin/master and integrate environment selection

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-11 06:18:05 -05:00
commit fed4802b72
159 files changed
+13953 -2027

No files matched your search

@@ -2,21 +2,148 @@ import { spawn } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadReleaseManifest } from "../../scripts/release-package-map.mjs";
// Leave three minutes for onboarding inside Playwright's existing five-minute
// startup deadline. This budget includes every install attempt and backoff.
export const INSTALL_BUDGET_MS = 120_000;
const MAX_INSTALL_ATTEMPTS = 3;
const STDERR_TAIL_CHARS = 64 * 1024;
export const TARBALL_METADATA_TIMEOUT_MS = 5_000;
const METADATA_LIMIT_BYTES = 64 * 1024;
const PRIMARY_REGISTRY = "https://registry.npmjs.org/";
const RELEASE_PACKAGE_NAMES = new Set(loadReleaseManifest()
.filter((entry) => entry.publishFromCi === true).map((entry) => entry.name));
function run(command, args, { env, signal, captureStderr = false }) {
function withAbort(pending, signal) {
return new Promise((resolve, reject) => {
const abort = () => {
signal.removeEventListener("abort", abort);
reject(signal.reason);
};
const finish = (settle) => (value) => {
signal.removeEventListener("abort", abort);
settle(value);
};
// Keep handlers on the original operation even after the bounded wait
// ends. A transport need not settle immediately when its signal aborts.
Promise.resolve(pending).then(finish(resolve), finish(reject));
signal.addEventListener("abort", abort, { once: true });
if (signal.aborted) abort();
});
}
export async function canRetryPublishedTarball404(stderr, version, {
signal, prefix, env = process.env, fetchMetadata = globalThis.fetch, runConfig = run,
}) {
// A package metadata 404, a third-party artifact, or another release must not
// be mistaken for availability of this canary's announced public tarball.
if (!/^\d+\.\d+\.\d+-canary\.\d+$/.test(version)) return false;
const codes = [...stderr.matchAll(/^npm (?:error|ERR!) code ([A-Z0-9_]+)\r?$/gm)];
const final = codes.at(-1);
if (codes.length !== 1 || final?.[1] !== "E404") return false;
const block = stderr.slice(final.index);
const requests = [...block.matchAll(/^npm (?:error|ERR!) 404 Not Found - GET (\S+) - Not found\r?$/gm)];
const httpLines = stderr.match(/^npm (?:error|ERR!).* - (?:GET|HEAD|POST|PUT|DELETE|PATCH) /gm) ?? [];
if (requests.length !== 1 || httpLines.length !== 1) return false;
const tarball = requests[0][1];
const packageName = [...RELEASE_PACKAGE_NAMES].find((name) => {
const basename = name.split("/").at(-1);
return tarball === `${PRIMARY_REGISTRY}${name}/-/${basename}-${version}.tgz`;
});
if (!packageName) return false;
const resources = [...block.matchAll(/^npm (?:error|ERR!) 404\s+(?:The requested resource )?'([^']+)'(?: could not be found| is not in this registry)/gm)];
if (resources.some((resource) => resource[1] !== `${packageName}@${tarball}`)) return false;
signal.throwIfAborted();
const timeout = new AbortController();
const timer = setTimeout(() => timeout.abort(new Error("Canary tarball metadata lookup timed out")), TARBALL_METADATA_TIMEOUT_MS);
const lookupSignal = AbortSignal.any([signal, timeout.signal]);
const metadataUrl = `${PRIMARY_REGISTRY}${encodeURIComponent(packageName)}/${version}`;
let response;
try {
// Query only registry values in the failed install's effective prefix,
// inherited cwd and environment. Keep even warnings out of the logs:
// configuration values can contain credentials. Config and HTTP share
// this short deadline, inside the unchanged installation budget.
const config = await runConfig(process.platform === "win32" ? "npm.cmd" : "npm", [
"config", "get", "registry", "@paperclipai:registry", "--prefix", prefix,
], { env, signal: lookupSignal, captureStdout: true, captureStderr: true, quiet: true, outputLimit: 4096 });
if (config.code !== 0 || config.signal || config.outputTruncated) return false;
const lines = config.stdout.trim().split(/\r?\n/);
if (lines.length !== 2 || !lines[0].startsWith("registry=") || !lines[1].startsWith("@paperclipai:registry=")) return false;
const globalRegistry = lines[0].slice("registry=".length);
const scoped = lines[1].slice("@paperclipai:registry=".length);
const effectiveRegistry = packageName.startsWith("@") && scoped !== "undefined" ? scoped : globalRegistry;
if (![PRIMARY_REGISTRY, PRIMARY_REGISTRY.slice(0, -1)].includes(effectiveRegistry)) return false;
response = await withAbort(Promise.resolve(fetchMetadata(metadataUrl, {
signal: lookupSignal, redirect: "error", credentials: "omit",
headers: { accept: "application/json", "cache-control": "no-cache" },
})).then((arrived) => {
if (lookupSignal.aborted) void arrived.body?.cancel().catch(() => {});
return arrived;
}), lookupSignal);
if (response.status !== 200 || response.redirected || response.url !== metadataUrl || !response.body) return false;
const reader = response.body.getReader();
const cancelReader = () => { void reader.cancel().catch(() => {}); };
lookupSignal.addEventListener("abort", cancelReader, { once: true });
const chunks = [];
let bytes = 0;
try {
while (true) {
lookupSignal.throwIfAborted();
const { done, value } = await withAbort(reader.read(), lookupSignal);
if (done) break;
bytes += value.byteLength;
if (bytes > METADATA_LIMIT_BYTES) return false;
chunks.push(value);
}
} finally {
// Initiate cleanup without extending either deadline for a transport's
// pending/rejected cancellation acknowledgement.
lookupSignal.removeEventListener("abort", cancelReader);
cancelReader();
reader.releaseLock();
}
lookupSignal.throwIfAborted();
const manifest = JSON.parse(Buffer.concat(chunks).toString("utf8"));
const integrity = manifest.dist?.integrity;
// This validates the integrity announced by primary metadata. No tarball
// is downloaded here, and this is not an independent content digest proof.
return manifest.name === packageName && manifest.version === version
&& manifest.dist?.tarball === tarball && typeof integrity === "string"
&& /^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity)
&& Buffer.from(integrity.slice(7), "base64").toString("base64") === integrity.slice(7);
} catch {
signal.throwIfAborted();
return false;
} finally {
void response?.body?.cancel().catch(() => {});
clearTimeout(timer);
}
}
function run(command, args, { env, signal, captureStderr = false, captureStdout = false, quiet = false, outputLimit = STDERR_TAIL_CHARS }) {
return new Promise((resolve, reject) => {
signal.throwIfAborted();
const grouped = process.platform !== "win32";
const child = spawn(command, args, {
env, detached: grouped, stdio: ["ignore", "inherit", captureStderr ? "pipe" : "inherit"],
env, detached: grouped, stdio: ["ignore", captureStdout ? "pipe" : "inherit", captureStderr ? "pipe" : "inherit"],
});
let stderr = "";
let stderr = quiet ? Buffer.alloc(0) : "";
let stdout = quiet ? Buffer.alloc(0) : "";
let outputTruncated = false;
const retain = (previous, chunk) => {
if (quiet) {
outputTruncated ||= previous.length + chunk.byteLength > outputLimit;
return Buffer.concat([previous, chunk]).subarray(-outputLimit);
}
// Preserve the existing install stderr character tail and ETARGET
// behavior, including multibyte diagnostics. Only private config capture
// uses a byte bound; any truncated E404 evidence is rejected.
const next = previous + chunk.toString();
outputTruncated ||= next.length > outputLimit;
return next.slice(-outputLimit);
};
let stopping;
const kill = (kind) => {
try {
@@ -34,15 +161,20 @@ function run(command, args, { env, signal, captureStderr = false }) {
};
signal.addEventListener("abort", stop, { once: true });
child.stderr?.on("data", (chunk) => {
process.stderr.write(chunk);
stderr = (stderr + chunk.toString()).slice(-STDERR_TAIL_CHARS);
if (!quiet) process.stderr.write(chunk);
stderr = retain(stderr, chunk);
});
child.stdout?.on("data", (chunk) => {
if (!quiet) process.stdout.write(chunk);
stdout = retain(stdout, chunk);
});
child.on("error", reject);
child.on("close", async (code, exitSignal) => {
signal.removeEventListener("abort", stop);
await stopping;
if (signal.aborted) reject(signal.reason);
else resolve({ code, signal: exitSignal, stderr });
else resolve({ code, signal: exitSignal,
stderr: quiet ? stderr.toString("utf8") : stderr, stdout: quiet ? stdout.toString("utf8") : stdout, outputTruncated });
});
});
}
@@ -85,7 +217,10 @@ export async function startPublishedCanary({
if (result.code === 0) break;
const codes = [...result.stderr.matchAll(/^npm (?:error|ERR!) code ([A-Z0-9_]+)\r?$/gm)];
const missingVersion = codes.at(-1)?.[1] === "ETARGET";
if (!missingVersion || result.signal || attempt === MAX_INSTALL_ATTEMPTS) {
const retryable = !result.signal && attempt < MAX_INSTALL_ATTEMPTS
&& (missingVersion || (!result.outputTruncated
&& await canRetryPublishedTarball404(result.stderr, version, { signal: installSignal, prefix, env })));
if (!retryable) {
throw new Error(`Canary npm installation failed (exit ${result.code ?? result.signal}); onboarding was not started`);
}
await rm(prefix, { recursive: true, force: true });
+1
View File
@@ -12,6 +12,7 @@ const api = async (route, method = "GET", body) => {
return response.json();
};
const run = await api(`/heartbeat-runs/${env.PAPERCLIP_RUN_ID}`);
assert.equal(run.contextSnapshot.paperclipEnvironment.driver, "local");
const issueId = run.contextSnapshot.issueId;
const repositories = await readdir(path.join(process.cwd(), ".paperclip-repositories"));
const repo = path.join(process.cwd(), ".paperclip-repositories", repositories.find((name) => !name.includes(".clone-")));