本文档描述
cicdkit的整体架构、模块职责、数据流、安全模型与运维要点。 面向需要理解、二次开发或排障的开发者。功能使用说明见README.zh.md/README.md。
cicdkit 是一个单机 / 本地优先的 CI/CD 发布管理平台,把「容器构建 → 发布到目标」这一闭环收敛到一个 Go 后端进程 + 一个 React 单页前端,无需外部数据库或消息队列。
它的核心目标是让「构建一个镜像并发布到本地 k3s / 远程裸机 / 集群」这一动作可以从一个 Web UI 里按按钮完成,而不用手敲 docker build / kubectl / ssh 一长串命令。平台本身直接在本机执行 docker、kubectl、ssh 等外部命令,因此它被设计成默认只监听本机回环地址,并强制 API Token 保护。
平台不解决「代码仓库 CI」(不替你跑单测、不打 Git tag),而是解决「我已经有个可构建的目录,怎么把它稳定地发到目标环境」。
┌──────────────────────────────────────────────────────────────────┐
│ 浏览器 (React SPA) │
│ App.jsx + components/* · api.js(X-API-Token / AUTO_TOKEN 注入) │
└───────────────┬──────────────────────────────────────────────────┘
│ HTTP /api/* (静态资源 /、/api/health 开放)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Go 后端 (cmd/server) │
│ │
│ api.Server ── 路由 / 鉴权(withAuth) / SPA 回退(+ token 注入) │
│ │ handlers.go:projects / runs / deployments / │
│ │ llm / kb / webhook / diagnose │
│ ▼ │
│ pipeline.Runner ── 并发信号量 · 取消 · 阶段编排 │
│ ├─ build.Run docker build / buildx(多架构) / push │
│ ├─ deploy.Run local-k3s | ssh | kubectl-* | helm │
│ ├─ probe.Run Postman 式 HTTP 服务探测 │
│ ├─ scan.Scan* 本地目录 → 项目草稿 │
│ └─ generate.* 缺失 Dockerfile/清单 脚手架 │
│ │
│ store (jsonStore:项目/运行/部署 落盘 store.json) │
│ kb (知识库:采纳的诊断 → kb.json) │
│ llm (可选 OpenAI 兼容 诊断,配置落 llm.json) │
│ config (配置加载 + 环境变量覆盖 + 本机绑定校验) │
└───────────────┬──────────────────────────────────────────────────┘
│ exec.CommandContext → 复用宿主工具链
▼
┌─────────────────────────────────────────────────────────┐
│ docker · kubectl · ssh · k3s ctr / crictl · helm │
└───────┬───────────────┬──────────────────┬──────────────┘
▼ ▼ ▼
本地 Docker k3s containerd 远程裸机 Docker
守护进程 (镜像导入) (ssh docker load/run)
关键设计取向
- 零三方依赖:后端仅用 Go 标准库(
go.mod只有module与go 1.22两行)。外部能力全部通过exec.CommandContext调用宿主机已安装的docker/kubectl/ssh等 CLI。这意味着没有供应链依赖风险、没有 cgo 编不过的麻烦,代价是「平台能力 ≈ 宿主机工具链能力」。 - 进程内全栈:前端以
embed.FS的形式编译进同一个 Go 二进制,生产运行无需单独的 Web 服务器。 - 文件即数据库:状态用三个 JSON 文件持久化,无外部存储。适合单机,迁移=拷文件。
- 命令执行即能力边界:所有「发布」最终都是拼一条命令交给宿主执行;因此安全模型(第 8 节)围绕「谁能触发命令 + 命令能是什么」展开。
| 层 | 技术 | 说明 |
|---|---|---|
| 后端语言 | Go 1.22 | 仅标准库 |
| 前端 | React 18 + Vite 5 | 无 UI 框架以外的依赖(手写 CSS) |
| 静态资源 | //go:embed all:web/dist |
构建时把 dist 编入二进制 |
| 持久化 | JSON 文件(原子 rename) | data/store.json / llm.json / kb.json |
| 外部工具 | docker / kubectl / ssh / k3s ctr / helm | 宿主机提供,平台不内置 |
| 鉴权 | API Token(Bearer / X-API-Token) + 可选 AUTO_TOKEN | 见第 8 节 |
cicdkit/
├── cmd/server/
│ ├── main.go # 入口:加载配置→建 store→建 runner→嵌前端→启动
│ └── web/ # React 前端源码(npm run build → dist)
│ ├── src/
│ │ ├── App.jsx # 主应用:列表、无限滚动、UI_BUILD 戳
│ │ ├── api.js # 前端 API 客户端 + 鉴权(tokenPrompting / autoToken)
│ │ └── components/ # ProjectForm / ScanModal / LogModal / ValidateModal /
│ │ # ProbeModal / GenerateFilesModal / SettingsModal /
│ │ # DiagnoseModal / KnowledgeModal / Modal
│ └── dist/ # 构建产物(被 Go 嵌入)
├── internal/
│ ├── api/ # HTTP 层:server.go(路由/鉴权/SPA) + handlers.go
│ ├── config/ # 配置模型 + 环境变量覆盖 + 本机绑定/路径校验
│ ├── store/ # 数据模型(models.go) + 文件存储(store.go) + 校验/脱敏(validate.go)
│ ├── pipeline/ # Runner:并发/取消/阶段编排/部署目标解析
│ ├── build/ # docker build / buildx 多架构 / push / login
│ ├── deploy/ # 5 种部署方式实现
│ ├── scan/ # 本地目录启发式扫描 → 项目草稿
│ ├── generate/ # 缺失 Dockerfile / k8s 清单 脚手架
│ ├── probe/ # Postman 式 HTTP 服务探测
│ ├── check/ # dry-run 校验(配置/daemon/kubeconfig/上下文/探测)
│ ├── kb/ # 知识库:采纳的诊断条目
│ └── llm/ # 可选 OpenAI 兼容诊断客户端
├── examples/ # 8 个可发布示例(见第 11 节)
├── scripts/ # kill-ports.sh / import-to-k3s.sh / seed-example.sh
├── Makefile # 构建/运行/测试/镜像目标
├── Dockerfile # 多阶段构建平台自身
├── .dockerignore # 排除 .env / data/ 进入镜像构建上下文
├── .gitignore # 忽略 .env / data/*.json / 含真实 IP 的 project-tencent-cloud.json
└── config.example.json / .env.example
Config含Server/DataDir/StoreFile/Runner四块。Load先读 JSON 文件,再用真实环境变量覆盖(.env通过LoadDotEnv()在main最早阶段注入)。ServerConfig.IsLoopback()判断监听地址是否仅本机;main.go据此强制:非本机绑定必须设API_TOKEN,否则直接log.Fatalf拒绝启动——因为本平台会在宿主执行命令,暴露给公网等于开放命令执行。FSRoots/FSPathAllowed限制服务器侧目录浏览器的可访问范围,默认仅当前工作目录,避免枚举家目录。
- 模型(
models.go):Project(含BuildSpec/DeploySpec/ProbeSpec/Targets)、Run(含Stages []StageResult、Diagnosis、Probe)、Deployment、DeployTarget、RunDiagnosis、ProbeResult。 - 存储(
store.go):jsonStore全量内存 + 延迟合并落盘(200ms 窗口,避免构建过程中每阶段一次写整文件);写入走临时文件 + 原子 rename,文件权限0600(含密钥)。MaxRuns=200自动裁剪历史,但永不淘汰未结束的 run。 - 向后兼容:
backfillLastDeploy/backfillRunProbe在加载时把部署历史回填到项目卡片,并把「探测失败但曾被记为成功」的旧 run 升级为失败。 - 脱敏(
validate.go):Redacted()把 registry 密码、SSH key 路径、kubeconfig 路径替换为********再经 API 外发;MergeSecrets()保证 UI 的「读取→编辑→保存」往返不会因看不到密钥而误清空。
withAuth中间件:静态资源与/api/health、/api/version开放;/api/webhook/*在配置了WebhookSecret时由自身校验;其余/api/*走 API Token(Bearer 或X-API-Token,常量时间比较)。serveApp提供 SPA:真实文件直接服务,未知路由回退index.html;当启用AUTO_TOKEN时把一次性 token 注入页面<script>,本机浏览器自动鉴权。- 路由覆盖:项目增删改查/扫描/校验/探测/生成、运行触发/取消/诊断/采纳、部署历史、LLM 配置与测试、知识库、Webhook。
Runner持有并发信号量(MaxConcurrent,默认 2),execute先抢信号量再跑阶段。launch在返回前就先把 run 注册进active(sync.Map),使「触发后立即取消」也能找到 run。- 阶段顺序:
build → (push) → deploy → probe。探测失败/不可达会把整次 run 升级为失败——容器起来了 ≠ 服务可用。 - 部署目标解析:
method覆盖(单次运行临时换方式)与命名DeployTarget(多目标如「腾讯云-prod」)二选一;命名目标取用其完整DeploySpec。 ssh+ 未配置 registry + 未显式ssh_pull时自动默认ssh_transfer(免仓库直传);ssh_transfer到异架构宿主时自动探测远端uname -m并构建对应架构镜像,避免 QEMU 模拟拖慢。
- 单架构走经典
docker build;多架构(Platforms含多项)走docker buildx build --platform … --output type=oci并复用专属cicd-multiarch容器化 builder。 - 平台解析优先级:
SSHBuildPlatforms→Build.Platforms→(ssh_transfer 单目标时)默认linux/amd64。 docker login仅在配置了 registry 用户名时执行,凭据优先从CICD_REGISTRY_*环境变量还原。
| 方式 | 机制 | 关键安全/正确性处理 |
|---|---|---|
local-k3s |
可选 docker save | k3s ctr import → kubectl apply 清单 → kubectl set-image → rollout status 等待 |
导入前预检镜像本地存在(否则 set-image 假成功、Pod 陷 ImagePullBackOff);每次部署都 re-apply 清单保证 Service 同步 |
ssh(transfer) |
docker save | ssh docker load 流式直传 + 远端 docker run |
远端磁盘预检;-p 端口由 SSHProbePort 独占、剥离用户多余 -p;容器名稳定单例 |
ssh(pull) |
远端 docker pull + docker run |
需配合 build.push 把镜像推进 registry |
kubectl-apply |
kubectl apply -f |
支持 --wait / --timeout |
kubectl-set-image |
kubectl set-image |
设 Wait 时等待 rollout 真正就绪 |
helm |
helm upgrade --install |
支持 --set image=… 注入镜像 |
命名
DeployTarget让单个代码库可以从 UI 一键发到多个云/主机,每个目标自带完整DeploySpec。
scan对本地目录做启发式扫描:识别 Dockerfile、语言(go/node/python/java/rust…)、k8s 清单,自动抽取 deployment/container/service 类型,并在检测到NodePort/LoadBalancer时自动开启服务探测。输出是「预填草稿」,不落盘,交给前端创建表单。- 浏览器目录选择器拿不到真实绝对路径,
scan.ScanFiles会故意留空构建上下文并提示用户补全,避免把目录名喂给docker build报path not found。 generate在「缺失 Dockerfile 或清单」时生成多阶段模板(按语言),Plan只预览、Apply才写盘并更新项目配置使其立即可发布;已有文件绝不覆盖。
- Postman 式 HTTP 检查(方法/URL/headers/body/期望状态码/响应包含),部署后自动跑、也可按需触发。
- 目标 URL 解析优先级:ssh 由解析后的连接(
.env的CICD_SSH_*+ 探针端口,真实 IP 不进项目 JSON)→ 按目标名的probe.urls[target]→ 全局probe.url→ 集群 Service 自动推导(NodePort依次试 ExternalIP/InternalIP/localhost;LoadBalancer用 ingress IP;ClusterIP不可直连则 skip 并说明)。 - 单次请求在传输错误时重试一次(应对滚动后短暂未监听)。
不构建不发布,只验证:配置合法、docker daemon 可达(尊重 DOCKER_HOST)、集群可达(cluster 类方式)、构建上下文存在、服务探测可用。UI 的「校验」按钮即调用它。
llm是可选 OpenAI 兼容/chat/completions客户端(配置落llm.json,API Key 永不经 API 明文回传)。诊断前会把日志里的 IPv4/IPv6 / user@host / 本地路径正则替换为占位符再外发。- 失败时「诊断」→ 得到分析 → 可「采纳」写入
kb;后续同类错误先匹配知识库复用结论,省一次 LLM 调用。知识库按采纳次数排序,相似度匹配取前 3。
UI 点击「发布」
→ POST /api/projects/:id/pipeline {tag?, method?, target?}
→ runner.TriggerPipeline → launch 先把 Run 写库并注册 active
→ go execute():
抢并发信号量 → 解析部署目标/方式 → 处理 ssh_transfer 架构
build.Run(docker build) [失败则 run=failed 返回]
(build.push? → build.Push)
deploy.Run(按 method 拼命令执行) [失败则 run=failed 返回]
probe.Enabled? → probe.Run(HTTP 探测),fail/err → probeFailed
SaveDeployment(写部署历史 + 回填项目 LastDeploy)
run = 成功(除非 probeFailed)
→ 前端轮询 /api/runs/:id 拿阶段日志与状态
→ 失败可「AI 诊断」→ 采纳进 kb
取消:POST /api/runs/:id/cancel → runner.Cancel 调 runCtl.cancel() → ctx 取消,在途的 docker/kubectl/ssh 进程被杀死。
- 技术:React 18 + Vite 5,手写 CSS,无组件库。构建产物
web/dist被 Goembed进后端二进制。 - 鉴权流(
api.js):请求携带X-API-Token(来自localStorage);401 时弹窗要 token,带tokenPrompting锁避免并发请求连环弹框;AUTO_TOKEN模式下后端把一次性 token 注入页面全局变量,浏览器静默鉴权。 - 列表:无限滚动哨兵 + 链式首屏填满,分页走后端
limit/offset;UI_BUILD戳与后端BuildStamp同显示在 footer,用于一眼确认浏览器连的是否最新构建。 - 模态组件:项目表单、目录扫描、运行日志、校验、探测、文件生成、设置(LLM)、AI 诊断、知识库。
平台在本机执行命令,安全模型围绕「谁能触发命令」与「命令能是什么」:
- 本机绑定 + Token:非 loopback 绑定必须设
API_TOKEN,否则拒绝启动;AUTO_TOKEN生成的一次性 token 会写进前端页面,仅在本机安全,外网绑定会拒绝。 - 镜像导入命令白名单(
validate.go):k3s_import_cmd只允许k3s/k3d/ctr/crictl/nerdctl/microk8s(可加sudo前缀),拒绝|;&$`><` 等 shell 元字符——否则它是任意命令执行原语。 - 路径越界防护:项目可触碰的文件路径(build.context / Dockerfile / manifest / chart / kubeconfig)必须落在
Runner.WorkDir内(pathWithinRoot),防目录穿越。 - SSH 字段 shell 元字符防护:所有会拼进远端
docker命令的 SSH 字段(host/user/container/key/run_args/image)禁止 shell 元字符;远端脚本里 container/image 再单引号兜底(纵深防御)。 - 密钥脱敏:API 响应
Redacted()把 registry 密码 / SSH key 路径 / kubeconfig 替换成********;store.json写权限0600;.gitignore已忽略.env、data/*.json、含真实云主机 IP 的project-tencent-cloud.json。 - LLM 外发日志脱敏:发往第三方模型的日志先替换 IPv4/IPv6 /
user@host//Users/ /home/ /root/路径为占位符,避免泄露内部基建。 - fs_roots 限制:服务器侧目录浏览器仅能在配置的根目录内枚举,默认仅工作目录。
注意:沙箱环境无
.git,无法核查 git 历史是否曾提交过敏感文件。若.env/data/store.json曾在被忽略前就被 commit,请用git log --all --oneline -- .env data/store.json examples/*/project-tencent-cloud.json排查并酌情git filter-repo清洗 + 轮换密钥。
| 文件 | 内容 | 权限 | 是否 git 忽略 |
|---|---|---|---|
data/store.json |
项目 / 运行 / 部署历史(含 ssh_key_path 路径、registry 密码占位) |
0600 | 是 |
data/llm.json |
LLM 配置(含真实 API Key) | 0600 | 是 |
data/kb.json |
采纳的 AI 诊断条目 | 0600 | 是(随 data/*.json) |
合并写入用「临时文件 + os.Rename 原子替换」,200ms 合并窗口避免高频写。CICD_REGISTRY_PASSWORD 配置时,落盘用占位符、运行时再从环境变量还原真实密码,使磁盘上永不留明文密钥。
- Makefile:
make build:先npm run build出dist,再CGO_ENABLED=1 go build -ldflags=-linkmode=external(规避 macOS 15 的LC_UUID/dyld 问题)编译嵌入前端二进制的bin/server。make build-linux/build-linux-arm64:CGO_ENABLED=0 GOOS=linux交叉编译静态单文件,可直接拷到同架构 Linux 运行。make run/dev:本地运行(dev用/api/health门控先后端再前端,消除竞态)。make test:go test -race(并发日志写与取消必须被测出)。make image/k3s-import/example:构建平台镜像 / 导入本地镜像到 k3s / 导入示例项目。
- Dockerfile(平台自身):多阶段(node 构建前端 → golang 编译 → alpine 运行时含
docker-cli+kubectl)。运行时挂载宿主/var/run/docker.sock复用宿主 daemon,/app/data挂卷持久化。 - 部署平台自身到 k3s:本质也是「构建镜像 → 导入 → 发布」,与示例项目一致。
examples/ 含 8 个可发布示例,全部基于 local-k3s:
| 示例 | 语言 | 最终镜像基础 | 非 root 做法 | 实测体积* |
|---|---|---|---|---|
| hello-cicd | Go | 静态二进制(极小基础) | — | ~12.6 MB |
| hello-node | Node.js | node:24-alpine |
USER node |
~162 MB |
| hello-java | Java | eclipse-temurin:21-jre |
JRE 运行 | 207–375 MB |
| hello-rust | Rust | debian:bookworm-slim + 二进制 |
USER nobody |
~76–98 MB |
| hello-ruby | Ruby | ruby:3.3.8-alpine |
adduser -D -u 1000 appuser |
~70–76 MB |
| hello-php | PHP | php:8.3.8-cli-alpine |
adduser -D -u 1000 appuser |
~96–105 MB |
| hello-dotnet | .NET | aspnet:8.0-bookworm-slim |
useradd -M -u 1000 appuser |
~218–250 MB |
| hello-python | Python | python:3.12.7-slim-bookworm |
useradd -m -u 1000 appuser |
~132–157 MB |
* 实测体积来自宿主 docker images,因基础镜像/依赖层/二进制符号略有浮动。
非 root 要点(踩坑固化):Alpine 系(ruby/php 的 *-alpine)用 adduser;debian/ubuntu 系(dotnet/python/rust 的 *-slim 基于 debian)用 useradd;rust 的 debian:slim 自带 nobody。不要假设官方 runtime 镜像自带某用户名(如 python 的 USER python 不存在),一律显式创建用户。
瘦身经验:hello-php 从 php:*-apache 完整版(400MB+)换 cli-alpine(~37MB 下载)小约 10 倍;rust/dotnet 用多阶段构建把 SDK 巨像挡在最终镜像外。
- 存储:
store.Store是接口,当前jsonStore满足单机;要上规模可在同接口后换 BoltDB / SQLite,API 与 runner 无需改动。 - 部署方式:
deploy.Run的switch spec.Method与store.IsValidDeployMethod是两处需同步的清单点;新增方式加一个分支即可。 - LLM 后端:
llm.Client仅依赖 OpenAI 兼容/chat/completions,换模型/网关改BaseURL即可(UI 与推理逻辑与具体模型解耦)。 - 示例:复制
examples/hello-cicd调整语言与project.json即可新增演示。
- 单节点、依赖宿主工具链:平台不在容器里自带 docker/kubectl,需宿主机已安装;平台自身若以容器运行则必须挂
/var/run/docker.sock。 - 镜像必须导入 k3s:
local-k3s手动路径下需docker save | k3s ctr images import(即project.json的k3s_import_cmd);OrbStack 等会自动共享镜像,此时该命令留空即可。漏导入是「set-image 成功但 Pod 卡 ImagePullBackOff」的头号根因。 - macOS 15 链接坑:本地
go build/go run/go test需CGO_ENABLED=1 -linkmode=external,否则二进制因缺LC_UUID一绑端口即 abort;Linux 交叉编译无此问题(Makefile 已处理)。 - 构建上下文必须是服务器可访问的绝对路径:浏览器目录选择器拿不到真实路径,创建项目时需手动补全
build.context。 - 网络/代理:国内环境
npm install可能需--registry https://registry.npmmirror.com;Go 纯标准库无此问题。
本文档基于源码静态分析生成,覆盖 cmd/、internal/ 全部模块与 examples/、Makefile、Dockerfile。如需补充某模块的时序图或接口字段表,可单独提出。