# DeliverKernelImage 增加 AWS BYOC 镜像同步 Action

| 项目 | 内容 |
| --- | --- |
| 状态 | Draft for review |
| 日期 | 2026-09-04 |
| 范围 | AWS BYOC、Kernel Image Delivery、DevOps Operation、GitHub Actions |
| 相关仓库 | `tidbcloud/devops`、`tidbcloud/docker-image-controller` |

## Introduction

本文将 AWS BYOC 镜像同步纳入 DevOps 的 `DeliverKernelImage` Operation。在 `tidbcloud/docker-image-controller` 新增一个按需 GitHub workflow：调用方直接传入本次需要同步的完整镜像信息（source registry、repository、tag 和期望 digest），workflow 只从 `byoc-targets-prod.yaml` 读取全部生产目标，并复用现有 runner 身份和客户 AWS Role 完成复制与 digest 校验。新 workflow 不读取 `image-sync-byoc-prod.yaml`，也不通过 tag 反向扫描或猜测 repository。

DevOps 在现有 `DeliverKernelImage` Action 之后增加自动 Action `SyncBYOCImage`。该 Action 只负责 dispatch workflow、持久化 run 信息并轮询结果，不读取客户列表、不 assume 客户 Role，也不在 DevOps worker 内复制镜像。生产 delivery batch 只有在 FID 和 BYOC 同步均成功后才能进入 `AVAILABLE`。

```text
ResolveKernelImageDigest（可选）
        ↓
DeliverKernelImage（FID）
        ↓
SyncBYOCImage（dispatch + wait GitHub workflow）
        ↓
Kernel release AVAILABLE
```

本方案替代此前“在 `UpdateNgGlobalComponentImage` 中由 DevOps worker 直接 assume role 并复制镜像”的方案。

## Motivation or Background

### 故障背景

2026-07-30，Operation 38525 将 PD 默认镜像更新为 `v26.3.4-nextgen`，但当时 `image-sync-byoc-prod.yaml` 没有允许该 tag。已有 BYOC 定时同步没有把镜像复制到客户 ECR，而 Config Center 已将该镜像作为默认值，新建 BYOC 集群因而无法拉取 PD 镜像。

根因是正式镜像交付与 BYOC 镜像可用性之间没有同一条发布门禁：

```text
正式镜像已交付并可被选为默认镜像
BYOC 客户 Registry 尚无对应镜像
```

### 当前实现

DevOps 当前为一个 kernel image delivery batch 创建以下 Action：

```text
ResolveKernelImageDigest（source digest 缺失时）
DeliverKernelImage
```

`DeliverKernelImage` 通过 FID 提交 batch 中的所有 source image，并轮询 FID 结果。FID 成功后，当前实现会立即把 batch 中的 release 标记为 `AVAILABLE`。

`docker-image-controller` 当前已有 `image-sync-byoc-prod.yml`：

- `push main`、每 4 小时定时或手工触发；
- 从 `byoc-targets-prod.yaml` 读取目标 Registry 和 `roleArn`；
- 从 `image-sync-byoc-prod.yaml` 读取 repository sets 以及 tag allow/deny；
- 手工触发可用 `target-name` 缩小目标 Registry，但仍同步同一整套配置；
- runner 通过当前 Web Identity 链路 assume 源 ECR Role 和客户 `auto-deploy-sync-image` Role；
- 使用 `regsync-cloud` 将镜像复制到客户 ECR。

现有定时 workflow 适合 reconciliation，但不适合作为单次 kernel release 的同步门禁：它依赖静态 allowlist、同步范围过大，而且 DevOps 无法将一次 release 与某次 workflow run 一一关联。

### Goals

- 在 `DeliverKernelImage` 生产 Operation 内增加 BYOC 同步门禁。
- 新 workflow 接收明确的完整 source image 和期望 digest，扫描 `byoc-targets-prod.yaml` 并同步到文件中的全部目标。
- 按需同步完全不依赖 `image-sync-byoc-prod.yaml` 及其 tag allowlist。
- DevOps 只 dispatch 和跟踪 workflow；客户列表、云身份和复制逻辑继续由 `docker-image-controller` 管理。
- 复用当前 GitHub runner 的 Google Web Identity 与客户 AWS Role 信任关系，不修改客户侧 IAM。
- 每个目标的目标 manifest digest 必须与源 digest 一致；任一目标失败则 delivery 失败。
- 支持一个 delivery batch 内包含多张镜像、进程重启、重复轮询和可审计的 workflow run URL。
- 保留现有 `image-sync-byoc-prod.yml` 作为定时 reconciliation/backfill。

### Non-goals

- 不在 `UpdateNgGlobalComponentImage` 中新增同步 Action。
- 不让 DevOps worker 读取 `byoc-targets-prod.yaml`、获取 runner GSA 凭证或 assume 客户 AWS Role。
- 不实现 Customer-pull；集群节点仍从客户 ECR 拉取镜像。
- 不修改现有客户 Role trust policy。
- 不替换现有 BYOC 定时全量同步 workflow。
- 本期只覆盖 `byoc-targets-prod.yaml` 对应的 AWS BYOC 生产目标，不扩展到 Azure/GCP BYOC。

## Detailed Design

### Design Decisions

| 主题 | 决策 |
| --- | --- |
| 触发点 | 在生产 `DeliverKernelImage` Operation 中，将 `SyncBYOCImage` 放在现有 `DeliverKernelImage` 之后。 |
| 执行边界 | DevOps 只 dispatch/poll；`docker-image-controller` 扫描目标、assume role、复制并校验。 |
| workflow | 新增 `.github/workflows/image-sync-byoc-on-demand-prod.yml`，不直接复用定时 workflow 的触发入口。 |
| 输入 | 每次 dispatch 整个 batch 的 `images` manifest；每项包含完整中央 ECR image ref 和期望 digest，并附带 `correlation-id`。 |
| batch 多镜像 | Action 从所有 release 的 FID 结果中选择中央生产 ECR image，稳定排序后一次 dispatch；不能只使用 operation 中第一张镜像。 |
| 镜像集合 | 只同步 `images` manifest 明确列出的镜像，不扫描任何 repository inventory，也不按 tag 扩展集合。 |
| 目标集合 | 每次 run 固定扫描当次 checkout revision 的全部 `byoc-targets-prod.yaml`，不接受 `target-name`。 |
| allowlist | 新 workflow 完全不读取或修改 `image-sync-byoc-prod.yaml`；该文件仅服务原有定时全量同步。 |
| 身份 | 复用现有 GitHub runner、`AWS_WEB_IDENTITY_TOKEN_FILE`、源 ECR Role 和客户 `roleArn`。 |
| 成功条件 | 唯一 workflow run 成功，且 manifest 中每张镜像在每个目标上的 digest 都与期望 digest 一致。 |
| 可用状态 | FID 成功只记录 FID 结果并保持 `SYNCING`；BYOC Action 成功后才标记 batch `AVAILABLE`。 |
| 适用环境 | 只对 `LocalDevOpsEnvironment == prod` 的 delivery batch 追加 Action；dev 链路保持现状。 |

### End-to-End Flow

```mermaid
flowchart LR
    A[Kernel build callback] --> B[Resolve digest optional]
    B --> C[DeliverKernelImage via FID]
    C --> D{FID succeeded?}
    D -->|No| X[Batch SYNC_FAILED]
    D -->|Yes| E[Keep batch SYNCING]
    E --> F[SyncBYOCImage builds exact images manifest]
    F --> G[Dispatch one workflow for the batch]
    G --> H[Workflow validates images and scans all BYOC targets]
    H --> I[Runner assumes each customer AWS Role]
    I --> J[Copy and verify digest]
    J --> K{Workflow and all targets succeeded?}
    K -->|No| X
    K -->|Yes| L[Batch AVAILABLE]
```

### New GitHub Workflow

#### Interface

在 `tidbcloud/docker-image-controller` 新增：

```yaml
name: Image-Sync-BYOC-AWS-On-Demand-Prod
run-name: BYOC sync ${{ inputs.correlation-id }}

on:
  workflow_dispatch:
    inputs:
      images:
        description: JSON array of exact source images and expected digests
        required: true
        type: string
      correlation-id:
        description: DevOps delivery batch correlation ID
        required: true
        type: string
```

约束：

- `images` 是 JSON 数组，每项包含 `source` 和 `digest`。`source` 必须是完整的中央生产 ECR `<registry>/<repository>:<tag>`，`digest` 必须是 `sha256:<64 hex>`；
- source registry 必须属于 workflow 内置或环境配置的可信中央 ECR allowlist，不能通过输入指定任意 Registry；
- `correlation-id` 使用 `DeliveryBatchUID`，用于 run-name、日志、检索与审计；
- workflow 固定为 prod，调用方不能传 target Registry、Role ARN、源 Role 或配置文件路径；
- 不提供 `target-name`。按需自动触发必须覆盖 target 文件中的全部目标，避免发布门禁只验证单个客户；
- dispatch 固定使用默认分支 `main`，run 日志记录 checkout commit SHA、输入镜像数量和目标数量。

输入示意：

```json
[
  {
    "source": "<central-prod-ecr>/tidbcloud/pd:v26.3.4-nextgen",
    "digest": "sha256:<expected-digest>"
  }
]
```

#### Direct Image Mapping

新 workflow 不做“tag → repository”发现。镜像集合由 `DeliverKernelImage` 的 FID 结果明确给出：FID 已把原始 GCR image 同步到中央生产 ECR，并返回目标 image URL；`SyncBYOCImage` 从每个 release 的 `DeliveryTargetImages` 中选择唯一的中央生产 ECR image，与该 release 的 `SourceDigest` 组成 `images` manifest。

例如：

```text
Build/FID input:       us.gcr.io/.../tidbx/pd:v26.3.4-nextgen
FID AWS target:        <central-prod-ecr>/tidbcloud/pd:v26.3.4-nextgen
BYOC workflow input:   { source: <FID AWS target>, digest: <release source digest> }
Customer destination:  <customer-ecr>/tidbcloud/pd:v26.3.4-nextgen
```

客户目标 repository 由 source image 去掉 registry hostname 后直接得到，因此不存在 basename 猜测：

```text
source      = <central-prod-ecr>/tidbcloud/pd:v26.3.4-nextgen
repository  = tidbcloud/pd
tag         = v26.3.4-nextgen
destination = <target-registry>/tidbcloud/pd:v26.3.4-nextgen
```

校验规则：

1. `images` 必须是非空 JSON 数组，镜像项不得重复；
2. source 必须包含 registry、非空 repository 和精确 tag，不接受 tag-only、digest-only 或通配表达式；
3. source registry 必须是允许的中央生产 ECR；
4. repository 必须满足 ECR repository name 规则，tag 必须满足 OCI tag 规则；
5. workflow 使用源 ECR Role 读取 source manifest，并确认其 digest 与输入 `digest` 完全一致；
6. 任一镜像非法、缺失或 digest 已漂移时，整个 workflow 在访问客户账户前失败；
7. 不读取、生成或提交 `image-sync-byoc-prod.yaml`。

伪代码：

```text
images = parseAndValidate(input.images)
for image in images:
    assert image.registry in allowedCentralSourceRegistries
    assert resolveDigest(image.source) == image.digest
    image.destinationRepository = repositoryPath(image.source)

targets = validateAll(byoc-targets-prod.yaml)
syncEveryImageToEveryTarget(images, targets)
```

所有目标收到的集合就是输入 manifest 中的集合，不会因为其他 repository 恰好存在同名 tag 而被扩大，也不会因为镜像未登记到 `image-sync-byoc-prod.yaml` 而漏掉。

#### Target Matrix and Sync

`prepare` job 完成以下工作：

- checkout repository 并记录 commit SHA；
- 解析并校验输入 `images` manifest；
- 使用源 ECR Role 确认每张 source image 的实际 digest 等于期望 digest；
- 校验 `byoc-targets-prod.yaml` 的全部记录；
- 输出已验证的 images 和 target matrix。

目标校验采用 fail-closed：

- target Registry 必须是合法 AWS ECR hostname；
- Registry account 与 `roleArn` account 必须一致；
- region 必须与 Registry hostname 一致；
- Registry 不得重复；
- 任一记录格式错误时整个 workflow 失败，不能静默跳过该目标；
- target 数量为零时生产 workflow 失败。

`sync-images` job 使用 `strategy.fail-fast: false` 和受控 `max-parallel` 对全部目标执行：

1. 登录源 Registry；
2. 设置目标 `AWS_ROLE_ARN` 和 region，沿用 runner 的 Web Identity token；
3. 登录目标 ECR；
4. 对 manifest 中的每张 image，按其 repository path 创建缺失的 destination repository；
5. 使用 digest-pinned source 将该 image 复制到相同 repository path 和 tag；
6. 对每个目标 image 读取 manifest digest，与输入的期望 digest 比较；
7. 任一缺失或 digest mismatch 均使该 matrix job 失败。

如果目标 tag 已存在，先读取 digest：相同 digest 直接幂等成功，不同 digest 则失败，不能覆盖后继续发布。显式的 post-sync digest 校验是成功门槛。

建议把以下共用逻辑提取为 `.github/scripts` 或 repository-local composite action，供定时 workflow 与新 workflow 复用：

- target schema validation；
- source/target ECR login；
- repository creation；
- exact image parsing and validation；
- digest verification。

#### Concurrency and Retry

```yaml
concurrency:
  group: byoc-on-demand-prod-${{ inputs.correlation-id }}
  cancel-in-progress: false
```

- 同一 delivery batch 的重复 dispatch 不并行；后一个 run 排队而不是取消正在执行的 release gate；
- 不同 batch 可以并行，但 matrix `max-parallel` 应低于当前 99，根据客户 Role/ECR 限流压测确定；
- workflow 设置明确的 `timeout-minutes`；
- 镜像复制与 digest 校验必须幂等，重复 dispatch 同一 images manifest 不改变正确结果；
- workflow run 失败后由 Operation retry 重新 dispatch。已成功复制的目标会在下一次 run 中快速通过 digest 校验。

### DevOps Operation Changes

#### Action Pipeline

`pkg/service/kernelimage/service/service.go` 的 action 构建逻辑调整为：

```go
if anyReleaseMissingDigest(releases) {
    actions = append(actions, ResolveKernelImageDigest)
}
actions = append(actions, DeliverKernelImage)
if first.LocalDevOpsEnv == "prod" && byocSyncEnabled {
    actions = append(actions, SyncBYOCImage)
}
```

所有 Action 使用 `ActionStartPolicyImmediate`。Operation 框架仍保证后一个 Action 只会在前序 Action succeeded/skipped 后启动。

dev 环境当前测试和 delivery 行为不变：

```text
dev:  Resolve optional → DeliverKernelImage → AVAILABLE
prod: Resolve optional → DeliverKernelImage → SyncBYOCImage → AVAILABLE
```

#### Action Contract

新增 `pkg/service/operation/actions/kernelimageaction/sync_byoc_image.go`：

```go
var ActionSyncBYOCImage = opent.ActionMetadata{
    Name: "SyncBYOCImage",
    Type: opent.ActionTypeAuto,
}
```

`PreCheck`：

- 校验 `ParamKernelImageDeliveryCorrelationID` 存在且为 UUID；
- 校验 `ParamKernelImageLocalDevOpsEnv == prod`；
- 校验 GitHub workflow 配置已启用。

`Start`：

1. 按 `DeliveryBatchUID` 查询全部 release，并复用 `validateBatchOperation` 校验它们属于当前 Operation；
2. 确认所有 release 已有 source digest，且状态仍为 `SYNCING`；
3. 对每个 release，从 FID 返回并已持久化的 `DeliveryTargetImages` 中选择唯一的中央生产 ECR image；零个或多个匹配都 fail closed；
4. 将中央 ECR image 与 release 的 `SourceDigest` 组成非空 `images` manifest，按 source 排序并检查重复；
5. 用整个 manifest 一次 dispatch `image-sync-byoc-on-demand-prod.yml`；
6. 将 `images_digest/run_id/run_url/dispatched_at` 作为 JSON 写入 instance output；`images_digest` 是规范化 manifest 的稳定 hash；
7. 返回 `RUNNING`。

输出示意：

```json
{
  "images_digest": "sha256:<manifest-hash>",
  "run_id": 123456789,
  "run_url": "https://github.com/tidbcloud/docker-image-controller/actions/runs/123456789",
  "dispatched_at": "2026-09-04T12:00:00Z"
}
```

`Result`：

1. 从 instance output 恢复 workflow run；
2. 查询 workflow run；
3. run 为 `queued/in_progress` 时返回 `RUNNING`；
4. run completed 且 conclusion 不是 `success` 时，将 batch 标为 `SYNC_FAILED` 并返回 `FAILED`；
5. run 成功时，将 batch 标为 `AVAILABLE`，记录完成时间并返回 `SUCCEEDED`；
6. 超过 Action 最大等待时间仍未完成时 fail closed，并在输出中保留 run URL。

Action 本身不下载 workflow artifact，也不解析客户级结果；GitHub workflow 只有在全部 target 的复制和 digest 校验都成功时才能得出 `success` conclusion。

#### Release Status Semantics

当前 `DeliverKernelImage.Result()` 在 FID 成功后调用 `MarkBatchSyncSucceeded`，会直接将 release 标为 `AVAILABLE`。引入后置 BYOC Action 后必须拆开该语义：

- 新增 `MarkBatchFIDDelivered`：保存 `DeliveryTargetImages` 和 FID digest 验证结果，release 保持 `SYNCING`；
- `DeliverKernelImage.Result()` 成功时调用 `MarkBatchFIDDelivered`；
- 新增或收敛为 `MarkBatchDeliverySucceeded`：只在 `SyncBYOCImage` 全部成功后设置 `AVAILABLE/AvailableAt`；
- FID 或 BYOC 任一失败均调用 `MarkBatchSyncFailed`。

否则即使 Operation 最终失败，其他调用方仍可能在 BYOC 同步完成前选中 `AVAILABLE` release，门禁将失去意义。

#### GitHub Client and Configuration

现有 `githubcli.Client` 是 repo-bound client，并被 Control Plane deployment 使用，不能为了本功能把它的 owner/repo 直接改成 `docker-image-controller`。建议增加一个用途受限的 client，例如：

```go
type BYOCImageSyncClient interface {
    Dispatch(ctx context.Context, images []ImageManifestItem, correlationID string) (*WorkflowRunRef, error)
    GetRun(ctx context.Context, runID int64) (*githubcli.WorkflowRun, error)
}
```

该 client 固定允许以下配置：

```yaml
kernelImage:
  byocSync:
    enabled: true
    owner: tidbcloud
    repo: docker-image-controller
    ref: main
    workflow: image-sync-byoc-on-demand-prod.yml
    timeout: 2h
```

认证复用生产环境 `.env.prod` 已注入的 GitHub token，不新增静态凭证。token 只需对 `tidbcloud/docker-image-controller` 具有 workflow dispatch/read 权限；不得授予 contents write、administration 或客户云权限。日志和 Operation output 只能记录 run ID/URL，不能记录 token、Web Identity token、AWS 临时凭证或 ECR password。

如果 dispatch API 未返回 run ID，则 client 使用 `correlation-id` 对应的唯一 `run-name` 在短窗口内查询 workflow runs；不得用“最近一次 run”作为关联依据。

### Failure Handling

| 场景 | 行为 |
| --- | --- |
| `images` 为空、JSON 非法或存在重复 image | workflow 不访问客户账户，直接失败。 |
| source image 不是允许的中央生产 ECR | workflow 失败，防止利用 workflow 从任意 Registry 搬运镜像。 |
| source image 缺失或实际 digest 与期望 digest 不同 | prepare 失败，不访问客户账户。 |
| target 文件任一记录非法 | prepare 失败；不跳过客户。 |
| 单个客户 assume-role 失败 | 对其他 matrix target 继续执行，最终 workflow 失败。 |
| 目标 tag 已存在且 digest 相同 | 幂等成功。 |
| 目标 tag 已存在但 digest 不同 | digest mismatch，workflow 失败并保留旧内容，不覆盖后继续发布。 |
| GitHub dispatch 暂时失败 | Action `Start` 返回错误，由 Operation 重试；镜像同步本身可重复。 |
| workflow cancelled/timed_out/failure | Action failed，batch 标为 `SYNC_FAILED`。 |
| DevOps 重启 | run 列表已存于 instance output，恢复后继续轮询，不重新 dispatch。 |
| `byoc-targets-prod.yaml` 在运行中变化 | 本次以 checkout commit 为快照；新目标由 onboarding 流程或定时 reconciliation 补齐。 |

### Observability and Audit

DevOps Operation 应展示：

- delivery batch UID；
- 本次 source image 列表及 manifest hash；
- GitHub workflow run ID、URL 和状态；
- 首次 dispatch 时间、完成时间和总耗时；
- 失败 conclusion 和统一错误码，例如 `BYOC_WORKFLOW_FAILED`、`BYOC_WORKFLOW_TIMEOUT`、`BYOC_WORKFLOW_RESULT_INVALID`。

GitHub workflow summary 应展示：

- checkout commit SHA；
- correlation ID、输入 image 数量和经校验的 source image；
- target 总数、成功数、失败数；
- 每个失败目标的非敏感名称和失败阶段；
- digest mismatch 的 source/target digest。

不得在公开日志或 Operation output 中输出 `byoc-targets-prod.yaml` 全量内容、Role session credentials 或 token 文件内容。

### Rollout and Rollback

1. 在 `docker-image-controller` 增加 image manifest/target 校验脚本、直接复制脚本和单元测试；
2. 先使用 dev target 文件和测试 Role 验证同一执行逻辑；
3. 合入并手工验证新的 prod workflow，确认非法 source、单 target 失败和 digest mismatch 都会使 run 失败；
4. 在 DevOps 增加 repo-bound client、`SyncBYOCImage` Action、状态拆分和测试；
5. 通过 feature flag 部署到 dev，确认 dev batch 不追加 prod BYOC Action；
6. 部署 prod 后开启 `byocSync.enabled`，观察首批 delivery 的耗时与 ECR throttling；
7. 保留原定时 workflow，作为配置漂移和后加入 target 的 reconciliation。

紧急回滚时关闭 `byocSync.enabled`，新建 Operation 回到原 action 链。已经创建且正在等待 `SyncBYOCImage` 的 Operation 不应静默跳过，需让对应 workflow 完成或由值班人员显式终止并记录处置结果。

## Test Design

### Functional Tests

`docker-image-controller`：

- `images` manifest 的空值、JSON 结构、重复项和数量限制；
- source Registry allowlist、repository、tag 和 digest 格式校验；
- 输入 image 不被当作 glob、regex 或 shell 表达式；
- source digest 等于期望 digest，不一致时在访问客户账户前失败；
- destination image 由 source repository path 和 tag 确定，不读取 `image-sync-byoc-prod.yaml`；
- target schema、account、region、重复 Registry 校验；
- source/target digest 相同、缺失和 mismatch；
- 任一 matrix target 失败时 workflow conclusion 为 failure。

DevOps 使用 Ginkgo/Gomega 和生成的 gomock：

- prod operation 在 `DeliverKernelImage` 后追加 `SyncBYOCImage`；
- dev operation 不追加该 Action；
- 从每个 release 的 `DeliveryTargetImages` 中选择唯一中央生产 ECR image；零个或多个匹配都失败；
- batch 内多张 image 稳定排序后只 dispatch 一次；
- `Start` 持久化 run ID/URL 并返回 running；
- `Result` 覆盖 queued、in_progress、success、failure、cancelled、timed_out 和查询错误；
- FID 成功后 release 仍为 `SYNCING`；
- BYOC workflow run 成功后 batch 才为 `AVAILABLE`；
- BYOC workflow run 失败后整个 batch 为 `SYNC_FAILED`；
- 重启后从 instance output 恢复并继续轮询。

### Scenario Tests

- 重放 `v26.3.4-nextgen` 场景：无需修改 `tags.allow`，将 FID 返回的中央 ECR PD image 直接同步到全部测试目标；
- 一个 batch 同时包含相同 tag 的 PD/TiDB/TiKV：manifest 明确列出三张 image，只 dispatch 一次且只同步这三张；
- 一个 batch 包含不同 tag：manifest 保留每张 image 的完整 repository/tag/digest，仍只 dispatch 一次；
- 某客户 Role 临时不可用：其他目标完成但总 run 失败，Operation 不进入 `AVAILABLE`；
- 相同 images manifest 重试：已同步目标通过 digest 校验，缺失目标被补齐；
- 同步过程中新增 target：本次按 commit 快照执行，定时 reconciliation 或 onboarding 补齐新增目标。

### Compatibility Tests

- 现有 `image-sync-byoc-prod.yml` 的 push/schedule/manual 行为不变；
- 现有 FID workflow 和 issue-comment 协议不变；
- 现有 Control Plane GitHub client 的 owner/repo 和 deployment 行为不变；
- dev kernel delivery 不触发生产 BYOC workflow；
- 历史已完成 release 状态不迁移；
- feature flag 关闭时维持原生产行为，便于逐步启用和回滚。

## Impacts & Risks

### Impacts

- BYOC 镜像成为正式 kernel delivery 的组成部分，避免默认版本已可选但客户 ECR 缺镜像。
- 客户清单和云权限仍集中在 `docker-image-controller`，DevOps 不需要新增跨云身份链或客户 IAM 权限。
- 单次发布时延增加为“FID 时间 + 最慢 BYOC target 同步时间”。
- 每个 delivery batch 触发一个 workflow；batch 中 image 越多，每个目标的复制与校验耗时越长。
- 现有定时同步继续提供 eventual reconciliation，与按需门禁形成主动同步加定时兜底。

### Risks

- 必须从 FID `DeliveryTargetImages` 中可靠选出中央生产 ECR image；如果环境配置错误导致零个或多个候选，BYOC Action 会 fail closed。
- 目标 repository 直接沿用中央 ECR repository path；如果未来某类镜像的客户 ECR 路径不同，需要在输入 manifest 中显式增加 destination repository，而不能重新引入全量 allowlist 扫描。
- `byoc-targets-prod.yaml` 只是 checkout 时点快照；运行后新增的客户不在本次成功范围内。
- 客户数量增长会增加 GitHub runner 占用、ECR 限流和发布尾延迟。
- GitHub Actions 服务、runner 池或 token 权限故障会阻塞 kernel delivery。
- dispatch 成功但 run ID 未正确关联可能产生重复 run；必须用 correlation ID 精确查询，并依赖同步幂等。
- 如果仍在 FID 成功时提前设置 `AVAILABLE`，新的 Action 只会成为表面步骤，无法形成真实发布门禁。
- 定时 workflow 与按需 workflow 可能同时操作同一 Registry/tag；两者都必须通过 digest 校验并避免把 conflict 当作成功。

## Unresolved Questions

1. 中央生产 ECR hostname 应由 DevOps 配置传入并同时固化在 workflow allowlist，还是以 FID target metadata 的 cloud/provider 字段选择？首版建议显式配置 hostname，并要求恰好命中一个 target image。
2. 是否有任何 kernel image 的客户 destination repository 与中央 ECR repository path 不同？如果存在，应在 `images` manifest 中显式传 `destination_repository`。
3. `byoc-targets-prod.yaml` 在运行期间新增 target 时，是否要求本次 Operation 重新 dispatch，还是由 BYOC onboarding 与 4 小时 reconciliation 保证？本设计首版采用 commit 快照加 reconciliation。
4. 生产 `max-parallel` 和 Action timeout 的最终值需要通过测试目标压测后确定；初始建议 `max-parallel=20`、timeout `2h`。
5. GitHub token 当前是否已经对 `tidbcloud/docker-image-controller` 具备最小的 Actions write/read 权限；上线前需只验证权限范围，不复制或打印 token。
