{"record":{"id":"4d4a494dbe6e5591","repo":"Hmbown/CodeWhale","slug":"refusing-to-replace-existing-assets-for-tag","errorCode":null,"errorMessage":"Refusing to replace existing assets for ${tag}: ${inventory}. A normal Release workflow rerun must never delete or overwrite public bytes.","messagePattern":"Refusing to replace existing assets for (.+?): (.+?)\\. A normal Release workflow rerun must never delete or overwrite public bytes\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"scripts/release/ensure-release-assets-absent.js","lineNumber":66,"sourceCode":"  } catch (error) {\n    throw new Error(`GitHub Release ${tag} returned invalid JSON: ${error.message}`);\n  }\n}\n\nfunction assertReleaseAssetsAbsent(release, tag) {\n  if (release === null) {\n    return;\n  }\n  if (!release || !Array.isArray(release.assets)) {\n    throw new Error(`GitHub Release ${tag} did not provide an asset inventory`);\n  }\n  if (release.assets.length === 0) {\n    return;\n  }\n\n  const names = release.assets.map((asset) => asset && asset.name).filter(Boolean);\n  const inventory = names.length > 0 ? names.join(\", \") : `${release.assets.length} unnamed asset(s)`;\n  throw new Error(\n    `Refusing to replace existing assets for ${tag}: ${inventory}. ` +\n      \"A normal Release workflow rerun must never delete or overwrite public bytes.\",\n  );\n}\n\nfunction main() {\n  if (process.argv.length !== 4) {\n    throw new Error(usage());\n  }\n  const repo = process.argv[2];\n  const tag = process.argv[3];\n  const release = fetchRelease(repo, tag);\n  assertReleaseAssetsAbsent(release, tag);\n  console.log(\n    release === null\n      ? `No existing GitHub Release assets found for ${tag}.`\n      : `Existing GitHub Release ${tag} has no assets; first upload may proceed.`,\n  );","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/release/ensure-release-assets-absent.js#L48-L84","documentation":"The immutability guard of ensure-release-assets-absent.js: the GitHub Release for this tag already has assets (names listed, or a count if unnamed), and a normal Release workflow rerun must never delete or overwrite public bytes that consumers may already have downloaded. The script aborts the pipeline instead of letting a rerun silently replace assets.","triggerScenarios":"Re-running the Release workflow after a partial failure where the upload step had already succeeded; re-running all jobs from scratch; the release having been created manually with assets beforehand.","commonSituations":"A release job failed at a late step (e.g. verification) after upload, and someone clicks 'Re-run all jobs'; retrying at workflow level rather than the single failed job; a re-cut of the same tag after a fix.","solutions":["Cut a new patch tag (vX.Y.(Z+1)) and release that — published bytes stay immutable by design","Re-run only the failed job if the upload step never executed in that attempt","If the assets are genuinely wrong and must be replaced, have a human deliberately delete them (gh release delete-asset OWNER/REPO TAG <asset> per file, or delete the release), acknowledging that consumers may hold the old bytes","Keep this script as a pre-upload gate in the release workflow so reruns stop here instead of overwriting"],"exampleFix":"# before: re-running the Release workflow for v1.2.3 that already uploaded assets\n\n# after: ship a new tag instead\ngit tag v1.2.4 && git push origin v1.2.4\n# workflow releases v1.2.4; guard passes for the new tag","handlingStrategy":"validation","validationCode":"#!/usr/bin/env bash\n# pre-check before any upload step: refuse to touch a tag that already has assets\nassets=$(gh api \"repos/$REPO/releases/tags/$TAG\" --jq '.assets | length' 2>/dev/null || echo 0)\nif [ \"$assets\" != \"0\" ]; then\n  echo \"refusing: $TAG already has $assets asset(s); cut a new tag\" >&2\n  exit 1\nfi","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run ensure-release-assets-absent.js as a mandatory gate immediately before the upload step of every release workflow","On partial failures, re-run only the failed job — never 'Re-run all jobs' once upload has executed","Treat this error as a stop, not an obstacle: the fix is a new version tag, not deleting public bytes","Manual asset deletion (gh release delete-asset) must be a documented, human-approved exception"],"tags":["release","github","immutability","safety","ci"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}