Skip to content

Tool-call inspection guards ThinkWatch's own data directory; the docs say what protects config.yaml #736

Tool-call inspection guards ThinkWatch's own data directory; the docs say what protects config.yaml

Tool-call inspection guards ThinkWatch's own data directory; the docs say what protects config.yaml #736

Workflow file for this run

# 每次改动都要过的那几关。
#
# **价目表的比对在这里**:内置快照和
# `crates/tw-pricing/data/verified.yaml` 对不上就构建失败 —— 那是挡住
# 「打包了错数据」的唯一手段。
name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
# 这个项目把警告当错误。**在 CI 上放松这一条,等于把它取消**
RUSTFLAGS: "-D warnings"
jobs:
check:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
# 插件沙箱里的 QuickJS 编成这个目标(crates/tw-plugin/build.rs)
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
# 编插件沙箱要一个能出 wasm 的 clang:这里是 Homebrew 的 llvm,由 build.rs
# 自己找到(顺便测了「装了 brew 的 llvm 就能编」这条路)
- name: Toolchain for the plugin sandbox
run: bash scripts/wasm-toolchain.sh
- name: Format
run: |
cargo fmt --all -- --check
# 沙箱里的那个小工程不在工作区里,上面一行管不到它
cargo fmt --manifest-path crates/tw-plugin/guest/Cargo.toml -- --check
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings
# 沙箱里的胶水(编成 wasm32 的 no_std 小工程)。build.rs 编它时不带 -D warnings
# —— 那是给外层工作区的 —— 所以它的告警只在这一步当错误
- name: clippy (plugin sandbox guest)
run: |
LLVM="$(brew --prefix llvm)/bin"
CC_wasm32_unknown_unknown="$LLVM/clang" AR_wasm32_unknown_unknown="$LLVM/llvm-ar" \
cargo clippy --manifest-path crates/tw-plugin/guest/Cargo.toml --target wasm32-unknown-unknown --target-dir target/tw-plugin-guest -- -D warnings
# 默认不跑打真实网络的那些(它们标了 #[ignore])—— CI 上的网络
# 抖动会变成一条和代码无关的红,而那种红看几次就没人看了。
- name: Test
run: cargo test --workspace
# 编进这一版的沙箱是哪个 clang 编的、wasm 的哈希
- name: Which compiler built the plugin sandbox
run: bash scripts/wasm-toolchain.sh record target
# 桌面端从 tw-api 导出前端类型(`ts` feature)。**默认关**,上面几步一行都
# 不编它 —— 而它坏掉的表现是桌面端接下一个 tag 时才发现导不出来。导出来
# 的文件再过一遍 tsc:ts-rs 生成的东西本身也可能不是合法的 TypeScript
- name: TypeScript bindings (tw-api, ts)
run: |
cargo clippy -p tw-api --all-targets --features ts -- -D warnings
cargo test -p tw-api --features ts
cargo run -p tw-api --features ts --example export_ts -- "$RUNNER_TEMP/tw-api-ts"
npx --yes -p typescript@5 tsc --noEmit --strict --target es2022 "$RUNNER_TEMP/tw-api-ts/tw-api.ts"
- name: Build
run: cargo build --release -p twcore
# **接缝上的失败模式单元测试看不见**:文件权限、socket、某个端点
# 在真二进制上根本没注册。这个项目的前四个真 bug 都是这么被逮到的。
# 脚本自己会 build,这一步只是跑它。
- name: Smoke (real binary, real socket, real data plane)
run: ./scripts/smoke.sh
# **第一层改了,企业版还编得过吗。**企业版钉 core 的 tag、只依赖第一层
# (tw-dialect、tw-guard、tw-breaker、tw-bedrock)。以前要等 core 发版、企业版升级那天
# 才知道某个改名或删掉的函数把它弄坏了,这里提前到 PR 上。
#
# 企业版取和这个 PR 同名的分支 —— 要改接口的一方在企业版开一个同名分支
# 把调用点跟上,两边一起绿;没有同名分支就取 dev。main 上的推送也对 dev。
# 两个仓库都是公开的,不需要令牌。企业版没有 `sqlx::query!` 这类编译期
# 连库的宏,`cargo check` 不需要数据库。
enterprise:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: core
- uses: dorny/paths-filter@v3
id: changed
with:
working-directory: core
filters: |
layer1:
- 'crates/tw-dialect/**'
- 'crates/tw-guard/**'
- 'crates/tw-breaker/**'
- 'crates/tw-bedrock/**'
- 'Cargo.toml'
- '.github/workflows/ci.yml'
- name: Pick the enterprise branch
if: steps.changed.outputs.layer1 == 'true'
id: branch
env:
HEAD_REF: ${{ github.head_ref }}
run: |
ref=dev
if [ -n "$HEAD_REF" ] && git ls-remote --exit-code --heads \
https://github.com/ThinkWatchProject/ThinkWatch.git "$HEAD_REF" >/dev/null; then
ref="$HEAD_REF"
fi
echo "enterprise branch: $ref"
echo "ref=$ref" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.changed.outputs.layer1 == 'true'
with:
repository: ThinkWatchProject/ThinkWatch
ref: ${{ steps.branch.outputs.ref }}
path: enterprise
- uses: dtolnay/rust-toolchain@stable
if: steps.changed.outputs.layer1 == 'true'
# 第一层全部指向这次的 core
- name: Point enterprise at this core
if: steps.changed.outputs.layer1 == 'true'
run: |
cat >> enterprise/Cargo.toml <<'TOML'
[patch."https://github.com/ThinkWatchProject/ThinkWatch-Core.git"]
tw-dialect = { path = "../core/crates/tw-dialect" }
tw-guard = { path = "../core/crates/tw-guard" }
tw-breaker = { path = "../core/crates/tw-breaker" }
tw-bedrock = { path = "../core/crates/tw-bedrock" }
TOML
# 企业版的 Cargo.lock 锁着它钉的那个版本;版本号不同时补丁会被
# 静默忽略(只报一条警告),编的还是旧的 core。
#
# 只更新企业版确实依赖的:它还没用上的第一层 crate 不在 lock 里,
# `cargo update -p` 对它直接报错(补丁用不上只是一条警告)
cd enterprise
pkgs=()
for c in tw-dialect tw-guard tw-breaker tw-bedrock; do
if grep -qx "name = \"$c\"" Cargo.lock; then pkgs+=(-p "$c"); fi
done
echo "updating: ${pkgs[*]}"
cargo update "${pkgs[@]}"
- uses: Swatinem/rust-cache@v2
if: steps.changed.outputs.layer1 == 'true'
with:
workspaces: enterprise -> target
- name: cargo check (enterprise)
if: steps.changed.outputs.layer1 == 'true'
working-directory: enterprise
run: cargo check --workspace --all-targets
# **同是 unix,不等于和 macOS 一样。**`getifaddrs` 给的标志、sysfs 里有
# 没有 `device`、pid 的上限、`stat` 的参数,这些在 macOS 上测过不说明
# 任何事 —— 而桌面版要在 22.04 上跑,所以就在 22.04 上测。
#
# 不重复跑 `cargo fmt`:格式和平台无关。
linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
# 发布页的正文只在推 tag 时才写(release.yml 的 publish)。脚本和
# `release-notes/` 下的说明在改它们的那个 PR 上就核对:链接对不对得上
# 发出去的文件、说明里有没有混进中文。只要 Python,放在编译之前
- name: Release page text
run: python3 scripts/release_notes_test.py
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
# 理由见下面 windows 那一段
cache-on-failure: true
# 镜像预装的 clang-15,显式指定(见脚本)
- name: Toolchain for the plugin sandbox
run: bash scripts/wasm-toolchain.sh
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
run: cargo test --workspace
- name: Which compiler built the plugin sandbox
run: bash scripts/wasm-toolchain.sh record target
# 真二进制、真 socket、真数据面,和 macOS 那一步是同一个脚本
- name: Smoke (real binary, real socket, real data plane)
run: ./scripts/smoke.sh
# **这台机器上没有 unix socket,也没有信号。**core 在这两件事上各有一套
# 平台实现,而在这个任务出现之前,Windows 那一半从来没有被编译过 ——
# 一个 `#[cfg(windows)]` 模块「检查通过」和「根本没人看过它」长得一模一样。
#
# 不重复跑 `cargo fmt`:格式和平台无关,跑两遍只是慢一倍。
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
# **失败也存。**这个 action 默认只在任务成功时保存缓存,而一个
# 刚立起来、还在逐条修的任务恰恰一次都不会成功 —— 于是「红 →
# 不存缓存 → 从零编译 → 还是红」自己维持住了,每一轮都要把
# rusqlite 的 SQLite 和 aws-lc-sys 重编一遍。
#
# cargo 自己按指纹判断哪些要重编,所以缓存下一个失败构建的
# target 目录是安全的,它只是省掉那些与失败无关的部分。
cache-on-failure: true
# 镜像预装的 LLVM(C:\Program Files\LLVM),由 build.rs 自己找到
- name: Toolchain for the plugin sandbox
shell: bash
run: bash scripts/wasm-toolchain.sh
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
run: cargo test --workspace
- name: Which compiler built the plugin sandbox
shell: bash
run: bash scripts/wasm-toolchain.sh record target
- name: Build
run: cargo build --release -p twcore
# macOS 那边的 smoke 脚本在这里跑不了(`stat -f`、一堆 BSD 的写法),
# 移植它是另一件事。但**至少要证明这个二进制真的起得来、控制面真的应答**
# —— 那两样恰恰是这个平台上和 unix 完全不同的实现:回环端口换掉了 socket,
# 握手是唯一的门。
#
# 不验这个的话,Windows 上的失败模式是「CI 全绿,而用户装上之后界面
# 永远停在连接页」。
- name: It starts, and the control plane answers only after the handshake
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$home_ = Join-Path $env:RUNNER_TEMP "tw"
New-Item -ItemType Directory -Force -Path $home_ | Out-Null
$env:THINKWATCH_HOME = $home_
$cfg = Join-Path $home_ "config.yaml"
$bin = "target\release\twcore.exe"
$proc = Start-Process -FilePath $bin `
-ArgumentList "serve","--config",$cfg,"--port","18999" `
-PassThru -NoNewWindow `
-RedirectStandardOutput (Join-Path $home_ "out.log") `
-RedirectStandardError (Join-Path $home_ "err.log")
# 端口是 core 绑到之后才写出来的,所以它出现就等于「听起来了」
$portFile = Join-Path $home_ "control.port"
$deadline = (Get-Date).AddSeconds(60)
while (-not (Test-Path $portFile)) {
if ($proc.HasExited) {
Get-Content (Join-Path $home_ "err.log") -ErrorAction SilentlyContinue
throw "core exited before it started listening (code $($proc.ExitCode))"
}
if ((Get-Date) -gt $deadline) {
Get-Content (Join-Path $home_ "err.log") -ErrorAction SilentlyContinue
throw "core never wrote $portFile"
}
Start-Sleep -Milliseconds 200
}
Start-Sleep -Milliseconds 200
$port = (Get-Content $portFile -Raw).Trim()
Write-Host "control plane on $port"
try {
# 不握手的 HTTP 进不来:core 回一个拒绝字节就断开
$plainGotIn = $false
try {
$no = Invoke-WebRequest -Uri "http://127.0.0.1:$port/status" -SkipHttpErrorCheck -TimeoutSec 10
if ($no.Content -match "api_version") { $plainGotIn = $true }
} catch {
Write-Host "plain HTTP was turned away: $($_.Exception.Message)"
}
if ($plainGotIn) { throw "plain HTTP without the handshake got a status back" }
# 握手之后:`twcore call` 读同一份配置里的钥匙,走和桌面端同一条握手
$out = & $bin --config $cfg call /status
if ($LASTEXITCODE -ne 0) { throw "twcore call /status failed ($LASTEXITCODE)" }
$status = $out | ConvertFrom-Json
if (-not $status.version) { throw "the status carried no version: $out" }
Write-Host "gateway $($status.version), api $($status.api_version)"
# **请它退出,它就该退。**这条路在这个平台上没有替代品:
# 没有 SIGTERM,而桌面端要靠它在改完配置后重启 core、在装更新
# 之前停掉它。只能强杀意味着 WAL 不收尾、在途请求断在半路。
$code = & $bin --config $cfg call -X POST --out (Join-Path $home_ "bye.json") /shutdown
if ("$code".Trim() -ne "202") { throw "asking it to exit answered $code" }
# 等**进程**没了,不是等端口没了 —— 一个卡住的进程也可能丢掉监听
if (-not $proc.WaitForExit(10000)) {
throw "it was asked to exit and is still running after 10s"
}
Write-Host "it exited on request"
} finally {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}