{"record":{"id":"a5950a411905c961","repo":"paperclipai/paperclip","slug":"cloud-source-verification-job-is-ambiguous","errorCode":null,"errorMessage":"Cloud source verification job is ambiguous.","messagePattern":"Cloud source verification job is ambiguous\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloud-source-verification.mjs","lineNumber":48,"sourceCode":"      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  }\n  return undefined;\n}\n\n// Statuses that say \"ask again\", not \"the answer is no\". A release must not be\n// blocked because GitHub returned a gateway error during a 45-minute poll.\nconst TRANSIENT_READ_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-source-verification.mjs#L30-L66","documentation":"After collecting jobs for the selected attempt, the script filters by the exact job name 'Cloud source verified v1'. If more than one job on that attempt has that name, the script cannot tell which one proves source verification and throws. The job name is the unique contract for this check.","triggerScenarios":"The cloud-readiness workflow (or a reusable-workflow caller) defines or renames a step so two jobs on the same run attempt end up named 'Cloud source verified v1' — e.g. duplicating the verification job in a matrix, a merge accident adding a second job, or a reusable workflow including the job twice.","commonSituations":"Someone copies the verification job block in .github/workflows/cloud-readiness.yml; a reusable workflow is invoked twice with the same display name; a name-mangling action renames jobs unexpectedly.","solutions":["Open .github/workflows/cloud-readiness.yml and remove or rename the duplicated job so exactly one job is named 'Cloud source verified v1'.","Check the run's job list (https://api.github.com/repos/paperclipai/paperclip/actions/runs/{id}/attempts/{n}/jobs) to see which duplicate exists and delete the wrong one from the workflow.","If a reusable workflow causes the duplicate, change its job display name or call it once.","Bump sourceVerificationJob (the versioned name) and the workflow together if a new contract name is intended."],"exampleFix":"# before: two jobs with the same display name\n  verify-a:\n    name: Cloud source verified v1\n  verify-b:\n    name: Cloud source verified v1\n# after\n  verify:\n    name: Cloud source verified v1","handlingStrategy":"validation","validationCode":"const jobs = (await api(jobsPath)).jobs ?? [];\nconst matches = jobs.filter((j) => j.name === 'Cloud source verified v1');\nif (matches.length > 1) throw new Error('duplicate verification job name in workflow');","typeGuard":"function isUniqueJobName(jobs, name) {\n  return jobs.filter((j) => j.name === name).length === 1;\n}","tryCatchPattern":"try {\n  const proof = await readSourceVerification(sha, api);\n} catch (e) {\n  if (e.message.includes('job is ambiguous')) {\n    console.error('Two jobs named Cloud source verified v1 — fix cloud-readiness.yml.');\n  }\n}","preventionTips":["Grep .github/workflows/cloud-readiness.yml for the job display name before merging workflow edits; it must appear exactly once.","Never copy the verification job block without renaming the display name.","When invoking reusable workflows multiple times, give each a distinct job name.","Keep sourceVerificationJob and the workflow name in sync — both must change together."],"tags":["github-actions","workflow-config","ambiguity"],"backgroundTag":"internal-invariant-violation","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"}