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

  1. Verify the run at https://github.com/paperclipai/paperclip/actions/runs/{run.id} still exists and attempt {run.run_attempt} is valid.
  2. 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.
  3. If using an injected api() in tests, make every page response include { jobs: [...] } (empty array on later pages is fine).
  4. 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

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

Related errors


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)