Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,42 @@ jobs:
fi
"$BIN" --help > /dev/null

# **两种形态。**裸二进制给桌面版的流水线和 `twcore upgrade`(它们只要
# 那一个文件);压缩包给服务器上的首次安装(`scripts/install.sh`):
# 里面还有 systemd unit,装的 unit 和装的二进制出自同一个 commit。
# 压缩包还保住了可执行位 —— 裸文件下载下来是 0644。
- name: Package
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
mkdir -p dist
cp target/${{ matrix.target }}/release/twcore dist/twcore-${{ matrix.target }}
# 压缩包里的目录和裸二进制同名,所以在 dist 外面搭
STAGE="$RUNNER_TEMP/stage/twcore-$TARGET"
mkdir -p dist "$STAGE"
cp "target/$TARGET/release/twcore" "dist/twcore-$TARGET"
install -m 0755 "target/$TARGET/release/twcore" "$STAGE/twcore"
install -m 0644 packaging/systemd/twcore.service LICENSE "$STAGE/"
tar -czf "dist/twcore-$TARGET.tar.gz" --owner=0 --group=0 -C "$RUNNER_TEMP/stage" "twcore-$TARGET"
cd dist
# 和另外两个平台同一种校验文件:`<sha> <文件名>`
sha256sum twcore-${{ matrix.target }} > twcore-${{ matrix.target }}.sha256
cat twcore-${{ matrix.target }}.sha256
sha256sum "twcore-$TARGET" > "twcore-$TARGET.sha256"
sha256sum "twcore-$TARGET.tar.gz" > "twcore-$TARGET.tar.gz.sha256"
cat ./*.sha256
tar -tzvf "twcore-$TARGET.tar.gz"

# 装脚本认的就是这个布局,这里照着它的步骤解一遍、跑一遍 —— 布局改了
# 而脚本没跟上,在这里就挂,而不是在用户的服务器上。
- name: The archive is what install.sh expects
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
T=$(mktemp -d)
(cd dist && sha256sum -c "twcore-$TARGET.tar.gz.sha256")
tar -xzf "dist/twcore-$TARGET.tar.gz" -C "$T"
test -x "$T/twcore-$TARGET/twcore"
test -f "$T/twcore-$TARGET/twcore.service"
"$T/twcore-$TARGET/twcore" --version

- uses: softprops/action-gh-release@v2
# 排练不发布。
Expand All @@ -269,4 +296,6 @@ jobs:
files: |
dist/twcore-${{ matrix.target }}
dist/twcore-${{ matrix.target }}.sha256
dist/twcore-${{ matrix.target }}.tar.gz
dist/twcore-${{ matrix.target }}.tar.gz.sha256
generate_release_notes: true
64 changes: 56 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,38 @@ clean the diff is:
connections to the same handshake before HTTP. The control key never
leaves through the control plane and cannot be changed through it.

## The configuration reference

`docs/config.md` and `docs/config.zh-CN.md` are written by hand, except the
field tables and the built-in rule lists: everything between
`<!-- generated: … -->` and `<!-- /generated -->` is rendered from
`crates/tw-config/tests/manual/schema.rs`, and
`cargo test -p tw-config --test manual` fails when the two differ.

That file declares every section of `config.yaml` against its Rust type, and
the test checks the declaration against the code rather than trusting it:

- **Field names** come from serde itself (a probe deserializer records the
names a derived `Deserialize` asks for), so a field added to a config
type and not to the manual fails with the field's name.
- **Defaults are proven.** A declared default is written into a minimal
section and parsed; it has to mean the same as leaving the field out. A
field marked required has to fail without it.
- **Enum values** (`protocol`, `mode`, `type` …) are read from serde, not
copied.
- **Examples** in the manuals are parsed as configuration.

When you change a config type, add or change its row (English and Chinese),
then regenerate:

```bash
UPDATE_CONFIG_DOCS=1 cargo test -p tw-config --test manual
```

A section that is designed but not in the code yet is declared as
`Ty::Pending`: it is rendered, and the test fails as soon as the code starts
reading that field, so the declaration gets switched to the real type.

## The price list

Prices come in two layers.
Expand All @@ -113,20 +145,36 @@ answered on the request itself.

## Cutting a release

`twcore` ships inside the desktop app's `.app`, so "which build is in
there" has to be a fact somebody can check rather than whatever sat in
a `target/` directory that afternoon.
`twcore` ships inside the desktop app, and on its own for servers, so
"which build is in there" has to be a fact somebody can check rather than
whatever sat in a `target/` directory that afternoon.

1. Bump `version` in the workspace `Cargo.toml`, land it on `main`.
2. Tag that commit `vX.Y.Z` and push the tag.
3. `release.yml` builds `twcore` for `aarch64-apple-darwin`, checks the
3. `release.yml` builds `twcore` for every target below, checks each
binary actually runs and reports the version on the tag, and attaches
it to a GitHub Release with a `sha256`.
them to a GitHub Release, each with a `.sha256` (`<sha> <file>`).

| Target | Files |
|---|---|
| `aarch64-apple-darwin` | `twcore-aarch64-apple-darwin` |
| `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc` | `twcore-<target>.exe` |
| `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu` | `twcore-<target>`, and `twcore-<target>.tar.gz` holding the binary, `twcore.service` and `LICENSE` |

The bare binaries are what the desktop app's pipeline bundles and what
`twcore upgrade` downloads. The Linux tarballs are what
`scripts/install.sh` installs on a server; they carry the systemd unit so
the unit and the binary come from the same commit. The file names are a
contract with both: `twcore upgrade` has a test that reads `release.yml`.

To try a change to `release.yml` without publishing, run it by hand
(`workflow_dispatch`): it builds and checks everything and uploads nothing.

The desktop app pins `tw-api` to the same tag and bundles the binary
from that release. Those two have to come from one commit: the binary
speaks a protocol, and the app compiles a mirror of it.

Apple Silicon only, deliberately. An Intel user downloading a file that
will not open is worse served than one who finds no download at all;
supporting them means a universal binary, which is its own decision.
On macOS, Apple Silicon only, deliberately. An Intel user downloading a
file that will not open is worse served than one who finds no download
at all; supporting them means a universal binary, which is its own
decision.
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ cargo run -p twcore -- check # validate only, don't start
cargo run -p twcore -- serve # start the gateway and control plane
```

Every field of `config.yaml` is described in the
[configuration reference](docs/config.md). To run `twcore` on a Linux server
and manage it from the desktop app, see
[Running core on a server](docs/server.md).

## What it does

Point a client (Claude Code, Codex, and friends) at a local port, and:
Expand Down
3 changes: 3 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ cargo run -p twcore -- check # 只校验,不启动
cargo run -p twcore -- serve # 起网关和控制面
```

`config.yaml` 的每个字段见[配置手册](docs/config.zh-CN.md)。在 Linux 服务器上运行
`twcore`、由桌面应用远程管理,见[在服务器上运行 core](docs/server.zh-CN.md)。

## 它做什么

把客户端(Claude Code、Codex 之类)指向本地的一个端口,然后:
Expand Down
7 changes: 7 additions & 0 deletions bin/twcore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ hyper = { workspace = true }
hyper-util = { workspace = true, features = ["tokio"] }
http-body-util = { workspace = true }
tracing-subscriber = { workspace = true }
# `twcore upgrade`:问 Release、下载、核对校验和。都是依赖树里已有的
reqwest = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }

[target.'cfg(unix)'.dependencies]
# 只剩一处在用:问一个 pid 还在不在(见 src/proc.rs)
Expand All @@ -45,3 +49,6 @@ windows-sys = { workspace = true, features = [

[dev-dependencies]
tempfile = "3"
# `twcore upgrade` 的测试对着一个假的 GitHub 跑
axum = { workspace = true }
serde_json = { workspace = true }
24 changes: 24 additions & 0 deletions bin/twcore/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use clap::{Parser, Subcommand};
mod call;
mod lockfile;
mod proc;
mod upgrade;

use lockfile::{LockFile, LockOutcome};

Expand Down Expand Up @@ -111,6 +112,20 @@ enum Command {
#[arg(short, long)]
out: Option<PathBuf>,
},
/// Replace this twcore with another release from GitHub; the configuration is left alone
//
// 只给单独装的 twcore 用(服务器上)。桌面应用包里的那份由应用更新,见 upgrade.rs
Upgrade {
/// Only compare with the release and report; change nothing
#[arg(long)]
check: bool,
/// Restart twcore.service afterwards when systemd runs it
#[arg(long)]
restart: bool,
/// Install this version rather than the latest, such as 0.47.0
#[arg(long)]
version: Option<String>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -185,6 +200,15 @@ fn main() -> Result<()> {
data,
out,
} => call::run(&path, &endpoint, &method, data, out),
Command::Upgrade {
check,
restart,
version,
} => upgrade::run(upgrade::Opts {
check,
restart,
version,
}),
}
}

Expand Down
Loading
Loading