"""Seed the ``cmdb.decommission_reconcile`` cron. Revision ID: 589 Revises: 588 ``decommission_scope`` is cloud-first, so the *submission* of a closure is honest. The *outcome* was not tracked at all: every cloud finishes the close asynchronously over weeks (AWS ~90d ``PENDING_CLOSURE``, GCP ~30d ``DELETE_REQUESTED``, Azure ~90d ``Disabled``, Aliyun check → silence period), and Helix wrote ``status='closed'`` at hour zero and never looked again. Three outcomes lived in that blind spot, two of them harmful: * the closure **fails** (Aliyun ``CheckFailed`` / ``DeleteFailed`` stop dead, no retry) — account alive and billable, Helix says closed; * a human **undoes** it — ``projects.undelete`` and Azure subscription re-activation are self-service for the whole grace window and tell Helix nothing. Helix has already disabled every grant on that scope and every sweep skips closed scopes, so the account comes back with no owner, no grants, no idle reminder, and no cost attribution. Both are the same failure the 2026-09-01 Aliyun fix addressed — Helix asserting a closure the cloud does not agree with — arriving weeks later by a different route. **No DDL.** The reconciler reads and writes columns that already exist (``decommission_provider_state``, ``updated_at``); this migration only creates the cron row. 04:20 local: after the nightly cost/sync crons have settled and well clear of the 01:30 leaver sweep, so a slow provider read cannot crowd them. ``limit`` is exposed in ``config`` because the per-run coverage cap is the one knob worth turning without a deploy — the in-flight set is normally single digits, but a bulk decommission could put a hundred rows in it at once. """ from __future__ import annotations import json import uuid import sqlalchemy as sa from alembic import op revision = "589" down_revision = "588" branch_labels = None depends_on = None _JOB = "cmdb.decommission_reconcile" def upgrade() -> None: bind = op.get_bind() exists = bind.execute( sa.text("SELECT 1 FROM scheduled_jobs WHERE name = :name LIMIT 1"), {"name": _JOB}, ).scalar() if exists: return bind.execute( sa.text( """ INSERT INTO scheduled_jobs (id, name, display_name, description, handler_key, trigger_type, interval_seconds, cron_expr, timezone, enabled, config, run_count, failure_count, created_at, updated_at) VALUES (:id, :name, :display_name, :description, :name, 'cron', NULL, '20 4 * * *', 'Asia/Shanghai', 1, :config, 0, 0, UTC_TIMESTAMP(), UTC_TIMESTAMP()) """ ), { "id": str(uuid.uuid4()), "name": _JOB, "display_name": "已下线云账号的云端终态对账", "description": ( "每日复核 Helix 已标记 closed、但云端关闭仍在进行中的 scope," "把云端当前说法写回 decommission_provider_state;" "关闭失败或账号又活了则写审计并告警管理员。" "只读云端,永不自动改回 status。" ), "config": json.dumps({"limit": 200}), }, ) def downgrade() -> None: op.get_bind().execute( sa.text("DELETE FROM scheduled_jobs WHERE name = :name"), {"name": _JOB}, )