mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Merge current master and local acceptance evidence into Claude platforms
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/claude-runner-default: docs: record reviewed Claude runtime proof and cloud qualification fix(ci): finish large Honeycomb profile exports (#16423) perf: batch retained-reader authorization reads (#16424)
This commit is contained in:
commit
75f5cb820e
10 files changed
+277
-42
No files matched your search
@@ -14,19 +14,35 @@ const get = (endpoint) => JSON.parse(gh([endpoint]).toString());
|
||||
const pages = (endpoint, field) => JSON.parse(gh(["--paginate", "--slurp", endpoint]).toString())
|
||||
.flatMap((page) => field ? page[field] : page);
|
||||
|
||||
export async function sendEvents(events, apiKey, dataset, fetcher = fetch) {
|
||||
export async function sendEvents(events, apiKey, dataset, fetcher = fetch, pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
||||
if (!apiKey) throw new Error("HONEYCOMB_CI_API_KEY is not configured");
|
||||
for (let offset = 0; offset < events.length; offset += 500) {
|
||||
const batch = events.slice(offset, offset + 500);
|
||||
const response = await fetcher(`https://api.honeycomb.io/1/batch/${encodeURIComponent(dataset)}`, {
|
||||
method: "POST", headers: { "X-Honeycomb-Team": apiKey, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(batch), redirect: "error", signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Honeycomb export failed (HTTP ${response.status})`);
|
||||
const results = await response.json();
|
||||
if (!Array.isArray(results) || results.length !== batch.length || results.some((result) => result.status !== 202)) {
|
||||
throw new Error("Honeycomb did not accept every span in the batch");
|
||||
const transient = new Set([429, 500, 502, 503, 504]);
|
||||
for (let offset = 0; offset < events.length; offset += 250) {
|
||||
let pending = events.slice(offset, offset + 250);
|
||||
for (let attempt = 0; pending.length; attempt += 1) {
|
||||
if (attempt >= 8) throw new Error(`Honeycomb did not accept ${pending.length} spans after retries`);
|
||||
const response = await fetcher(`https://api.honeycomb.io/1/batch/${encodeURIComponent(dataset)}`, {
|
||||
method: "POST", headers: { "X-Honeycomb-Team": apiKey, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(pending), redirect: "error", signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (!transient.has(response.status)) throw new Error(`Honeycomb export failed (HTTP ${response.status})`);
|
||||
await pause(Math.min(30_000, 1000 * 2 ** attempt));
|
||||
continue;
|
||||
}
|
||||
const results = await response.json();
|
||||
if (!Array.isArray(results) || results.length !== pending.length) {
|
||||
throw new Error("Honeycomb returned an unexpected batch response");
|
||||
}
|
||||
const rejected = results.flatMap((result, index) => result.status === 202 ? [] : [{ event: pending[index], status: result.status }]);
|
||||
const permanent = rejected.find(({ status }) => !transient.has(status));
|
||||
if (permanent) throw new Error(`Honeycomb rejected ${rejected.length} spans (status ${permanent.status})`);
|
||||
pending = rejected.map(({ event }) => event);
|
||||
if (pending.length) await pause(Math.min(30_000, 1000 * 2 ** attempt));
|
||||
}
|
||||
// The full suite can exceed Honeycomb's burst allowance. Leave capacity
|
||||
// for the team's other telemetry rather than exhausting it in one export.
|
||||
if (offset + 250 < events.length) await pause(1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ export function buildTrace(run, jobs, records = []) {
|
||||
invocations.set(invocation, state);
|
||||
continue;
|
||||
}
|
||||
if (!["file", "test"].includes(record.kind) || !text(record.id) || !text(record.name)
|
||||
if (!["file", "test"].includes(record.kind) || !text(record.id)
|
||||
|| (record.kind === "file" ? !text(record.name) : typeof record.name !== "string")
|
||||
|| !text(record.file) || !["vitest", "playwright", "node"].includes(record.framework)
|
||||
|| !Number.isFinite(record.start) || !Number.isFinite(record.duration) || record.duration < 0
|
||||
|| record.start < millis(job.started_at) - 1000
|
||||
@@ -128,7 +129,7 @@ export function buildTrace(run, jobs, records = []) {
|
||||
&& candidate.conclusion !== "skipped");
|
||||
const parent = files.get(`${invocation}:${record.parent}`)
|
||||
|| (step ? id(`${jobId}:step:${step.number}`) : jobId);
|
||||
span(recordId(entry), parent, record.name, record.start, record.start + record.duration, {
|
||||
span(recordId(entry), parent, record.kind === "test" ? record.name.slice(0, 256) : record.name, record.start, record.start + record.duration, {
|
||||
"ci.kind": record.kind, "ci.job": job.name, "test.framework": record.framework,
|
||||
"test.file": record.file, "test.status": typeof record.status === "string" ? record.status.slice(0, 32) : "unknown", error: failed(record.status),
|
||||
"test.retries": Number.isFinite(record.retries) ? record.retries : 0,
|
||||
@@ -140,7 +141,9 @@ export function buildTrace(run, jobs, records = []) {
|
||||
const expectedReceipts = jobs.filter((job) => job.steps?.some((step) => step.name === "Initialize CI profile"));
|
||||
const missing = expectedReceipts.filter((job) => !receipts.has(job.id));
|
||||
if (missing.length) warnings.push(`Missing timing artifacts for ${missing.length} jobs`);
|
||||
const incomplete = [...invocations.values()].filter((value) => !value.expected || !value.start || !value.end).length;
|
||||
// Some package scripts invoke a reporter without using the launcher. Its
|
||||
// start/end pair proves collection even when no intent record was written.
|
||||
const incomplete = [...invocations.values()].filter((value) => !value.start || !value.end).length;
|
||||
if (incomplete) warnings.push(`${incomplete} test invocations did not finish reporting`);
|
||||
if (rejected) warnings.push(`${rejected} invalid timing records ignored`);
|
||||
if (run.conclusion !== "success") warnings.push("Workflow did not succeed; later tests or steps may not have run");
|
||||
@@ -159,8 +162,8 @@ export function buildTrace(run, jobs, records = []) {
|
||||
if (expectedReceipts.length && missingTests.length) warnings.push(`Missing test detail for ${missingTests.length} test jobs (including jobs that failed before testing)`);
|
||||
root["ci.test_count"] = valid.filter(({ record }) => record.kind === "test").length;
|
||||
root["ci.file_count"] = valid.filter(({ record }) => record.kind === "file").length;
|
||||
root["ci.expected_invocations"] = [...invocations.values()].filter((value) => value.expected).length;
|
||||
root["ci.reported_invocations"] = [...invocations.values()].filter((value) => value.expected && value.start && value.end).length;
|
||||
root["ci.expected_invocations"] = invocations.size;
|
||||
root["ci.reported_invocations"] = [...invocations.values()].filter((value) => value.start && value.end).length;
|
||||
root["ci.profile_complete"] = warnings.length === 0 && run.run_attempt === 1;
|
||||
root["ci.coverage_warnings"] = warnings.join("; ");
|
||||
return { traceId, events, warnings, wallMs: end - start, jobCount: jobs.length };
|
||||
|
||||
Reference in new issue
Block a user