节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
13.05 KiB
Stardust
# 流水线功能 Web 端统一实现 Spec ## Why 当前 Stardust 流水线(CI/CD)功能存在**两套实现**: - **Web 端** `Stardust.Web/Services/PipelineService.cs`:负责手动触发,复用 `DeployService.Compile/Control`,行为与手动点击「编译上传」「发布」一致。 - **Server 端** `Stardust.Server/Services/PipelineService.cs`:负责 webhook 触发,**自行重写** `ExecuteCompileAsync` / `ExecuteInstallAsync`,行为与手动分叉。 根本原因:`Stardust.Server` 不引用 `Stardust.Web`(项目依赖单向 Web→Server),Server 端无法调用 `DeployService`,只能自己实现一遍编译/部署逻辑。这导致: 1. **行为不一致**(webhook vs 手动): - 编译:Server 端调 `SendNodeCommand`(已 `[Obsolete]`),Web 端调 `SendNodeCommandAsync`。 - 部署 install:Server 端不调 `DeployService.Install()`(不设 `Enable=true`),Web 端会调。 - 部署 args:Server 端带 `verId`,Web 端不带。 - timeout:Server 端 5/30 秒,Web 端 0(无超时)。 2. **"使用版本"步骤缺失**:流水线编译后只设 `run.AppVersionId`,未更新 `app.Version`。Web 端 `Control(install)` 不传 `verId`,部署节点按 `app.Version` 取包 → **可能部署到旧版本**。 3. **重复实现**:`CompileCommand` 构造、`SendNodeCommand` 调用、`AppDeployHistory` 写入在 Server 端被复制了一遍。 **目标**:撤销 Server 端流水线全部实现,统一在 Web 端实现。让 webhook 与手动两条路径共用同一套 `DeployService.Compile → 更新 app.Version → DeployService.Control(install)` 编排,**严格复用手动点击操作,不做额外逻辑实现**。 > 本功能未上线,无已配置 webhook 需兼容,按最佳方案重新设计。 ## What Changes ### 撤销(Stardust.Server,全部删除) - 删除 `Stardust.Server/Services/PipelineService.cs` - 删除 `Stardust.Server/Controllers/PipelineWebhookController.cs` - 删除 `Stardust.Server/Controllers/PipelineController.cs` - `Stardust.Server/Startup.cs` 移除 `services.AddSingleton<PipelineService>()` 一行 ### 新增(Stardust.Web) - 新增 `Stardust.Web/Areas/Deployment/Controllers/PipelineWebhookController.cs`:接收 git push webhook,委托 `PipelineService.HandleWebhookAsync` - `Stardust.Web/Services/PipelineService.cs` 增补: - `HandleWebhookAsync(token, body, signature)` 公开入口 - `ParsePayload` / `VerifySignature` / `MatchBranch` / `IsDuplicate` 等 webhook 专属辅助方法(从 Server 端迁移) ### 修改(Stardust.Web) - `PipelineService.RunAsync` 编译成功并查到 `AppDeployVersion` 后,补「使用版本」步骤:`app.Version = version.Version; app.Update();` - `AppPipelineController.CopyWebhook` 确认 webhook URL 指向 Web 端地址 ## Impact - Affected specs: - [add-pipeline-feature](/NewLife/Stardust/Blob/master/.trae/specs/move-pipeline-to-web/../add-pipeline-feature/spec.md) — **被取代**:webhook 与编排实现全部迁移到 Web 端 - [refactor-pipeline-service](/NewLife/Stardust/Blob/master/.trae/specs/move-pipeline-to-web/../refactor-pipeline-service/spec.md) — **保留**:Web 端 `PipelineService` 架构(Trigger/Reprocess/RunAsync)不变,仅扩展 - Affected code: - `Stardust.Server/Services/PipelineService.cs` — **删除** - `Stardust.Server/Controllers/PipelineWebhookController.cs` — **删除** - `Stardust.Server/Controllers/PipelineController.cs` — **删除** - `Stardust.Server/Startup.cs` — 移除一行注册 - `Stardust.Web/Services/PipelineService.cs` — 增补 webhook 入口 + 「使用版本」逻辑 - `Stardust.Web/Areas/Deployment/Controllers/PipelineWebhookController.cs` — **新增** - 不受影响: - `Stardust.Web/Services/DeployService.cs`(保持原样,被复用) - `DeployAgent` / `StarAgent` / `AppDeploy` / `AppBuildNode` / `AppDeployNode` 任何已有逻辑 - 手动「编译上传」「使用版本」「发布」三条 UI 路径 ## 核心设计 ### 1. Webhook 路由 ``` POST /Pipeline/Webhook?token={AppPipeline.Token} Body: 通用 JSON 负载(GitHub/GitLab/Gitee/自建 git 都能解析) Header: X-Hub-Signature-256(可选,配合 Secret 使用) ``` 路由策略:在 Web 端 `PipelineWebhookController` 上用 `[Route("Pipeline/[action]")]` 显式声明,URL 保持 `/Pipeline/Webhook?token=...` 简短形式,便于 git 平台配置。 ### 2. 复用清单(不改动这些文件) | 复用对象 | 来源 | 用途 | |---|---|---| | `DeployService.Compile` | `Stardust.Web/Services/DeployService.cs` | 手动「编译上传」按钮的方法,内部构造 `CompileCommand`、调 `SendNodeCommandAsync`、写 `AppDeployHistory` | | `DeployService.Control(install)` | 同上 | 手动「发布」按钮的方法,内部调 `Install()` 设 `Enable=true`、调 `SendNodeCommandAsync`、写 `AppDeployHistory` | | `StarFactory.SendNodeCommandAsync` | `Stardust` | 与手动一致的非 Obsolete 异步下发 | | `AppDeployVersion.FindAllByDeployId` | `Stardust.Data` | 查编译产物 | | `AppDeployHistory.Create` | `Stardust.Data` | 由 `DeployService` finally 块调用 | ### 3. Webhook 触发流 ``` PipelineWebhookController.Webhook(token) 1. 读取原始 body + X-Hub-Signature-256 header 2. 调用 PipelineService.HandleWebhookAsync(token, body, signature) a. token 空 → 200 {result:"error", reason:"missing token"} b. AppPipeline.FindByToken(token) 未命中 → 200 {result:"error", reason:"invalid token"} c. pipeline.Enable=false → 200 {result:"ignored", reason:"pipeline disabled"} d. 配置了 Secret 时校验 X-Hub-Signature-256: - 未携带 → 200 {result:"error", reason:"missing signature"} - 校验失败 → 200 {result:"error", reason:"invalid signature"} e. ParsePayload(body) 解析 ref / commitId / message / author / time - 无分支 → 200 {result:"ignored", reason:"no branch"} f. MatchBranch(pipeline.Branch, actual) - 不匹配 → 200 {result:"ignored", reason:"branch mismatch"} g. IsDuplicate(pipelineId, commitId, 5min) - 重复 → 200 {result:"skipped", reason:"duplicate"} h. 创建 AppPipelineRun: - TriggerSource = "webhook" - Status = Pending - CommitId/Message/Author/Time/Branch/BuildNodeId 从 payload + pipeline 取 i. 异步 _ = Task.Run(() => RunAsync(run, pipeline, app, buildNode, userHost, CancellationToken.None)) 3. 立即返回 200 {result:"accepted", runId, status} ``` ### 4. 手动触发流(沿用 refactor-pipeline-service 架构) ``` AppPipelineController.Trigger(id) → PipelineService.Trigger(pipelineId, userHost) AppPipelineRunController.Reprocess(id) → PipelineService.Reprocess(runId, userHost) 两者最终都调用私有 RunAsync(run, pipeline, app, buildNode, userHost, ct) ``` ### 5. 统一编排流程(RunAsync,webhook 与手动共用) ``` PipelineService.RunAsync(run, pipeline, app, buildNode, userHost, ct) 1. run.TraceId = span?.TraceId; run.Update() 2. run.Status = Building; run.BuildStartedTime = now; run.Update() 3. ★ 调用 _deployService.Compile(app, buildNode, "Build-Upload", userHost, ct) ← 复用手动「编译上传」操作 4. 编译成功,查最新版本: if buildNode.UploadPackage: version = AppDeployVersion.FindAllByDeployId(pipeline.DeployId, 1).FirstOrDefault() run.AppVersionId = version?.Id ?? 0 5. ★【使用版本】if version != null: app.Version = version.Version app.Update() ← 对齐手动「使用版本」操作,确保后续 install 部署该版本 6. run.BuildFinishedTime = now; run.Status = UploadSucceeded; run.Update() 7. if pipeline.AutoDeploy: run.Status = Deploying; run.DeployStartedTime = now; run.Update() 遍历 pipeline.DeployNodeIds(逗号分隔): var dn = AppDeployNode.FindById(id) if dn != null && dn.Enable && dn.DeployId == pipeline.DeployId: ★ 调用 _deployService.Control(app, dn, "install", userHost, 0, 0, null, ct) ← 复用手动「发布」操作 run.DeployFinishedTime = now 8. run.Status = Success; run.Update() 9. 异常 catch: run.Status = Failed; run.Remark = ex.Message; run.Update() ``` ### 6. 严格不做额外逻辑 `PipelineService.RunAsync` **不**重新实现以下内容(这些由 `DeployService` 内部完成,直接复用即可): - 不重新构造 `CompileCommand`(由 `DeployService.Compile` 内部完成) - 不直接调用 `SendNodeCommand` / `SendNodeCommandAsync`(由 `DeployService.Compile/Control` 内部完成) - 不直接写 `AppDeployHistory`(由 `DeployService.Compile/Control` 的 finally 块完成) - 不直接调用 `DeployService.Install()`(由 `DeployService.Control(install)` 内部 switch 调用) - 不写 `AppPipelineStep`(webhook 路径暂不写 step,作为后续独立扩展) ### 7. Webhook URL 来源 `AppPipelineController.CopyWebhook(id)` 返回的 URL 使用 `StarSetting.Current.ServiceAddress`(同进程部署时即 Web 站地址),格式: ``` {serviceAddress}/Pipeline/Webhook?token={pipeline.Token} ``` ## ADDED Requirements ### Requirement: Webhook 接入在 Web 端实现 系统 SHALL 在 `Stardust.Web` 提供 `POST /Pipeline/Webhook?token={token}` 接口,接收通用 git push JSON 负载并触发流水线运行。webhook 处理复用 Web 端 `PipelineService.HandleWebhookAsync`,编排复用 `DeployService.Compile/Control`。 #### Scenario: GitHub push 触发 - **WHEN** git 平台调用 webhook URL,body 包含 `ref=refs/heads/main`、`head_commit.id=abc123` - **THEN** 系统找到 Branch=main 的流水线,创建一条 run(TriggerSource=webhook),异步调用 `RunAsync`,30 秒内编译节点开始拉取 #### Scenario: 分支不匹配 - **WHEN** webhook 触发的分支与流水线配置的 Branch 不匹配(精确匹配或末尾通配符 `release/*`) - **THEN** 系统返回 200 `{result:"ignored", reason:"branch mismatch"}`,不创建 run #### Scenario: 同 commit 短时间内重复触发 - **WHEN** 同一 CommitId 在 5 分钟内已有 Pending/Building/UploadSucceeded/Deploying/Success 状态的 run - **THEN** 系统返回 200 `{result:"skipped", reason:"duplicate"}`,不创建新 run #### Scenario: 签名校验失败 - **WHEN** 流水线配置了 Secret,但请求未携带 `X-Hub-Signature-256` 或签名不匹配 - **THEN** 系统返回 200 `{result:"error", reason:"missing/invalid signature"}`,不创建 run #### Scenario: 流水线未启用 - **WHEN** webhook 命中的流水线 `Enable=false` - **THEN** 系统返回 200 `{result:"ignored", reason:"pipeline disabled"}`,不创建 run ### Requirement: 编排严格复用手动操作 `PipelineService.RunAsync` SHALL 通过调用 `DeployService.Compile` 和 `DeployService.Control(install)` 完成编译与部署,**不得**重新构造 `CompileCommand` 或直接调用 `SendNodeCommand`。webhook 与手动两条触发路径 SHALL 走同一份 `RunAsync` 实现。 #### Scenario: webhook 触发与手动触发行为一致 - **WHEN** 同一流水线分别由 webhook 和手动触发 - **THEN** 两次运行的编译命令构造方式、`SendNodeCommandAsync` 调用方式、`AppDeployHistory` 记录方式完全一致 #### Scenario: 编译失败 - **WHEN** `DeployService.Compile` 抛出异常 - **THEN** run.Status=Failed,run.Remark 记录异常信息,不进入部署阶段 ### Requirement: 编译后更新应用版本 `PipelineService.RunAsync` 在编译成功并查到 `AppDeployVersion` 后 SHALL 更新 `AppDeploy.Version`,使后续 `Control(install)` 部署到该版本,对齐手动「使用版本」操作。 #### Scenario: 编译后自动使用版本 - **WHEN** 编译节点上传成功(`buildNode.UploadPackage=true`),查到新的 `AppDeployVersion` - **THEN** `AppDeploy.Version` 被更新为该版本号,后续 `Control(install)` 部署该版本 #### Scenario: 仅编译不上传 - **WHEN** `buildNode.UploadPackage=false` - **THEN** 不查 version,不更新 `app.Version`,`run.AppVersionId=0`;若 `AutoDeploy=true` 仍调用 `Control(install)`(部署 `app.Version` 当前值),最终 `Status=Success` ## REMOVED Requirements ### Removed: Server 端流水线编排逻辑 撤销 [add-pipeline-feature](/NewLife/Stardust/Blob/master/.trae/specs/move-pipeline-to-web/../add-pipeline-feature/spec.md) 中关于 `Stardust.Server/Services/PipelineService.cs` 与 `Stardust.Server/Controllers/PipelineWebhookController.cs` / `PipelineController.cs` 的需求。Server 端不再承担流水线任何职责。 ### Removed: Server 端 ExecuteCompileAsync / ExecuteInstallAsync 撤销 Server 端 `PipelineService.ExecuteCompileAsync`(自行构造 `CompileCommand` 并调用 `[Obsolete] SendNodeCommand`)和 `ExecuteInstallAsync`(自行拼装 args 并调用 `SendNodeCommand`,带 `verId`)的设计。这些职责由 Web 端 `DeployService` 统一承担。 ## 不在本次范围 - 灰度发布(按比例 / 按节点标签) - 流水线阶段(多 step 编排,AppPipelineStep 暂不写入) - 定时触发(cron) - 钉钉/飞书/企微审批 - GitHub Checks API / PR 状态回写 - 多租户/项目隔离(用现有 ProjectId 字段即可,UI 不做改动) - 兼容已配置的旧 webhook URL(功能未上线,无此约束) 如需后续支持,`AppPipeline` 已留 `Remark` / `Enable` 等扩展位。