chore: upgrade CPA baseline and automate core deployment
This commit is contained in:
+1
-1
Submodule .externals/CLIProxyAPI updated: 7efe0a7c11...ee2c494788
@@ -34,8 +34,8 @@
|
||||
|
||||
- 本地 CPA 在 WSL 中运行,项目运行目录统一使用 `cpa-plugin/.runtime`;Windows 客户端经 `127.0.0.1` 访问 WSL 时,localhost 转发层可能不会立即向 CPA 传递客户端断开,取消链路测试优先在 WSL 内直连执行。
|
||||
- 从 PowerShell 跨 WSL 或 SSH 执行包含 Bash 变量、命令替换或多层引号的逻辑时,优先在 `.runtime/tmp/` 创建临时脚本后传入目标环境执行,不拼接复杂的单行 shell;临时脚本不提交。
|
||||
- CLIProxyAPI `v7.2.133` 的宿主补丁保存在 `patch/`,必须按 `usage-context`、`usage-identity`、`request-lifecycle-cancel` 的顺序应用;升级 CPA 后先复测补丁是否仍有必要。
|
||||
- 构建本项目 patched CPA 时必须按上游 release workflow 使用 `-ldflags` 注入 `main.Version`、`main.Commit` 和 `main.BuildDate`。`Version` 固定使用“当前上游最新 tag + `-dev`”格式(例如 `v7.2.133-dev`),用于明确区分官方 Release 与本项目补丁构建;不得发布只显示 `dev`、`none` 或 `unknown` 的产物。上传前先在本地执行二进制并核对完整版本输出。
|
||||
- CLIProxyAPI 宿主补丁保存在 `patch/`,当前已验证基线为 `v7.2.135` / `ee2c4947`,必须按 `usage-context`、`usage-identity`、`request-lifecycle-cancel` 的顺序应用;升级 CPA 后按 `docs/operations.md` 核对补丁和已知上游测试例外。
|
||||
- 构建本项目 patched CPA 时必须按上游 release workflow 使用 `-ldflags` 注入 `main.Version`、`main.Commit` 和 `main.BuildDate`。`Version` 固定使用“当前上游最新 tag + `-dev`”格式(例如 `v7.2.135-dev`),用于明确区分官方 Release 与本项目补丁构建;不得发布只显示 `dev`、`none` 或 `unknown` 的产物。上传前先在本地执行二进制并核对完整版本输出。
|
||||
- CPA Linux 可执行文件解压后约 60 MiB,`gzip -9` 后约 20 MiB。上传远端时使用 gzip/tar.gz 或 `scp -C`,不要直接传输未压缩二进制;部署后的运行文件仍需解压为原始可执行文件。
|
||||
- 实际服务器为 `root@akko.pchuan.top`,CPA 根目录为 `/root/cpa`。只维护本项目的 `billing` 和已要求启用的 `keeper`,不启用或部署其他参考插件。
|
||||
- 远端替换 CPA 或 billing 前,先停止 `/root/cpa/cpa.sh` 管理的进程,并把旧二进制、插件和 `data/cpa-ext.db*` 备份到 `/root/cpa/backups/<timestamp>/`;新版本启动失败时恢复旧二进制和插件。
|
||||
|
||||
@@ -66,7 +66,7 @@ CLIProxyAPI 负责 HTTP 接入、协议转换、上游凭证和实际请求执
|
||||
|
||||
## 当前兼容目标
|
||||
|
||||
- CLIProxyAPI 基线:`v7.2.132` / `78f0c4079e3e6273d65d03b5549cffc898703264`,由 `.externals/CLIProxyAPI` submodule 锁定,需依次应用 `patch/` 中三项宿主补丁
|
||||
- CLIProxyAPI 基线:`v7.2.135` / `ee2c494788f8a089c58f7ae9aa6ebd1b422211cb`,由 `.externals/CLIProxyAPI` submodule 锁定,需依次应用 `patch/` 中三项宿主补丁
|
||||
- 插件版本:`0.1.0`
|
||||
- Native ABI:`1`
|
||||
- RPC schema:最高 `4`,注册时按宿主版本向下协商;schema v4 提供 Usage 请求身份
|
||||
@@ -141,7 +141,7 @@ CGO_ENABLED=1 go test ./...
|
||||
|
||||
请求明细由服务端分页,每页 100 条,默认按请求时间和稳定 ID 倒序展示。管理接口支持时间范围、用户 Key、模型、结果、上游 Auth ID、端点和完整 Request ID 筛选;连续翻页使用与筛选条件绑定的游标,数字页码跳转使用 SQLite 索引定位。用户页的今日汇总、各用户用量和近 7 日 Token 由数据库独立聚合,不受当前明细页影响。
|
||||
|
||||
升级已有数据库时,插件会从原始 Usage 和请求终态事实自动建立轻量查询投影。事实表、计费账目和历史统计保持不变;没有 Usage 的取消或失败请求仍然可见。当前 CPA Usage 契约不提供 Request ID 或 Execution ID,因此只在模型和时间足够接近且匹配关系唯一时关联请求终态;存在并发歧义时保留为独立记录,不强行合并。
|
||||
升级已有数据库时,插件会从原始 Usage 和请求终态事实自动建立轻量查询投影。事实表、计费账目和历史统计保持不变;没有 Usage 的取消或失败请求仍然可见。patched CPA 使用 RPC schema v4 向 billing 传递 Usage Request ID 和 Trace ID,可以按 Request ID 精确关联请求终态。旧宿主或旧 schema 没有请求身份时,只有模型和时间足够接近且匹配关系唯一才关联;存在并发歧义时保留为独立记录。
|
||||
|
||||
## 工程布局
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ git submodule update --init --recursive
|
||||
|
||||
需要构建本项目使用的 CPA 时,再按 [`../patch/README.md`](../patch/README.md) 的顺序应用三个宿主补丁。submodule 本身始终锁定官方基线,补丁来源以根仓库 `patch/` 为准。
|
||||
|
||||
升级 submodule、构建宿主、本地替换、验收和远端发布使用 [`operations.md`](operations.md)。该文档同时记录已确认的上游测试失败及免复测条件。
|
||||
|
||||
开发按依赖从少到多推进:先确定数据和规则,再实现存储与服务,最后接入外部协议和界面。核心逻辑不依赖框架类型,协议转换集中在边界层。涉及持久化时,先定义迁移和历史数据语义
|
||||
|
||||
开发的核心在于每一步的可预见性与可测试性
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# CLIProxyAPI 升级、构建与部署记录
|
||||
|
||||
## 当前基线
|
||||
|
||||
| 项目 | 当前值 |
|
||||
|---|---|
|
||||
| CLIProxyAPI Tag | `v7.2.135` |
|
||||
| submodule 提交 | `ee2c494788f8a089c58f7ae9aa6ebd1b422211cb` |
|
||||
| patched 构建版本 | `v7.2.135-dev` |
|
||||
| Native ABI | `1` |
|
||||
| RPC schema | `4` |
|
||||
| Go | `1.26.6` |
|
||||
| 本地运行目录 | `.runtime` |
|
||||
| 本地管理页面 | `http://127.0.0.1:8317/management.html` |
|
||||
| 远端目录 | `root@akko.pchuan.top:/root/cpa` |
|
||||
|
||||
DeepSeek 必须配置在 CPA 的 `codex-api-key` 通道,`base-url` 使用 `https://api.deepseek.com`。不要把该凭证配置为 `openai-compatibility`。Windows Codex CLI 连接本地 CPA 时,provider 的 `base_url` 使用 `http://127.0.0.1:8317/v1`,`wire_api` 使用 `responses`。
|
||||
|
||||
当前本地测试状态保存在被 Git 忽略的 `.runtime`:
|
||||
|
||||
- CPA 配置:`.runtime/config.yaml`;
|
||||
- billing 数据库:`.runtime/data/billing.db`;
|
||||
- 管理 Key 和默认下游 Key:`000000`;
|
||||
- 测试模型:`deepseek-v4-flash`;
|
||||
- 默认额度:`$10`,最大并发数:`4`;
|
||||
- 当前价格是本地验收价格,不作为生产价格依据。
|
||||
|
||||
升级宿主或插件时保留配置和数据库,不重复初始化 Key、价格和额度。
|
||||
|
||||
## 已知上游测试失败
|
||||
|
||||
CLIProxyAPI `ee2c4947` 在 Debian WSL 中运行 `go test ./... -count=1` 时有以下失败:
|
||||
|
||||
| 测试 | 实际值 | 期望值 |
|
||||
|---|---|---|
|
||||
| `TestApplyClaudeHeaders_DisableDeviceProfileStabilization` | `X-Stainless-Os=MacOS` | `Linux` |
|
||||
| `TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForClaudeClients` | `X-Stainless-Os=MacOS` | `Linux` |
|
||||
| `TestClaudeExecutor_NonClaudeRequestUsesClaudeCode220CLIFingerprint` | `X-Stainless-Os=MacOS` | `Linux` |
|
||||
|
||||
2026-08-18 已在未应用任何 billing 补丁的纯上游 `ee2c4947` 临时 worktree 中复现相同结果。三项补丁没有修改 `internal/runtime/executor`,因此同一提交、同一 WSL 环境、同一失败内容再次出现时,直接记为已知上游失败,不再创建干净 worktree复测。
|
||||
|
||||
出现以下任一情况时重新验证:
|
||||
|
||||
- submodule 提交不再是 `ee2c4947`;
|
||||
- 上游修改了 `internal/runtime/executor/claude_executor_test.go`、`claude_executor_request.go` 或 `helps/claude_device_profile.go`;
|
||||
- 失败测试、行号、实际值或期望值发生变化;
|
||||
- 三项补丁开始修改 `internal/runtime/executor`。
|
||||
|
||||
## submodule 升级
|
||||
|
||||
当前 submodule 工作区包含三项已应用补丁。升级前必须按反方向撤销,再切换上游提交:
|
||||
|
||||
```bash
|
||||
git -C .externals/CLIProxyAPI apply --reverse ../../patch/cli-proxy-api-request-lifecycle-cancel.patch
|
||||
git -C .externals/CLIProxyAPI apply --reverse ../../patch/cli-proxy-api-usage-identity.patch
|
||||
git -C .externals/CLIProxyAPI apply --reverse ../../patch/cli-proxy-api-usage-context.patch
|
||||
git -C .externals/CLIProxyAPI fetch --tags --prune origin
|
||||
git -C .externals/CLIProxyAPI switch --detach origin/main
|
||||
```
|
||||
|
||||
重新应用补丁:
|
||||
|
||||
```bash
|
||||
git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-usage-context.patch
|
||||
git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-usage-identity.patch
|
||||
git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-request-lifecycle-cancel.patch
|
||||
git -C .externals/CLIProxyAPI diff --check
|
||||
```
|
||||
|
||||
应用后 submodule 显示 `dirty` 是预期结果。根仓库提交新的 gitlink 指针和必要的 `patch/` 更新,不在 submodule 中创建派生提交。
|
||||
|
||||
## 测试顺序
|
||||
|
||||
WSL 使用以下环境。Go bootstrap 可能低于 `1.26.6`,有效 toolchain 由 Go 自动下载;`gofmt` 位于有效 `GOROOT/bin`,因此需要补充 `PATH`:
|
||||
|
||||
```bash
|
||||
export GOPROXY=https://goproxy.cn,direct
|
||||
export PATH="$(go env GOROOT)/bin:$PATH"
|
||||
go version
|
||||
gofmt -h >/dev/null
|
||||
```
|
||||
|
||||
从 PowerShell 调用包含 Bash 变量、命令替换或多层引号的命令时,把内容写入 `.runtime/tmp/*.sh` 后交给 WSL 执行。`.runtime/tmp` 不提交。
|
||||
|
||||
每次升级执行以下检查:
|
||||
|
||||
```bash
|
||||
cd .externals/CLIProxyAPI
|
||||
go test -race ./internal/pluginhost ./sdk/api/handlers ./sdk/cliproxy/usage -count=1
|
||||
go test ./... -count=1
|
||||
|
||||
cd ../..
|
||||
go test ./... -count=1
|
||||
go test -race ./... -count=1
|
||||
```
|
||||
|
||||
补丁正确性的最低标准如下:
|
||||
|
||||
- 三项补丁按顺序应用成功,`git diff --check` 通过;
|
||||
- `internal/pluginhost`、`sdk/api/handlers`、`sdk/cliproxy/usage` race 测试通过;
|
||||
- billing 全量测试和全量 race 测试通过;
|
||||
- CPA 全量测试除已记录的纯上游失败外通过;
|
||||
- CPA 和 billing 均可编译。
|
||||
|
||||
## 构建
|
||||
|
||||
CPA 版本必须使用当前最新 Tag 加 `-dev`,提交取 submodule HEAD:
|
||||
|
||||
```bash
|
||||
cd .externals/CLIProxyAPI
|
||||
version=v7.2.135-dev
|
||||
commit="$(git rev-parse --short=8 HEAD)"
|
||||
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
CGO_ENABLED=1 go build -buildvcs=false \
|
||||
-ldflags="-s -w -X main.Version=$version -X main.Commit=$commit -X main.BuildDate=$build_date" \
|
||||
-o ../../.runtime/bin/cli-proxy-api.new ./cmd/server/
|
||||
../../.runtime/bin/cli-proxy-api.new -h >/dev/null
|
||||
```
|
||||
|
||||
`-h` 会在第一行输出完整版本。上游没有 `--version` 参数,不要使用该参数核对版本。
|
||||
|
||||
构建插件:
|
||||
|
||||
```bash
|
||||
cd ../..
|
||||
bash scripts/build.sh
|
||||
cp bin/billing.so .runtime/plugins/billing.so.new
|
||||
sha256sum bin/billing.so .runtime/plugins/billing.so.new
|
||||
```
|
||||
|
||||
两个插件哈希必须一致。
|
||||
|
||||
## 本地替换与验收
|
||||
|
||||
本地 CPA 只使用 `.runtime`。替换前停止进程并保存旧产物:
|
||||
|
||||
```bash
|
||||
bash scripts/stop-test-host.sh
|
||||
backup=".runtime/backups/$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup"
|
||||
cp .runtime/bin/cli-proxy-api "$backup/"
|
||||
cp .runtime/plugins/billing.so "$backup/"
|
||||
mv .runtime/bin/cli-proxy-api.new .runtime/bin/cli-proxy-api
|
||||
mv .runtime/plugins/billing.so.new .runtime/plugins/billing.so
|
||||
bash scripts/run-test-host.sh
|
||||
```
|
||||
|
||||
启动后检查:
|
||||
|
||||
1. `/healthz` 返回 `status=ok`;
|
||||
2. `/v0/management/plugins` 中 billing 的 `registered`、`enabled`、`effective_enabled` 都为 `true`;
|
||||
3. 日志版本与构建参数一致,且没有插件错误或 panic;
|
||||
4. 无下游 Key 的请求返回 401;
|
||||
5. 有效 billing Key 的 `/v1/responses` 请求返回 200;
|
||||
6. 请求只新增一条明细,包含唯一 Request ID 和 Token;
|
||||
7. 额度只扣减一次,`active_requests` 回到 0。
|
||||
|
||||
本地 CPA 通过 WSL localhost 转发供 Windows 使用。取消链路测试需要在 WSL 内直连 `127.0.0.1:8317`,避免 Windows localhost 转发延迟传递断开。
|
||||
|
||||
## 远端发布
|
||||
|
||||
核心升级使用 PowerShell 脚本:
|
||||
|
||||
```powershell
|
||||
./scripts/push.ps1
|
||||
```
|
||||
|
||||
脚本默认执行以下操作:
|
||||
|
||||
- fetch `origin/main` 并要求 submodule HEAD 与其一致;
|
||||
- 要求 submodule 修改文件集合与 `patch/*.patch` 声明一致;
|
||||
- 运行三个补丁相关包的 race 测试;
|
||||
- 使用最新 Tag 加 `-dev` 和当前提交、构建时间编译核心;
|
||||
- 生成 gzip 最高压缩级别的核心包和 SHA-256;
|
||||
- 上传后在远端重新校验压缩包、版本和核心哈希;
|
||||
- 等待 billing `active_requests=0`,备份后只替换 CPA 核心;
|
||||
- 核对配置、所有插件动态库、插件注册状态和独立 keeper;
|
||||
- 失败时恢复旧核心并按部署前状态启动 CPA Manager Plus。
|
||||
|
||||
只执行本地构建、测试和打包,不上传或部署:
|
||||
|
||||
```powershell
|
||||
./scripts/push.ps1 -WhatIf
|
||||
```
|
||||
|
||||
复用已有核心时使用 `-SkipBuild -BinaryPath <path>`;跳过补丁相关 race 测试使用 `-SkipTests`;部署锁定但不是当前 `origin/main` 的提交时显式使用 `-SkipUpstreamCheck`。请求排空默认等待 120 秒,可通过 `-DrainTimeoutSeconds` 和 `-DrainPollSeconds` 调整。
|
||||
|
||||
远端只部署本项目的 billing 和已要求启用的 keeper。发布步骤如下:
|
||||
|
||||
1. 本地完成真实模型请求、协议、计费和取消链路验收;
|
||||
2. 对 CPA 二进制和插件计算 SHA-256;
|
||||
3. 使用 gzip、tar.gz 或 `scp -C` 上传;
|
||||
4. 停止 `/root/cpa/cpa.sh` 管理的进程;
|
||||
5. 把旧 CPA、billing、keeper 和 `data/cpa-ext.db*` 保存到 `/root/cpa/backups/<timestamp>/`;
|
||||
6. 替换二进制和插件,保留配置及 secret 文件;
|
||||
7. 启动进程并检查健康状态、版本、插件注册状态和日志;
|
||||
8. 启动失败时恢复同一备份目录中的文件并重启。
|
||||
|
||||
远端不发送 `/v1/responses`、`/v1/chat/completions` 或 compact 请求。管理密钥和下游 Key 在远端脚本内部从 secret 文件或管理 API 读取,不输出到终端和普通日志。
|
||||
|
||||
`cpa.sh start` 会先把管理 Key 同步到 `config.yaml`,CPA 启动后会重新保存该字段的哈希,因此每次启动后 `config.yaml` 的原始 SHA-256 可能变化。部署校验应把 `remote-management.secret-key` 的值归一化后比较其余配置内容,不得因为原始配置哈希变化回滚核心。
|
||||
|
||||
远端存在持续请求时,在远端部署脚本内部先完成压缩包和插件检查,再每 5 秒读取 billing `active_requests`。检测到 0 后立即停止 CPA,避免在本地轮询与远端执行之间出现新请求。
|
||||
|
||||
## 2026-08-18 验证记录
|
||||
|
||||
`v7.2.135` / `ee2c4947` 已完成以下验证:
|
||||
|
||||
- 三项补丁重新应用成功,共修改 11 个文件,增加 289 行,删除 10 行;
|
||||
- 补丁相关三个包的 race 测试通过;
|
||||
- billing 全量测试和全量 race 测试通过;
|
||||
- CPA 和 billing Linux amd64 产物编译成功;
|
||||
- CPA 运行版本为 `v7.2.135-dev / ee2c4947`;
|
||||
- billing `0.1.0` 加载并注册;
|
||||
- DeepSeek Codex `/v1/responses` 返回 200 和 `CODEX_READY`;
|
||||
- 该请求记录 129 Token,只产生一条计费记录,`active_requests=0`;
|
||||
- 本地替换前产物保存在 `.runtime/backups/20260818-154252-v7.2.135/`。
|
||||
|
||||
同日已完成远端核心升级:
|
||||
|
||||
- 只替换 `/root/cpa/bin/cli-proxy-api`,插件、配置、数据库和独立 keeper 未替换;
|
||||
- 远端运行版本为 `v7.2.135-dev / ee2c4947`,核心 SHA-256 为 `d702d8d65a06911b5bec78476d84e228bcfb57e03c511b4c1d89df8f460c41fd`;
|
||||
- CPA PID 为 `278177`,独立 keeper PID 保持为 `230299`;
|
||||
- billing 和 keeper `0.1.0` 均已加载、注册和启用;
|
||||
- `/healthz` 正常,启动日志没有 panic 或插件错误;
|
||||
- 未向远端模型端点发送验收请求;
|
||||
- 升级前文件保存在 `/root/cpa/backups/20260818T081102Z-core-v7.2.135/`。
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
# CLIProxyAPI 宿主补丁
|
||||
|
||||
这里保存 billing 在 CLIProxyAPI `v7.2.133` 上需要的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用:
|
||||
这里保存 billing 最初为 CLIProxyAPI `v7.2.133` 制作、已在 `v7.2.135` 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用:
|
||||
|
||||
1. [`cli-proxy-api-usage-context.patch`](cli-proxy-api-usage-context.patch)
|
||||
2. [`cli-proxy-api-usage-identity.patch`](cli-proxy-api-usage-identity.patch)
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
适用基线:
|
||||
|
||||
- CLIProxyAPI:`v7.2.133`
|
||||
- 提交:`7efe0a7c11adfecb7fe82f2d1128f873a9f4cbf3`
|
||||
- CLIProxyAPI:`v7.2.135`
|
||||
- 提交:`ee2c494788f8a089c58f7ae9aa6ebd1b422211cb`
|
||||
- Native ABI:`1`
|
||||
- 第二项补丁把 RPC schema 从 `3` 提升到 `4`
|
||||
|
||||
@@ -84,8 +84,8 @@ git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-request-lifecycle-
|
||||
cd .externals/CLIProxyAPI
|
||||
gofmt -w internal/pluginhost sdk/api/handlers sdk/cliproxy/usage sdk/pluginabi sdk/pluginapi
|
||||
go test -race ./internal/pluginhost ./sdk/api/handlers ./sdk/cliproxy/usage -count=1
|
||||
version=v7.2.133-dev
|
||||
commit=7efe0a7c
|
||||
version=v7.2.135-dev
|
||||
commit=ee2c4947
|
||||
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
CGO_ENABLED=1 go build -buildvcs=false \
|
||||
-ldflags="-s -w -X main.Version=$version -X main.Commit=$commit -X main.BuildDate=$build_date" \
|
||||
@@ -129,7 +129,7 @@ billing 必须使用 RPC schema v4 重新构建;旧 billing 动态库会向下
|
||||
- billing 全部测试、billing race 测试及 CLIProxyAPI 相关包 race 测试通过;
|
||||
- schema v3 对照插件仍能正常注册,新增字段不会发送给旧 schema。
|
||||
|
||||
升级到 CLIProxyAPI `v7.2.133` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试、billing 全仓 race 测试和两个 Linux amd64 产物构建均通过。宿主全仓 `go test ./...` 只剩 2 个与补丁无关的 `reviewedInPlaceByteWrites` 陈旧清单失败:`internal/home/client.go` 和 `internal/pluginstore/auth.go` 已不再包含清单记录的原地 byte 写入。
|
||||
升级到 CLIProxyAPI `v7.2.135` / `ee2c4947` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试、billing 全仓 race 测试和两个 Linux amd64 产物构建均通过。宿主全仓 `go test ./...` 有 3 个与补丁无关的 Claude 指纹测试失败;相同失败已在未应用补丁的纯上游 `ee2c4947` 上复现,详见 [`../docs/operations.md`](../docs/operations.md)。
|
||||
|
||||
## 性能影响
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ printf '%s\n' \
|
||||
"target=linux/amd64" \
|
||||
"native_abi=1" \
|
||||
"rpc_schema=4" \
|
||||
"cliproxyapi_revision=78f0c4079e3e6273d65d03b5549cffc898703264+usage-context+usage-identity+request-lifecycle-cancel" \
|
||||
"cliproxyapi_revision=ee2c494788f8a089c58f7ae9aa6ebd1b422211cb+usage-context+usage-identity+request-lifecycle-cancel" \
|
||||
>"$package_dir/VERSION.txt"
|
||||
|
||||
archive="$root/dist/$name.tar.gz"
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[string]$SshTarget = 'root@akko.pchuan.top',
|
||||
[string]$RemoteRoot = '/root/cpa',
|
||||
[string]$WslDistribution = 'Debian',
|
||||
[int]$DrainTimeoutSeconds = 120,
|
||||
[int]$DrainPollSeconds = 5,
|
||||
[string]$BinaryPath = '',
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipTests,
|
||||
[switch]$SkipUpstreamCheck
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Resolve-RequiredTool {
|
||||
param([Parameter(Mandatory)][string]$Name)
|
||||
|
||||
$command = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($null -eq $command) {
|
||||
throw "Required tool is missing from PATH: $Name"
|
||||
}
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
function Invoke-NativeCommand {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FilePath,
|
||||
[Parameter(Mandatory)][string[]]$ArgumentList
|
||||
)
|
||||
|
||||
& $FilePath @ArgumentList
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Command failed with exit code $LASTEXITCODE`: $FilePath $($ArgumentList -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NativeOutput {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FilePath,
|
||||
[Parameter(Mandatory)][string[]]$ArgumentList
|
||||
)
|
||||
|
||||
$output = & $FilePath @ArgumentList 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Command failed with exit code $LASTEXITCODE`: $FilePath $($ArgumentList -join ' ')`n$($output -join "`n")"
|
||||
}
|
||||
return (($output -join "`n").Trim())
|
||||
}
|
||||
|
||||
function Write-Utf8NoBom {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$Content
|
||||
)
|
||||
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, ($Content -replace "`r`n", "`n"), $encoding)
|
||||
}
|
||||
|
||||
if ($DrainTimeoutSeconds -lt 0) {
|
||||
throw 'DrainTimeoutSeconds must be zero or greater.'
|
||||
}
|
||||
if ($DrainPollSeconds -lt 1) {
|
||||
throw 'DrainPollSeconds must be at least one second.'
|
||||
}
|
||||
if ($SshTarget -notmatch '^[A-Za-z0-9_.@-]+$') {
|
||||
throw "Unsupported SSH target format: $SshTarget"
|
||||
}
|
||||
if ($RemoteRoot -notmatch '^/[A-Za-z0-9._/-]+$') {
|
||||
throw "Unsupported remote root format: $RemoteRoot"
|
||||
}
|
||||
if ($WslDistribution -notmatch '^[A-Za-z0-9._-]+$') {
|
||||
throw "Unsupported WSL distribution name: $WslDistribution"
|
||||
}
|
||||
|
||||
$git = Resolve-RequiredTool 'git'
|
||||
$ssh = Resolve-RequiredTool 'ssh'
|
||||
$scp = Resolve-RequiredTool 'scp'
|
||||
$wsl = Resolve-RequiredTool 'wsl.exe'
|
||||
|
||||
$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||
$hostRoot = Join-Path $repoRoot '.externals\CLIProxyAPI'
|
||||
$runtimeRoot = Join-Path $repoRoot '.runtime'
|
||||
$tmpRoot = Join-Path $runtimeRoot 'tmp'
|
||||
$deployRoot = Join-Path $runtimeRoot 'deploy'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $hostRoot -PathType Container)) {
|
||||
throw "CLIProxyAPI submodule is missing: $hostRoot"
|
||||
}
|
||||
|
||||
[System.IO.Directory]::CreateDirectory($tmpRoot) | Out-Null
|
||||
[System.IO.Directory]::CreateDirectory($deployRoot) | Out-Null
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
if (-not $SkipUpstreamCheck) {
|
||||
Invoke-NativeCommand $git @('-C', $hostRoot, 'fetch', '--tags', '--prune', 'origin')
|
||||
}
|
||||
|
||||
$head = Get-NativeOutput $git @('-C', $hostRoot, 'rev-parse', 'HEAD')
|
||||
if (-not $SkipUpstreamCheck) {
|
||||
$originMain = Get-NativeOutput $git @('-C', $hostRoot, 'rev-parse', 'origin/main')
|
||||
if ($head -ne $originMain) {
|
||||
throw "Submodule HEAD is not origin/main. HEAD=$head origin/main=$originMain"
|
||||
}
|
||||
}
|
||||
|
||||
$tag = Get-NativeOutput $git @('-C', $hostRoot, 'describe', '--tags', '--abbrev=0', $head)
|
||||
$commit = Get-NativeOutput $git @('-C', $hostRoot, 'rev-parse', '--short=8', $head)
|
||||
$version = "$tag-dev"
|
||||
$buildDate = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
|
||||
foreach ($value in @($tag, $version, $commit)) {
|
||||
if ($value -notmatch '^[A-Za-z0-9._+-]+$') {
|
||||
throw "Unsupported version value: $value"
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-NativeCommand $git @('-C', $hostRoot, 'diff', '--check')
|
||||
|
||||
$expectedModifiedFiles = Get-ChildItem -LiteralPath (Join-Path $repoRoot 'patch') -Filter '*.patch' |
|
||||
Select-String -Pattern '^diff --git a/([^ ]+) b/' |
|
||||
ForEach-Object { $_.Matches[0].Groups[1].Value } |
|
||||
Sort-Object -Unique
|
||||
$actualModifiedFiles = Get-NativeOutput $git @('-C', $hostRoot, 'diff', '--name-only') |
|
||||
ForEach-Object { $_ -split "`n" } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
ForEach-Object { $_.Trim() } |
|
||||
Sort-Object -Unique
|
||||
$untrackedHostFiles = Get-NativeOutput $git @('-C', $hostRoot, 'ls-files', '--others', '--exclude-standard')
|
||||
if (-not [string]::IsNullOrWhiteSpace($untrackedHostFiles)) {
|
||||
throw "CLIProxyAPI worktree contains untracked files:`n$untrackedHostFiles"
|
||||
}
|
||||
$unexpectedChanges = Compare-Object -ReferenceObject $expectedModifiedFiles -DifferenceObject $actualModifiedFiles
|
||||
if ($unexpectedChanges) {
|
||||
$details = $unexpectedChanges | ForEach-Object { "$($_.SideIndicator) $($_.InputObject)" }
|
||||
throw "CLIProxyAPI worktree does not match the files declared by patch/*.patch:`n$($details -join "`n")"
|
||||
}
|
||||
|
||||
$wslRepoRoot = Get-NativeOutput $wsl @('-d', $WslDistribution, '--', 'wslpath', '-a', ($repoRoot -replace '\\', '/'))
|
||||
$archiveName = "cli-proxy-api-$version-$commit-linux-amd64.tar.gz"
|
||||
$archivePath = Join-Path $deployRoot $archiveName
|
||||
$checksumPath = "$archivePath.sha256"
|
||||
$wslArchivePath = "$wslRepoRoot/.runtime/deploy/$archiveName"
|
||||
|
||||
if ($SkipBuild) {
|
||||
if ([string]::IsNullOrWhiteSpace($BinaryPath)) {
|
||||
$BinaryPath = Join-Path $runtimeRoot 'bin\cli-proxy-api'
|
||||
}
|
||||
$resolvedBinary = (Resolve-Path -LiteralPath $BinaryPath).Path
|
||||
$wslBinaryPath = Get-NativeOutput $wsl @('-d', $WslDistribution, '--', 'wslpath', '-a', ($resolvedBinary -replace '\\', '/'))
|
||||
$buildMode = '0'
|
||||
} else {
|
||||
$resolvedBinary = Join-Path $deployRoot "cli-proxy-api-$version-$commit"
|
||||
$wslBinaryPath = "$wslRepoRoot/.runtime/deploy/cli-proxy-api-$version-$commit"
|
||||
$buildMode = '1'
|
||||
}
|
||||
|
||||
$runTests = if ($SkipTests) { '0' } else { '1' }
|
||||
$localScriptPath = Join-Path $tmpRoot 'push-build-core.sh'
|
||||
$localScript = @"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root='$wslRepoRoot'
|
||||
host="`$root/.externals/CLIProxyAPI"
|
||||
binary='$wslBinaryPath'
|
||||
archive='$wslArchivePath'
|
||||
version='$version'
|
||||
commit='$commit'
|
||||
build_date='$buildDate'
|
||||
build_mode='$buildMode'
|
||||
run_tests='$runTests'
|
||||
|
||||
export GOPROXY=https://goproxy.cn,direct
|
||||
export PATH="`$(go env GOROOT)/bin:`$PATH"
|
||||
|
||||
if [[ "`$build_mode" == 1 ]]; then
|
||||
cd "`$host"
|
||||
git diff --check
|
||||
if [[ "`$run_tests" == 1 ]]; then
|
||||
go test -race ./internal/pluginhost ./sdk/api/handlers ./sdk/cliproxy/usage -count=1
|
||||
fi
|
||||
CGO_ENABLED=1 go build -buildvcs=false \
|
||||
-ldflags="-s -w -X main.Version=`$version -X main.Commit=`$commit -X main.BuildDate=`$build_date" \
|
||||
-o "`$binary" ./cmd/server/
|
||||
fi
|
||||
|
||||
test -f "`$binary"
|
||||
chmod 0755 "`$binary"
|
||||
help_output="`$("`$binary" -h 2>&1)"
|
||||
first_line="`${help_output%%`$'\n'*}"
|
||||
expected_prefix="CLIProxyAPI Version: `$version, Commit: `$commit, BuiltAt: "
|
||||
[[ "`$first_line" == "`$expected_prefix"* ]]
|
||||
|
||||
stage="`$(mktemp -d)"
|
||||
trap 'rm -rf "`$stage"' EXIT
|
||||
install -m 0755 "`$binary" "`$stage/cli-proxy-api"
|
||||
tar -C "`$stage" -cf - cli-proxy-api | gzip -9 >"`$archive"
|
||||
(
|
||||
cd "`$(dirname "`$archive")"
|
||||
sha256sum "`$(basename "`$archive")" >"`$(basename "`$archive").sha256"
|
||||
)
|
||||
|
||||
printf 'version_line=%s\n' "`$first_line"
|
||||
sha256sum "`$binary" "`$archive"
|
||||
stat -c 'archive_size=%s' "`$archive"
|
||||
"@
|
||||
Write-Utf8NoBom -Path $localScriptPath -Content $localScript
|
||||
|
||||
Write-Host "Preparing $version / $commit"
|
||||
$wslLocalScriptPath = Get-NativeOutput $wsl @('-d', $WslDistribution, '--', 'wslpath', '-a', ($localScriptPath -replace '\\', '/'))
|
||||
Invoke-NativeCommand $wsl @('-d', $WslDistribution, '--', 'bash', $wslLocalScriptPath)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $archivePath -PathType Leaf)) {
|
||||
throw "Archive was not created: $archivePath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $checksumPath -PathType Leaf)) {
|
||||
throw "Checksum file was not created: $checksumPath"
|
||||
}
|
||||
|
||||
$binaryHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedBinary).Hash.ToLowerInvariant()
|
||||
$archiveHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant()
|
||||
|
||||
$remoteScriptPath = Join-Path $tmpRoot 'push-remote-core.sh'
|
||||
$remoteScript = @'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
archive_name="$1"
|
||||
expected_archive_hash="$2"
|
||||
expected_binary_hash="$3"
|
||||
expected_version="$4"
|
||||
expected_commit="$5"
|
||||
drain_timeout="$6"
|
||||
drain_poll="$7"
|
||||
root="$8"
|
||||
|
||||
archive="$root/upload/$archive_name"
|
||||
checksum="$archive.sha256"
|
||||
cpa_script="$root/cpa.sh"
|
||||
tmp=''
|
||||
backup=''
|
||||
rollback_needed=0
|
||||
manager_was_running=0
|
||||
keeper_pid=''
|
||||
|
||||
start_services() {
|
||||
if [[ "$manager_was_running" -eq 1 ]]; then
|
||||
"$cpa_script" start --plus
|
||||
else
|
||||
"$cpa_script" start
|
||||
fi
|
||||
}
|
||||
|
||||
finish() {
|
||||
rc=$?
|
||||
trap - EXIT
|
||||
if [[ "$rc" -ne 0 && "$rollback_needed" -eq 1 ]]; then
|
||||
printf 'DEPLOYMENT_FAILED_ROLLING_BACK\n'
|
||||
"$cpa_script" stop || true
|
||||
if [[ -n "$backup" && -f "$backup/bin/cli-proxy-api" ]]; then
|
||||
install -o root -g root -m 0755 "$backup/bin/cli-proxy-api" "$root/bin/cli-proxy-api"
|
||||
fi
|
||||
start_services || true
|
||||
fi
|
||||
if [[ -n "$tmp" && -d "$tmp" ]]; then
|
||||
rm -rf "$tmp"
|
||||
fi
|
||||
exit "$rc"
|
||||
}
|
||||
trap finish EXIT
|
||||
|
||||
printf 'VERIFY_UPLOAD\n'
|
||||
test -f "$archive"
|
||||
test -f "$checksum"
|
||||
actual_archive_hash="$(sha256sum "$archive" | awk '{print $1}')"
|
||||
test "$actual_archive_hash" = "$expected_archive_hash"
|
||||
(
|
||||
cd "$root/upload"
|
||||
sha256sum -c "$(basename "$checksum")"
|
||||
)
|
||||
test "$(tar -tzf "$archive")" = 'cli-proxy-api'
|
||||
|
||||
available_kb="$(df -Pk "$root" | awk 'NR==2 {print $4}')"
|
||||
test "$available_kb" -ge 262144
|
||||
|
||||
tmp="$(mktemp -d "$root/.deploy-core.XXXXXX")"
|
||||
tar -xzf "$archive" -C "$tmp"
|
||||
test -f "$tmp/cli-proxy-api"
|
||||
test ! -L "$tmp/cli-proxy-api"
|
||||
chmod 0755 "$tmp/cli-proxy-api"
|
||||
test "$(sha256sum "$tmp/cli-proxy-api" | awk '{print $1}')" = "$expected_binary_hash"
|
||||
help_output="$("$tmp/cli-proxy-api" -h 2>&1)"
|
||||
first_line="${help_output%%$'\n'*}"
|
||||
expected_prefix="CLIProxyAPI Version: $expected_version, Commit: $expected_commit, BuiltAt: "
|
||||
[[ "$first_line" == "$expected_prefix"* ]]
|
||||
printf '%s\n' "$first_line"
|
||||
|
||||
printf 'PREFLIGHT\n'
|
||||
curl -fsS http://127.0.0.1:8317/healthz >/dev/null
|
||||
admin="$(cat "$root/secrets/cpa-management-key")"
|
||||
curl -fsS -H "Authorization: Bearer $admin" \
|
||||
http://127.0.0.1:8317/v0/management/plugins >"$tmp/plugins-before.json"
|
||||
|
||||
python3 - "$tmp/plugins-before.json" "$tmp/plugins-before.normalized.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
source, target = sys.argv[1:]
|
||||
payload = json.load(open(source, encoding="utf-8"))
|
||||
items = []
|
||||
for plugin in payload.get("plugins", []):
|
||||
item = {
|
||||
"id": plugin.get("id"),
|
||||
"path": plugin.get("path"),
|
||||
"version": (plugin.get("metadata") or {}).get("version"),
|
||||
"registered": plugin.get("registered"),
|
||||
"enabled": plugin.get("enabled"),
|
||||
"effective_enabled": plugin.get("effective_enabled"),
|
||||
}
|
||||
if not all((item["registered"], item["enabled"], item["effective_enabled"])):
|
||||
raise SystemExit(f"plugin preflight failed: {item}")
|
||||
items.append(item)
|
||||
items.sort(key=lambda value: value["id"] or "")
|
||||
with open(target, "w", encoding="utf-8") as handle:
|
||||
json.dump(items, handle, sort_keys=True, separators=(",", ":"))
|
||||
print(f"plugins={len(items)}")
|
||||
PY
|
||||
|
||||
if [[ -s "$root/run/manager.pid" ]] && kill -0 "$(cat "$root/run/manager.pid")" 2>/dev/null; then
|
||||
manager_was_running=1
|
||||
fi
|
||||
if [[ -s "$root/keeper/run/keeper.pid" ]] && kill -0 "$(cat "$root/keeper/run/keeper.pid")" 2>/dev/null; then
|
||||
keeper_pid="$(cat "$root/keeper/run/keeper.pid")"
|
||||
fi
|
||||
|
||||
attempts=$((drain_timeout / drain_poll + 1))
|
||||
active=-1
|
||||
for attempt in $(seq 1 "$attempts"); do
|
||||
curl -fsS -H "Authorization: Bearer $admin" \
|
||||
http://127.0.0.1:8317/v0/management/plugins/billing/keys >"$tmp/keys-before.json"
|
||||
active="$(python3 - "$tmp/keys-before.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
keys = json.load(open(sys.argv[1], encoding="utf-8")).get("keys", [])
|
||||
print(sum(int((item.get("billing") or {}).get("active_requests") or 0) for item in keys))
|
||||
PY
|
||||
)"
|
||||
printf 'drain_attempt=%s active_requests=%s\n' "$attempt" "$active"
|
||||
if [[ "$active" -eq 0 ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "$attempt" -lt "$attempts" ]]; then
|
||||
sleep "$drain_poll"
|
||||
fi
|
||||
done
|
||||
test "$active" -eq 0
|
||||
|
||||
backup="$root/backups/$(date -u +%Y%m%dT%H%M%SZ)-core-$expected_version"
|
||||
mkdir "$backup"
|
||||
mkdir -p "$backup/bin" "$backup/data"
|
||||
|
||||
printf 'STOP_AND_BACKUP\n'
|
||||
rollback_needed=1
|
||||
"$cpa_script" stop
|
||||
|
||||
cp -a "$root/bin/cli-proxy-api" "$backup/bin/cli-proxy-api"
|
||||
cp -a "$root/plugins" "$backup/plugins"
|
||||
cp -a "$root/config.yaml" "$backup/config.yaml"
|
||||
find "$root/data" -maxdepth 1 -type f \
|
||||
\( -name 'billing.db*' -o -name 'cpa-ext.db*' \) \
|
||||
-exec cp -a -t "$backup/data" {} +
|
||||
|
||||
(
|
||||
cd "$root"
|
||||
find plugins -type f -name '*.so' -print0 | sort -z | xargs -0 sha256sum
|
||||
) >"$backup/plugins.sha256"
|
||||
sed -E 's/^([[:space:]]+secret-key:).*/\1 <normalized>/' \
|
||||
"$root/config.yaml" >"$backup/config.normalized.yaml"
|
||||
cp "$tmp/plugins-before.normalized.json" "$backup/plugins.normalized.json"
|
||||
sha256sum "$backup/bin/cli-proxy-api" >"$backup/old-core.sha256"
|
||||
sha256sum "$archive" >"$backup/new-archive.sha256"
|
||||
|
||||
printf 'INSTALL_CORE\n'
|
||||
install -o root -g root -m 0755 "$tmp/cli-proxy-api" "$root/bin/cli-proxy-api.new"
|
||||
test "$(sha256sum "$root/bin/cli-proxy-api.new" | awk '{print $1}')" = "$expected_binary_hash"
|
||||
mv -f "$root/bin/cli-proxy-api.new" "$root/bin/cli-proxy-api"
|
||||
|
||||
printf 'START_AND_VERIFY\n'
|
||||
start_services
|
||||
curl -fsS http://127.0.0.1:8317/healthz >/dev/null
|
||||
|
||||
help_output="$("$root/bin/cli-proxy-api" -h 2>&1)"
|
||||
first_line="${help_output%%$'\n'*}"
|
||||
[[ "$first_line" == "$expected_prefix"* ]]
|
||||
test "$(sha256sum "$root/bin/cli-proxy-api" | awk '{print $1}')" = "$expected_binary_hash"
|
||||
(cd "$root" && sha256sum -c "$backup/plugins.sha256")
|
||||
|
||||
sed -E 's/^([[:space:]]+secret-key:).*/\1 <normalized>/' \
|
||||
"$root/config.yaml" >"$tmp/config-after.normalized.yaml"
|
||||
cmp "$backup/config.normalized.yaml" "$tmp/config-after.normalized.yaml"
|
||||
|
||||
if [[ -n "$keeper_pid" ]]; then
|
||||
test "$(cat "$root/keeper/run/keeper.pid")" = "$keeper_pid"
|
||||
kill -0 "$keeper_pid"
|
||||
fi
|
||||
if [[ "$manager_was_running" -eq 1 ]]; then
|
||||
test -s "$root/run/manager.pid"
|
||||
kill -0 "$(cat "$root/run/manager.pid")"
|
||||
fi
|
||||
|
||||
admin="$(cat "$root/secrets/cpa-management-key")"
|
||||
curl -fsS -H "Authorization: Bearer $admin" \
|
||||
http://127.0.0.1:8317/v0/management/plugins >"$tmp/plugins-after.json"
|
||||
python3 - "$tmp/plugins-after.json" "$tmp/plugins-after.normalized.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
source, target = sys.argv[1:]
|
||||
payload = json.load(open(source, encoding="utf-8"))
|
||||
items = []
|
||||
for plugin in payload.get("plugins", []):
|
||||
item = {
|
||||
"id": plugin.get("id"),
|
||||
"path": plugin.get("path"),
|
||||
"version": (plugin.get("metadata") or {}).get("version"),
|
||||
"registered": plugin.get("registered"),
|
||||
"enabled": plugin.get("enabled"),
|
||||
"effective_enabled": plugin.get("effective_enabled"),
|
||||
}
|
||||
items.append(item)
|
||||
items.sort(key=lambda value: value["id"] or "")
|
||||
with open(target, "w", encoding="utf-8") as handle:
|
||||
json.dump(items, handle, sort_keys=True, separators=(",", ":"))
|
||||
PY
|
||||
cmp "$backup/plugins.normalized.json" "$tmp/plugins-after.normalized.json"
|
||||
|
||||
grep -q "CLIProxyAPI Version: $expected_version, Commit: $expected_commit" "$root/logs/cpa-console.log"
|
||||
if grep -Eqi 'panic|pluginhost:.*(error|failed)' "$root/logs/cpa-console.log"; then
|
||||
tail -n 100 "$root/logs/cpa-console.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rollback_needed=0
|
||||
printf 'DEPLOYMENT_OK\n'
|
||||
printf 'version=%s\n' "$first_line"
|
||||
printf 'backup=%s\n' "$backup"
|
||||
printf 'core_sha256=%s\n' "$expected_binary_hash"
|
||||
printf 'cpa_pid=%s\n' "$(cat "$root/run/cpa.pid")"
|
||||
if [[ -n "$keeper_pid" ]]; then
|
||||
printf 'keeper_pid=%s\n' "$keeper_pid"
|
||||
fi
|
||||
'@
|
||||
Write-Utf8NoBom -Path $remoteScriptPath -Content $remoteScript
|
||||
|
||||
Write-Host "Archive: $archivePath"
|
||||
Write-Host "Archive SHA-256: $archiveHash"
|
||||
Write-Host "Binary SHA-256: $binaryHash"
|
||||
|
||||
if (-not $PSCmdlet.ShouldProcess("$SshTarget`:$RemoteRoot", "Deploy CPA core $version / $commit")) {
|
||||
return
|
||||
}
|
||||
|
||||
Invoke-NativeCommand $ssh @('-o', 'BatchMode=yes', $SshTarget, "test -d '$RemoteRoot/upload' -a -x '$RemoteRoot/cpa.sh'")
|
||||
Invoke-NativeCommand $scp @('-C', $archivePath, $checksumPath, "$SshTarget`:$RemoteRoot/upload/")
|
||||
|
||||
$remoteCommand = "tr -d '\r' | bash -s -- '$archiveName' '$archiveHash' '$binaryHash' '$version' '$commit' '$DrainTimeoutSeconds' '$DrainPollSeconds' '$RemoteRoot'"
|
||||
Get-Content -Raw -LiteralPath $remoteScriptPath | & $ssh '-o' 'BatchMode=yes' $SshTarget $remoteCommand
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Remote deployment failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user