{"record":{"id":"d916cc0ea1cd465a","repo":"paperclipai/paperclip","slug":"immutable-campaign-campaign-campaignid-already","errorCode":null,"errorMessage":"Immutable campaign ${campaign.campaignId} already exists with a different digest","messagePattern":"Immutable campaign (.+?) already exists with a different digest","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":500,"sourceCode":"  );\n  const history = await enrichProtocolEvalHistory(mergedHistory, {\n    currentCampaign: campaign,\n    loadCampaign: async (id) => {\n      if (!SAFE_CAMPAIGN.test(id)) throw new Error(\"Unsafe historical campaign ID\");\n      return downloadJson(validatedDestination.bucket,\n        `${validatedDestination.prefix}/campaigns/${id}/campaign.json`,\n        join(temporary, `${id}.json`));\n    },\n  });\n  const campaignPrefix = `${validatedDestination.prefix}/campaigns/${campaign.campaignId}`;\n  const manifestKey = `${campaignPrefix}/bundle-manifest.json`;\n  const existing = await downloadJson(\n    validatedDestination.bucket,\n    manifestKey,\n    join(temporary, \"existing-manifest.json\"),\n  );\n  if (existing && existing.bundleDigest !== manifest.bundleDigest) {\n    throw new Error(\n      `Immutable campaign ${campaign.campaignId} already exists with a different digest`,\n    );\n  }\n  if (!existing) {\n    await uploadImmutableReport(\n      validatedDestination.bucket,\n      campaignPrefix,\n      reportRoot,\n    );\n    const manifestFile = join(temporary, \"bundle-manifest.json\");\n    await writeFile(manifestFile, json(manifest));\n    await uploadFile(\n      validatedDestination.bucket,\n      manifestKey,\n      manifestFile,\n      \"public,max-age=31536000,immutable\",\n    );\n  }","sourceCodeStart":482,"sourceCodeEnd":518,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L482-L518","documentation":"Published campaign bundles are immutable: once bundle-manifest.json exists in S3 for a campaign, republishing must produce the identical bundleDigest (a SHA-256 over the sorted file path/sha256/bytes entries). If the existing manifest's digest differs, the publisher throws instead of overwriting immutable public content. This catches any change to the report for the same campaignId.","triggerScenarios":"Regenerating the report for the same GHA run/attempt with any changed file (different totals, viewer assets, timestamps embedded in HTML); publishing from a different machine/toolchain that produces byte-different assets for the same campaign; a partially failing upload leaving a manifest for a different bundle version.","commonSituations":"Re-running the publish step on the same workflow run after code changed between attempts with the same attempt number; fixing a typo in generated HTML and re-publishing the same campaign ID; using a different viewer build than the original publication.","solutions":["Publish the corrected report under a new campaignId (new run/attempt or -report-<rev> suffix)","Reproduce the exact original inputs/toolchain so the bundle digest matches byte-for-byte","Delete the S3 campaign prefix (campaigns/<id>/) intentionally if you truly must replace it, understanding this breaks public immutability","Diff the existing manifest's files array against the new manifest to see which file changed before deciding"],"exampleFix":"// before: same id, edited HTML\n// campaign.json: \"campaignId\": \"gha-5-1\"  (re-published after edit)\n// after: distinct revision id\n// campaign.json: \"campaignId\": \"gha-5-1-report-fix1\"","handlingStrategy":"validation","validationCode":"const existing = JSON.parse(await downloadManifest(campaignId)); // null if absent\nconst fresh = buildManifest(reportRoot, campaignId);\nif (existing && existing.bundleDigest !== fresh.bundleDigest)\n  throw new Error(`bundle for ${campaignId} changed: pick a new campaignId`);","typeGuard":null,"tryCatchPattern":"try {\n  await publishProtocolEvalHistory({ reportRoot, destination, viewerRoot });\n} catch (e) {\n  if (String(e.message).includes(\"already exists with a different digest\")) {\n    console.error(\"compare existing bundle-manifest.json files[] to your new manifest to find the changed file; republish under a new id\");\n  }\n  throw e;\n}","preventionTips":["Never republish a campaignId with modified report content; suffix a new -report-<rev> ID","Compare old vs new bundle manifests (path/sha256) to identify what changed before republishing","Build reports reproducibly; pin the viewer build used for publication"],"tags":["immutability","s3","digest","publishing"],"backgroundTag":"checksum-mismatch","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}