ci: complete release pipeline

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-07-02 12:09:22 +08:00
co-authored by Codex
parent b1f061c21d
commit cd4b91a81e
13 changed files with 459 additions and 402 deletions
+64
View File
@@ -0,0 +1,64 @@
name: CI
on:
push:
branches:
- '**'
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
backend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: backend -> target
- name: Rust format
run: cargo fmt --manifest-path backend/Cargo.toml --check
- name: Rust clippy
run: cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
- name: Rust tests
run: cargo test --manifest-path backend/Cargo.toml --test main --locked
frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 11.7.0
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Frontend build
run: pnpm --dir frontend build
+89 -56
View File
@@ -4,6 +4,12 @@ on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release, for example v0.1.0'
required: true
type: string
permissions:
contents: write
@@ -19,88 +25,97 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
- name: Derive version
id: version
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "build_time=${BUILD_TIME}" >> "$GITHUB_OUTPUT"
echo "git_sha=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
- name: Write VERSION file
shell: bash
run: printf '%s\n' '${{ steps.version.outputs.version }}' > backend/build/VERSION
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@11.7.0 --activate
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Build frontend
run: pnpm --dir frontend build
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: backend -> target
- name: Build release binaries
env:
CPR_VERSION: ${{ steps.version.outputs.version }}
CPR_GIT_SHA: ${{ steps.version.outputs.git_sha }}
CPR_BUILD_TIME: ${{ steps.version.outputs.build_time }}
CPR_BUILD_TYPE: release
run: cargo build --manifest-path backend/Cargo.toml --release --locked --bin codex-proxy-rs
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
run: |
VERSION="${RELEASE_TAG#v}"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
GIT_SHA="$(git rev-parse HEAD)"
{
echo "tag=${RELEASE_TAG}"
echo "version=${VERSION}"
echo "build_time=${BUILD_TIME}"
echo "git_sha=${GIT_SHA}"
} >> "$GITHUB_OUTPUT"
- name: Verify Cargo package version
env:
RELEASE_VERSION: ${{ steps.version.outputs.version }}
run: |
PACKAGE_VERSION="$(python3 - <<'PY'
import pathlib
import tomllib
manifest = tomllib.loads(pathlib.Path("backend/Cargo.toml").read_text())
print(manifest["package"]["version"])
PY
)"
if [ "${PACKAGE_VERSION}" != "${RELEASE_VERSION}" ]; then
echo "Tag version ${RELEASE_VERSION} does not match backend/Cargo.toml package version ${PACKAGE_VERSION}" >&2
exit 1
fi
- name: Setup QEMU
uses: docker/setup-qemu-action@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Package release assets
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
PACKAGE_DIR="dist-package/codex-proxy-rs"
mkdir -p "${PACKAGE_DIR}/web"
cp backend/target/release/codex-proxy-rs "${PACKAGE_DIR}/"
cp -r frontend/dist "${PACKAGE_DIR}/web/dist"
tar -czf "codex-proxy-rs_${VERSION}_linux_x86_64.tar.gz" -C dist-package codex-proxy-rs
sha256sum "codex-proxy-rs_${VERSION}_linux_x86_64.tar.gz" > checksums.txt
rm -rf dist-package checksums.txt
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
docker buildx build \
--file deploy/Dockerfile \
--target release-asset \
--platform linux/amd64,linux/arm64 \
--output type=local,dest=dist-package \
--cache-from type=gha,scope=release \
--cache-to type=gha,mode=max,scope=release \
--build-arg CPR_VERSION="${VERSION}" \
--build-arg CPR_GIT_SHA="${{ steps.version.outputs.git_sha }}" \
--build-arg CPR_BUILD_TIME="${{ steps.version.outputs.build_time }}" \
--build-arg CPR_BUILD_TYPE=release \
.
find dist-package -type f -name 'codex-proxy-rs_*.tar.gz' -exec cp {} . \;
test -f "codex-proxy-rs_${VERSION}_linux_amd64.tar.gz"
test -f "codex-proxy-rs_${VERSION}_linux_arm64.tar.gz"
sha256sum "codex-proxy-rs_${VERSION}_linux_amd64.tar.gz" \
"codex-proxy-rs_${VERSION}_linux_arm64.tar.gz" > checksums.txt
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push app image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: deploy/Dockerfile
target: runtime
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
build-args: |
CPR_VERSION=${{ steps.version.outputs.version }}
CPR_GIT_SHA=${{ steps.version.outputs.git_sha }}
CPR_BUILD_TIME=${{ steps.version.outputs.build_time }}
CPR_BUILD_TYPE=release
cache-from: type=gha,scope=release
cache-to: type=gha,mode=max,scope=release
labels: |
org.opencontainers.image.title=Codex Proxy RS
org.opencontainers.image.version=${{ steps.version.outputs.version }}
@@ -109,12 +124,30 @@ jobs:
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.version.outputs.git_sha }}
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
generate_release_notes: true
body: |
## Installation
**Docker:**
```bash
# GitHub Container Registry
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
```
**Manual download:**
Download the appropriate archive for your platform from the assets below.
## Documentation
- [GitHub Repository](https://github.com/${{ github.repository }})
- [Installation Guide](https://github.com/${{ github.repository }}/blob/main/deploy/README.md)
files: |
codex-proxy-rs_${{ steps.version.outputs.version }}_linux_x86_64.tar.gz
codex-proxy-rs_${{ steps.version.outputs.version }}_linux_amd64.tar.gz
codex-proxy-rs_${{ steps.version.outputs.version }}_linux_arm64.tar.gz
checksums.txt
+64
View File
@@ -0,0 +1,64 @@
name: Security Scan
on:
push:
branches:
- '**'
pull_request:
schedule:
- cron: '0 3 * * 1'
permissions:
contents: read
concurrency:
group: security-scan-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
backend-security:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: backend -> target
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Run cargo audit
working-directory: backend
run: cargo audit
frontend-security:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 11.7.0
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Run pnpm audit
run: pnpm --dir frontend audit --prod --audit-level=high
+2 -19
View File
@@ -103,24 +103,7 @@ logging:
## 架构
```mermaid
flowchart LR
Client["客户端 / SDK"] -->|OpenAI API| Proxy["/v1/*"]
Browser["浏览器"] -->|Web| Admin["/api/admin/*"]
subgraph Gateway["codex-proxy-rs"]
Proxy --> Auth["API Key 鉴权"]
Auth --> Dispatch["调度层<br/>选账号 · 限并发 · 亲和性"]
Dispatch --> Transport["上游传输<br/>HTTP SSE / WebSocket"]
Dispatch --> Pool["账号池<br/>内存状态 + SQLite"]
Admin --> SQLite[("SQLite")]
Pool --> SQLite
end
Transport --> Upstream["ChatGPT / Codex API"]
Proxy -.->|静态资源| SPA["Vue 3 管理端"]
Admin -.-> SPA
```
Codex Proxy RS 是单主服务架构:Rust 后端同时承载 OpenAI 兼容代理、管理端 API、SQLite 持久化、前端静态资源托管和在线更新。详细模块边界、请求链路、发布和更新流程见 [架构说明](docs/architecture.md)。
## 项目结构
@@ -131,7 +114,7 @@ backend/
build/ 构建脚本
frontend/ Vue 3 管理控制台
deploy/ Dockerfile、Compose、示例配置
docs/ 设计与迁移文档
docs/ 架构与维护文档
```
## 开发
-1
View File
@@ -1 +0,0 @@
0.1.0
+4 -16
View File
@@ -1,19 +1,14 @@
use std::{env, fs};
const VERSION_FILE: &str = "build/VERSION";
use std::env;
fn main() {
println!("cargo:rerun-if-changed={VERSION_FILE}");
println!("cargo:rerun-if-env-changed=CPR_VERSION");
println!("cargo:rerun-if-env-changed=CPR_GIT_SHA");
println!("cargo:rerun-if-env-changed=CPR_BUILD_TIME");
println!("cargo:rerun-if-env-changed=CPR_BUILD_TYPE");
let version = version_env_value("CPR_VERSION")
.or_else(version_file)
.unwrap_or_else(|| {
env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0-dev".to_string())
});
let version = version_env_value("CPR_VERSION").unwrap_or_else(|| {
env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0-dev".to_string())
});
let git_sha = env_value("CPR_GIT_SHA").unwrap_or_else(|| "unknown".to_string());
let build_time = env_value("CPR_BUILD_TIME").unwrap_or_else(|| "unknown".to_string());
let build_type = env_value("CPR_BUILD_TYPE").unwrap_or_else(|| "source".to_string());
@@ -34,10 +29,3 @@ fn env_value(key: &str) -> Option<String> {
fn version_env_value(key: &str) -> Option<String> {
env_value(key).map(|value| value.trim_start_matches('v').to_string())
}
fn version_file() -> Option<String> {
fs::read_to_string(VERSION_FILE)
.ok()
.map(|value| value.trim().trim_start_matches('v').to_string())
.filter(|value| !value.is_empty())
}
-59
View File
@@ -614,65 +614,6 @@ fn health_tone(bucket: UsageWindow, success_rate: f64, is_future: bool) -> char
}
}
#[cfg(test)]
mod tests {
use super::{health_tone, UsageWindow};
#[test]
fn health_tone_should_classify_availability_levels() {
assert_eq!(health_tone(UsageWindow::default(), 0.0, true), '0');
assert_eq!(health_tone(UsageWindow::default(), 0.0, false), '1');
assert_eq!(
health_tone(
UsageWindow {
requests: 2,
errors: 2,
..UsageWindow::default()
},
0.0,
false,
),
'2'
);
assert_eq!(
health_tone(
UsageWindow {
requests: 10,
errors: 2,
..UsageWindow::default()
},
80.0,
false,
),
'3'
);
assert_eq!(
health_tone(
UsageWindow {
requests: 2,
errors: 0,
..UsageWindow::default()
},
100.0,
false,
),
'4'
);
assert_eq!(
health_tone(
UsageWindow {
requests: 3,
errors: 0,
..UsageWindow::default()
},
100.0,
false,
),
'5'
);
}
}
fn trend_summary(
kind: DashboardTrendKind,
points: &[DashboardTrendPointData],
+30 -6
View File
@@ -815,8 +815,8 @@ fn select_release_archive<'a>(
release: &'a GitHubRelease,
version: &str,
) -> Result<&'a GitHubAsset, AdminError> {
let os = env::consts::OS;
let arch = env::consts::ARCH;
let os_aliases = platform_os_aliases();
let arch_aliases = platform_arch_aliases();
let normalized = normalize_version_tag(version);
release
.assets
@@ -825,23 +825,47 @@ fn select_release_archive<'a>(
let name = asset.name.as_str();
name.contains(APP_BINARY_NAME)
&& name.contains(&normalized)
&& name.contains(os)
&& name.contains(arch)
&& asset_matches_current_platform(name, os_aliases, arch_aliases)
&& !name.ends_with(".txt")
})
.or_else(|| {
release.assets.iter().find(|asset| {
let name = asset.name.as_str();
name.contains(os) && name.contains(arch) && !name.ends_with(".txt")
asset_matches_current_platform(name, os_aliases, arch_aliases)
&& !name.ends_with(".txt")
})
})
.ok_or_else(|| {
AdminError::conflict(format!(
"No compatible release archive found for {os}/{arch}"
"No compatible release archive found for {}/{}",
env::consts::OS,
env::consts::ARCH
))
})
}
fn platform_os_aliases() -> &'static [&'static str] {
match env::consts::OS {
"macos" => &["macos", "darwin"],
"windows" => &["windows", "win32"],
"linux" => &["linux"],
_ => &[env::consts::OS],
}
}
fn platform_arch_aliases() -> &'static [&'static str] {
match env::consts::ARCH {
"x86_64" => &["x86_64", "amd64"],
"aarch64" => &["aarch64", "arm64"],
_ => &[env::consts::ARCH],
}
}
fn asset_matches_current_platform(name: &str, os_aliases: &[&str], arch_aliases: &[&str]) -> bool {
os_aliases.iter().any(|os| name.contains(os))
&& arch_aliases.iter().any(|arch| name.contains(arch))
}
async fn download_file(
url: &str,
dest: &Path,
+9 -1
View File
@@ -364,7 +364,7 @@ async fn mount_latest_release_with_archive(
let archive_name = format!(
"codex-proxy-rs_{version}_{}_{}.tar.gz",
std::env::consts::OS,
std::env::consts::ARCH
release_arch()
);
let archive = release_archive(binary, index);
let checksum = format!(
@@ -406,6 +406,14 @@ async fn mount_latest_release_with_archive(
.await;
}
fn release_arch() -> &'static str {
match std::env::consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
arch => arch,
}
}
fn release_archive(binary: &str, index: &str) -> Vec<u8> {
let mut data = Vec::new();
{
+19 -1
View File
@@ -11,10 +11,12 @@ RUN pnpm build
FROM rust:1.95-bookworm AS backend-builder
WORKDIR /app/backend
ARG TARGETARCH
ARG CPR_VERSION=
ARG CPR_GIT_SHA=unknown
ARG CPR_BUILD_TIME=unknown
ARG CPR_BUILD_TYPE=release
ENV CARGO_TARGET_DIR=/app/backend/target/${TARGETARCH}
ENV CPR_VERSION=${CPR_VERSION}
ENV CPR_GIT_SHA=${CPR_GIT_SHA}
ENV CPR_BUILD_TIME=${CPR_BUILD_TIME}
@@ -27,7 +29,23 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/app/backend/target \
cargo build --release --locked --bin codex-proxy-rs \
&& cp /app/backend/target/release/codex-proxy-rs /tmp/codex-proxy-rs
&& cp "${CARGO_TARGET_DIR}/release/codex-proxy-rs" /tmp/codex-proxy-rs
FROM backend-builder AS release-asset-builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /out
RUN mkdir -p /tmp/release/codex-proxy-rs/web \
&& cp /tmp/codex-proxy-rs /tmp/release/codex-proxy-rs/ \
&& cp -r /app/backend/web/dist /tmp/release/codex-proxy-rs/web/dist \
&& tar -czf "/out/codex-proxy-rs_${CPR_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz" \
-C /tmp/release codex-proxy-rs
FROM scratch AS release-asset
COPY --from=release-asset-builder /out /
FROM debian:bookworm-slim AS runtime
+170
View File
@@ -0,0 +1,170 @@
# Codex Proxy RS 架构说明
## 总览
Codex Proxy RS 是一个单主服务应用:Rust 后端提供 OpenAI 兼容代理、管理端 API、SQLite 持久化、静态前端托管和在线更新能力;Vue 管理端作为 SPA 由同一个后端进程托管。
```mermaid
flowchart LR
Client["客户端 / SDK"] -->|"OpenAI API /v1/*"| Http["Axum HTTP Router"]
Browser["浏览器"] -->|"管理端 SPA"| Web["web 静态资源托管"]
Browser -->|"/api/admin/*"| Http
subgraph App["codex-proxy-rs 单主进程"]
Http --> Proxy["proxy<br/>API Key 鉴权<br/>OpenAI 兼容协议"]
Http --> Admin["admin<br/>账号 / Key / 设置 / 监控 / 系统更新"]
Http --> Web
Proxy --> Dispatch["dispatch<br/>账号选择 / 限流 / 亲和性 / 失败回退"]
Dispatch --> Protocol["upstream protocol<br/>请求转换 / SSE 转换"]
Protocol --> Transport["upstream transport<br/>HTTP SSE / WebSocket"]
Admin --> Runtime["runtime services<br/>账号池 / 后台任务 / 更新状态"]
Dispatch --> Runtime
Runtime --> Store[("SQLite<br/>账号 / 会话 / 使用记录 / 设置")]
Admin --> Store
end
Transport --> Upstream["ChatGPT / Codex Upstream"]
Web --> Frontend["frontend/dist<br/>Vue 3 管理端"]
Release["GitHub Release<br/>tar.gz + checksums"] --> Update["在线更新<br/>下载 / 校验 / 替换 / 重启"]
Update --> Admin
GHCR["GHCR<br/>linux/amd64 + linux/arm64"] --> Docker["Docker Compose<br/>restart: unless-stopped"]
```
## 仓库边界
- `backend/`:Rust/Axum 后端、SQLite schema、运行时服务、集成测试和 Cargo 构建脚本。
- `frontend/`:Vue 3 管理端,使用 Vite、Tailwind v4、Pinia、Vue Router 和 ECharts。
- `deploy/`:Dockerfile、Compose 文件、Docker ignore 文件和部署样例配置。
- `docs/`:长期维护文档,只记录当前真实架构和约定,不保留迁移过程作为开发约束。
- `skills/`:项目本地 Codex skill。
## 后端结构
后端入口是 `backend/src/main.rs`,公共库入口是 `backend/src/lib.rs`。运行时由 `runtime::bootstrap` 初始化,服务集合在 `runtime::services` 构造,并通过 `AppState` 注入到路由。
主要模块:
- `admin/`:管理端 API,包括认证、账号、API Key、监控、设置和系统更新。
- `proxy/`:OpenAI 兼容代理入口、鉴权、请求转换和账号调度。
- `upstream/`:ChatGPT/Codex 上游协议、传输层、账号模型、token 刷新、quota 刷新和 fingerprint。
- `config/`:配置加载、运行时设置和配置类型。
- `infra/`:数据库、日志、时间、格式化、JSON 和 schema。
- `http/`:HTTP router、中间件和统一请求上下文。
- `web/`:前端静态资源和 SPA fallback 托管。
测试只放在 `backend/tests/`,不要在 `backend/src/` 中新增测试模块。
## 数据和运行时
后端以 SQLite 作为主要持久化存储。默认运行时数据位于 `.runtime/`,Docker 部署时通过 Compose 挂载到 `/app/data` 和 `/app/logs`。
运行时状态分两类:
- 持久化状态:账号、cookie、API Key、session、使用记录、时间桶、fingerprint、运行时设置等。
- 内存状态:账号池、调度状态、运行中任务、SSE 更新日志广播、缓存的 release 检查结果等。
运行时设置通过管理端 API 修改后写入数据库,并同步到内存服务。
## 代理请求链路
客户端请求进入 `/v1/*`:
1. `proxy/auth` 校验客户端 API Key。
2. `proxy/openai` 解析 OpenAI 兼容请求。
3. `proxy/dispatch` 选择账号、处理限流、亲和性和失败回退。
4. `upstream/protocol` 转换 Codex/ChatGPT 上游协议。
5. `upstream/transport` 使用 HTTP SSE 或 WebSocket 访问上游。
6. 响应被转换回 OpenAI 兼容格式,并记录使用数据。
账号运行时状态由 SQLite 快照恢复,并在请求、刷新、quota 检查和失败回退时更新。
## 管理端
管理端 API 位于 `/api/admin/*`,使用 session cookie 保护。前端入口由后端托管,不单独部署 Node 服务。
前端结构:
- `frontend/src/api/modules`:管理端 API 客户端。
- `frontend/src/components/base`:基础 UI 组件。
- `frontend/src/layout`:主布局、侧边栏、系统更新入口和更新弹窗。
- `frontend/src/views`:各业务页面。
- `frontend/src/stores`:Pinia 状态。
- `frontend/src/styles/tokens.css`:亮色/暗色主题 token。
前端只展示后端返回的版本、状态和更新日志,不在前端做状态文案映射。
## 版本和发布
版本只有两个来源层级:
- 源码默认版本:`backend/Cargo.toml` 的 `[package].version`。
- 发布版本:Git tag 去掉 `v` 前缀后得到的版本。
发布 workflow 会校验 tag 版本必须等于 `backend/Cargo.toml` 的 package version。发布构建时通过 `CPR_VERSION`、`CPR_GIT_SHA`、`CPR_BUILD_TIME` 和 `CPR_BUILD_TYPE=release` 注入编译期元数据。
`backend/build/build.rs` 负责把这些值写入二进制。没有 `CPR_VERSION` 时,版本回落到 Cargo 的 `CARGO_PKG_VERSION`。不要重新引入独立 `VERSION` 文件。
GitHub Release 产物:
```text
codex-proxy-rs_<version>_linux_amd64.tar.gz
codex-proxy-rs_<version>_linux_arm64.tar.gz
checksums.txt
```
GHCR 镜像发布为 `linux/amd64` 和 `linux/arm64` 多平台 manifest。
## 在线更新
在线更新由主服务自己处理,不依赖独立 updater sidecar。核心接口:
- `GET /api/admin/system/version`
- `GET /api/admin/system/check-updates`
- `GET /api/admin/system/update-events`
- `GET /api/admin/system/update-status`
- `POST /api/admin/system/update`
- `POST /api/admin/system/restart`
更新流程:
1. 后端检查 GitHub latest release。
2. 按当前 OS/arch 选择匹配 asset,兼容 `amd64/x86_64` 和 `arm64/aarch64` 命名。
3. 下载 release tar 包并用 `checksums.txt` 校验。
4. 解压二进制和 `web/dist`。
5. 替换当前二进制和前端静态资源。
6. 前端通过 SSE 实时展示后端推送的中文更新日志。
7. Docker 部署依靠 `restart: unless-stopped` 在自重启后拉起新进程。
替换逻辑必须支持跨文件系统 fallback,因为容器文件系统里 `rename` 可能遇到 `Invalid cross-device link`。
## Docker 部署
Docker 构建入口是 `deploy/Dockerfile`,上下文保持仓库根目录。
构建阶段:
- `frontend-builder`:构建 Vue SPA。
- `backend-builder`:构建 Rust release 二进制,并复制前端 dist。
- `release-asset`:导出 GitHub Release tar 包。
- `runtime`:最小运行镜像,基于 `debian:bookworm-slim`,只包含 CA、后端二进制和 `web/dist`。
Compose 文件是 `deploy/docker-compose.yml`。示例配置只保留 `deploy/config.example.yaml`,用户复制为 `deploy/config.yaml` 后部署。
## CI 和安全扫描
常规 CI 位于 `.github/workflows/ci.yml`:
- Rust fmt、clippy、集成测试。
- 前端依赖安装和 build。
- Compose 配置校验。
安全扫描位于 `.github/workflows/security-scan.yml`:
- `cargo audit` 扫描 Rust 依赖。
- `pnpm audit --prod --audit-level=high` 扫描前端生产依赖。
- 每周定时运行,同时覆盖 push 和 pull request。
发布流程位于 `.github/workflows/release.yml`,只由 `v*` tag 或手动指定 tag 触发。
-237
View File
@@ -1,237 +0,0 @@
# Project Layout Restructure
## 背景
项目目录已按 Sub2API 的边界收敛:
- `backend/` 承载后端应用、后端构建元信息和后端测试。
- `frontend/` 承载前端应用。
- `deploy/` 承载 Docker、Compose、部署脚本和部署示例配置。
根目录只保留仓库级入口和文档,不再承载后端源码、前端源码或具体部署实现。
## 完成后结构
```text
codex-proxy-rs/
├── backend/
│ ├── Cargo.toml
│ ├── Cargo.lock
│ ├── build/
│ │ ├── build.rs
│ │ └── VERSION
│ ├── rust-toolchain.toml
│ ├── src/
│ └── tests/
├── frontend/
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── pnpm-workspace.yaml
│ ├── vite.config.ts
│ └── src/
├── deploy/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ ├── config.example.yaml
│ └── README.md
├── docs/
├── README.md
└── .github/
```
说明:
- `backend/build/VERSION` 是唯一的源码默认版本文件。它对应 Sub2API 的 `backend/cmd/server/VERSION`,但本项目的 Rust 入口仍保留在 `backend/src/main.rs`,所以版本元信息归入 `backend/build/`。
- `backend/build/build.rs` 读取 `CPR_VERSION`,否则读取 `backend/build/VERSION`,最后才回落到 `CARGO_PKG_VERSION`。
- `frontend/` 替代原 `web/`。前端只负责展示后端返回的版本、构建类型、部署类型和更新进度。
- `deploy/` 替代根目录里的部署文件归属。根目录只保留仓库级入口和文档,不承载具体部署实现。
- 测试代码继续放在测试目录,禁止放入 `backend/src`。
## 路径映射
| 原路径 | 新路径 | 备注 |
| --- | --- | --- |
| `src/` | `backend/src/` | Rust 后端源码。 |
| `tests/` | `backend/tests/` | Rust 集成测试,仍在测试目录,不进入源码目录。 |
| `Cargo.toml` | `backend/Cargo.toml` | 后端 package manifest。 |
| `Cargo.lock` | `backend/Cargo.lock` | 后端锁文件,随后端移动。 |
| `build.rs` | `backend/build/build.rs` | 后端构建脚本。 |
| `VERSION` | `backend/build/VERSION` | 后端应用版本源。 |
| `rust-toolchain.toml` | `backend/rust-toolchain.toml` | 后端工具链配置。 |
| `web/` | `frontend/` | 前端应用。 |
| `Dockerfile` | `deploy/Dockerfile` | 生产镜像构建入口。 |
| `docker-compose.yml` | `deploy/docker-compose.yml` | Docker 部署入口。 |
| `config.yaml` | `deploy/config.example.yaml` | 根目录不保留本地运行配置。 |
## Cargo 组织方式
优先采用 Sub2API 风格:后端是自包含子项目。
```text
backend/
├── Cargo.toml
├── Cargo.lock
├── build/
│ ├── build.rs
│ └── VERSION
├── src/
└── tests/
```
后续命令从:
```bash
cargo test --test main admin::system --locked
cargo clippy --all-targets --all-features --locked -- -D warnings
```
调整为:
```bash
cargo test --manifest-path backend/Cargo.toml --test main admin::system --locked
cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
```
如果后续需要根目录统一调度,可以再加一个很薄的 root workspace `Cargo.toml`,但不要让根目录重新成为后端版本源。
根目录 `target/` 不是项目构建目标目录。迁移后后端构建产物位于 `backend/target/`;如果本地编辑器或 rust-analyzer 在根目录生成 `target/flycheck*`,它只是本地检查临时产物,可以直接删除,且已被 `.gitignore` 忽略。
## 前端迁移点
`web/` 已迁移到 `frontend/`,同步点:
- `.github/workflows/release.yml` 中所有 `pnpm --dir web ...` 改为 `pnpm --dir frontend ...`。
- Dockerfile 中所有 `web/package.json`、`web/pnpm-lock.yaml`、`web/dist` 改为 `frontend/...`。
- README、部署文档和开发命令里的 `web` 改为 `frontend`。
- 前端 API 类型和更新弹窗不做业务重写,只做路径迁移。
## 部署迁移点
`deploy/` 负责部署相关文件:
- `deploy/Dockerfile`
- `deploy/docker-compose.yml`
- `deploy/config.example.yaml`
- `deploy/README.md`
Docker 构建上下文仍建议使用仓库根目录:
```bash
docker build -f deploy/Dockerfile -t codex-proxy-rs .
```
这样 Dockerfile 可以同时访问 `backend/` 和 `frontend/`,避免把构建上下文拆散。
Compose 文件迁移到 `deploy/` 后,默认挂载路径随 Compose 文件落在 `deploy/` 目录,保持部署文件和部署状态集中;需要接入外部配置或数据目录时,通过环境变量覆盖:
```yaml
services:
codex-proxy-rs:
volumes:
- ${CPR_CONFIG_FILE:-./config.yaml}:/app/config.yaml:ro
- ${CPR_DATA_DIR:-./data}:/app/data
- ${CPR_LOG_DIR:-./logs}:/app/logs
```
使用 `deploy/config.example.yaml` 作为样例,运行时配置由用户复制为 `deploy/config.yaml` 或通过 `CPR_CONFIG_FILE` 挂载提供。
## 发布链路调整
发布版本必须只有一个来源:
1. Git tag,例如 `v0.1.1`。
2. CI 去掉前缀 `v`,得到 `0.1.1`。
3. CI 写入 `backend/build/VERSION`。
4. CI 构建后端时传入:
- `CPR_VERSION=0.1.1`
- `CPR_GIT_SHA=<sha>`
- `CPR_BUILD_TIME=<utc>`
- `CPR_BUILD_TYPE=release`
5. `backend/build/build.rs` 将这些值注入二进制。
6. 后端 `/api/admin/system/version` 和 `/api/admin/system/check-updates` 使用二进制内的版本值。
7. 前端只展示后端返回的数据,不做版本映射。
需要同步修改:
- `.github/workflows/release.yml`
- `deploy/Dockerfile`
- `backend/build/build.rs`
- `backend/src/admin/system/routes.rs`
- `backend/tests/admin/system/mod.rs`
## Dockerfile 调整
目标 Dockerfile 阶段:
```dockerfile
FROM node:24-bookworm-slim AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY frontend ./
RUN pnpm build
FROM rust:1.95-bookworm AS backend-builder
WORKDIR /app/backend
COPY backend/Cargo.toml backend/Cargo.lock ./
COPY backend/build ./build
COPY backend/src ./src
COPY --from=frontend-builder /app/frontend/dist ./web/dist
RUN cargo build --release --locked --bin codex-proxy-rs
FROM debian:bookworm-slim AS runtime
WORKDIR /app
ENV CPR_DEPLOYMENT_MODE=docker
COPY --from=backend-builder /app/backend/target/release/codex-proxy-rs /usr/local/bin/codex-proxy-rs
COPY --from=frontend-builder /app/frontend/dist ./web/dist
CMD ["codex-proxy-rs"]
```
注意:
- Docker runtime 镜像不设置 `CPR_VERSION`,避免运行时环境变量成为第二版本源。
- Docker runtime 仍只包含主服务,不恢复 updater sidecar。
- `deploy/Dockerfile` 的构建上下文应保持仓库根目录。
## 迁移记录
本次迁移执行项:
1. 新建 `backend/`,用 `git mv` 移动 `src/`、`tests/`、`Cargo.toml`、`Cargo.lock`、`rust-toolchain.toml`,并将 `build.rs`、`VERSION` 归入 `backend/build/`。
2. 新建 `frontend/`,用 `git mv web frontend`。
3. 新建 `deploy/`,用 `git mv Dockerfile deploy/Dockerfile`、`git mv docker-compose.yml deploy/docker-compose.yml`,并移动部署样例配置。
4. 修改 `.github/workflows/release.yml` 的工作目录、构建路径、打包路径和 Dockerfile 路径。
5. 修改 `deploy/Dockerfile` 的 `COPY` 路径。
6. 修改 `.dockerignore` 和 `.gitignore`,使用 `frontend/node_modules`、`frontend/dist`、`backend/target`。
7. 修改文档和 README 中的开发、测试、构建命令。
## 验收命令
迁移完成后至少运行:
```bash
cargo test --manifest-path backend/Cargo.toml --test main admin::system --locked
cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
pnpm --dir frontend build
docker build -f deploy/Dockerfile -t codex-proxy-rs:latest .
docker compose -f deploy/docker-compose.yml config
git diff --check
```
如果保留 root workspace,则还需要确认:
```bash
cargo metadata --format-version 1
```
输出中不能重新出现旧的 updater binary,也不能把根 `VERSION` 当成版本源。
## 完成标准
- 顶层不再有 `src/` 和 `web/`。
- 后端源码、测试和版本源都归属 `backend/`。
- 前端应用归属 `frontend/`。
- Dockerfile、Compose 和部署样例归属 `deploy/`。
- `backend/build/VERSION` 是唯一源码默认版本文件。
- 发布 tag、release asset、Docker image label、后端 `/version`、前端更新弹窗显示同一个版本。
- Docker 部署仍是单主服务模式,不恢复 updater sidecar。
@@ -14,11 +14,11 @@
## 仓库结构
- `backend/`:Rust 1.95 Axum 后端,包含 SQLite 存储、代理调度、管理端 API、web 静态资源托管、在线更新逻辑。
- `backend/build/`:构建脚本和 `VERSION`;release workflow 会写入 `backend/build/VERSION`。
- `backend/build/`:Cargo 构建脚本,负责把版本、Git SHA、构建时间写入编译期环境变量。
- `backend/tests/`:集成测试和 fixtures。测试代码只放这里,禁止放进 `backend/src`。
- `frontend/`:Vue 3 管理端 SPA,使用 Vite 8、Tailwind v4、Pinia、Vue Router、Axios、lucide icons、ECharts。
- `deploy/`:Dockerfile、Dockerfile 专用 ignore 文件、Compose 文件、`config.example.yaml`。
- `docs/`:只在用户要求时放设计说明或迁移文档。
- `docs/`:只放长期维护文档,例如架构说明;不要保留过程型迁移文档作为开发约束。
- `skills/`:项目本地 Codex skills。
## 后端规范
@@ -89,13 +89,15 @@ docker compose -f deploy/docker-compose.yml up -d
- Release workflow:`.github/workflows/release.yml`。
- `v*` tag 触发发布。
- release 时版本来源是去掉 `v` 前缀的 Git tag;workflow 会写入 `backend/build/VERSION`。
- release 时版本来源是去掉 `v` 前缀的 Git tag;workflow 通过 `CPR_VERSION` build arg 写入编译期元数据。
- 源码默认版本使用 `backend/Cargo.toml` 的 `[package].version`;不要重新引入独立 `VERSION` 文件。
- 运行时版本接口展示编译期元数据:`CPR_VERSION`、`CPR_GIT_SHA`、`CPR_BUILD_TIME`。
- 当前发布的 Docker 平台是 `linux/amd64`,因为 release asset 目前只有 `linux_x86_64`。
- 当前发布的 Docker 镜像平台是 `linux/amd64` 和 `linux/arm64`。
- Release asset 命名必须匹配在线更新选择器:
```text
codex-proxy-rs_<version>_linux_x86_64.tar.gz
codex-proxy-rs_<version>_linux_amd64.tar.gz
codex-proxy-rs_<version>_linux_arm64.tar.gz
checksums.txt
```
@@ -173,4 +175,4 @@ docker compose -f deploy/docker-compose.yml config
- README、docs、发布和更新说明要和真实命令、真实行为一致。
- UI 保持当前安静、偏运维控制台的风格;避免营销式布局、卡片套卡片、图标过大、信息重复。
- 版本统一从发布 tag 到编译期元数据再到前端显示;不要引入第二个运行时版本源。
- 历史迁移文档可能包含阶段性方案;实现和 README、workflow、Dockerfile 不一致时,以当前代码和真实命令为准。
- 架构说明、README、workflow、Dockerfile 需要和当前代码和真实命令保持一致。