{"record":{"id":"da2df5e5b998d4f5","repo":"paperclipai/paperclip","slug":"cloud-readiness-job-listing-is-malformed","errorCode":null,"errorMessage":"Cloud readiness job listing is malformed.","messagePattern":"Cloud readiness job listing is malformed\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloud-source-verification.mjs","lineNumber":42,"sourceCode":"  const workflow = await api(`/repos/${repository}/actions/workflows/cloud-readiness.yml`);\n  if (workflow.path !== workflowPath || !Number.isSafeInteger(workflow.id) || workflow.id < 1) {\n    throw new Error(\"Cloud readiness workflow identity does not match.\");\n  }\n  const listing = await api(`/repos/${repository}/actions/workflows/${workflow.id}/runs?head_sha=${sha}&event=push&branch=master&per_page=100`);\n  if (!Array.isArray(listing.workflow_runs) || !Number.isSafeInteger(listing.total_count) ||\n      listing.total_count < 0 || listing.total_count > 100 || listing.workflow_runs.length !== listing.total_count) {\n    throw new Error(\"Cloud readiness run listing is incomplete.\");\n  }\n  const run = listing.workflow_runs.filter((candidate) => trustedRun(candidate, sha, workflow.id))\n    .sort((a, b) => b.id - a.id)[0];\n  if (!run) return undefined;\n\n  // Attempt-specific jobs prevent an earlier successful attempt from blessing\n  // a later rerun. Keep pagination even though today's matrix fits one page.\n  const jobs = [];\n  for (let page = 1; page <= 10; page += 1) {\n    const batch = await api(`/repos/${repository}/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100&page=${page}`);\n    if (!Array.isArray(batch.jobs)) throw new Error(\"Cloud readiness job listing is malformed.\");\n    jobs.push(...batch.jobs);\n    if (batch.jobs.length < 100) break;\n    if (page === 10) throw new Error(\"Cloud readiness job listing is incomplete.\");\n  }\n  const matches = jobs.filter((job) => job.name === sourceVerificationJob);\n  if (matches.length > 1) throw new Error(\"Cloud source verification job is ambiguous.\");\n  const job = matches[0];\n  if (job?.status === \"completed\" && job.conclusion === \"success\" &&\n      job.head_sha === sha && job.run_id === run.id && job.run_attempt === run.run_attempt) {\n    // Re-read the run after its jobs: a rerun that started during polling must\n    // not let the previous attempt through. Changes are retried next poll.\n    const current = await api(`/repos/${repository}/actions/runs/${run.id}`);\n    if (!trustedRun(current, sha, workflow.id) || current.run_attempt !== run.run_attempt) return undefined;\n    return { sha, runId: run.id, attempt: run.run_attempt, jobId: job.id };\n  }\n  if (job?.status === \"completed\" || run.status === \"completed\") {\n    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.`);\n  }","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-source-verification.mjs#L24-L60","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst api = async () => ({});\n// after\nconst api = async () => ({ jobs: [], total_count: 0 });","handlingStrategy":"type-guard","validationCode":"const batch = await api(jobsPath);\nif (!batch || !Array.isArray(batch.jobs)) throw new Error('jobs page malformed');","typeGuard":"function hasJobsArray(b) {\n  return typeof b === 'object' && b !== null && Array.isArray(b.jobs);\n}","tryCatchPattern":"try {\n  const proof = await readSourceVerification(sha, api);\n} catch (e) {\n  if (e.message.includes('job listing is malformed')) {\n    console.error('Jobs page missing jobs array — verify the run/attempt still exists.');\n  }\n}","preventionTips":["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."],"tags":["github-actions","api-response-shape","pagination"],"backgroundTag":"unexpected-response-shape","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}