{"record":{"id":"a9050e041d9007d5","repo":"koala73/worldmonitor","slug":"cloudflare-rules-changed-after-planning-run-plan-again","errorCode":null,"errorMessage":"Cloudflare rules changed after planning. Run --plan again before applying.","messagePattern":"Cloudflare rules changed after planning\\. Run --plan again before applying\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloudflare-agent-readiness.mjs","lineNumber":57,"sourceCode":"    }\n  }\n\n  return changes;\n}\n\nexport async function runAgentReadiness(mode, { env = process.env, fetchImpl } = {}) {\n  if (!['--plan', '--check', '--apply'].includes(mode)) throw new Error('Use --plan, --check, or --apply');\n  const token = resolveToken(env);\n  const zoneId = await resolveZoneId(token, { env, fetchImpl });\n  const read = (phase) => cloudflareRequest(`/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`, { token, fetchImpl });\n  const firewall = await read(FIREWALL_PHASE);\n  const changes = planAgentReadiness(firewall);\n  if (mode !== '--apply' || changes.length === 0) return { zone: 'worldmonitor.app', ready: changes.length === 0, changes };\n\n  for (const change of changes) {\n    const current = await read(change.phase);\n    if (!isDeepStrictEqual(current.rules, firewall.rules)) {\n      throw new Error('Cloudflare rules changed after planning. Run --plan again before applying.');\n    }\n    const updated = await cloudflareRequest(`/zones/${zoneId}/rulesets/${change.rulesetId}/rules/${change.ruleId}`, {\n      token, fetchImpl, method: change.method, body: change.body,\n    });\n    firewall.rules = updated.rules;\n  }\n  const remaining = planAgentReadiness(await read(FIREWALL_PHASE));\n  if (remaining.length) throw new Error('Cloudflare verification failed after apply');\n  return { zone: 'worldmonitor.app', ready: true, applied: changes.map((change) => change.description) };\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n  const mode = process.argv[2];\n  if (process.argv.length !== 3 || !['--plan', '--check', '--apply'].includes(mode)) {\n    console.error('Usage: node scripts/cloudflare-agent-readiness.mjs --plan|--check|--apply');\n    process.exitCode = 1;\n  } else {\n    loadEnvFile(import.meta.url, { only: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ALL_ACCESS_TOKEN', 'CLOUDFLARE_ZONE_ID'] });","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/cloudflare-agent-readiness.mjs#L39-L75","documentation":"During --apply, runAgentReadiness re-reads each phase's ruleset right before writing and compares it deep-strictly against the ruleset snapshot taken during planning. If the rules changed between plan and apply, the plan is stale, so it throws instead of applying blind edits. This is an optimistic-concurrency guard against lost updates.","triggerScenarios":"Another operator or automation modifies any rule in the phase between the initial plan read and the apply-time re-read, so the freshly fetched rules array differs from the planned snapshot.","commonSituations":"A teammate editing firewall rules in the Cloudflare dashboard while the script runs, Terraform/CI concurrently reconciling rules, or two apply runs racing.","solutions":["Re-run the script with --plan to take a fresh snapshot, then run --apply again.","Coordinate with whoever changed the rules and ensure no concurrent automation is touching the phase during apply.","If changes keep racing, serialize applies through a single pipeline or lock."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// re-plan immediately before apply and bail if the plan already differs from the last snapshot\nconst fresh = planAgentReadiness(await readFirewall(token));\nif (JSON.stringify(fresh) !== JSON.stringify(lastPlan)) {\n  console.error('plan is stale; re-run --plan');\n  process.exit(2);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await runAgentReadiness('--apply', { env, fetchImpl });\n} catch (e) {\n  if (e.message.includes('changed after planning')) {\n    console.error('Concurrent Cloudflare edit detected; re-running plan/apply once.');\n    await runAgentReadiness('--plan', { env, fetchImpl });\n    await runAgentReadiness('--apply', { env, fetchImpl });\n    return;\n  }\n  throw e;\n}","preventionTips":["Freeze manual dashboard edits during automated apply windows.","Run applies from a single serialized pipeline.","Keep plan→apply latency short (run them back-to-back).","Subscribe to Cloudflare audit/change notifications for the zone."],"tags":["cloudflare","race-condition","stale-plan"],"backgroundTag":"conflicting-config-options","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}