paperclipai/paperclip · error · Error
Cloud readiness job listing is malformed.
Error message
Cloud readiness job listing is malformed.
What it means
While paging jobs for the selected workflow run attempt, each batch response must contain a jobs array. This error means a page of GET /repos/.../actions/runs/{run.id}/attempts/{run.run_attempt}/jobs returned a body whose jobs field is not an array (missing, null, or another shape). The script treats it as a malformed listing and aborts the poll rather than guessing.
Solutions
- Verify the run at https://github.com/paperclipai/paperclip/actions/runs/{run.id} still exists and attempt {run.run_attempt} is valid.
- Check the raw jobs endpoint response: https://api.github.com/repos/paperclipai/paperclip/actions/runs/{id}/attempts/{n}/jobs — confirm it contains a jobs array.
- If using an injected api() in tests, make every page response include { jobs: [...] } (empty array on later pages is fine).
- Rerun the release poll; a fresh readSourceVerification picks a fresh run/attempt pair.
Example fix
// before
const api = async () => ({});
// after
const api = async () => ({ jobs: [], total_count: 0 }); Defensive patterns
Strategy: type-guard
Validate before calling
const batch = await api(jobsPath);
if (!batch || !Array.isArray(batch.jobs)) throw new Error('jobs page malformed'); Type guard
function hasJobsArray(b) {
return typeof b === 'object' && b !== null && Array.isArray(b.jobs);
} Try / catch
try {
const proof = await readSourceVerification(sha, api);
} catch (e) {
if (e.message.includes('job listing is malformed')) {
console.error('Jobs page missing jobs array — verify the run/attempt still exists.');
}
} Prevention
- Never delete GitHub Actions runs while a release poll is in progress.
- In test stubs, include { jobs: [] } on every page response, including empty ones.
- Fetch the jobs endpoint once manually to confirm the envelope before automating against it.
- Handle possible 404 envelopes from GitHub (message field, no jobs) before parsing.
When it happens
Trigger: The paginated jobs call returns an object without an jobs array — e.g. the run id/attempt no longer resolves, the API returns an error-shaped JSON body (with message instead of jobs), or an injected api() stub omits jobs.
Common situations: The run was deleted between listing runs and fetching jobs; GitHub returns a 404-style JSON envelope the reader decoded (non-OK statuses normally throw in createActionsReader, but stubbed/mocked readers skip that); test fixtures missing the jobs key.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Cloud readiness run listing is incomplete.
- Cloud readiness job listing is incomplete.
- Cloud artifacts timed out for
- Cloud source verification did not pass for
- Cloud source verification job is ambiguous.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/da2df5e5b998d4f5.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/cloud-source-verification.mjs:42
const workflow = await api(`/repos/${repository}/actions/workflows/cloud-readiness.yml`);
if (workflow.path !== workflowPath || !Number.isSafeInteger(workflow.id) || workflow.id < 1) {
throw new Error("Cloud readiness workflow identity does not match.");
}
const listing = await api(`/repos/${repository}/actions/workflows/${workflow.id}/runs?head_sha=${sha}&event=push&branch=master&per_page=100`);
if (!Array.isArray(listing.workflow_runs) || !Number.isSafeInteger(listing.total_count) ||
listing.total_count < 0 || listing.total_count > 100 || listing.workflow_runs.length !== listing.total_count) {
throw new Error("Cloud readiness run listing is incomplete.");
}
const run = listing.workflow_runs.filter((candidate) => trustedRun(candidate, sha, workflow.id))
.sort((a, b) => b.id - a.id)[0];
if (!run) return undefined;
// Attempt-specific jobs prevent an earlier successful attempt from blessing
// a later rerun. Keep pagination even though today's matrix fits one page.
const jobs = [];
for (let page = 1; page <= 10; page += 1) {
const batch = await api(`/repos/${repository}/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100&page=${page}`);
if (!Array.isArray(batch.jobs)) throw new Error("Cloud readiness job listing is malformed.");
jobs.push(...batch.jobs);
if (batch.jobs.length < 100) break;
if (page === 10) throw new Error("Cloud readiness job listing is incomplete.");
}
const matches = jobs.filter((job) => job.name === sourceVerificationJob);
if (matches.length > 1) throw new Error("Cloud source verification job is ambiguous.");
const job = matches[0];
if (job?.status === "completed" && job.conclusion === "success" &&
job.head_sha === sha && job.run_id === run.id && job.run_attempt === run.run_attempt) {
// Re-read the run after its jobs: a rerun that started during polling must
// not let the previous attempt through. Changes are retried next poll.
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
if (!trustedRun(current, sha, workflow.id) || current.run_attempt !== run.run_attempt) return undefined;
return { sha, runId: run.id, attempt: run.run_attempt, jobId: job.id };
}
if (job?.status === "completed" || run.status === "completed") {
throw new Error(`Cloud source verification did not pass for ${sha} (run ${run.id}, attempt ${run.run_attempt}). Rerun Cloud readiness before retrying the release.`);
}View on GitHub (pinned to 3f1d897a7c)