{"record":{"id":"25aafb5c73787b59","repo":"paperclipai/paperclip","slug":"cloud-readiness-requires-positive-finite-timeout-and-poll","errorCode":null,"errorMessage":"Cloud readiness requires positive finite timeout and poll interval.","messagePattern":"Cloud readiness requires positive finite timeout and poll interval\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloud-readiness.mjs","lineNumber":70,"sourceCode":"      \"--cert-identity\", `https://github.com/${repository}/${workflow}@refs/heads/master`,\n      \"--deny-self-hosted-runners\"], { stdio: \"inherit\", timeout: 60_000 });\n  } finally { rmSync(scratch, { recursive: true, force: true }); }\n}\n\n/** Read-only availability gate. Deployment still resolves and pins artifacts. */\nexport async function waitForCloudArtifacts(sha, {\n  fetchImpl = fetch,\n  token = process.env.GH_TOKEN,\n  verifyProvenance = verifyManifestProvenance,\n  now = () => performance.now(),\n  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n  timeoutMs = 30 * 60_000,\n  intervalMs = 20_000,\n  log = console.log,\n} = {}) {\n  const version = versionFor(sha);\n  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isFinite(intervalMs) || intervalMs <= 0) {\n    throw new Error(\"Cloud readiness requires positive finite timeout and poll interval.\");\n  }\n  const deadline = now() + timeoutMs;\n  let previous;\n  let missing = [\"image\", \"migrator\"];\n  while (now() < deadline) {\n    // Recheck the image and exact-source publisher on the successful poll.\n    // Only a missing/pending producer waits; failed publication fails closed.\n    const results = await Promise.all([\n      imageExists(sha, fetchImpl),\n      migratorPublished(sha, fetchImpl, token),\n    ]);\n    missing = [\"image\", \"migrator\"].filter((_, index) => !results[index]);\n    if (missing.length === 0) {\n      // Verify the exact signed bytes and all pinned downloads after the\n      // publisher succeeds. An inaccessible or corrupt artifact cannot pass.\n      await verifyPublished(sha, fetchImpl, { verifyProvenance });\n      log(`Cloud artifacts available for ${sha}: verified image and exact-source migrator ${version}.`);\n      return { version: 1, sha, packageVersion: version };","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-readiness.mjs#L52-L88","documentation":"waitForCloudArtifacts validates its timeoutMs and intervalMs options before starting the poll loop; both must be finite positive numbers. Non-finite (NaN/Infinity), zero, or negative values throw immediately.","triggerScenarios":"Calling waitForCloudArtifacts(sha, { timeoutMs: 0 }) or intervalMs: -1000, or passing NaN (e.g. Number(process.env.TIMEOUT) with an empty env var), or Infinity.","commonSituations":"Parsing env vars with Number()/parseInt on missing values yielding NaN; config wiring passing seconds where milliseconds are expected as 0; unit tests passing sentinel values.","solutions":["Pass positive finite numbers: timeoutMs defaults to 1,800,000 (30 min) and intervalMs to 20,000 (20s).","Guard env-derived values: validate with Number.isFinite(v) && v > 0 before passing.","If reading from env, default when empty: const t = Number(process.env.TIMEOUT_MS) || 30*60_000."],"exampleFix":"// before\nconst timeoutMs = Number(process.env.CLOUD_TIMEOUT_MS); // NaN when unset\n// after\nconst timeoutMs = Number(process.env.CLOUD_TIMEOUT_MS) || 30 * 60_000;\nif (!(Number.isFinite(timeoutMs) && timeoutMs > 0)) throw new Error(\"CLOUD_TIMEOUT_MS must be positive\");","handlingStrategy":"validation","validationCode":"const opts = {};\nif (process.env.CLOUD_TIMEOUT_MS) opts.timeoutMs = Number(process.env.CLOUD_TIMEOUT_MS);\nif (process.env.CLOUD_INTERVAL_MS) opts.intervalMs = Number(process.env.CLOUD_INTERVAL_MS);\nfor (const [k, v] of Object.entries(opts)) {\n  if (!Number.isFinite(v) || v <= 0) throw new Error(`${k} must be a positive finite number, got ${v}`);\n}\nawait waitForCloudArtifacts(sha, opts);","typeGuard":"const positiveFinite = (v) => typeof v === \"number\" && Number.isFinite(v) && v > 0;","tryCatchPattern":"try {\n  await waitForCloudArtifacts(sha, opts);\n} catch (error) {\n  if (/positive finite timeout/.test(error.message)) {\n    console.error(\"Bad poll options — use e.g. { timeoutMs: 1800000, intervalMs: 20000 }.\");\n    process.exitCode = 1;\n  } else throw error;\n}","preventionTips":["Never pass raw Number(envVar) without a fallback for missing values","Keep units explicit (ms) in option names","Write a unit test covering NaN/0/negative options"],"tags":["configuration","validation"],"backgroundTag":"invalid-argument-value","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"}