Add D601 Tekton CI
This commit is contained in:
@@ -34,6 +34,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
|||||||
- `bun scripts/cli.ts ssh <providerId> [ssh-like args...]`:通过 provider-gateway 的 Host SSH / WSL SSH 维护桥打开近似原生 ssh 的交互会话或远端命令,并在远端 PATH 注入 `apply_patch`、`glob` 与 `skill-discover`;`apply-patch`、`py`、`skills`、结构化 `find`、`glob` 和 `argv` 子命令用于避免远端补丁、Python stdin、skill 发现与常用只读命令的嵌套转义问题,使用规则见 `docs/reference/cli.md` 和 `docs/reference/provider-gateway.md`。
|
- `bun scripts/cli.ts ssh <providerId> [ssh-like args...]`:通过 provider-gateway 的 Host SSH / WSL SSH 维护桥打开近似原生 ssh 的交互会话或远端命令,并在远端 PATH 注入 `apply_patch`、`glob` 与 `skill-discover`;`apply-patch`、`py`、`skills`、结构化 `find`、`glob` 和 `argv` 子命令用于避免远端补丁、Python stdin、skill 发现与常用只读命令的嵌套转义问题,使用规则见 `docs/reference/cli.md` 和 `docs/reference/provider-gateway.md`。
|
||||||
- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,OA Event Flow/Todo Note/Baidu Netdisk on main-server、k3s Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。
|
- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,OA Event Flow/Todo Note/Baidu Netdisk on main-server、k3s Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。
|
||||||
- `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json] [--service <id>]`:按根目录 `deploy.json` 的服务 repo 和 commit 期望状态校验或更新用户服务,目标侧自行 fetch、构建、部署和 live commit 验证;规则见 `docs/reference/deploy.md`。
|
- `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json] [--service <id>]`:按根目录 `deploy.json` 的服务 repo 和 commit 期望状态校验或更新用户服务,目标侧自行 fetch、构建、部署和 live commit 验证;规则见 `docs/reference/deploy.md`。
|
||||||
|
- `bun scripts/cli.ts ci install/status/run/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,只做每 commit 检查和 Code Queue 只读性能门禁,不部署 CD;规则见 `docs/reference/ci.md`。
|
||||||
- `bun scripts/cli.ts codex deploy <commitId>`:Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build 与 live commit 验证路径;规则见 `docs/reference/codex-deploy.md`。
|
- `bun scripts/cli.ts codex deploy <commitId>`:Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build 与 live commit 验证路径;规则见 `docs/reference/codex-deploy.md`。
|
||||||
- `bun scripts/cli.ts codex task <taskId>`:按 Code Queue 任务 ID 查询初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,便于新任务引用历史 session。
|
- `bun scripts/cli.ts codex task <taskId>`:按 Code Queue 任务 ID 查询初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,便于新任务引用历史 session。
|
||||||
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
||||||
@@ -65,5 +66,6 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
|||||||
- `docs/reference/pipeline-oa-event-flow.md`:Pipeline/OA 事件流、审核/无审核流转、单步调试、甘特图渲染和最终去残留规则。
|
- `docs/reference/pipeline-oa-event-flow.md`:Pipeline/OA 事件流、审核/无审核流转、单步调试、甘特图渲染和最终去残留规则。
|
||||||
- `docs/reference/pipeline-model-proxy.md`:Pipeline v2 model proxy 链路架构、D601 宿主 proxy 服务部署、harness token 注入规则和 smoke test 验证流程。
|
- `docs/reference/pipeline-model-proxy.md`:Pipeline v2 model proxy 链路架构、D601 宿主 proxy 服务部署、harness token 注入规则和 smoke test 验证流程。
|
||||||
- `docs/reference/deploy.md`:`deploy.json` desired-state、target-side build、一次性构建 proxy、直管/代管服务部署 executor 和 live commit 验证规则。
|
- `docs/reference/deploy.md`:`deploy.json` desired-state、target-side build、一次性构建 proxy、直管/代管服务部署 executor 和 live commit 验证规则。
|
||||||
|
- `docs/reference/ci.md`:D601 k3s Tekton CI、只读主数据库性能门禁和 CLI 入口规则。
|
||||||
- `docs/reference/codex-deploy.md`:D601 Code Queue `codex deploy <commitId>` 异步部署管线、路径约定和验证入口。
|
- `docs/reference/codex-deploy.md`:D601 Code Queue `codex deploy <commitId>` 异步部署管线、路径约定和验证入口。
|
||||||
- `reference`:兼容旧路径的符号链接,指向 `docs/reference/`。
|
- `reference`:兼容旧路径的符号链接,指向 `docs/reference/`。
|
||||||
|
|||||||
@@ -107,6 +107,10 @@
|
|||||||
|
|
||||||
随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Code Queue`,确认页面显示默认模型 `gpt-5.5`、默认执行 Provider `D601`、默认工作目录 `/workspace`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Trace 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY`、`CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 D601 env-file 运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。
|
随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Code Queue`,确认页面显示默认模型 `gpt-5.5`、默认执行 Provider `D601`、默认工作目录 `/workspace`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Trace 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY`、`CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 D601 env-file 运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。
|
||||||
|
|
||||||
|
## T23A D601 k3s CI Gate
|
||||||
|
|
||||||
|
阅读 `AGENTS.md` 和 `docs/reference/ci.md`,运行 `bun scripts/cli.ts ci install`,确认 Tekton Pipelines `v1.12.0`、Tekton Triggers `v0.34.0` 和 `unidesk-ci` Pipeline/Task/EventListener 已部署到 D601 原生 k3s;随后运行 `bun scripts/cli.ts ci run --revision <已push的commitId> --wait-ms 1200000`,确认 PipelineRun 只执行 clone/check/performance,不调用 `deploy apply` 或 `codex deploy`,并确认临时 `code-queue-ci-read` 使用主 PostgreSQL 只读查询 Code Queue 首屏、TraceView summary、TraceView steps 和 step detail 的性能指标。若失败,使用 `bun scripts/cli.ts ci logs <pipelineRun>` 查看 TaskRun 和 Pod 日志;交付说明必须记录性能预算是否通过。
|
||||||
|
|
||||||
## T24 MET Nonlinear D601 GPU User Service
|
## T24 MET Nonlinear D601 GPU User Service
|
||||||
|
|
||||||
阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 D601 `~/met_nonlinear` 中存在 `docker-compose.unidesk.yml`、`docker/unidesk/Dockerfile.ml`、`unidesk/server/src/index.ts` 和 `docs/reference/unidesk_microservice.md`;运行 `bun scripts/cli.ts microservice list`,确认 `met-nonlinear` 显示为 `providerId=D601`、`public=false`、`frontendOnly=true`、`127.0.0.1:3288` 后端映射和 `met-nonlinear-ts` 容器摘要;运行 `bun scripts/cli.ts microservice health met-nonlinear`、`bun scripts/cli.ts microservice proxy met-nonlinear /api/queue`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=projects&limit=500'`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=ex_projects&limit=500'`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects/config?path=projects/<name>' --raw` 和 `bun scripts/cli.ts microservice proxy met-nonlinear /api/images`,确认链路通过 backend-core、D601 provider-gateway 和 D601 本机 TS 后端,项目详情包含 `config`、`progress`、`data`、`model`、`metrics` 字段;最后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / MET Nonlinear`,确认项目库按 `projects/` 和 `ex_projects/` 文件树层级展示且文件夹 Project 数与后端返回数量一致,点击项目行能看到结构化 `config.json`、`data/` 训练状态、模型参数量和指标;通过 UI 选择已有 source Project,设置训练轮数和最大并发,使用 `Fork Project` 创建新的 `projects/unidesk_forks/` Project,确认新 Project 被自动勾选但不会直接训练,再点击 `加入待启动队列` 和 `启动队列`;完整验收可用 UI 输入 `Fork 数量=10`、`训练轮数=200`、`最大并发=3`,但这个规模只能由输入框配置,不能作为硬编码按钮。确认最多按 UI 设置的并发数运行、目标 GPU 是 2080Ti、显存余量低于 20% 时自动限制并发、任务最终进入已完成或失败诊断标签且训练容器自动销毁。页面必须以 React 控件显示项目库、待启动/排队/训练中、已完成、失败诊断、GPU/镜像、训练进度、ETA、`epoch/h` 训练速度和历史记录;项目库、当前队列、已完成和失败列表中的项目必须可点击打开详情;默认没有裸 JSON,只有点击 `查看原始JSON` 才显示原始数据;前端不得再提供 `创建10个10轮任务` 这类硬编码测试按钮。
|
阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 D601 `~/met_nonlinear` 中存在 `docker-compose.unidesk.yml`、`docker/unidesk/Dockerfile.ml`、`unidesk/server/src/index.ts` 和 `docs/reference/unidesk_microservice.md`;运行 `bun scripts/cli.ts microservice list`,确认 `met-nonlinear` 显示为 `providerId=D601`、`public=false`、`frontendOnly=true`、`127.0.0.1:3288` 后端映射和 `met-nonlinear-ts` 容器摘要;运行 `bun scripts/cli.ts microservice health met-nonlinear`、`bun scripts/cli.ts microservice proxy met-nonlinear /api/queue`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=projects&limit=500'`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=ex_projects&limit=500'`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects/config?path=projects/<name>' --raw` 和 `bun scripts/cli.ts microservice proxy met-nonlinear /api/images`,确认链路通过 backend-core、D601 provider-gateway 和 D601 本机 TS 后端,项目详情包含 `config`、`progress`、`data`、`model`、`metrics` 字段;最后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / MET Nonlinear`,确认项目库按 `projects/` 和 `ex_projects/` 文件树层级展示且文件夹 Project 数与后端返回数量一致,点击项目行能看到结构化 `config.json`、`data/` 训练状态、模型参数量和指标;通过 UI 选择已有 source Project,设置训练轮数和最大并发,使用 `Fork Project` 创建新的 `projects/unidesk_forks/` Project,确认新 Project 被自动勾选但不会直接训练,再点击 `加入待启动队列` 和 `启动队列`;完整验收可用 UI 输入 `Fork 数量=10`、`训练轮数=200`、`最大并发=3`,但这个规模只能由输入框配置,不能作为硬编码按钮。确认最多按 UI 设置的并发数运行、目标 GPU 是 2080Ti、显存余量低于 20% 时自动限制并发、任务最终进入已完成或失败诊断标签且训练容器自动销毁。页面必须以 React 控件显示项目库、待启动/排队/训练中、已完成、失败诊断、GPU/镜像、训练进度、ETA、`epoch/h` 训练速度和历史记录;项目库、当前队列、已完成和失败列表中的项目必须可点击打开详情;默认没有裸 JSON,只有点击 `查看原始JSON` 才显示原始数据;前端不得再提供 `创建10个10轮任务` 这类硬编码测试按钮。
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# UniDesk CI On D601 k3s
|
||||||
|
|
||||||
|
UniDesk CI is hosted on the D601 native k3s cluster with Tekton Pipelines and Tekton Triggers. It is CI only. CD remains the existing `deploy.json` / `deploy apply` / `codex deploy <commit>` path, and no Tekton task may roll out production services.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- Tekton Pipelines: `v1.12.0`.
|
||||||
|
- Tekton Triggers: `v0.34.0`.
|
||||||
|
- UniDesk CI namespace: `unidesk-ci`.
|
||||||
|
- Manifests: `src/components/microservices/k3sctl-adapter/k3s/ci/`.
|
||||||
|
- CLI entry: `bun scripts/cli.ts ci install|status|run|logs`.
|
||||||
|
|
||||||
|
The CLI reaches D601 through the existing `k3sctl-adapter` Host SSH maintenance bridge and then runs native `KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl ...`. It does not require backend-core to be running and does not expose a new public port.
|
||||||
|
|
||||||
|
## Pipeline Scope
|
||||||
|
|
||||||
|
Each commit CI run performs:
|
||||||
|
|
||||||
|
- `git clone` and checkout of the requested repository revision.
|
||||||
|
- `bun install --frozen-lockfile` at the repo root and `src/`, because `bun scripts/cli.ts check` compiles all `src/components` and needs the component workspace lockfile for frontend React dependencies.
|
||||||
|
- `bun scripts/cli.ts check`.
|
||||||
|
- Temporary `code-queue-ci-read` Deployment and ClusterIP Service in `unidesk-ci`.
|
||||||
|
- Code Queue read performance checks against the production PostgreSQL through `d601-tcp-egress-gateway`.
|
||||||
|
|
||||||
|
`ci install` also prewarms the D601 k3s containerd runtime with the Tekton entrypoint/workingdir helper images, `oven/bun:1-debian`, `alpine/git:2.45.2` and `unidesk-code-queue:d601`. Missing images are pulled through the node-local provider-gateway WS egress proxy and then imported into native k3s containerd with digests preserved, so PipelineRun pods do not hang on external registry pulls.
|
||||||
|
|
||||||
|
Git clone and dependency downloads inside the repo check task use `d601-provider-egress-proxy.unidesk.svc.cluster.local:18789`; the NO_PROXY list keeps the in-cluster read service, D601 TCP egress gateway and any in-cluster CI Git mirror on the cluster network.
|
||||||
|
|
||||||
|
Steps that call the Kubernetes API directly clear inherited proxy variables so service-account HTTPS calls to `kubernetes.default.svc` do not accidentally use the Code Queue image's Docker Compose proxy defaults.
|
||||||
|
The rollout poll reads the Deployment main resource rather than the `/status` subresource, keeping CI RBAC limited to the same app/service resources it creates and deletes.
|
||||||
|
The performance probe scans recent Code Queue tasks until it finds one with trace steps, so a newly selected task without persisted step detail does not make the whole gate fail before measuring the trace endpoints.
|
||||||
|
|
||||||
|
The temporary Code Queue service uses:
|
||||||
|
|
||||||
|
- `CODE_QUEUE_SERVICE_ROLE=read`.
|
||||||
|
- `CODE_QUEUE_SCHEDULER_ENABLED=false`.
|
||||||
|
- `CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED=false`.
|
||||||
|
- `CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED=false`.
|
||||||
|
- D601 k3s `d601-provider-egress-proxy` for external/OA Event Flow fetches, with `d601-tcp-egress-gateway` and the CI read service in `NO_PROXY`.
|
||||||
|
- EmptyDir state/log mounts.
|
||||||
|
|
||||||
|
This means the CI service can read existing tasks, Trace summaries, Trace steps and Trace step details from the main database, but it must not schedule, mutate, notify, backfill or become deployment truth.
|
||||||
|
|
||||||
|
## Performance Gate
|
||||||
|
|
||||||
|
The initial budgets live in `unidesk-ci/unidesk-ci-budgets`:
|
||||||
|
|
||||||
|
- Code Queue first overview payload through the temporary read service, used as the service-side first-paint proxy: `2000ms`.
|
||||||
|
- `GET /api/tasks/{id}/trace-summary`: `700ms`.
|
||||||
|
- `GET /api/tasks/{id}/trace-steps`: `900ms`.
|
||||||
|
- `GET /api/tasks/{id}/trace-step`: `700ms`.
|
||||||
|
- `GET /api/tasks/overview` p95 over 10 samples: `900ms`.
|
||||||
|
|
||||||
|
These are absolute budgets. Historical relative baselines can be added later by writing metrics to a dedicated CI table or object store; they should not be mixed into production task tables.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Install or refresh CI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun scripts/cli.ts ci install
|
||||||
|
```
|
||||||
|
|
||||||
|
Check status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun scripts/cli.ts ci status
|
||||||
|
```
|
||||||
|
|
||||||
|
Run CI manually for a commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun scripts/cli.ts ci run --revision <commit>
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect a run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun scripts/cli.ts ci logs <pipelineRunName>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trigger Boundary
|
||||||
|
|
||||||
|
`unidesk-ci.triggers.yaml` installs the EventListener, TriggerBinding and TriggerTemplate, but the EventListener remains a normal in-cluster Service. Do not expose it through NodePort, LoadBalancer or an unrestricted public ingress. If GitHub or another Git remote needs webhook delivery, add a UniDesk-controlled frontend/backend route with secret verification and then proxy to the EventListener; keep frontend and provider ingress as the only unrestricted public entry points.
|
||||||
@@ -73,6 +73,12 @@ The reconciler selects the executor from `config.json`:
|
|||||||
|
|
||||||
Existing service-specific commands such as Code Queue deploy should converge onto this reconciler path instead of keeping a parallel implementation.
|
Existing service-specific commands such as Code Queue deploy should converge onto this reconciler path instead of keeping a parallel implementation.
|
||||||
|
|
||||||
|
## CI Separation
|
||||||
|
|
||||||
|
Continuous integration is intentionally separate from this deploy reconciler. D601 k3s hosts Tekton CI resources described in `docs/reference/ci.md`, but those PipelineRuns only clone, check and run read-only performance gates. They must not call `deploy apply`, `codex deploy`, `kubectl rollout restart` for production services, or mutate `deploy.json`.
|
||||||
|
|
||||||
|
The Code Queue performance gate may create a temporary `code-queue-ci-read` service and read the main PostgreSQL through the existing `d601-tcp-egress-gateway`. Because it runs with `CODE_QUEUE_SERVICE_ROLE=read`, scheduler/backfill/notification disabled and EmptyDir state, it is not deployment truth and does not need a temporary database for the current read-only checks.
|
||||||
|
|
||||||
## Version Stamping And Verification
|
## Version Stamping And Verification
|
||||||
|
|
||||||
Every successful deployment must stamp the source version in the runtime:
|
Every successful deployment must stamp the source version in the runtime:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
- command.ts (Bounded command execution helpers)
|
- command.ts (Bounded command execution helpers)
|
||||||
- output.ts (JSON output helpers)
|
- output.ts (JSON output helpers)
|
||||||
- e2e.ts (Public frontend/provider ingress, internal core/database, and Playwright frontend E2E checks)
|
- e2e.ts (Public frontend/provider ingress, internal core/database, and Playwright frontend E2E checks)
|
||||||
|
- ci.ts (D601 k3s Tekton CI install/status/manual-run/logs helpers; CI only, no CD)
|
||||||
- logs/ (Generated service logs; ignored by git)
|
- logs/ (Generated service logs; ignored by git)
|
||||||
- .state/ (Generated job state and compose env; ignored by git)
|
- .state/ (Generated job state and compose env; ignored by git)
|
||||||
- docs/
|
- docs/
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
- provider-gateway.md (Provider connection and host SSH maintenance bridge)
|
- provider-gateway.md (Provider connection and host SSH maintenance bridge)
|
||||||
- observability.md (Logs and status visibility)
|
- observability.md (Logs and status visibility)
|
||||||
- e2e.md (Delivery gate, Playwright frontend E2E, and database persistence checks)
|
- e2e.md (Delivery gate, Playwright frontend E2E, and database persistence checks)
|
||||||
|
- ci.md (D601 k3s Tekton CI, read-only production database performance gate, and trigger boundary)
|
||||||
- src/ (TypeScript component monorepo)
|
- src/ (TypeScript component monorepo)
|
||||||
- package.json (Component workspace metadata)
|
- package.json (Component workspace metadata)
|
||||||
- bun.lock (Component dependency lockfile)
|
- bun.lock (Component dependency lockfile)
|
||||||
@@ -88,4 +90,5 @@
|
|||||||
- code-queue/ (Codex/OpenCode queue backend; k3s-managed when exposed through UniDesk)
|
- code-queue/ (Codex/OpenCode queue backend; k3s-managed when exposed through UniDesk)
|
||||||
- oa-event-flow/ (Unified OA event ledger, tag stream, and Trace/STEP stats center)
|
- oa-event-flow/ (Unified OA event ledger, tag stream, and Trace/STEP stats center)
|
||||||
- k3sctl-adapter/ (D601 k3s control-plane adapter and managed service manifests)
|
- k3sctl-adapter/ (D601 k3s control-plane adapter and managed service manifests)
|
||||||
|
- k3s/ci/ (Tekton CI install marker, Pipeline/Task, and in-cluster Trigger manifests)
|
||||||
- example-service/
|
- example-service/
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
interface TimingSample {
|
||||||
|
label: string;
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
durationMs: number;
|
||||||
|
bytes: number;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
||||||
|
function envNumber(name: string, fallback: number): number {
|
||||||
|
const raw = process.env[name];
|
||||||
|
if (raw === undefined || raw.length === 0) return fallback;
|
||||||
|
const value = Number(raw);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
|
||||||
|
return Math.floor(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseUrl(): string {
|
||||||
|
return (process.env.CI_CODE_QUEUE_URL ?? "http://code-queue-ci-read.unidesk-ci.svc.cluster.local:4222").replace(/\/+$/u, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSample(label: string, url: string, timeoutMs = 30_000): Promise<TimingSample> {
|
||||||
|
const started = performance.now();
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||||
|
const text = await response.text();
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
method: "GET",
|
||||||
|
url,
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
durationMs: Math.round((performance.now() - started) * 10) / 10,
|
||||||
|
bytes: text.length,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
method: "GET",
|
||||||
|
url,
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
durationMs: Math.round((performance.now() - started) * 10) / 10,
|
||||||
|
bytes: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentile(values: number[], percentileValue: number): number {
|
||||||
|
if (values.length === 0) return 0;
|
||||||
|
const sorted = values.slice().sort((left, right) => left - right);
|
||||||
|
if (percentileValue <= 0) return sorted[0] ?? 0;
|
||||||
|
if (percentileValue >= 100) return sorted[sorted.length - 1] ?? 0;
|
||||||
|
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((percentileValue / 100) * sorted.length) - 1));
|
||||||
|
return sorted[index] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function candidateTaskIds(url: string): Promise<string[]> {
|
||||||
|
const response = await fetch(`${url}/api/tasks/overview?limit=24&transcriptLimit=0&compact=1&selected=1&includeActive=0&stats=0&skipTrace=1`, {
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
});
|
||||||
|
const body = await response.json() as { selected?: { task?: { id?: string } }; tasks?: Array<{ id?: string }> };
|
||||||
|
const ids = [
|
||||||
|
body.selected?.task?.id,
|
||||||
|
...(body.tasks ?? []).map((task) => task.id),
|
||||||
|
].filter((id): id is string => typeof id === "string" && id.length > 0);
|
||||||
|
return [...new Set(ids)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function traceSeq(url: string, taskId: string): Promise<number | null> {
|
||||||
|
const response = await fetch(`${url}/api/tasks/${encodeURIComponent(taskId)}/trace-steps?tail=1&limit=8`, {
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
});
|
||||||
|
const body = await response.json() as { steps?: Array<{ seq?: number }> };
|
||||||
|
const seq = body.steps?.find((step) => Number.isFinite(Number(step.seq)))?.seq;
|
||||||
|
if (!Number.isFinite(Number(seq))) return null;
|
||||||
|
return Number(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function traceTarget(url: string): Promise<{ taskId: string; seq: number; skippedTaskIds: string[] }> {
|
||||||
|
const ids = await candidateTaskIds(url);
|
||||||
|
if (ids.length === 0) throw new Error("Code Queue CI perf could not find a task id in the production PostgreSQL task table");
|
||||||
|
const skippedTaskIds: string[] = [];
|
||||||
|
for (const taskId of ids) {
|
||||||
|
const seq = await traceSeq(url, taskId);
|
||||||
|
if (seq !== null) return { taskId, seq, skippedTaskIds };
|
||||||
|
skippedTaskIds.push(taskId);
|
||||||
|
}
|
||||||
|
throw new Error(`Code Queue CI perf could not find a task with trace steps among ${ids.length} candidates: ${skippedTaskIds.join(",")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureFirstPaint(url: string): Promise<Record<string, unknown>> {
|
||||||
|
const sample = await fetchSample("code-queue-read-first-paint-proxy", `${url}/api/tasks/overview?limit=12&transcriptLimit=1&compact=1&selected=0&includeActive=0&stats=0&skipTrace=1`, 60_000);
|
||||||
|
return {
|
||||||
|
ok: sample.ok,
|
||||||
|
url: sample.url,
|
||||||
|
firstPaintMs: sample.durationMs,
|
||||||
|
apiTimings: [sample],
|
||||||
|
consoleErrors: [],
|
||||||
|
note: "Code Queue service is API-only in k3s; this measures the first overview payload used by the frontend Code Queue page.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const url = baseUrl();
|
||||||
|
const budgets = {
|
||||||
|
firstPaintMs: envNumber("FIRST_PAINT_BUDGET_MS", 2000),
|
||||||
|
traceSummaryMs: envNumber("TRACE_SUMMARY_BUDGET_MS", 700),
|
||||||
|
traceStepsMs: envNumber("TRACE_STEPS_BUDGET_MS", 900),
|
||||||
|
traceStepDetailMs: envNumber("TRACE_STEP_DETAIL_BUDGET_MS", 700),
|
||||||
|
overviewP95Ms: envNumber("OVERVIEW_P95_BUDGET_MS", 900),
|
||||||
|
};
|
||||||
|
const health = await fetchSample("health", `${url}/health`);
|
||||||
|
if (!health.ok) throw new Error(`Code Queue CI read health failed: ${JSON.stringify(health)}`);
|
||||||
|
const target = await traceTarget(url);
|
||||||
|
const { taskId, seq } = target;
|
||||||
|
const firstPaint = await measureFirstPaint(url);
|
||||||
|
const traceSummary = await fetchSample("trace-summary", `${url}/api/tasks/${encodeURIComponent(taskId)}/trace-summary`);
|
||||||
|
const traceSteps = await fetchSample("trace-steps", `${url}/api/tasks/${encodeURIComponent(taskId)}/trace-steps?tail=1&limit=20`);
|
||||||
|
const traceStepDetail = await fetchSample("trace-step-detail", `${url}/api/tasks/${encodeURIComponent(taskId)}/trace-step?seq=${encodeURIComponent(String(seq))}`);
|
||||||
|
const overviewSamples: TimingSample[] = [];
|
||||||
|
for (let index = 0; index < 10; index += 1) {
|
||||||
|
overviewSamples.push(await fetchSample("overview", `${url}/api/tasks/overview?limit=12&transcriptLimit=1&compact=1&selected=0&includeActive=0&stats=0&skipTrace=1&__ci=${Date.now()}-${index}`));
|
||||||
|
}
|
||||||
|
const overviewSuccessful = overviewSamples.filter((sample) => sample.ok).map((sample) => sample.durationMs);
|
||||||
|
const overviewP95Ms = Math.round(percentile(overviewSuccessful, 95) * 10) / 10;
|
||||||
|
const firstPaintMs = Number((firstPaint as { firstPaintMs?: number }).firstPaintMs ?? 0);
|
||||||
|
const checks = [
|
||||||
|
{ name: "first-paint", ok: firstPaintMs <= budgets.firstPaintMs, valueMs: firstPaintMs, budgetMs: budgets.firstPaintMs },
|
||||||
|
{ name: "trace-summary", ok: traceSummary.ok && traceSummary.durationMs <= budgets.traceSummaryMs, valueMs: traceSummary.durationMs, budgetMs: budgets.traceSummaryMs },
|
||||||
|
{ name: "trace-steps", ok: traceSteps.ok && traceSteps.durationMs <= budgets.traceStepsMs, valueMs: traceSteps.durationMs, budgetMs: budgets.traceStepsMs },
|
||||||
|
{ name: "trace-step-detail", ok: traceStepDetail.ok && traceStepDetail.durationMs <= budgets.traceStepDetailMs, valueMs: traceStepDetail.durationMs, budgetMs: budgets.traceStepDetailMs },
|
||||||
|
{ name: "overview-p95", ok: overviewSamples.every((sample) => sample.ok) && overviewP95Ms <= budgets.overviewP95Ms, valueMs: overviewP95Ms, budgetMs: budgets.overviewP95Ms },
|
||||||
|
];
|
||||||
|
const result = {
|
||||||
|
ok: checks.every((check) => check.ok),
|
||||||
|
measuredAt: new Date().toISOString(),
|
||||||
|
url,
|
||||||
|
taskId,
|
||||||
|
seq,
|
||||||
|
skippedTaskIds: target.skippedTaskIds,
|
||||||
|
budgets,
|
||||||
|
checks,
|
||||||
|
health,
|
||||||
|
firstPaint,
|
||||||
|
traceSummary,
|
||||||
|
traceSteps,
|
||||||
|
traceStepDetail,
|
||||||
|
overview: {
|
||||||
|
p50Ms: Math.round(percentile(overviewSuccessful, 50) * 10) / 10,
|
||||||
|
p95Ms: overviewP95Ms,
|
||||||
|
samples: overviewSamples,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
if (!result.ok) process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -13,6 +13,7 @@ import { runCodeQueueDeployCompatCommand, runDeployCommand } from "./src/deploy"
|
|||||||
import { runProviderCommand } from "./src/provider-attach";
|
import { runProviderCommand } from "./src/provider-attach";
|
||||||
import { runScheduleCommand } from "./src/schedules";
|
import { runScheduleCommand } from "./src/schedules";
|
||||||
import { parseNetworkPerfOptions, runNetworkPerf } from "./src/network-perf";
|
import { parseNetworkPerfOptions, runNetworkPerf } from "./src/network-perf";
|
||||||
|
import { runCiCommand } from "./src/ci";
|
||||||
|
|
||||||
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
|
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
|
||||||
const args = remoteOptions.args;
|
const args = remoteOptions.args;
|
||||||
@@ -58,6 +59,7 @@ function help(): unknown {
|
|||||||
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
||||||
{ command: "debug task <taskId|latest>", description: "Read a dispatched task record from internal core for CLI debugging." },
|
{ command: "debug task <taskId|latest>", description: "Read a dispatched task record from internal core for CLI debugging." },
|
||||||
{ command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." },
|
{ command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." },
|
||||||
|
{ command: "ci install|status|run|logs", description: "Manage D601 k3s Tekton CI only; does not deploy CD. CI reads the production PostgreSQL through a temporary read-only Code Queue service." },
|
||||||
{ command: "e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]", description: "Run selected public/internal/Playwright E2E checks; use --only for focused iteration and rerun without filters for final regression." },
|
{ command: "e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]", description: "Run selected public/internal/Playwright E2E checks; use --only for focused iteration and rerun without filters for final regression." },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -247,6 +249,11 @@ async function main(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (top === "ci") {
|
||||||
|
emitJson(commandName, runCiCommand(config, args.slice(1)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (top === "e2e" && sub === "run") {
|
if (top === "e2e" && sub === "run") {
|
||||||
const result = await runE2E(config, parseE2ERunOptions(args.slice(2)));
|
const result = await runE2E(config, parseE2ERunOptions(args.slice(2)));
|
||||||
const ok = (result as { ok?: unknown }).ok === true;
|
const ok = (result as { ok?: unknown }).ok === true;
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { runCommand } from "./command";
|
||||||
|
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||||
|
import { startJob } from "./jobs";
|
||||||
|
|
||||||
|
const k3sctlContainerName = "k3sctl-adapter";
|
||||||
|
const k3sctlSshKey = "/run/host-ssh/id_ed25519";
|
||||||
|
const d601SshTarget = "ubuntu@host.docker.internal";
|
||||||
|
const d601Kubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||||
|
const tektonPipelineVersion = "v1.12.0";
|
||||||
|
const tektonTriggersVersion = "v0.34.0";
|
||||||
|
const tektonPipelineReleaseUrl = `https://infra.tekton.dev/tekton-releases/pipeline/previous/${tektonPipelineVersion}/release.yaml`;
|
||||||
|
const tektonTriggersReleaseUrl = `https://infra.tekton.dev/tekton-releases/triggers/previous/${tektonTriggersVersion}/release.yaml`;
|
||||||
|
const tektonTriggersInterceptorsUrl = `https://infra.tekton.dev/tekton-releases/triggers/previous/${tektonTriggersVersion}/interceptors.yaml`;
|
||||||
|
const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789";
|
||||||
|
const ciRuntimeImages = [
|
||||||
|
"rancher/mirrored-pause:3.6",
|
||||||
|
"rancher/mirrored-library-busybox:1.36.1",
|
||||||
|
"cgr.dev/chainguard/busybox@sha256:19f02276bf8dbdd62f069b922f10c65262cc34b710eea26ff928129a736be791",
|
||||||
|
"ghcr.io/tektoncd/pipeline/entrypoint-bff0a22da108bc2f16c818c97641a296:v1.12.0",
|
||||||
|
"ghcr.io/tektoncd/pipeline/workingdirinit-0c558922ec6a1b739e550e349f2d5fc1:v1.12.0",
|
||||||
|
"ghcr.io/tektoncd/pipeline/nop-8eac7c133edad5df719dc37b36b62482:v1.12.0",
|
||||||
|
"ghcr.io/tektoncd/pipeline/events-a9042f7efb0cbade2a868a1ee5ddd52c:v1.12.0",
|
||||||
|
"ghcr.io/tektoncd/triggers/eventlistenersink-7ad1faa98cddbcb0c24990303b220bb8:v0.34.0",
|
||||||
|
"oven/bun:1-debian",
|
||||||
|
"alpine/git:2.45.2",
|
||||||
|
"unidesk-code-queue:d601",
|
||||||
|
];
|
||||||
|
|
||||||
|
interface CiOptions {
|
||||||
|
repoUrl: string;
|
||||||
|
revision: string;
|
||||||
|
waitMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringOption(args: string[], name: string): string | null {
|
||||||
|
const index = args.indexOf(name);
|
||||||
|
if (index === -1) return null;
|
||||||
|
const value = args[index + 1];
|
||||||
|
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberOption(args: string[], name: string, fallback: number): number {
|
||||||
|
const raw = stringOption(args, name);
|
||||||
|
if (raw === null) return fallback;
|
||||||
|
const value = Number(raw);
|
||||||
|
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireRevision(value: string | null): string {
|
||||||
|
if (value === null || value.length === 0) throw new Error("ci run requires --revision <commit-or-ref>");
|
||||||
|
if (!/^[A-Za-z0-9._/@:-]{1,160}$/u.test(value)) throw new Error("ci --revision contains unsupported characters");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replace(/'/gu, "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockerExecK3sctl(args: string[]) {
|
||||||
|
return runCommand(["docker", "exec", k3sctlContainerName, ...args], repoRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockerExecK3sctlWithInput(args: string[], input: string) {
|
||||||
|
const command = ["docker", "exec", "-i", k3sctlContainerName, ...args];
|
||||||
|
const result = spawnSync(command[0], command.slice(1), {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: "utf8",
|
||||||
|
input,
|
||||||
|
maxBuffer: 1024 * 1024 * 8,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
cwd: repoRoot,
|
||||||
|
exitCode: result.status,
|
||||||
|
stdout: result.stdout ?? "",
|
||||||
|
stderr: result.stderr ?? result.error?.message ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteKubectlCommand(script: string): string[] {
|
||||||
|
return [
|
||||||
|
"sh",
|
||||||
|
"-lc",
|
||||||
|
[
|
||||||
|
"ssh",
|
||||||
|
"-i",
|
||||||
|
shellQuote(k3sctlSshKey),
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/tmp/unidesk-ci-known-hosts",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=10",
|
||||||
|
shellQuote(d601SshTarget),
|
||||||
|
shellQuote(`KUBECONFIG=${d601Kubeconfig} bash -lc ${shellQuote(script)}`),
|
||||||
|
].join(" "),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function runRemoteKubectl(script: string) {
|
||||||
|
const result = dockerExecK3sctl(remoteKubectlCommand(script));
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`D601 kubectl command failed: ${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteApplyManifest(path: string): void {
|
||||||
|
const absolute = rootPath(path);
|
||||||
|
if (!existsSync(absolute)) throw new Error(`manifest not found: ${path}`);
|
||||||
|
const result = dockerExecK3sctlWithInput([
|
||||||
|
"sh",
|
||||||
|
"-lc",
|
||||||
|
[
|
||||||
|
"ssh",
|
||||||
|
"-i",
|
||||||
|
shellQuote(k3sctlSshKey),
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/tmp/unidesk-ci-known-hosts",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=10",
|
||||||
|
shellQuote(d601SshTarget),
|
||||||
|
shellQuote(`KUBECONFIG=${d601Kubeconfig} kubectl apply -f -`),
|
||||||
|
].join(" "),
|
||||||
|
], readFileSync(absolute, "utf8"));
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`kubectl apply failed for ${path}: ${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prewarmCiRuntimeImages(): void {
|
||||||
|
const images = ciRuntimeImages.map(shellQuote).join(" ");
|
||||||
|
runRemoteKubectl([
|
||||||
|
"set -euo pipefail",
|
||||||
|
"export DOCKER_CONFIG=/tmp/unidesk-ci-docker-config",
|
||||||
|
"mkdir -p \"$DOCKER_CONFIG\"",
|
||||||
|
"printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
|
||||||
|
`images=(${images})`,
|
||||||
|
"for image in \"${images[@]}\"; do",
|
||||||
|
" if ! docker image inspect \"$image\" >/dev/null 2>&1; then",
|
||||||
|
" echo ci_runtime_image_pull=$image",
|
||||||
|
` HTTP_PROXY=${shellQuote(providerGatewayWsEgressProxyUrl)} HTTPS_PROXY=${shellQuote(providerGatewayWsEgressProxyUrl)} ALL_PROXY=${shellQuote(providerGatewayWsEgressProxyUrl)} NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal docker pull --platform linux/amd64 "$image"`,
|
||||||
|
" else",
|
||||||
|
" echo ci_runtime_image_cached=$image",
|
||||||
|
" fi",
|
||||||
|
"done",
|
||||||
|
"pause_entrypoint=$(docker image inspect rancher/mirrored-pause:3.6 --format '{{json .Config.Entrypoint}}' 2>/dev/null || true)",
|
||||||
|
"if ! printf '%s' \"$pause_entrypoint\" | grep -q '\"/pause\"'; then echo native_k3s_pause_image_invalid_entrypoint=$pause_entrypoint >&2; exit 1; fi",
|
||||||
|
"rm -f /tmp/unidesk-ci-runtime-images.tar",
|
||||||
|
"docker save \"${images[@]}\" -o /tmp/unidesk-ci-runtime-images.tar",
|
||||||
|
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import --digests --all-platforms /tmp/unidesk-ci-runtime-images.tar >/tmp/unidesk-ci-runtime-images-import.log",
|
||||||
|
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/rancher/mirrored-pause:3.6' >/dev/null",
|
||||||
|
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/oven/bun:1-debian' >/dev/null",
|
||||||
|
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/alpine/git:2.45.2' >/dev/null",
|
||||||
|
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/library/unidesk-code-queue:d601' >/dev/null",
|
||||||
|
].join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): Record<string, unknown> {
|
||||||
|
const summary = runRemoteKubectl([
|
||||||
|
"set -euo pipefail",
|
||||||
|
"printf 'tekton_pipelines='",
|
||||||
|
"kubectl get deploy -n tekton-pipelines -o name 2>/dev/null | tr '\\n' ' ' || true",
|
||||||
|
"printf '\\ntekton_triggers='",
|
||||||
|
"kubectl get deploy -n tekton-pipelines-resolvers -o name 2>/dev/null | tr '\\n' ' ' || true",
|
||||||
|
"printf '\\nunidesk_ci='",
|
||||||
|
"kubectl get pipeline,task,pipelinerun,eventlistener,svc -n unidesk-ci -o name 2>/dev/null | tr '\\n' ' ' || true",
|
||||||
|
"printf '\\n'",
|
||||||
|
].join("\n"));
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
providerId: "D601",
|
||||||
|
orchestrator: "native-k3s",
|
||||||
|
tekton: {
|
||||||
|
pipelineVersion: tektonPipelineVersion,
|
||||||
|
triggersVersion: tektonTriggersVersion,
|
||||||
|
},
|
||||||
|
summary: summary.stdout.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(): Record<string, unknown> {
|
||||||
|
if (!existsSync(rootPath("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"))) {
|
||||||
|
throw new Error("CI manifests are missing");
|
||||||
|
}
|
||||||
|
prewarmCiRuntimeImages();
|
||||||
|
runRemoteKubectl([
|
||||||
|
"set -euo pipefail",
|
||||||
|
`kubectl apply -f ${shellQuote(tektonPipelineReleaseUrl)}`,
|
||||||
|
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s",
|
||||||
|
`kubectl apply -f ${shellQuote(tektonTriggersReleaseUrl)}`,
|
||||||
|
`kubectl apply -f ${shellQuote(tektonTriggersInterceptorsUrl)}`,
|
||||||
|
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s",
|
||||||
|
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines-resolvers --timeout=900s",
|
||||||
|
].join("\n"));
|
||||||
|
remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/tekton-install.yaml");
|
||||||
|
remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml");
|
||||||
|
remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.triggers.yaml");
|
||||||
|
return status();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pipelineRunManifest(options: CiOptions): string {
|
||||||
|
const safeSuffix = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14).toLowerCase();
|
||||||
|
return `apiVersion: tekton.dev/v1
|
||||||
|
kind: PipelineRun
|
||||||
|
metadata:
|
||||||
|
generateName: unidesk-ci-${safeSuffix}-
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
unidesk.ai/revision: ${JSON.stringify(options.revision)}
|
||||||
|
spec:
|
||||||
|
pipelineRef:
|
||||||
|
name: unidesk-ci
|
||||||
|
taskRunTemplate:
|
||||||
|
serviceAccountName: unidesk-ci-runner
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
value: ${JSON.stringify(options.repoUrl)}
|
||||||
|
- name: revision
|
||||||
|
value: ${JSON.stringify(options.revision)}
|
||||||
|
workspaces:
|
||||||
|
- name: shared-workspace
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: unidesk-ci-cache
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteCreatePipelineRun(manifest: string): string {
|
||||||
|
const result = dockerExecK3sctlWithInput([
|
||||||
|
"sh",
|
||||||
|
"-lc",
|
||||||
|
[
|
||||||
|
"ssh",
|
||||||
|
"-i",
|
||||||
|
shellQuote(k3sctlSshKey),
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/tmp/unidesk-ci-known-hosts",
|
||||||
|
shellQuote(d601SshTarget),
|
||||||
|
shellQuote(`KUBECONFIG=${d601Kubeconfig} kubectl create -f - -o jsonpath='{.metadata.name}'`),
|
||||||
|
].join(" "),
|
||||||
|
], manifest);
|
||||||
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(options: CiOptions): Record<string, unknown> {
|
||||||
|
const name = remoteCreatePipelineRun(pipelineRunManifest(options));
|
||||||
|
const wait = options.waitMs > 0 ? dockerExecK3sctl(remoteKubectlCommand([
|
||||||
|
"set -euo pipefail",
|
||||||
|
`deadline=$((SECONDS + ${Math.ceil(options.waitMs / 1000)}))`,
|
||||||
|
"while [ \"$SECONDS\" -lt \"$deadline\" ]; do",
|
||||||
|
` condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`,
|
||||||
|
" case \"$condition\" in",
|
||||||
|
" True*)",
|
||||||
|
" echo \"$condition\"",
|
||||||
|
` kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
|
||||||
|
" exit 0",
|
||||||
|
" ;;",
|
||||||
|
" False*)",
|
||||||
|
" echo \"$condition\"",
|
||||||
|
` kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
|
||||||
|
" exit 1",
|
||||||
|
" ;;",
|
||||||
|
" esac",
|
||||||
|
" sleep 2",
|
||||||
|
"done",
|
||||||
|
`echo "Timed out waiting for pipelinerun/${name}" >&2`,
|
||||||
|
`kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
|
||||||
|
"exit 124",
|
||||||
|
].join("\n"))) : null;
|
||||||
|
return {
|
||||||
|
ok: wait === null || wait.exitCode === 0,
|
||||||
|
pipelineRun: name,
|
||||||
|
namespace: "unidesk-ci",
|
||||||
|
repoUrl: options.repoUrl,
|
||||||
|
revision: options.revision,
|
||||||
|
wait: wait === null ? null : {
|
||||||
|
stdoutTail: wait.stdout.slice(-6000),
|
||||||
|
stderrTail: wait.stderr.slice(-6000),
|
||||||
|
},
|
||||||
|
next: [
|
||||||
|
`bun scripts/cli.ts ci logs ${name}`,
|
||||||
|
"bun scripts/cli.ts ci status",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function logs(name: string): Record<string, unknown> {
|
||||||
|
if (name.length === 0) throw new Error("ci logs requires PipelineRun name");
|
||||||
|
const result = runRemoteKubectl([
|
||||||
|
"set -euo pipefail",
|
||||||
|
`kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o wide`,
|
||||||
|
`kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o wide`,
|
||||||
|
`for pod in $(kubectl get pods -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o name); do echo "===== $pod"; kubectl logs -n unidesk-ci "$pod" --all-containers=true --tail=160; done`,
|
||||||
|
].join("\n"));
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
pipelineRun: name,
|
||||||
|
output: result.stdout,
|
||||||
|
stderr: result.stderr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function help(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
command: "ci install|status|run|logs",
|
||||||
|
description: "Manage the D601 k3s Tekton CI gate. This intentionally does not deploy CD.",
|
||||||
|
examples: [
|
||||||
|
"bun scripts/cli.ts ci install",
|
||||||
|
"bun scripts/cli.ts ci run --revision <commit>",
|
||||||
|
"bun scripts/cli.ts ci logs <pipelineRun>",
|
||||||
|
],
|
||||||
|
tekton: {
|
||||||
|
pipelineVersion: tektonPipelineVersion,
|
||||||
|
triggersVersion: tektonTriggersVersion,
|
||||||
|
sources: {
|
||||||
|
pipeline: tektonPipelineReleaseUrl,
|
||||||
|
triggers: tektonTriggersReleaseUrl,
|
||||||
|
interceptors: tektonTriggersInterceptorsUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runCiCommand(_config: UniDeskConfig, args: string[]): Record<string, unknown> {
|
||||||
|
const [action = "status", nameArg] = args;
|
||||||
|
if (action === "help" || action === "--help" || action === "-h") return help();
|
||||||
|
if (action === "install") return install();
|
||||||
|
if (action === "status") return status();
|
||||||
|
if (action === "run") {
|
||||||
|
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
|
||||||
|
const revision = requireRevision(stringOption(args, "--revision") ?? stringOption(args, "--commit"));
|
||||||
|
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||||
|
return run({ repoUrl, revision, waitMs });
|
||||||
|
}
|
||||||
|
if (action === "logs") return logs(nameArg ?? "");
|
||||||
|
throw new Error("ci command must be one of: install, status, run, logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startCiInstallJob(): Record<string, unknown> {
|
||||||
|
const job = startJob("ci_install", ["bun", "scripts/cli.ts", "ci", "install"], "Install/refresh Tekton CI on D601 k3s");
|
||||||
|
return { ok: true, job };
|
||||||
|
}
|
||||||
@@ -9,5 +9,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["cli.ts", "src/**/*.ts"]
|
"include": ["cli.ts", "src/**/*.ts", "../scripts/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: unidesk-tekton-install
|
||||||
|
namespace: unidesk
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
data:
|
||||||
|
pipelineVersion: "v1.12.0"
|
||||||
|
triggersVersion: "v0.34.0"
|
||||||
|
pipelineReleaseUrl: "https://infra.tekton.dev/tekton-releases/pipeline/previous/v1.12.0/release.yaml"
|
||||||
|
triggersReleaseUrl: "https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/release.yaml"
|
||||||
|
triggersInterceptorsReleaseUrl: "https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/interceptors.yaml"
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
unidesk.ai/purpose: ci
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
namespace: unidesk-ci
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods", "pods/log", "services"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "delete", "patch"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "delete", "patch"]
|
||||||
|
- apiGroups: ["tekton.dev"]
|
||||||
|
resources: ["pipelineruns", "taskruns"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "delete", "patch"]
|
||||||
|
- apiGroups: ["triggers.tekton.dev"]
|
||||||
|
resources: ["eventlisteners", "triggers", "triggerbindings", "triggertemplates", "interceptors"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
namespace: unidesk-ci
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
namespace: unidesk-ci
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-trigger-reader
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["triggers.tekton.dev"]
|
||||||
|
resources: ["clusterinterceptors", "clustertriggerbindings"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-trigger-reader
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
namespace: unidesk-ci
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: unidesk-ci-trigger-reader
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-cross-namespace
|
||||||
|
namespace: unidesk
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["services"]
|
||||||
|
verbs: ["get", "list"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-cross-namespace
|
||||||
|
namespace: unidesk
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: unidesk-ci-runner
|
||||||
|
namespace: unidesk-ci
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: unidesk-ci-cross-namespace
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-cache
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-budgets
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
data:
|
||||||
|
firstPaintMs: "2000"
|
||||||
|
traceSummaryMs: "700"
|
||||||
|
traceStepsMs: "900"
|
||||||
|
traceStepDetailMs: "700"
|
||||||
|
overviewP95Ms: "900"
|
||||||
|
---
|
||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: Task
|
||||||
|
metadata:
|
||||||
|
name: unidesk-repo-check
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/component: repo-check
|
||||||
|
spec:
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
type: string
|
||||||
|
- name: revision
|
||||||
|
type: string
|
||||||
|
- name: image
|
||||||
|
type: string
|
||||||
|
default: unidesk-code-queue:d601
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
volumes:
|
||||||
|
- name: docker-sock
|
||||||
|
hostPath:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
type: Socket
|
||||||
|
steps:
|
||||||
|
- name: clone
|
||||||
|
image: alpine/git:2.45.2
|
||||||
|
env:
|
||||||
|
- name: HTTP_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: HTTPS_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: ALL_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,ci-git-mirror,ci-git-mirror.unidesk-ci,ci-git-mirror.unidesk-ci.svc,ci-git-mirror.unidesk-ci.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local"
|
||||||
|
- name: http_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: https_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: all_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: no_proxy
|
||||||
|
value: "localhost,127.0.0.1,::1,ci-git-mirror,ci-git-mirror.unidesk-ci,ci-git-mirror.unidesk-ci.svc,ci-git-mirror.unidesk-ci.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local"
|
||||||
|
script: |
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
rm -rf "$(workspaces.source.path)/repo"
|
||||||
|
git clone --filter=blob:none "$(params.repo-url)" "$(workspaces.source.path)/repo"
|
||||||
|
cd "$(workspaces.source.path)/repo"
|
||||||
|
git fetch --depth=1 origin "$(params.revision)"
|
||||||
|
git checkout --detach FETCH_HEAD
|
||||||
|
git rev-parse HEAD | tee "$(workspaces.source.path)/commit.txt"
|
||||||
|
- name: install-and-check
|
||||||
|
image: "$(params.image)"
|
||||||
|
env:
|
||||||
|
- name: DOCKER_HOST
|
||||||
|
value: unix:///var/run/docker.sock
|
||||||
|
- name: BUN_INSTALL_CACHE_DIR
|
||||||
|
value: "$(workspaces.source.path)/cache/bun"
|
||||||
|
- name: HTTP_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: HTTPS_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: ALL_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local"
|
||||||
|
- name: http_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: https_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: all_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: no_proxy
|
||||||
|
value: "localhost,127.0.0.1,::1,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local"
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-sock
|
||||||
|
mountPath: /var/run/docker.sock
|
||||||
|
script: |
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(workspaces.source.path)/repo"
|
||||||
|
command -v bun
|
||||||
|
command -v git
|
||||||
|
command -v docker
|
||||||
|
docker compose version >/dev/null
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
(cd src && bun install --frozen-lockfile)
|
||||||
|
bun scripts/cli.ts check
|
||||||
|
---
|
||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: Task
|
||||||
|
metadata:
|
||||||
|
name: unidesk-code-queue-read-perf
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/component: code-queue-performance
|
||||||
|
spec:
|
||||||
|
params:
|
||||||
|
- name: revision
|
||||||
|
type: string
|
||||||
|
- name: app-image
|
||||||
|
type: string
|
||||||
|
default: unidesk-code-queue:d601
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
steps:
|
||||||
|
- name: start-read-service
|
||||||
|
image: "$(params.app-image)"
|
||||||
|
env:
|
||||||
|
- name: HTTP_PROXY
|
||||||
|
value: ""
|
||||||
|
- name: HTTPS_PROXY
|
||||||
|
value: ""
|
||||||
|
- name: ALL_PROXY
|
||||||
|
value: ""
|
||||||
|
- name: NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local"
|
||||||
|
- name: http_proxy
|
||||||
|
value: ""
|
||||||
|
- name: https_proxy
|
||||||
|
value: ""
|
||||||
|
- name: all_proxy
|
||||||
|
value: ""
|
||||||
|
- name: no_proxy
|
||||||
|
value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local"
|
||||||
|
script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
kube_api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}"
|
||||||
|
kube_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
|
||||||
|
kube_ca="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||||
|
kube_namespace="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)"
|
||||||
|
kube() {
|
||||||
|
local method="$1"
|
||||||
|
shift
|
||||||
|
curl -fsS --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X "$method" "$@"
|
||||||
|
}
|
||||||
|
cat >/tmp/code-queue-ci-read-deployment.yaml <<YAML
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: code-queue-ci-read
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: code-queue
|
||||||
|
app.kubernetes.io/component: ci-read
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
unidesk.ai/ci-revision: "$(params.revision)"
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: code-queue
|
||||||
|
app.kubernetes.io/component: ci-read
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: code-queue
|
||||||
|
app.kubernetes.io/component: ci-read
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
unidesk.ai/node-id: D601
|
||||||
|
spec:
|
||||||
|
nodeSelector:
|
||||||
|
unidesk.ai/node-id: D601
|
||||||
|
terminationGracePeriodSeconds: 10
|
||||||
|
containers:
|
||||||
|
- name: code-queue
|
||||||
|
image: "$(params.app-image)"
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 4222
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: code-queue-env
|
||||||
|
optional: true
|
||||||
|
env:
|
||||||
|
- name: HOST
|
||||||
|
value: "0.0.0.0"
|
||||||
|
- name: PORT
|
||||||
|
value: "4222"
|
||||||
|
- name: DATABASE_URL
|
||||||
|
value: "postgres://unidesk:unidesk_dev_password@d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk"
|
||||||
|
- name: CODE_QUEUE_INSTANCE_ID
|
||||||
|
value: "CI-read"
|
||||||
|
- name: CODE_QUEUE_SERVICE_ROLE
|
||||||
|
value: "read"
|
||||||
|
- name: CODE_QUEUE_SCHEDULER_ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
|
||||||
|
value: "true"
|
||||||
|
- name: CODE_QUEUE_EGRESS_PROXY_URL
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
|
||||||
|
- name: HTTP_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: HTTPS_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: ALL_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
|
||||||
|
- name: http_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: https_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: all_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: no_proxy
|
||||||
|
value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
|
||||||
|
- name: CODE_QUEUE_DATA_DIR
|
||||||
|
value: "/var/lib/unidesk/code-queue-ci"
|
||||||
|
- name: CODE_QUEUE_WORKDIR
|
||||||
|
value: "/workspace"
|
||||||
|
- name: CODE_QUEUE_CODEX_HOME
|
||||||
|
value: "/var/lib/unidesk/code-queue-ci/codex-home"
|
||||||
|
- name: CODE_QUEUE_OPENCODE_XDG_DIR
|
||||||
|
value: "/var/lib/unidesk/code-queue-ci/opencode-xdg"
|
||||||
|
- name: CODE_QUEUE_DEFAULT_MODEL
|
||||||
|
value: "gpt-5.5"
|
||||||
|
- name: CODE_QUEUE_MODELS
|
||||||
|
value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7"
|
||||||
|
- name: CODE_QUEUE_DATABASE_POOL_MAX
|
||||||
|
value: "2"
|
||||||
|
- name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS
|
||||||
|
value: "5"
|
||||||
|
- name: CODE_QUEUE_IN_MEMORY_EVENT_RECORDS
|
||||||
|
value: "5"
|
||||||
|
- name: OA_EVENT_FLOW_BASE_URL
|
||||||
|
value: "http://d601-tcp-egress-gateway.unidesk.svc.cluster.local:4255"
|
||||||
|
- name: LOG_FILE
|
||||||
|
value: "/var/log/unidesk/code-queue-ci-read.jsonl"
|
||||||
|
- name: NODE_OPTIONS
|
||||||
|
value: "--max-old-space-size=512"
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /live
|
||||||
|
port: http
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 20
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /live
|
||||||
|
port: http
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 6
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: state
|
||||||
|
mountPath: /var/lib/unidesk/code-queue-ci
|
||||||
|
- name: logs
|
||||||
|
mountPath: /var/log/unidesk
|
||||||
|
volumes:
|
||||||
|
- name: state
|
||||||
|
emptyDir: {}
|
||||||
|
- name: logs
|
||||||
|
emptyDir: {}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: code-queue-ci-read
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: code-queue
|
||||||
|
app.kubernetes.io/component: ci-read
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: code-queue
|
||||||
|
app.kubernetes.io/component: ci-read
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 4222
|
||||||
|
targetPort: http
|
||||||
|
YAML
|
||||||
|
csplit -s -f /tmp/code-queue-ci-read- /tmp/code-queue-ci-read-deployment.yaml '/^---$/' '{*}'
|
||||||
|
kube PATCH \
|
||||||
|
-H "Content-Type: application/apply-patch+yaml" \
|
||||||
|
--data-binary @/tmp/code-queue-ci-read-00 \
|
||||||
|
"$kube_api/apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read?fieldManager=unidesk-ci&force=true" >/dev/null
|
||||||
|
kube PATCH \
|
||||||
|
-H "Content-Type: application/apply-patch+yaml" \
|
||||||
|
--data-binary @/tmp/code-queue-ci-read-01 \
|
||||||
|
"$kube_api/api/v1/namespaces/$kube_namespace/services/code-queue-ci-read?fieldManager=unidesk-ci&force=true" >/dev/null
|
||||||
|
deadline=$((SECONDS + 180))
|
||||||
|
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||||
|
status="$(kube GET "$kube_api/apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read")"
|
||||||
|
replicas="$(printf '%s' "$status" | jq -r '.spec.replicas // 1')"
|
||||||
|
available="$(printf '%s' "$status" | jq -r '.status.availableReplicas // 0')"
|
||||||
|
updated="$(printf '%s' "$status" | jq -r '.status.updatedReplicas // 0')"
|
||||||
|
observed="$(printf '%s' "$status" | jq -r '.status.observedGeneration // 0')"
|
||||||
|
generation="$(printf '%s' "$status" | jq -r '.metadata.generation // 0')"
|
||||||
|
if [ "$available" -ge "$replicas" ] && [ "$updated" -ge "$replicas" ] && [ "$observed" -ge "$generation" ]; then
|
||||||
|
echo "code_queue_ci_read_rollout=available replicas=$available generation=$generation"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "code_queue_ci_read_rollout=timeout" >&2
|
||||||
|
kube GET "$kube_api/apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read" >&2
|
||||||
|
exit 1
|
||||||
|
- name: measure
|
||||||
|
image: "$(params.app-image)"
|
||||||
|
workingDir: "$(workspaces.source.path)/repo"
|
||||||
|
env:
|
||||||
|
- name: CI_CODE_QUEUE_URL
|
||||||
|
value: "http://code-queue-ci-read.unidesk-ci.svc.cluster.local:4222"
|
||||||
|
- name: HTTP_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: HTTPS_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: ALL_PROXY
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local"
|
||||||
|
- name: http_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: https_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: all_proxy
|
||||||
|
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||||
|
- name: no_proxy
|
||||||
|
value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local"
|
||||||
|
- name: FIRST_PAINT_BUDGET_MS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: unidesk-ci-budgets
|
||||||
|
key: firstPaintMs
|
||||||
|
- name: TRACE_SUMMARY_BUDGET_MS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: unidesk-ci-budgets
|
||||||
|
key: traceSummaryMs
|
||||||
|
- name: TRACE_STEPS_BUDGET_MS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: unidesk-ci-budgets
|
||||||
|
key: traceStepsMs
|
||||||
|
- name: TRACE_STEP_DETAIL_BUDGET_MS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: unidesk-ci-budgets
|
||||||
|
key: traceStepDetailMs
|
||||||
|
- name: OVERVIEW_P95_BUDGET_MS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: unidesk-ci-budgets
|
||||||
|
key: overviewP95Ms
|
||||||
|
script: |
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
bun scripts/ci-code-queue-read-perf.ts
|
||||||
|
- name: cleanup
|
||||||
|
image: "$(params.app-image)"
|
||||||
|
env:
|
||||||
|
- name: HTTP_PROXY
|
||||||
|
value: ""
|
||||||
|
- name: HTTPS_PROXY
|
||||||
|
value: ""
|
||||||
|
- name: ALL_PROXY
|
||||||
|
value: ""
|
||||||
|
- name: NO_PROXY
|
||||||
|
value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local"
|
||||||
|
- name: http_proxy
|
||||||
|
value: ""
|
||||||
|
- name: https_proxy
|
||||||
|
value: ""
|
||||||
|
- name: all_proxy
|
||||||
|
value: ""
|
||||||
|
- name: no_proxy
|
||||||
|
value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local"
|
||||||
|
script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
kube_api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}"
|
||||||
|
kube_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
|
||||||
|
kube_ca="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||||
|
kube_namespace="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)"
|
||||||
|
delete_resource() {
|
||||||
|
local path="$1"
|
||||||
|
local code
|
||||||
|
code="$(curl -sS -o /tmp/unidesk-ci-delete-response -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X DELETE "$kube_api/$path")"
|
||||||
|
if [ "$code" = "200" ] || [ "$code" = "202" ] || [ "$code" = "404" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
cat /tmp/unidesk-ci-delete-response >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
delete_resource "apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read"
|
||||||
|
delete_resource "api/v1/namespaces/$kube_namespace/services/code-queue-ci-read"
|
||||||
|
---
|
||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: Pipeline
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
spec:
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
type: string
|
||||||
|
default: https://github.com/pikasTech/unidesk
|
||||||
|
- name: revision
|
||||||
|
type: string
|
||||||
|
- name: check-image
|
||||||
|
type: string
|
||||||
|
default: unidesk-code-queue:d601
|
||||||
|
- name: code-queue-image
|
||||||
|
type: string
|
||||||
|
default: unidesk-code-queue:d601
|
||||||
|
workspaces:
|
||||||
|
- name: shared-workspace
|
||||||
|
tasks:
|
||||||
|
- name: repo-check
|
||||||
|
taskRef:
|
||||||
|
name: unidesk-repo-check
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
value: "$(params.repo-url)"
|
||||||
|
- name: revision
|
||||||
|
value: "$(params.revision)"
|
||||||
|
- name: image
|
||||||
|
value: "$(params.check-image)"
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: shared-workspace
|
||||||
|
- name: code-queue-read-perf
|
||||||
|
runAfter:
|
||||||
|
- repo-check
|
||||||
|
taskRef:
|
||||||
|
name: unidesk-code-queue-read-perf
|
||||||
|
params:
|
||||||
|
- name: revision
|
||||||
|
value: "$(params.revision)"
|
||||||
|
- name: app-image
|
||||||
|
value: "$(params.code-queue-image)"
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: shared-workspace
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
apiVersion: triggers.tekton.dev/v1beta1
|
||||||
|
kind: EventListener
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/component: triggers
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
spec:
|
||||||
|
serviceAccountName: unidesk-ci-runner
|
||||||
|
triggers:
|
||||||
|
- name: github-push
|
||||||
|
interceptors:
|
||||||
|
- ref:
|
||||||
|
name: cel
|
||||||
|
params:
|
||||||
|
- name: filter
|
||||||
|
value: >-
|
||||||
|
body.ref.startsWith('refs/heads/') &&
|
||||||
|
body.after.matches('^[0-9a-f]{40}$')
|
||||||
|
bindings:
|
||||||
|
- ref: unidesk-ci-github-push
|
||||||
|
template:
|
||||||
|
ref: unidesk-ci
|
||||||
|
---
|
||||||
|
apiVersion: triggers.tekton.dev/v1beta1
|
||||||
|
kind: TriggerBinding
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci-github-push
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/component: triggers
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
spec:
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
value: $(body.repository.clone_url)
|
||||||
|
- name: revision
|
||||||
|
value: $(body.after)
|
||||||
|
---
|
||||||
|
apiVersion: triggers.tekton.dev/v1beta1
|
||||||
|
kind: TriggerTemplate
|
||||||
|
metadata:
|
||||||
|
name: unidesk-ci
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/component: triggers
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
spec:
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
default: https://github.com/pikasTech/unidesk
|
||||||
|
- name: revision
|
||||||
|
resourcetemplates:
|
||||||
|
- apiVersion: tekton.dev/v1
|
||||||
|
kind: PipelineRun
|
||||||
|
metadata:
|
||||||
|
generateName: unidesk-ci-
|
||||||
|
namespace: unidesk-ci
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: unidesk-ci
|
||||||
|
app.kubernetes.io/part-of: unidesk
|
||||||
|
unidesk.ai/revision: $(tt.params.revision)
|
||||||
|
spec:
|
||||||
|
pipelineRef:
|
||||||
|
name: unidesk-ci
|
||||||
|
taskRunTemplate:
|
||||||
|
serviceAccountName: unidesk-ci-runner
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
value: $(tt.params.repo-url)
|
||||||
|
- name: revision
|
||||||
|
value: $(tt.params.revision)
|
||||||
|
workspaces:
|
||||||
|
- name: shared-workspace
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: unidesk-ci-cache
|
||||||
Reference in New Issue
Block a user