1. 页面概览

1.1 是什么

「CI/CD 流水线」页(对应前端源码 action/web/src/views/apollo/PipelinesPage.vue,页面标题「Apollo CI/CD 流水线」)是 LightApollo 的 GitOps 交付流水线管理台(对齐 TAD-03)。它把「定义一次流水线」到「观察一次运行逐步走完」的管理闭环收敛到一张表上:一条流水线定义本身不是可执行脚本,而是一个指针——它指向 Git 仓库(git_repo_id)内某个相对路径的流水线文件(pipeline_file_path,如 .apollo/pipeline.yaml),页面只维护「哪条仓库里的哪个文件、配了哪些 Triggers、当前是启用还是停用」;真正被读取执行的是仓库文件里声明的 stage / step(shell 命令或 lightapollo_deploy 部署触发)。这种「定义即代码」的形态让流水线的版本化、评审、回滚都跟随 Git 仓库走,页面本身不承载流水线内容。

在整个 Apollo 产品流程中,本页处于「Git 仓库与同步目标 → 流水线定义 → 运行 → 阶段 → 步骤 → 日志」的下钻链路中段:Git 仓库在「Git 仓库」页登记(本页下拉复用它),lightapollo_deploy 步骤最终落到 GitOps 同步目标触发一次部署(与「部署与漂移」页共享同一套 SyncEngine);本页负责让用户发起执行(手动 run / external-trigger)并观察执行过程(运行历史、运行详情、逐步骤日志)。输入是用户在弹窗里填的定义字段或触发参数;输出是一条运行记录及其 stage/step/日志明细,可逐层穿透到「一次 shell 命令的退出码与输出」。

典型使用链路(演示视角的完整闭环):① 先在「Git 仓库」页登记一个仓库并在其工作目录写好流水线文件(含 stage/step,deploy 步骤要声明 sync_target_id);② 本页「新建流水线」填名称 + 仓库 + 文件路径 + Triggers(可选 JSON),列表出现一条 enabled(绿)定义;③ 点「运行」手动触发,若后端 worker 已启动则运行异步进入队列,展开该行「运行历史」能看到新运行(trigger=manualpending→running);④ 若步骤是 shell 命令,worker 逐 stage、逐 step 串行执行并把退出码与日志落库;若含 lightapollo_deploy 步骤,则转调 GitOps 部署触发;⑤ 点该运行行「详情」,弹窗展示运行概要(状态/触发类型/耗时/Commit/分支),运行中每 3 秒轮询自动刷新;⑥ 点步骤「日志」查看该步完整输出;⑦ 结束后可对进行中运行「取消」(杀进程树)或对定义「停用」(拒绝再次触发);对不再需要的定义「删除」。

1.2 核心价值/能力表

能力 说明 对应页面操作
定义列表 集中查看流水线的 Git 仓库、流水线文件、Triggers JSON 摘要、启停状态与创建时间 页面上部定义列表卡片
新建/编辑定义 名称 + Git 仓库 + 流水线文件 + Triggers JSON(可选),保存即校验 JSON 「新建流水线」「编辑」+ 弹窗表单
启用/停用 定义状态在 enabled(绿)/ disabled(灰)间切换,停用后后端拒绝触发 行内「启用/停用」按钮
手动运行 run 触发一次完整执行(全部 stage/step),异步入队或同步执行 行内「运行」按钮
外部触发 external-trigger 直达 lightapollo_deploy 步骤,跳过 build/test 行内「外部触发」+ 弹窗
运行历史 按流水线维度查看每次运行的触发类型/状态/耗时/Commit/分支/开始时间 展开行「运行历史」子区
运行详情下钻 运行概要 + Stage 列表 + Step 列表 + 步骤日志,running 时 3 秒轮询 运行行「详情」→ 弹窗
取消运行 对 running/pending 的运行中止,杀活跃子进程树并置 cancelled 运行行「取消」(仅 active 显示)
审计留痕 创建/更新/删除/运行/外部触发均写审计日志 全部写操作

1.3 一句话总结

本页把「Git 仓库里的流水线文件」变成一条可触发、可启停、可逐层下钻到退出码与日志的交付执行入口,定义与执行分离、观察与取消齐备。

2. 访问入口

2.1 路由与菜单

路由 path /apollo/pipelines
路由 name ApolloPipelines
meta.title Apollo CI/CD 流水线
侧边栏入口 ApolloLayout 侧边栏「流水线」,位于「制品管理」之后、「监控」之前
前端源码 action/web/src/views/apollo/PipelinesPage.vue
API 客户端 action/web/src/api/apolloClient.js
路由注册 action/web/src/router/index.js/apollo 子路由 children 下,component: ApolloPipelinesPage,挂 ApolloLayout

相邻页(侧边栏顺序):制品管理 artifacts.md(前)、监控 monitoring.md(后)、Git 仓库 git-repos.md(下拉数据源)、部署与漂移 deployments.mdlightapollo_deploy 步骤的落点)。

2.2 认证与权限

2.3 端口与 API 前缀

3. 界面布局


┌─────────────────────────────────────────────────────────────┐
│ Apollo CI/CD 流水线                              [新建流水线]  │
│ [顶部说明 alert-info(定义指针 + 可运行/外部触发/启停 说明)]      │
│ [操作结果提示条(v-if alert.message,可「关闭」)]              │
├─────────────────────────────────────────────────────────────┤
│ ┌ 流水线定义列表(loading / 空态 / 表格 三态卡片)────────────┐ │
│ │ ▸运行历史 | ID | 名称 | Git 仓库 | 流水线文件 | Triggers |  │ │
│ │            状态 | 创建时间 | 操作                          │ │
│ │ 操作列:启用/停用 | 运行 | 外部触发 | 编辑 | 删除            │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌ 运行历史子区(流水线行展开后显示,按行 v-for)───────────────┐ │
│ │ [刷新] 运行历史(N)— 流水线 {name}                        │ │
│ │ 运行号 | 触发类型 | 状态 | 耗时 | Commit | 分支 | 开始时间 |  │ │
│ │          操作(详情 / 取消)                              │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌ 新建/编辑流水线弹窗(modal)───────────────────────────────┐ │
│ │ 名称(name)* | Git 仓库(git_repo_id)* |                  │ │
│ │ 流水线文件路径(pipeline_file_path)* | Triggers(JSON可选)│ │
│ │ [取消] [创建/保存]                                        │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌ 外部触发弹窗(modal)─────────────────────────────────────┐ │
│ │ Commit SHA(git_commit_sha)| 分支(git_branch)           │ │
│ │ [取消] [触发]                                            │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌ 运行详情弹窗(modal-lg)──────────────────────────────────┐ │
│ │ 运行概要(状态/触发/耗时/Commit/分支/开始时间)             │ │
│ │ (running 时)「运行中」徽标 + 每 3 秒自动刷新…             │ │
│ │ Stages(N)表:顺序 | Stage 名称 | 状态                    │ │
│ │ Steps(N)表:Step 名称 | 类型 | 状态 | 退出码 | 操作(日志) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌ 日志弹窗(modal-lg)──────────────────────────────────────┐ │
│ │ 深色 code block(logModal.content / 「(无日志内容)」)    │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

各板块职责:

板块 职责
页头 + 提示条 标题、「新建流水线」按钮、固定顶部说明、全局操作结果提示
流水线定义列表卡片 全量定义展示(loading/空态/表格三态),行内展开运行历史、行内六个操作按钮
运行历史子区 按流水线维度缓存 runsMap,展开自动加载、可手动刷新,逐条操作「详情/取消」
新建/编辑弹窗 收集 name/git_repo_id/pipeline_file_path/triggers 四字段并提交(共用一套表单)
外部触发弹窗 收集 git_commit_sha / git_branch 两个可选参数并提交 external-trigger
运行详情弹窗 运行概要 + Stage 表 + Step 表,active 状态启动 3 秒轮询,Step 行内「日志」
日志弹窗 展示单步日志全文(深色 code block)

4. 交互元素

4.1 页头、顶部说明与操作结果提示条

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
按钮「新建流水线」 页头右侧 打开新建弹窗 始终可用;busy 进行中不置灰(页面仅提交类按钮受 busy 限制) 打开新建弹窗,表单预填 git_repo_id = 仓库列表第一个 无(弹窗提交才调后端) 仓库列表为空时 git_repo_id 为 null,需手输
顶部说明 列表上方 alert-info 向用户解释定义语义与可用操作 常驻 纯说明:流水线定义指向 Git 仓库中的流水线文件(pipeline_file_path),可手动运行(run)、外部触发(external-trigger)或启用/停用(enable/disable);展开行可查看运行历史、stage/step 明细与日志。 与源码逐字一致,不随数据变化
提示条「关闭」 提示条右侧 关闭当前结果提示 有 alert.message 才显示 alert.message='' 清空提示 单槽提示,新操作覆盖旧提示

提示条形态:<div class="alert" :class="alert.type">,类型有 alert-info(默认)、alert-successalert-error。页面所有成功反馈用 success、失败用 error。每条关键路径文案在 4.2~4.11 逐条列出(按钮文字、提示文案与源码逐字一致)。

4.2 流水线定义列表卡片

加载三态:loading 时显示「加载中...」;pipelines.length===0 时显示 暂无流水线定义,点击"新建流水线"添加(项目 #{{ PROJECT_ID }})。;有数据渲染表格。列头依次为 (展开按钮)/ ID / 名称 / Git 仓库 / 流水线文件 / Triggers / 状态 / 创建时间 / 操作。单元格渲染细节:名称加粗;Git 仓库列 repoLabel(field(p,'git_repo_id'))——先去 repos 里按 id 找名称显示 {r.name}(#{r.id}),找不到则显示 #<id>,空则 -;流水线文件列等宽字体并超长省略;Triggers 列取 triggers(别名 trigger_config),JSON.stringify 后超 40 字符截断加 ;创建时间 formatTime 去掉 T 截取前 19 位,空则 -

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
行首「▸/▾ 运行历史」 每行首列 展开/收起该流水线运行历史 始终可点,title 提示「展开运行历史/收起运行历史」 展开:expanded[p.id]=true,若 runsMap[p.id] 无缓存则调 loadRuns(p.id);收起:置 false 首次展开:GET /pipelines/:id/runs 展开一次后缓存,之后展开直接用缓存,需点子区「刷新」强制重拉
状态徽标 状态列 定义启停状态 pipeStatus(p)status(别名 enabled),空回退 disabled enabled/active 绿(class status-online)、disabled 灰(status-offline),未知灰 pipeEnabled 判定 ['enabled','active'].includes(...)
操作列按钮组 每行末列 六个操作 busy 非空时全部禁用(.op-col 内五个按钮均 :disabled="!!busy" 见 4.3~4.7 见各节 busy 是页面级互斥锁,值为当前操作键(如 run-3savePipe),任何写操作期间全局不可并发

4.3 「新建流水线」/「编辑流水线」弹窗与保存

「新建流水线」打开 create 模式,标题 新建流水线(项目 #{{ PROJECT_ID }});行内「编辑」打开 edit 模式,标题 编辑流水线 #{{pipe.id}}(项目 #{{ PROJECT_ID }})。两模式共用一套表单与保存逻辑。

字段 类型 必填 校验/默认 保存逻辑
名称(name)* 文本输入 placeholder「如 deploy-to-prod」;HTML required;空串 JS 层再拦 trim() 后随请求体 name 提交
Git 仓库(git_repo_id)* 下拉或数字输入 repos.length>0 时下拉(选项 {r.name}(#{r.id}),v-model 数字);repos 为空时降级数字输入(min=1,placeholder「仓库 ID(仓库列表加载失败时手输)」) Number() 后随请求体 git_repo_id 提交
流水线文件路径(pipeline_file_path)* 文本输入 placeholder「如 .apollo/pipeline.yaml」;HTML required trim() 后随请求体 pipeline_file_path 提交
Triggers(JSON,可选) textarea rows=3 placeholder 如 [{"type":"git_push","branch":"main"}] 或 {"push":true,"schedule":"0 2 * * *"};留空不配置;填写时保存前先 JSON.parse 校验 合法则解析为对象/数组随请求体 triggers 提交;非法则拦截不发请求

保存流程(提交按钮文案 create 模式「创建」/ edit 模式「保存」,busy==='savePipe' 时显示「保存中...」并禁用;旁边「取消」纯关闭弹窗不清表单,右上「关闭」同):

① 空校验(源码文案 名称(name)、Git 仓库(git_repo_id)与流水线文件路径必填);② Triggers JSON 校验,非法提示 Triggers 不是合法 JSON,请检查格式(error)不发请求;③ busy='savePipe'buildPayload() 组装 → create 调 POST /projects/1/pipelines,edit 调 PUT /pipelines/{pipe.id};④ 成功 create 提示 流水线创建成功${newId?(id=${newId}):''}(success,newId 取 data.id || data.pipeline.id,create 响应为创建的完整定义对象故必有 id),edit 提示 流水线 #${id} 更新成功(success);随后关闭弹窗并 fetchPipelines() 刷新列表;⑤ 失败提示 保存流水线失败:<后端 message>(error)。

后端语义:create 时 (project_id, name) 复合唯一冲突返回 409 DUPLICATE_ENTRY(文案 流水线定义 (project_id=1, name=xxx) 已存在);pipeline_file_path 为空时后端兜底默认 .lightapollo.yml;状态默认 enabled。update 为指针字段全量更新(前端 PUT 时把 name/git_repo_id/pipeline_file_path/triggers 全量带上)。

4.4 「启用」/「停用」按钮

操作列第一个按钮按当前状态显示反向动作:pipeEnabled(p) 为真显示「停用」、否则显示「启用」。

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「停用」/「启用」 行操作列 切换定义 enabled/disabled busy 时禁用;无二次确认 成功后按方向提示 流水线 "${name}" 已启用(状态:${data.status||'enabled'})流水线 "${name}" 已停用(状态:${data.status||'disabled'})(success),随后刷新列表 POST /pipelines/:id/enablePOST /pipelines/:id/disable 失败提示 ${方向}流水线 "${name}" 失败:<message>;后端 enable/disable 恒返回 {id, status:"enabled"/"disabled"}
(禁用语义) - 定义状态为 disabled 后 - - - 后端 StartRun/ExternalTrigger 对 disabled 定义返回 400 流水线 "%s" 已禁用,拒绝启动/拒绝外部触发;但行内「运行/外部触发」按钮不按状态隐藏,点停用后的运行会得到该 400

4.5 「运行」(手动触发)

行内「运行」按钮直接触发(无二次确认、无中间弹窗),busy='run-{id}'

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「运行」 行操作列(btn-success) 手动触发一次完整运行 busy 时禁用;disabled 定义也显示(点了报 400) 成功提示 流水线 "${name}" 已触发运行${runId?(id=${runId}):''}:${msg}(success)。runIddata.id || data.run.id || data.run_idmsgdata.message,缺省时若 runId 非空则 运行号 #${runId},再缺省则 运行已触发;随后若该行已展开则 loadRuns(p.id, true) 强制刷新运行历史 POST /pipelines/:id/run(body 无内容,后端可接收可选 triggered_by 缺省当前用户) 失败提示 触发运行流水线 "${name}" 失败:<message>。后端响应是完整 run 对象data.id 为运行记录主键(非运行号,运行号为 data.run_number),故成功文案里的「运行号 #N」实为 run 主键 id,是文案用词与字段语义的偏差(见 8 章)

手动 run 的后端行为:定义不存在 → 404;定义 disabled → 400;定义 enabled → 建 run(run_number=max+1status=pendingtrigger_type=manualtriggered_by=当前用户名缺省 manual/system、started_at=now)→ 读取并解析仓库内流水线文件:文件缺失/解析失败则运行直接置 failed 并返回错误;解析成功则提交 worker 队列(worker 池已启动时异步)或同步 executeRun(未启动时,便于单机/自测)。

4.6 「外部触发」弹窗

行内「外部触发」打开弹窗,标题 外部触发:{{pipe.name}}(#{{pipe.id}})。这是为「外部系统/GitOps 同步引擎代发」设计的入口——只执行流水线里的 lightapollo_deploy 步骤,跳过 build/test。

字段/控件 类型 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
Commit SHA(git_commit_sha) 文本输入(等宽) 外部触发的 commit 上下文 选填;placeholder「如 3f2a9c1...」 非空才随请求体 git_commit_sha 提交 见提交按钮 当前后端 handler 不解析请求体,该字段提交后被丢弃(见 8 章)
分支(git_branch) 文本输入 外部触发的分支上下文 选填;placeholder「如 main」 非空才随请求体 git_branch 提交 同上 同上,运行记录的 git_branch 恒为空
按钮「触发」 弹窗底部提交 提交外部触发 busy==='extTrigger' 时显示「触发中...」并禁用 成功提示 外部触发 "${name}" 成功${runId?(运行 id=${runId}):''}:commit ${payload.git_commit_sha||'-'} / branch ${payload.git_branch||'-'}(success);随后关闭弹窗并尝试刷新该流水线运行历史 POST /pipelines/:id/external-trigger 失败提示 外部触发失败:<message>。「刷新」分支有前端缺陷:关闭弹窗后读取 extModal.value.pipe?.id 已是 undefined,展开行的运行历史不会被刷新(见 8 章)
按钮「取消」/「关闭」 弹窗 关闭弹窗 始终可用 关闭并清空 commit_sha/branch 点遮罩(@click.self)也可关闭

后端 external-trigger 行为(ExternalTrigger同步执行于请求内、不经 worker 队列):定义 disabled → 400 流水线 "%s" 已禁用,拒绝外部触发;enabled → 建 run(run_number=max+1trigger_type=externalstatus=runningstarted_at=now)→ 解析流水线文件(失败则 run 置 failed 返回错误)→ 逐 stage 先建一条 skipped 的 stage_run,仅对其中的 lightapollo_deploy 步骤建 running 的 step_run 并调用 runDeployStep(经 DeployTrigger.TriggerDeploy(sync_target_id, triggered_by) 触发 GitOps 部署):任一 deploy 步骤失败 → 该 stage 与 run 置 failed;全部成功 → stage 置 success、run 置 success。返回的 run 对象此时已是终态(success/failed)。

4.7 「删除」

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「删除」 行操作列(btn-danger) 删除流水线定义 busy 时禁用 先弹原生 confirm:确定删除流水线 "${name}"(#${id})吗?历史运行记录可能一并删除。 取消中止;确认后 DELETE,成功提示 流水线已删除(success)、收起该行展开态并刷新列表 DELETE /pipelines/:id 失败提示 删除流水线失败:<message>后端只删定义行DeleteDefinition 仅删 apollo_pipeline_definitions,关联的 run/stage/step 记录并不删除(保留为孤儿数据),confirm 文案与后端行为不一致(见 8 章)

4.8 运行历史子区(行展开)

每行展开后渲染 .runs-block 子区,头部 运行历史({{runsMap[p.id]?runsMap[p.id].length:0}})— 流水线 {{p.name}} + 右侧「刷新」按钮(loadRuns(p.id, true) 强制清缓存重拉)。三态:runsLoading[p.id] 显示「加载中...」;空显示 暂无运行记录,点击"运行"或"外部触发"发起一次运行。;有数据显示表格。运行表列头 运行号 / 触发类型 / 状态 / 耗时 / Commit / 分支 / 开始时间 / 操作

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「刷新」 子区头部 强制重载该流水线运行历史 busy 时禁用 loadRuns(p.id, true):先清 runsMap 缓存再拉 GET /pipelines/:id/runs 失败提示 加载运行历史失败:<message> 并把该 pid 的 runs 置空数组
运行行单元格 运行表 运行号/触发/状态/耗时/Commit/分支/开始时间 状态徽标颜色见下 #{{run_number}}、trigger 取别名 trigger、耗时 formatDuration(<1s 显示 N ms、<60s 显示 N.N 秒、以上显示 N 分 N 秒,空或 0 显示 -)、commit 取 git_commit_sha/commit_sha、分支取 git_branch/branch、时间取 started_at/start_time/created_at - 各类字段做多别名兜底(field()),任一为空显示 -
「详情」 运行行操作列 打开运行详情弹窗 busy 时禁用 见 4.9 GET /pipeline-runs/:id(打开时) 打开即拉取一次详情
「取消」 运行行操作列(btn-danger) 取消进行中运行 isRunActive(run)(running/pending/queued)显示;busy 时禁用 见 4.11 POST /pipeline-runs/:id/cancel 非 active 运行不显示该按钮

运行状态徽标(RUN_STATUS_META class 映射,未知灰):running/pending/queued 蓝(status-registering);succeeded/success/completed 绿(status-online);failed/error 红(status-revoked);canceled/cancelled 灰(status-offline)。后端实际产出的 run 状态为 pending/running/success/failed/cancelled(另有常量 skipped 供未来扩展),页面映射表冗余覆盖了 succeeded/completed/canceled/error/queued 等别名以便兼容。

4.9 「详情」与运行详情弹窗

点击「详情」openRunDetail(run):先 stopPoll() 清理旧定时器,置弹窗 loading,调 GET /pipeline-runs/:idparseRunDetail 解析后填入弹窗;若运行 isRunActivesetInterval(refreshRunDetail, 3000) 启动 3 秒轮询。弹窗标题 运行详情:'#' + run_number

弹窗内容自上而下:

parseRunDetail 的宽松解析链:run 本体取 data.run || data.pipeline_run || data;stages 取 data.stages(别名 stage_runs)、steps 取 data.steps(别名 step_runs);两者都空时再从 run 本体里找;steps 仍空但 stage 内含 steps 时扁平展开补齐(for s of stages: push(...s.steps))。也就是说前端有能力消费嵌套 stage/step 的响应——但当前后端 GET /pipeline-runs/:id 只返回 run 本体(无嵌套,且 GetStageRuns/GetStepRuns 未注册 HTTP 路由),所以弹窗 Stages/Steps 恒为 0、日志按钮不可达(见 8 章)。

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「关闭」/ 遮罩 弹窗 关闭详情弹窗 始终可用 closeRunDetail():停轮询 + 关闭 点遮罩 @click.self
3 秒轮询 弹窗内自动 刷新运行概要/Stage/Step isRunActive(run) 才启动;运行结束自动 stopPoll 每 3 秒重拉一次详情并重解析 GET /pipeline-runs/:id(周期) 刷新失败或运行结束立即停轮询并提示 刷新运行详情失败:<message>;弹窗关闭与页面卸载(onBeforeUnmount)都清理定时器防泄漏
Stage/Step 状态徽标 两个子表 Stage/Step 状态 STEP_STATUS_META:succeeded/success/completed/passed 绿、running/pending/queued 蓝、failed/error 红、skipped/canceled/cancelled 灰、未知灰 纯展示 Stage 的 skipped 用于「前一阶段失败后被跳过」;external-trigger 场景非 deploy 的 stage 全部 skipped

4.10 「日志」按钮与日志弹窗

Step 行内「日志」viewStepLog(step):runId 取 runModal.run.id,stepId 取 step.id || step.step_name || step.name;调 GET /pipeline-runs/:id/logs/:step_id(stepId encodeURIComponent 编码)。成功弹窗标题 步骤日志:{step_name||stepId},正文深色 code block 展示全文;内容为空显示 (无日志内容)

parseLog 兼容形态:原始字符串直接返回;否则取 content || log || logs || text,都不是则 JSON.stringify 兜底。失败提示 加载日志失败:<message>,内容置空。

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「日志」 Step 表操作列 查看单步日志 busy 时禁用;Steps 为空时整表(含此按钮)不渲染 打开日志弹窗异步加载 GET /pipeline-runs/:id/logs/:step_id 后端:step 不存在或不属于该 run → 404 step run %d not found in run %d;step LogPath 为空 → 返回空串(页面显示「(无日志内容)」);读取文件失败 → 404 读取流水线日志失败: ...lightapollo_deploy 步骤无日志文件,点日志恒为「(无日志内容)」
「关闭」/遮罩 日志弹窗 关闭 始终可用 关闭 loading 中亦可关

4.11 「取消」运行

控件 位置 含义 必填/默认/可用条件 操作效果 触发后端调用 边界与细节
「取消」 运行行操作列 中止进行中运行 仅 running/pending/queued 显示;busy 禁用 弹原生 confirm:确定取消运行 #${run_number}(id=${run.id})吗? 取消中止;确认后请求 POST /pipeline-runs/:id/cancel 成功提示 运行 #${run_number} 已取消(状态:${data.status||'canceled'})(success),随后关闭详情弹窗(若开着)并用 findPipelineIdByRun 反查该运行所属流水线强制刷新运行历史;失败提示 取消运行失败:<message>

后端取消语义:仅 pending/running 可取消(pending 排队中的运行尚无进程,直接置 cancelled);running(worker 正在执行某 step)时先落库 cancelled,再经 runner 的 ProcessTracker.KillRun 终止该 run 所有活跃 step 子进程(进程树)——先置 cancelled 再杀进程,worker 在每步执行前检查 cancelled,避免取消后新起进程;kill 失败仅记日志降级(不影响取消语义);已终态运行取消是幂等 no-op。executor 侧 finishRun 守卫 cancelled 终态不被后续 success/failed 覆盖。

5. 后端关联

5.1 API 客户端

action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器对所有请求附加 Authorization: Bearer <token>(token 取 apollo_token,无则回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 时把 response.data 替换为业务数据(页面 const { data } = ... 直接得到业务对象/数组);HTTP 2xx 但 code!==0 拒绝;4xx/5xx 提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——401 清 token 并跳 /apollo/login(登录页自身 401 不跳,避免死循环),403 alert('无权限执行该操作')。页面各 catch 统一用 errMsg(err)err.response.data.message || err.response.data.error || err.message || '未知错误')展示后端错误。

5.2 端点表

方法 路径 权限点 请求体/Query 页面触发点
GET /projects/:id/pipelines pipeline:read :id=项目 id(页面固定 1) 加载定义列表
POST /projects/:id/pipelines pipeline:write {name, git_repo_id, pipeline_file_path, triggers?} 新建定义
GET /pipelines/:id pipeline:read - (已注册,页面未直接使用)
PUT /pipelines/:id pipeline:write {name, git_repo_id, pipeline_file_path, triggers?} 编辑定义
DELETE /pipelines/:id pipeline:write - 删除定义
POST /pipelines/:id/enable pipeline:execute - 启用
POST /pipelines/:id/disable pipeline:execute - 停用
POST /pipelines/:id/run pipeline:execute 可选 {triggered_by} 手动运行
POST /pipelines/:id/external-trigger pipeline:execute (前端发 {git_commit_sha?, git_branch?}后端未解析 外部触发
GET /pipelines/:id/runs pipeline:read - 运行历史子区
GET /pipeline-runs/:id pipeline:read - 运行详情弹窗(含轮询)
POST /pipeline-runs/:id/cancel pipeline:execute - 取消运行
GET /pipeline-runs/:id/logs/:step_id pipeline:read - 步骤日志
GET /pipeline-runs/:id/artifacts pipeline:read - (已注册,页面未使用;阶段 E 批4a 产物清单)
GET /projects/:id/git-repos git_repo:read - Git 仓库下拉数据源

端点注册于 action/products/apollo/server/server.go protected 组(流水线定义/运行 878-891 行),全部为权限点映射路由;页面只使用上述带「页面触发点」的行。

5.3 响应结构示例

统一 envelope 成功形态(拦截器已解包,业务代码只见 data):


{ "code": 0, "message": "ok", "data": { "id": 3 }, "request_id": "req_b63f..." }

字段说明:code 0 恒为成功;message 固定 okdata 业务数据;request_id 全链路追踪 ID(req_+unix 毫秒+4 位 hex,也写响应头 X-Request-ID)。创建类接口 HTTP 201,其余 200。定义列表 data 直接是数组(ok(c, list));页面 toList 仍兼容 {items,...} 双形态以防其它页/未来分页变化。

流水线定义对象(GET /projects/:id/pipelines 列表项与 create/update 响应,即 pipeline.PipelineDefinition):


{
  "id": 3,
  "project_id": 1,
  "name": "deploy-to-prod",
  "git_repo_id": 2,
  "pipeline_file_path": ".apollo/pipeline.yaml",
  "triggers": [
    { "type": "git_push", "branch": "main" },
    { "type": "schedule", "cron": "0 2 * * *" }
  ],
  "status": "enabled",
  "created_by": "admin",
  "created_at": "2026-09-07T09:00:00+08:00",
  "updated_at": "2026-09-07T09:00:01+08:00"
}

字段含义:id 主键;project_id 所属项目(页面固定 1);name 定义名(项目内唯一);git_repo_id 关联 Git 仓库;pipeline_file_path 仓库内流水线文件相对路径(后端缺省 .lightapollo.yml);triggers 触发配置 JSON(git_push/schedule/manual/external,页面仅记录展示不执行判定);status enabled/disabledcreated_by 创建用户名;created_at/updated_at 时间戳。页面行内 Git 仓库列按 git_repo_idrepos 下拉数据里查名称(查不到显示 #id)。

Git 仓库对象(GET /projects/:id/git-repos 列表项,即 gitops.GitRepository,仅作为本页下拉数据源):

GitRepository JSON 示例(下拉只消费 id + name)

{
  "id": 2,
  "project_id": 1,
  "name": "apollo-demo-repo",
  "url": "D:/work/apollo-demo-repo",
  "auth_type": "none",
  "default_branch": "main",
  "sync_interval_seconds": 0,
  "status": "active",
  "last_sync_at": null,
  "created_at": "2026-09-07T08:00:00+08:00"
}

字段含义:id/name 下拉选项取值与文案({name}(#{id}));url 为本地目录路径(自研轻量 GitOps 模拟,不用 go-git);default_branch 默认 main;sync_interval_seconds 轮询间隔(0=仅 webhook);status active 等;last_sync_at 最近成功同步时间。

运行对象(POST /pipelines/:id/runPOST /pipelines/:id/external-triggerGET /pipelines/:id/runs 列表项、GET /pipeline-runs/:id 本体,即 pipeline.PipelineRun):


{
  "id": 12,
  "definition_id": 3,
  "run_number": 5,
  "trigger_type": "manual",
  "git_commit_sha": "",
  "git_branch": "",
  "status": "success",
  "duration_ms": 3840,
  "triggered_by": "admin",
  "started_at": "2026-09-07T10:00:00+08:00",
  "finished_at": "2026-09-07T10:00:04+08:00",
  "artifacts": [
    { "step_id": 88, "step_name": "build", "path": "dist/app.zip", "size_bytes": 2048, "sha256": "a1b2..." }
  ],
  "created_at": "2026-09-07T10:00:00+08:00"
}

字段含义:id 运行记录主键(页面详情/取消/日志的路径参数,也是成功提示里的「id=...」);definition_id 所属定义;run_number 同一定义内自增运行号(max+1,页面展示为「运行号 #N」);trigger_type manual/external/git_push/schedulegit_commit_sha/git_branch 触发 commit 上下文(当前仅 manual/external 有值域,external-trigger 因 handler 未解析 body 恒为空,见 8 章);status pending/running/success/failed/cancelled(另常量 skipped);duration_ms 执行耗时(finishRun 由 started→finished 计算);triggered_by 触发人;started_at/finished_atartifacts 产物清单 JSON(阶段 E 批4a,run 成功时按步骤产物声明收集;manual 全流程 run 有值,external-trigger 无 shell 步骤恒空);created_at 创建时间。运行状态枚举与前端映射对照:后端 success(页面 RUN_STATUS_META 绿覆盖 success/succeeded/completed)、failed 红、cancelled 灰、running/pending 蓝。

enable / disable / cancel / delete / 日志 的轻量响应(源码逐字一致):

enable/disable/cancel/delete/logs 响应示例

{ "id": 3, "status": "enabled" }

enable 成功:statusenabled。disable 成功:{ "id": 3, "status": "disabled" }。页面提示 流水线 "x" 已启用(状态:enabled)


{ "id": 12, "status": "cancelled" }

cancel 成功:statuscancelled。页面提示 运行 #N 已取消(状态:cancelled)


{ "deleted": 3 }

delete 成功:仅返回被删定义 id,无其它字段。


{ "run_id": 12, "step_id": 88, "log": "+ echo 'deploy artifact'\n...\n" }

日志成功:log 为步骤日志文件全文(lightapollo_deploy 步骤无日志文件,后端返回空串)。

错误形态(统一 envelope,HTTP 4xx/5xx):

常见错误示例(disabled 触发 / 重名 / 运行不存在 / 日志跨 run)

{ "code": 40002, "message": "流水线 \"deploy-to-prod\" 已禁用,拒绝启动", "request_id": "req_..." }

停用定义上点「运行」:HTTP 400,业务码 40002(校验)。external-trigger 对应文案 流水线 "x" 已禁用,拒绝外部触发


{ "code": 40901, "message": "流水线定义 (project_id=1, name=deploy-to-prod) 已存在", "request_id": "req_..." }

同名创建:HTTP 409,业务码 40901(冲突)。日志跨 run 是 404(step run 88 not found in run 12),定义/运行不存在为 404(pipeline definition 3 not found/pipeline run 12 not found),均经 fail 统一包装。

错误码速查(api.go statusToCode/codeForError):40001 参数 / 40002 校验 / 40101 认证 / 40301 权限 / 40401 不存在 / 40901 冲突 / 42201 语义 / 42901 限流 / 50001 其余。HTTP 状态码由 ierr 承载,业务码与 HTTP 码不一定同号。

5.4 关键机制

运行号与定义生命周期。定义表 (project_id, name) 复合唯一(冲突 409);运行号 RunNumber 在同一定义内由 max+1 自增(并发下由表索引与单写服务保证不重号)。CreateDefinition 缺省 pipeline_file_path=.lightapollo.ymlstatus=enabled;DisableDefinition 只翻状态位,停用后 StartRun/ExternalTrigger 双双 400 拒绝。UpdateDefinition 为指针字段全量更新(前端 PUT 全量带 body)。DeleteDefinition 只删定义行,不级联删除 run/stage/step(代码注释明确「关联运行的级联清理由调用方按需处理,简化不删」)——所以删除后历史运行记录仍在库中,只是不再挂在任何定义下。

WorkerPool 与执行队列。流水线服务内置内存 WorkerPool:channel 队列(容量 100)+ 固定 worker(默认 3,NewPipelineService 时注入,server 启动调 StartWorker)。started 标志决定触发路径:worker 已启动 → s.queue <- run.ID 异步执行;未启动 → 请求内同步 executeRun(便于单机/自测,external-trigger 则无论是否启动都同步执行于请求内)。executeRun:run 置 running → 逐 stage 串行(顺序 pf.Stages 数组序)→ 每 stage 内步骤串行 → 任一 shell/deploy 步骤失败 → 该 stage 失败、后续 stage 全 skipped → run 失败;全部成功 → saveRunArtifacts(按步骤产物声明收集登记,见下)→ run 置 success。运行取消的竞态守卫在 isRunCancelled:worker 每步执行前检查,已 cancelled 则本 stage 剩余步骤标记 skipped、不新起进程,finishRun 以 cancelled 为终态不被 success/failed 覆盖。

两类步骤执行器。① shell:Runner.RunStep 执行命令并把输出写日志文件,落库 exit_code + log_path;支持 Artifacts 产物声明,完成后 collectStepArtifacts 把产物文件信息(step_id/step_name/path/size_bytes/sha256)收集进 manifest,run 成功时统一存 run.Artifacts JSON。② lightapollo_deployrunDeployStep 取步骤的 sync_target_id(缺失 → lightapollo_deploy 步骤 %q 缺少 sync_target_id 内部错误),转调注入的 DeployTrigger.TriggerDeploy(sync_target_id, triggered_by)——server 层把它接到 gitops.SyncEngine,即与「部署与漂移」页同源的 GitOps 部署触发链路(pipeline 包通过接口注入避免与 gitops 循环依赖)。此步骤无日志文件(LogPath 空),点「日志」返回空。

external-trigger 的「直达 deploy」语义。与手动 run 不同,ExternalTrigger 不读取也无视 shell/build/test:只为每个 stage 建一条 skipped stage_run 占位,仅对 stage 内 lightapollo_deploy 类型步骤建 running step_run 并立即执行,命中失败即 stage/run 置 failed。因此 external-trigger 返回的 run 已是终态,历史上该 run 的 Stage 表会是「全 skipped + 一个实际执行过的 stage」的形态(能否看到取决于 8 章所述 stage/step 下钻缺陷是否修复)。

取消即杀进程树(阶段 E 批4a)。CancelRun 先对 pending/running 落库 cancelled 终态,再通过 runner.(ProcessTracker).KillRun(runID) 终止该 run 全部活跃 step 子进程(含子进程树),避免孤儿进程继续写日志/改产物;pending 排队中尚无进程 → KillRun 幂等返回 nil;已终态取消 no-op。kill 失败仅 logger.Warn 降级,取消语义不受影响。

日志与产物读取GetStepLog 校验 step 属于该 run(step.RunID != runID → 404),LogPath 为空返回空串,否则 os.ReadFile 全文返回(LogOffset 字段预留增量拉取扩展,当前简化返回全文)。GetRunArtifacts(页面未使用)返回 run.Artifacts 内 []ArtifactEntry(无产物空数组);产物引用解析 resolveArtifactRefs 支持 run 内 @step_name/path 引用替换(阶段 E 批4a)。

6. 权限与安全

7. 常见问题与排错

1. 点「运行」提示 流水线 "x" 已禁用,拒绝启动(40002):原因是该定义当前为 disabled(可能被「停用」过或初始建为停用),但行内「运行/外部触发」按钮并不按状态隐藏,照样可点。处理:先点「启用」把定义翻回 enabled 再触发;若确认定义状态栏是灰的即可直接判断。

2. 新建保存提示 流水线定义 (project_id=1, name=xxx) 已存在(409):原因是定义名在项目内唯一,与既有定义重名。处理:改名后重新提交;或先在列表找到同名定义用「编辑」修改而非新建。

3. 运行详情弹窗 Stages/Steps 恒为「暂无 Stage/Step 信息」:原因是后端 GET /pipeline-runs/:id 只返回 run 本体、不返回嵌套 stage/step(GetStageRuns/GetStepRuns 存在但未注册 HTTP 路由),前端 parseRunDetail 即便兼容嵌套形态也无数据可解析(前端缺陷/契约缺口,见 8 章)。处理:运行是否成功以运行历史/详情概要的状态徽标与耗时为准;步骤日志因 Steps 表为空当前无法从本页打开。需由前端/后端配合修复后才有 Stage/Step/日志下钻。

4. 「外部触发」成功后展开行的运行历史没出现新运行:原因是 handleExternalTrigger 在关闭弹窗后读取 extModal.value.pipe?.id——弹窗对象已被重置为 {visible:false}pipe 为 null,expanded.value[undefined] 恒为 falsy,刷新分支永远不执行(前端逻辑 bug,见 8 章)。处理:在运行历史子区点「刷新」手动重拉;修复前这是稳定复现的行为。

5. 外部触发创建的历史行 Commit/分支列显示 -:原因是外部触发弹窗里填的 commit_sha/branch 前端会放进请求体,但后端 handleExternalTriggerPipeline 不解析 body、ExternalTrigger 创建 run 时也不回填 git_commit_sha/git_branch,两字段恒为空。处理:以「触发类型=external」与状态判断该运行即可;Commit/分支溯源待后端解析 body 后才有值(见 8 章)。

6. 保存时提示 Triggers 不是合法 JSON,请检查格式:原因是 Triggers 文本框内容不是合法 JSON(前端 JSON.parse 抛错,请求未发出)。处理:按 placeholder 填 JSON 数组(如 [{"type":"git_push","branch":"main"}])或对象;留空则不配置(不传 triggers 字段)。

7. 编辑弹窗里 Git 仓库下拉变成了数字输入框:原因是 GET /projects/1/git-repos 加载失败或仓库列表为空(fetchRepos catch 静默置空数组、不阻断页面)。处理:手动输入仓库 ID(先到「Git 仓库」页确认该 ID 存在);或修复仓库下拉数据源后重新打开弹窗。新建弹窗此时 git_repo_id 默认值为 null,必须手输。

8. 步骤日志弹窗显示「(无日志内容)」:原因分两类——① 该步骤是 lightapollo_deploy 类型(无日志文件,LogPath 空,后端返回空串);② shell 步骤确实没输出或日志文件被清。处理:确认步骤类型(Steps 表「类型」列);对 deploy 步骤日志为空属正常,执行结果看运行/阶段状态;shell 步骤日志为空则检查命令是否真的无 stdout。

9. 取消运行后状态未变 / 取消按钮消失但状态仍 running:原因是后端先落 cancelled 再杀进程树,若 worker 正卡在子进程 I/O,落库已 cancelled 但列表数据可能未刷新。处理:点子区「刷新」强制重拉;若仍 running 且反复取消无果,看后端日志确认 KillRun 是否降级失败(仅 Warn 不阻断取消语义)。

8. 已知缺陷与边界

缺陷/边界 说明
运行详情 Stage/Step 恒为空 GET /pipeline-runs/:id 只返回 run 本体,无嵌套 stage/step;后端 GetStageRuns/GetStepRuns 有实现但未注册 HTTP 路由。前端 parseRunDetail 兼容嵌套形态也无数据可解析 → 详情弹窗恒显示「暂无 Stage/Step 信息」,步骤「日志」按钮不可达。需前端(改契约消费单拆的 stages/steps 接口)与后端(补路由或嵌套响应)配合修复
外部触发后运行历史不刷新 PipelinesPage.vue handleExternalTrigger 成功分支:先把 extModal 重置(visible:falsepipe:null),下一行再读 expanded.value[extModal.value.pipe?.id] → undefined 恒 falsy,「已展开则刷新」分支永不执行;即便修复此读取顺序,读到的也是已被清空的 pipe.id。前端逻辑 bug,需先把 pipe 引用存局部变量再判断/刷新
external-trigger 丢弃 commit/branch 前端外部触发弹窗收集 git_commit_sha/git_branch 并放请求体,后端 handleExternalTriggerPipeline 不解析请求体,ExternalTrigger 建 run 不回填 → 运行历史 Commit/分支列恒为 -,弹窗填的上下文无溯源。需后端解析 body 字段回填(或前端改查询参数)
「运行号」提示与字段语义偏差 run 成功提示里 ${runId?(id=${runId}):''}:运行号 #${runId} 的 runId 取 data.id(运行记录主键),并非 run_number——提示把主键 id 当运行号展示,与运行历史列里的 #run_number 可能不一致。纯文案/取值易误导
删除定义不删历史运行 confirm 文案 历史运行记录可能一并删除,但后端 DeleteDefinition 只删 apollo_pipeline_definitions 行,关联 run/stage/step 保留为孤儿数据(不级联删除)。文案与行为不符,删除后无法从页面再看到这些运行
停用定义仍显示触发按钮 「运行/外部触发」按钮不按 disabled 状态隐藏,停用后点击得 400(见 7.1)。可接受(后端兜底)但易用性差
项目 id 硬编码 PROJECT_ID = 1 常量(default 项目),多项目场景需改为从 URL 参数 /project_id 读取
全量无分页 定义列表与运行历史均一次全量拉取(后端 ListDefinitions/ListRuns limit=0),定义多/单定义运行多时列表与响应变长
运行详情轮询开销 active 运行每 3 秒全量重拉 run 详情,多开详情弹窗并发轮询;无增量/长轮询机制。弹窗关闭与页面卸载已做清理(stopPoll/onBeforeUnmount
Triggers 仅 JSON 文本 无图形化配置,需手写 JSON(数组或对象均可,仅记录展示不参与触发判定);前端校验 JSON 但无 schema 校验
shell 执行为本地模拟 URL 指向本地目录、shell 步骤在 worker 进程内执行,非真实 CI runner;日志为文件全文返回(LogOffset 预留未用)

附录 A:速查——常用字段别名与提示文案清单

场景 源码取值/文案(逐字)
页面标题 Apollo CI/CD 流水线
顶部说明 流水线定义指向 Git 仓库中的流水线文件(pipeline_file_path),可手动运行(run)、外部触发(external-trigger)或启用/停用(enable/disable);展开行可查看运行历史、stage/step 明细与日志。
定义空态 暂无流水线定义,点击"新建流水线"添加(项目 #{{ PROJECT_ID }})。
运行空态 暂无运行记录,点击"运行"或"外部触发"发起一次运行。
前端必填校验 名称(name)、Git 仓库(git_repo_id)与流水线文件路径必填
Triggers 非法 Triggers 不是合法 JSON,请检查格式
创建成功 流水线创建成功(id=${data.id})
编辑成功 流水线 #${id} 更新成功
运行成功 流水线 "x" 已触发运行(id=${runId}):运行号 #${runId}
启停成功 流水线 "x" 已启用(状态:enabled) / 流水线 "x" 已停用(状态:disabled)
外部触发成功 外部触发 "x" 成功(运行 id=${runId}):commit ${sha||'-'} / branch ${branch||'-'}
删除确认 确定删除流水线 "x"(#id)吗?历史运行记录可能一并删除。
取消确认 确定取消运行 #${run_number}(id=${run.id})吗?
取消成功 运行 #${run_number} 已取消(状态:cancelled)
轮询提示 <运行中徽标> 每 3 秒自动刷新…