# JiraPort L1/L2/L3 Assign 所有权切换方案

## 背景与结论

新的 Assign 服务负责：

- L1 Assign：`TCID`、`ST` Project；
- L2 Assign：`TCOC` Project；
- L3 Assign：`TCOC` Project。

切换完成后，新服务是上述 Project 和 Support Tier 的初始 Assign 决策及写入方。
这里的“Assign”指进入相应 Support Tier 时选择工程师并首次写入 L2 Assignee 或
标准 Assignee，以及该次 Assign 自身的失败重试/fallback。

JiraPort 停止这些初始 Assign，但继续负责已经确认保留的通知、Guardian、
backfill、report、客户专项 CC，以及 TCOC P0/P1 Assign 后未确认时的
`oncall-urgent` 运维升级。后者仍可能重新修改标准 Assignee，因此新服务不是所有
Jira 字段的唯一写入方，也不负责完整的告警升级生命周期。

JiraPort 的 `prod-tidbcloud` 配置还承载 `GTOC`、`O11Y`、`TCA` 等 Project，
因此不能全局删除 `assign`、`custom`、`escalate` Action，也不能在该实例中直接
关闭 `need-assign`。下线必须在 Project 路由层完成。

## 目标所有权

| Project | JiraPort 保留 | JiraPort 下线 |
| --- | --- | --- |
| `TCID`、`ST` | Assignee 变更通知、Guardian/backfill、CC | L1 Assign |
| `TCOC` | L2/L3 通知、`oncall-urgent` 运维升级、客户专项 CC、report/backfill | 初始 L2/L3 Assign 及其失败重试/fallback |
| `GTOC` | 现有 L2/L3 Assign 和通知 | 暂不修改 |
| `O11Y`、`TCA` | 现有告警 Assign、Escalate 和通知 | 暂不修改 |

## TCID/ST/TCOC 保留项说明

### TCID 保留项

#### ticket-cc

作用：当工单进入 `ESCALATE TO L2` 时通知 Ticket 团队，使人工协作链路知道工单
已经升级。它不选择 L1/L2 工程师，也不修改 Jira Assignee。

具体行为：

1. 接收 `jira:issue_updated`；
2. 要求 changelog 同时包含 `<status>` 和目标值 `<ESCALATE TO L2>`；
3. 创建或复用 `ticket-cc` Notification；
4. 使用 `oncall` 卡片模板向静态 `ticket` 群组发送一张飞书卡片；
5. Policy 进入 `Finished`，同一工单以后再次进入 `ESCALATE TO L2` 不会重新发送。

该流程不启用 `log_on_comment`，因此不会因为发送成功而在 Jira 增加通知记录评论。

#### ticket-notify-assignee

作用：把新的标准 Assignee 直接通知到被分配的工程师。新 L1 服务完成 Assign 后，
JiraPort 只消费结果并通知，不参与选人。

具体行为：

1. 接收 changelog 包含 `<assignee>` 的 `jira:issue_updated`；
2. JiraPort 根据 Atlassian Account ID 从 Engineer 数据中补全 Assignee Email；
3. `assignee` Receiver 使用该 Email 查找工程师及其飞书身份；
4. 使用 `oncall` 模板向该工程师发送飞书卡片；
5. 发送成功后在 Jira 写入一条包含通知对象和消息标识的评论；
6. 后续再次修改 Assignee 时会再次执行通知。

如果 Assignee 无法映射到 Engineer 数据，Receiver 为空，通知 Action 会失败，但不会
回写或改派 Jira Assignee。

#### guardian-jsm-ticket

作用：对配置白名单客户的新建 TCID 工单启动 Guardian 分析。这是诊断辅助能力，
与 L1 Assign 无关。

具体行为：

1. 仅处理 `jira:issue_created`，并要求 TiDB Cloud Org Asset 命中配置白名单；
2. 先执行 `backfill`；
3. 当前 `backfill` 只实现了 `O11Y` 和 `ST-` 分支，因此对 TCID 实际为空操作并
   直接返回成功；
4. 随后调用 DevOps Guardian `CreateAnalysis(issueKey)` 创建分析任务；
5. Guardian 配置为把分析结果回写为 Jira Comment；TCID 不上传诊断包附件；
6. JiraPort 再把返回的 Guardian Analysis UUID 写入 Jira Comment。

### ST 保留项

#### ticket-cc 与 ticket-notify-assignee

ST 与 TCID 使用相同的触发条件、飞书模板和状态机：

- `ticket-cc` 只通知 Ticket 群组，不修改 Assignee；
- `ticket-notify-assignee` 在标准 Assignee 变化后通知新 Assignee，并记录 Jira
  Comment；
- 同样依赖 Atlassian Account ID 到 Engineer Email/飞书身份的映射。

#### guardian-jsm-ticket

ST 与 TCID 使用相同的客户 Org Asset 白名单，但其 `backfill` 有实际行为：

1. 新建 ST 工单命中白名单后进入 `guardian-jsm-ticket`；
2. 如果 Jira 已有 Cluster Asset（`customfield_10111`）或 Cluster ID
   （`customfield_10088`），跳过 backfill；
3. 否则通过 DevOps 根据 ST Issue Key 查询 Cluster ID；
4. 查询到结果后，把 Cluster ID 回填到 Jira；
5. backfill 成功后创建 Guardian Analysis，并把 Analysis UUID 写入 Jira Comment。

如果 DevOps 查询 Cluster ID 失败，当前实现记录错误后仍把 backfill 视为成功，
因此仍会继续创建 Guardian Analysis；如果 Jira Cluster ID 回填失败，backfill
Action 返回错误，Guardian 不会继续执行。

### TCOC 保留项

#### oncall：标准 Assignee 普通通知

作用：通知新的标准 Assignee，作为新 L3 Assign 服务写入 Jira 后的结果通知。
它本身不修改 Assignee。

触发范围：

- 非 P0/P1：标准 Assignee 变化且工单未进入 Done；
- P0/P1：标准 Assignee 变化、工单未进入 Done，并且 `Alert Required=No` 或
  `Alarm=No`。

具体行为是通过 Assignee Email 解析飞书用户、发送 `oncall` 卡片，并把成功通知
记录到 Jira Comment。Notification 建立后，后续 Assignee 或适用的 Priority
变化也可能再次发送卡片。

#### l2-notify：非紧急 L2 通知

作用：通知写入 `customfield_10053` 的 L2 Assignee，不参与 L2 选人。

具体行为：

1. 接收非 P0/P1 工单的 `<L2 assignee>` changelog；
2. 从 `customfield_10053.emailAddress` 读取 L2 工程师 Email；
3. 从 Engineer 数据解析飞书身份；
4. 向该工程师发送 `oncall` 卡片；
5. 在 Jira Comment 中记录通知结果；
6. 以后再次修改 L2 Assignee 时重新发送。

因此，新 L2 服务如果不再写 Jira `customfield_10053`，该通知链路也不会触发。

#### l2-urgent-notify：P0/P1 L2 电话通知

作用：对 P0/P1 的 L2 Assignee 做重复电话提醒。该 Policy 不修改 Assignee，也不
自动升级到 L3。

具体行为：

1. `<L2 assignee>` 变化后立即向该 L2 工程师发送第一次飞书卡片和电话加急；
2. 等待 480 秒后发送第二次；
3. 再等待 480 秒后发送第三次；
4. 第三次完成后结束。

收到飞书确认、工单进入 In Progress/Done，或者 Priority 降为非 P0/P1 时，
Policy 会提前进入 `Finished`。

#### oncall-urgent：确认保留的 Assign 后升级链路

`oncall-urgent` 不是纯通知项。它处理 P0/P1、标准 Assignee 变化、工单未 Done，
且 `Alert Required` 和 `Alarm` 都不是 `No` 的工单。

当前行为包括：

1. 对当前标准 Assignee 进行多轮电话通知，每轮间隔 480 秒；
2. 连续未确认后执行 `escalate`，把 Jira Assignee 改为 Oncall Team Manager；
3. 通知新的 Assignee，继续等待确认；
4. 再次无法升级或确认时通知 `firemaster`，之后还可能通知静态 `siddon` Receiver；
5. 飞书确认、状态进入 In Progress 或 Priority 降级会终止/降级该流程。

当前状态机没有处理 `UpdatedStatusToDoneEvent`。虽然路由不会为已经 Done 的工单
新建 Policy，但运行中的工单后来进入 Done 时，不能保证现有 timeout 自动停止。

该链路确认保留，由 JiraPort 继续拥有“初始 L3 Assign 完成后，未确认时的电话
升级和重新指派”职责。新服务只负责初始 L3 Assign，不处理这条后续升级链路，
也不能因为 JiraPort 后续修改了 Assignee 而重新执行初始 Assign。

#### 客户专项 CC

TCOC 新建工单还存在多条与 Assign 无关的静态通知路由：

- 指定 Cluster ID：OSS、Certik、NFTScan、WeTech 等客户群组；
- 指定 Tenant ID：Japan Advocate 群组；
- 指定 Component、Priority、Visibility：TiDB X 内部 Oncall 群组。

这些路由确认保留。它们在 `jira:issue_created` 时向配置的静态飞书群发送一次卡片，
不选择工程师、不修改 L2 Assignee 或标准 Assignee。

#### generate-report：确认保留，但存在已知实现问题

作用上，它会在符合 Priority、Alert Required、Alarm 和 Status 条件的 TCOC 工单
创建时读取 Tenant ID/Cluster ID，调用 Clinic 生成报告，并把报告地址写入 Jira
Comment。

但当前 `ReportCommand` 执行 Cluster ID extractor 后错误地再次赋值给
`tenantID`，从未设置 `clusterID`。因此按当前代码会在
`cluster id or tenant id is empty` 处失败，无法生成报告。

该路由确认保留，Assign 切换不改变其触发条件；上述问题是现有独立缺陷，应另行
修复，不能把切换后的失败归因于新 Assign 服务。

#### backfill：确认保留，但当前对 TCOC 是空操作

TCOC 新建工单会命中 `backfill` 路由，但 `BackfillCommand` 只实现：

- `O11Y`：补 Tenant ID 和版本；
- `ST-`：补 Cluster ID。

TCOC 不命中任何实现分支，Action 直接返回成功，不读取或修改 Jira 字段。
该路由确认保留，Assign 切换不会影响它；如果期望它产生实际效果，需要另行定义
TCOC 要补的字段并实现对应分支。

## 新 Assign 对保留项的影响

总体结论：只要新系统仍通过 Jira 的既有字段完成 Assign，并保留现有 Jira Webhook
投递，确认保留的功能可以继续运行。需要满足的契约如下。

| Project | 保留项 | 新 Assign 的影响 | 必要条件 |
| --- | --- | --- | --- |
| `TCID` | `ticket-cc` | 无直接影响 | 新系统不改变 `ESCALATE TO L2` Status 流程和 changelog |
| `TCID` | `ticket-notify-assignee` | 新系统写入 Assignee 后会触发通知 | 写 Jira 标准 `assignee`；Account ID 能映射到 Engineer |
| `TCID` | Guardian | 无直接影响 | 保留 `issue_created` Webhook 和 Org Asset 字段 |
| `ST` | `ticket-cc` | 无直接影响 | 保留 Status 变更事件 |
| `ST` | `ticket-notify-assignee` | 新系统写入 Assignee 后会触发通知 | 写标准 `assignee`；保留 Assignee changelog |
| `ST` | Cluster ID backfill/Guardian | 无直接影响 | 保留 `issue_created`、Org Asset 和 DevOps/Jira 权限 |
| `TCOC` | `oncall` | 新 L3 Assign 会触发普通 Assignee 通知 | 写标准 `assignee`，并让 JiraPort 收到独立 changelog |
| `TCOC` | `l2-notify`/`l2-urgent-notify` | 新 L2 Assign 会触发卡片或电话通知 | 写 `customfield_10053`，字段 changelog 名仍为 `L2 assignee` |
| `TCOC` | `oncall-urgent` | 新 L3 Assign 会启动该升级状态机 | 新系统不得消费其后续 Assignee 变更并重新覆盖人员 |
| `TCOC` | 客户专项 CC | 无直接影响 | 保留 `issue_created` 及现有 Cluster/Tenant/Component 字段 |
| `TCOC` | report/backfill | 无直接影响 | 保留 `issue_created`；现有失败/空操作问题独立处理 |

### 字段写入契约

- TCID/ST L1 和 TCOC L3 必须写 Jira 标准 `assignee`，不能只更新新系统内部的
  Dispatch Context；
- TCOC L2 必须写 Jira `customfield_10053`，且 Jira Webhook changelog 的显示名
  仍需为 `L2 assignee`；
- Assignee 写入的 Atlassian Account ID 必须能在 Engineer 数据中解析出 Email 和
  飞书身份，否则 Assign 成功但 JiraPort 通知失败；
- Status、标准 Assignee、L2 Assignee 最好分开提交 Jira 更新。JiraPort 当前一次
  Webhook 只处理第一个匹配 Event，合并更新可能漏掉通知；
- 新系统不应以任意 Assignee 变更作为重新 Assign 的触发器。`oncall-urgent` 保留后
  会合法地修改标准 Assignee，新系统必须把这种变更视为后续运维升级结果。

### 不受 Assign 切换影响的写操作

保留项中仍存在非初始 Assign 的 Jira 写操作：

- `ticket-notify-assignee`、`oncall`、L2 通知会写 Jira Comment；
- ST backfill 会写 Cluster ID；
- Guardian 会创建分析并回写 Jira Comment；
- report 修复后会把报告地址写入 Jira Comment；
- `oncall-urgent` 会在未确认升级时重新写标准 Assignee。

这些写操作不应通过关闭整个 JiraPort Jira 凭据或写权限的方式下线。

## 当前 JiraPort 行为

### TCID/ST

`config/prod-global-ticket/policy-route.yml` 中的 `ticket-assign`、
`ticket-reassign` 和 `ticket-notify-l2` 路由已经被注释。当前仍启用：

- `ticket-cc`：Status 变为 `ESCALATE TO L2` 时发送通知；
- `ticket-notify-assignee`：标准 Assignee 变化时发送通知；
- `guardian-jsm-ticket`：指定客户的新建工单执行 Guardian/backfill。

因此 TCID/ST 当前没有活跃的 JiraPort L1 Assign 路由，但仍残留不可达的
Assign Policy 和 `need-assign: true` 配置。

### TCOC/GTOC L2

`config/prod-tidbcloud/policy-route.yml` 在 Priority 变化时创建：

- `l2-assign`；
- `l2-assign-direct-l3`。

两条链路都会先执行 `custom customfield_10053`，将 L2 工程师写入 Jira
`customfield_10053`（L2 Assignee）。

普通 `l2-assign` 失败后每 7200 秒重试，完成四轮重试后执行标准 `assign`
作为 L3 fallback。`l2-assign-direct-l3` 第一次 L2 失败后立即执行标准
`assign`。

### TCOC/GTOC L3

Status 变为 `ESCALATE TO L3` 时，`oncall-assign` 等待 5 秒后执行标准
`assign`，更新 Jira Assignee。

此外，P0/P1 的标准 Assignee 变化会进入 `oncall-urgent`。该 Policy 不只是
发送电话通知：连续未确认后会执行 `escalate`，再次修改 Jira Assignee。

## JiraPort 修改方案

### 1. 下线 TCID/ST L1 Assign

在 `config/prod-global-ticket` 中：

1. 保留 `ticket-cc`、`ticket-notify-assignee` 和 `guardian-jsm-ticket`；
2. 删除已经不可达的 `ticket-assign`、`ticket-reassign` Policy；
3. 删除对应的注释路由，避免以后误恢复；
4. 将 `config.yml` 中的 `need-assign` 设置为 `false`，作为实例级写保护；
5. 保留 Jira 凭据，因为通知 Action 的 `log_on_comment` 仍可能向 Jira 写评论。

### 2. 下线 TCOC L2/L3 Assign

在 `config/prod-tidbcloud/policy-route.yml` 中，将下列写操作路由的 Project
范围从：

```yaml
project: ^(TCOC|GTOC)$
```

收窄为：

```yaml
project: ^GTOC$
```

需要修改的路由：

- `oncall-assign`；
- `l2-assign`；
- 两条 `l2-assign-direct-l3`。

不要删除它们对应的 `policy.yml` 状态机、`l2-assign.yml`、`assign.yml`、
`assign-oncall.yml` 或 Go Action，因为 GTOC/O11Y/TCA 仍在使用共享能力。

### 3. 保留全部 TCOC Assign 结果通知

JiraPort 确认继续负责通知，保留 TCOC 的以下路由：

- `oncall`：标准 Assignee 变化后的普通通知；
- `l2-notify`：非 P0/P1 的 L2 Assignee 通知；
- `l2-urgent-notify`：P0/P1 的 L2 Assignee 电话通知。

新 Assign 服务必须继续写入：

- L2：Jira `customfield_10053`，Webhook changelog 字段名为
  `<L2 assignee>`；
- L3：Jira 标准 `assignee` 字段。

### 4. 保留 oncall-urgent 运维升级

TCOC 保留 `oncall-urgent`。JiraPort 继续负责 P0/P1 初始 L3 Assign 完成后的电话
通知、未确认升级、Manager 重新指派和 Firemaster/Siddon 通知。

新系统只负责初始 L3 Assign。它不能同时处理上述升级，也不能在检测到 JiraPort
修改 Assignee 后把人员恢复成初始 L3 Assignee，否则会形成覆盖循环。

## 上线切换顺序

仅修改路由只能阻止新建 Assign Notification，不能停止已经创建的状态机。
推荐按以下顺序切换：

1. 暂停新的 TCOC Assign 触发，或先停止 JiraPort Resolver；
2. 查询 TCOC 中 Policy 为 `oncall-assign`、`l2-assign`、
   `l2-assign-direct-l3` 的非终态 Notification；
3. 不终止 `oncall-urgent` Notification，它属于确认保留的运维升级链路；
4. 将旧 Assign Notification 终止，并处理其 Pending Action，确保旧 timeout
   不会再产生 `custom` 或初始 `assign`；
5. 部署收窄后的 JiraPort 路由并重启 API、Resolver；
6. 启用新的 Assign 服务；
7. 使用测试工单验证写入和通知；
8. 观察 JiraPort 日志：TCOC 不应再从 `oncall-assign`、`l2-assign*` 执行
   `assign`/`custom`；`escalate` 只允许来自保留的 `oncall-urgent`。

不要在 JiraPort 旧写链路仍运行时先启用新服务，否则 Priority/Status 事件可能
同时触发两个写入方。

## 验证矩阵

| 场景 | 预期结果 |
| --- | --- |
| TCID/ST 触发 L1 Assign | 新服务修改标准 Assignee；JiraPort 发送保留通知，不重新选人 |
| TCOC Priority 从 Undefined 变化 | JiraPort 不创建 `l2-assign*` Notification |
| TCOC L2 Assignee 更新 | JiraPort 不修改字段；按约定发送 L2 通知 |
| TCOC Status 变为 ESCALATE TO L3 | JiraPort 不创建 `oncall-assign` Notification |
| TCOC 标准 Assignee 更新 | JiraPort 发送通知；符合条件时创建/推进 `oncall-urgent` |
| TCOC P0/P1 未确认 | JiraPort `oncall-urgent` 执行后续升级；新系统不覆盖其结果 |
| TCOC 客户专项工单创建 | 原有静态客户群通知继续发送 |
| TCOC report/backfill | 保持当前行为；已知 report 失败和 backfill 空操作单独治理 |
| GTOC Priority/Status 变化 | 现有 JiraPort L2/L3 行为保持不变 |
| O11Y/TCA 告警创建 | 现有 Assign、Escalate 和通知保持不变 |

建议增加针对生产 `policy-route.yml` 的 Matcher 测试，至少覆盖 TCOC 不再命中
写路由、GTOC 继续命中写路由，以及 TCOC 仍能命中保留的通知路由。

## 风险清单

### 存量 Notification 与 Pending Action

Action 内容会保存到数据库。已经进入 `WaitTimeoutL2_*` 的 TCOC Notification
不会因为路由修改自动消失，未来仍可能重试 L2 或 fallback 到 L3。

### oncall-urgent 与新 Assign 的职责交界

`oncall-urgent` 名称看起来像通知，但其后续状态会执行 `escalate` 并修改标准
Assignee。该写入确认保留。风险不在于 JiraPort 执行它，而在于新系统如果监听
Assignee 变化并重新执行初始 L3 Assign，会和 JiraPort 形成覆盖循环。

### TCID/ST 重复通知

`ticket-cc` 和 `ticket-notify-assignee` 同时存在于 `prod-global-ticket` 和
`prod-tidbcloud`。需要核对 Jira Webhook 实际投递目标，确保同一个事件不会被
两个实例重复处理。

### 单个 Webhook 只处理一个 Event

JiraPort 的 Event Matcher 最终只采用第一个匹配结果。新服务不应在同一次 Jira
更新中同时修改 Status、Assignee 和 L2 Assignee，否则可能只处理其中一个事件并
漏掉通知。

### timeout 依赖进程存活

JiraPort 的 timeout 在 Resolver 进程内运行，启动时重试目前未启用。Pod 重启会
使 GTOC 等保留流程的长时间 timeout 失效，遗留 Pending Action。

### 配置所有权过宽

`prod-tidbcloud` 的 `need-assign: true` 同时服务多个 Project，不能作为 TCOC
单项目开关。后续新增写路由时必须检查 Project 范围，避免重新把 TCOC 纳入。

### 明文凭据

生产配置中存在 Jira、数据库及内部服务的明文凭据。应轮换现有凭据，并迁移到
Kubernetes Secret、AWS Secrets Manager 或等价的 Secret 管理机制。

## 相关文件

- `config/prod-global-ticket/policy-route.yml`
- `config/prod-global-ticket/policy.yml`
- `config/prod-global-ticket/config.yml`
- `config/prod-tidbcloud/policy-route.yml`
- `config/prod-tidbcloud/event-matcher.yml`
- `config/prod-tidbcloud/policy.yml`
- `pkg/resolver/handler/action/assign.go`
- `pkg/resolver/handler/action/custom.go`
- `pkg/resolver/handler/action/escalate.go`
- `pkg/resolver/handler/action/timeout.go`
- `pkg/resolver/handler/jira/jira.go`
- `pkg/db/notification.go`
