"""Correct the admin-facing ``software_seat.idle_reclaim`` description. Data-only. The job's reclaim step changed (same PR): instead of removing the single bound product group and punting multi-group holders to manual review, it now enqueues a ``target_type='product_access'`` provisioning task (the same pipeline self-cancel / admin-revoke use) which strips the holder from EVERY group conferring the product — multi-group holders (``jira-dev-engineers`` / ``RD Leader``) are genuinely reclaimed, with sibling add-on cascade and revert-on-failure from the pipeline. The ``scheduled_jobs.description`` row seeded by migration 204 still describes the old semantics ("多组授权一律不自动回收"), which is now an admin-facing lie. Mirrors the 209 precedent (same table, same reason): WHERE-guarded to the stale seeded text so an admin edit is never clobbered; reversible. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "276" down_revision = "275" branch_labels = None depends_on = None _JOB = "software_seat.idle_reclaim" # Matches both the 204-seeded text and the handler-registry copy — the # stale claim is the same in both: multi-group holders are never # auto-reclaimed. The guard keys on that claim. _STALE_MARKERS = ("%多组授权一律不自动回收%", "%多个组拿到同一产品一律不自动回收%") _NEW_DESCRIPTION = ( "每天 UTC 02:00 扫描 Helix 跟踪的 Jira Software / JSM / Confluence " "active 坐席,交叉 Atlassian 生产站 last_active(每人每产品取生产站 " "MAX,排除 sandbox / 外部站点)。idle_days>阈值(默认90)且 " "last_active 非空 → 警告 + 缓冲(默认14天)+ 自助『保留坐席』;" "缓冲后仍闲置且未保留 → 入队 product_access 撤销任务(与自助退订/" "管理员撤销同一管道):从该产品的全部授权组移除(applicationrole ∪ " "admin-declared,等价 Atlassian 后台 Remove access,多组持有者一并" "真实回收;绝不停账号、不动身份组和其他产品)+ 兄弟 add-on 座席级联 " "+ 失败自动回滚座席并 DM 管理员。护栏:管理员、离职员工、last_active " "为空一律不自动回收;seat_revocable=false 的 Marketplace add-on plan " "整体跳过。天数按员工本地时区算。幂等。" ) # The pre-275 seeded text (204), restored verbatim on downgrade. _OLD_DESCRIPTION = ( "每天 UTC 02:00 扫描 Helix 跟踪的 Jira Software / JSM / Confluence " "active 坐席,交叉 Atlassian 生产站 last_active(每人每产品取生产站 " "MAX,排除 sandbox / 外部站点)。idle_days>阈值(默认90)且 " "last_active 非空 → 警告 + 缓冲(默认14天)+ 自助『保留坐席』;" "缓冲后仍闲置 → 移除该产品组(不停账号)+ 通知 + 审计。护栏:管理员、" "离职员工、last_active 为空、多组授权一律不自动回收。默认 enabled=0 " "— 生产 worker 确认 Org Admin API product_access JSON 形状后再开启。" ) def upgrade() -> None: bind = op.get_bind() for marker in _STALE_MARKERS: bind.execute( sa.text( "UPDATE scheduled_jobs SET description = :d " "WHERE name = :n AND description LIKE :m" ), {"d": _NEW_DESCRIPTION, "n": _JOB, "m": marker}, ) def downgrade() -> None: bind = op.get_bind() bind.execute( sa.text( "UPDATE scheduled_jobs SET description = :d " "WHERE name = :n AND description = :cur" ), {"d": _OLD_DESCRIPTION, "n": _JOB, "cur": _NEW_DESCRIPTION}, )