Tool-call inspection guards ThinkWatch's own data directory; the docs say what protects config.yaml #736
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 每次改动都要过的那几关。 | |
| # | |
| # **价目表的比对在这里**:内置快照和 | |
| # `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 | |
| } |