Skip to content

Commit d1e4e6e

Browse files
authored
Merge pull request #423 from pionxe/release/v2.0-split-build
feat(gateway): RFC#420 全量落地(双产物发布、统一启动内核、第三方接入文档与CI验收)
2 parents 7c51b80 + f32b04b commit d1e4e6e

25 files changed

Lines changed: 2983 additions & 431 deletions

.github/workflows/ci.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,92 @@ jobs:
3737
- name: Build
3838
run: go build ./...
3939

40+
- name: Gateway-only smoke
41+
shell: bash
42+
run: |
43+
set -euo pipefail
44+
45+
socket_path="/tmp/neocode-gateway-${RANDOM}.sock"
46+
http_port="$((18080 + RANDOM % 1000))"
47+
http_addr="127.0.0.1:${http_port}"
48+
gateway_bin="/tmp/neocode-gateway"
49+
gateway_log="/tmp/neocode-gateway.log"
50+
51+
go build -o "${gateway_bin}" ./cmd/neocode-gateway
52+
"${gateway_bin}" --listen "${socket_path}" --http-listen "${http_addr}" --log-level info >"${gateway_log}" 2>&1 &
53+
gateway_pid=$!
54+
55+
cleanup() {
56+
if kill -0 "${gateway_pid}" >/dev/null 2>&1; then
57+
kill "${gateway_pid}" || true
58+
wait "${gateway_pid}" || true
59+
fi
60+
rm -f "${socket_path}" "${gateway_bin}" /tmp/gateway-healthz.json /tmp/gateway-rpc.json
61+
}
62+
trap cleanup EXIT
63+
64+
for _ in $(seq 1 60); do
65+
if curl -fsS "http://${http_addr}/healthz" > /tmp/gateway-healthz.json; then
66+
break
67+
fi
68+
sleep 0.2
69+
done
70+
test -s /tmp/gateway-healthz.json
71+
72+
rpc_status="$(curl -sS -o /tmp/gateway-rpc.json -w "%{http_code}" \
73+
-X POST "http://${http_addr}/rpc" \
74+
-H "Content-Type: application/json" \
75+
-d '{"jsonrpc":"2.0","id":"smoke-1","method":"gateway.ping","params":{}}')"
76+
if [[ "${rpc_status}" != "401" ]]; then
77+
echo "unexpected /rpc status: ${rpc_status}" >&2
78+
cat /tmp/gateway-rpc.json >&2
79+
cat "${gateway_log}" >&2 || true
80+
exit 1
81+
fi
82+
grep -q '"gateway_code":"unauthorized"' /tmp/gateway-rpc.json
83+
4084
- name: Test with coverage
4185
run: go test ./... -covermode=atomic -coverprofile=coverage.out
4286

87+
- name: Install script dry-run regression (bash)
88+
shell: bash
89+
env:
90+
NEOCODE_INSTALL_LATEST_TAG: v0.0.0-test
91+
run: |
92+
set -euo pipefail
93+
94+
full_output="$(bash ./scripts/install.sh --flavor full --dry-run)"
95+
gateway_output="$(bash ./scripts/install.sh --flavor gateway --dry-run)"
96+
97+
echo "${full_output}" | grep -Eq '^asset=neocode_(Linux|Darwin)_(x86_64|arm64)\.tar\.gz$'
98+
echo "${gateway_output}" | grep -Eq '^asset=neocode-gateway_(Linux|Darwin)_(x86_64|arm64)\.tar\.gz$'
99+
echo "${full_output}" | grep -Eq '^download_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/neocode_(Linux|Darwin)_(x86_64|arm64)\.tar\.gz$'
100+
echo "${gateway_output}" | grep -Eq '^download_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/neocode-gateway_(Linux|Darwin)_(x86_64|arm64)\.tar\.gz$'
101+
echo "${full_output}" | grep -Eq '^checksum_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/checksums\.txt$'
102+
echo "${gateway_output}" | grep -Eq '^checksum_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/checksums\.txt$'
103+
104+
- name: Install script dry-run regression (PowerShell)
105+
shell: pwsh
106+
env:
107+
NEOCODE_INSTALL_LATEST_TAG: v0.0.0-test
108+
run: |
109+
$fullLines = & ./scripts/install.ps1 -Flavor full -DryRun
110+
$gatewayLines = & ./scripts/install.ps1 -Flavor gateway -DryRun
111+
112+
$fullAsset = ($fullLines | Where-Object { $_ -like 'asset=*' } | Select-Object -First 1)
113+
$gatewayAsset = ($gatewayLines | Where-Object { $_ -like 'asset=*' } | Select-Object -First 1)
114+
$fullDownload = ($fullLines | Where-Object { $_ -like 'download_url=*' } | Select-Object -First 1)
115+
$gatewayDownload = ($gatewayLines | Where-Object { $_ -like 'download_url=*' } | Select-Object -First 1)
116+
$fullChecksum = ($fullLines | Where-Object { $_ -like 'checksum_url=*' } | Select-Object -First 1)
117+
$gatewayChecksum = ($gatewayLines | Where-Object { $_ -like 'checksum_url=*' } | Select-Object -First 1)
118+
119+
if ($fullAsset -notmatch '^asset=neocode_Windows_(x86_64|arm64)\.zip$') { throw "Unexpected full asset line: $fullAsset" }
120+
if ($gatewayAsset -notmatch '^asset=neocode-gateway_Windows_(x86_64|arm64)\.zip$') { throw "Unexpected gateway asset line: $gatewayAsset" }
121+
if ($fullDownload -notmatch '^download_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/neocode_Windows_(x86_64|arm64)\.zip$') { throw "Unexpected full download URL: $fullDownload" }
122+
if ($gatewayDownload -notmatch '^download_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/neocode-gateway_Windows_(x86_64|arm64)\.zip$') { throw "Unexpected gateway download URL: $gatewayDownload" }
123+
if ($fullChecksum -notmatch '^checksum_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/checksums\.txt$') { throw "Unexpected full checksum URL: $fullChecksum" }
124+
if ($gatewayChecksum -notmatch '^checksum_url=https://github.com/1024XEngineer/neo-code/releases/download/.+/checksums\.txt$') { throw "Unexpected gateway checksum URL: $gatewayChecksum" }
125+
43126
- name: Upload coverage to Codecov
44127
continue-on-error: true
45128
uses: codecov/codecov-action@v5

.goreleaser.yaml

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
# .goreleaser.yaml
21
project_name: neocode
3-
version: 2 # 必须声明为 v2 语法
2+
version: 2
43

54
before:
65
hooks:
7-
# 每次构建前清理模块并下载依赖
86
- go mod tidy
97
- go mod download
108

119
builds:
12-
- env:
13-
- CGO_ENABLED=0 # 禁用 CGO,确保生成纯静态链接的二进制文件
10+
- id: neocode
11+
env:
12+
- CGO_ENABLED=0
1413
ldflags:
1514
- -s -w -X 'neo-code/internal/version.Version={{.Version}}'
1615
goos:
@@ -20,27 +19,57 @@ builds:
2019
goarch:
2120
- amd64
2221
- arm64
23-
# 指定 main.go 的路径(根据 NeoCode 的实际目录调整)
24-
main: ./cmd/neocode/main.go
25-
# 编译出的二进制文件名
22+
main: ./cmd/neocode/main.go
2623
binary: neocode
2724

25+
- id: neocode-gateway
26+
env:
27+
- CGO_ENABLED=0
28+
ldflags:
29+
- -s -w -X 'neo-code/internal/version.Version={{.Version}}'
30+
goos:
31+
- linux
32+
- windows
33+
- darwin
34+
goarch:
35+
- amd64
36+
- arm64
37+
main: ./cmd/neocode-gateway/main.go
38+
binary: neocode-gateway
39+
2840
archives:
29-
- format: tar.gz
30-
# 为 Windows 提供单独的 zip 格式
41+
- id: neocode
42+
ids:
43+
- neocode
44+
format: tar.gz
45+
format_overrides:
46+
- goos: windows
47+
format: zip
48+
name_template: >-
49+
neocode_
50+
{{- title .Os }}_
51+
{{- if eq .Arch "amd64" }}x86_64
52+
{{- else if eq .Arch "386" }}i386
53+
{{- else }}{{ .Arch }}{{ end }}
54+
{{- if .Arm }}v{{ .Arm }}{{ end }}
55+
56+
- id: neocode-gateway
57+
ids:
58+
- neocode-gateway
59+
format: tar.gz
3160
format_overrides:
3261
- goos: windows
3362
format: zip
3463
name_template: >-
35-
{{ .ProjectName }}_
64+
neocode-gateway_
3665
{{- title .Os }}_
3766
{{- if eq .Arch "amd64" }}x86_64
3867
{{- else if eq .Arch "386" }}i386
3968
{{- else }}{{ .Arch }}{{ end }}
4069
{{- if .Arm }}v{{ .Arm }}{{ end }}
4170
4271
checksum:
43-
name_template: 'checksums.txt'
72+
name_template: checksums.txt
4473

4574
changelog:
4675
sort: asc

Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
.PHONY: install-skills docs-gateway docs-gateway-check
22

3+
GATEWAY_DOCS_GENERATOR := go run -tags gatewaydocgen ./scripts/generate_gateway_rpc_examples.go
4+
35
install-skills:
46
@./scripts/install_skills.sh
57

68
docs-gateway:
7-
@go run ./scripts/generate_gateway_rpc_examples
9+
@$(GATEWAY_DOCS_GENERATOR)
810

911
docs-gateway-check:
10-
@go run ./scripts/generate_gateway_rpc_examples
11-
@git diff --exit-code -- docs/reference/gateway-rpc-api.md
12+
@$(GATEWAY_DOCS_GENERATOR)
13+
@go run ./scripts/check_gateway_docs
14+
@git diff --exit-code -- docs/generated/gateway-rpc-examples.json

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,20 @@ Skill 内部调用脚本 `scripts/create_issue.sh` 创建 issue。你也可以
274274
- `wake.openUrl`:处理 `neocode://` 唤醒请求
275275
- `gateway.event`:网关推送通知事件(notification)
276276

277+
## 双产物与启动兼容(RFC#420)
278+
279+
- 发布产物:
280+
- `neocode`(完整客户端,含 `gateway` 子命令)
281+
- `neocode-gateway`(Gateway-Only 入口)
282+
- `url-dispatch` 网关不可达时的拉起优先级固定为:
283+
- `NEOCODE_GATEWAY_BIN`
284+
- `PATH``neocode-gateway`
285+
- `neocode gateway`
286+
- 第三方接入与协议文档见:
287+
- [`docs/guides/gateway-integration-guide.md`](docs/guides/gateway-integration-guide.md)
288+
- [`docs/gateway-rpc-api.md`](docs/gateway-rpc-api.md)
289+
- [`docs/gateway-error-catalog.md`](docs/gateway-error-catalog.md)
290+
277291
## License
278292

279293
MIT

cmd/neocode-gateway/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"neo-code/internal/cli"
9+
)
10+
11+
func main() {
12+
if err := cli.ExecuteGatewayServer(context.Background(), os.Args[1:]); err != nil {
13+
fmt.Fprintf(os.Stderr, "neocode-gateway: %v\n", err)
14+
os.Exit(1)
15+
}
16+
}

cmd/neocode-gateway/main_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
"testing"
10+
)
11+
12+
func TestMainHelpPathDoesNotExit(t *testing.T) {
13+
originalArgs := os.Args
14+
defer func() {
15+
os.Args = originalArgs
16+
}()
17+
18+
os.Args = []string{"neocode-gateway", "--help"}
19+
main()
20+
}
21+
22+
func TestMainReturnsExitCodeOneOnCommandError(t *testing.T) {
23+
if os.Getenv("NEOCODE_GATEWAY_MAIN_HELPER") == "1" {
24+
os.Args = []string{"neocode-gateway", "--log-level", "trace"}
25+
main()
26+
return
27+
}
28+
29+
command := exec.Command(os.Args[0], "-test.run=TestMainReturnsExitCodeOneOnCommandError")
30+
command.Env = append(os.Environ(), "NEOCODE_GATEWAY_MAIN_HELPER=1")
31+
var stderr bytes.Buffer
32+
command.Stderr = &stderr
33+
34+
err := command.Run()
35+
if err == nil {
36+
t.Fatal("expected subprocess to exit with non-zero status")
37+
}
38+
var exitErr *exec.ExitError
39+
if !errors.As(err, &exitErr) {
40+
t.Fatalf("error type = %T, want *exec.ExitError", err)
41+
}
42+
if exitErr.ExitCode() != 1 {
43+
t.Fatalf("exit code = %d, want %d", exitErr.ExitCode(), 1)
44+
}
45+
if !strings.Contains(stderr.String(), "neocode-gateway:") {
46+
t.Fatalf("stderr = %q, want contains %q", stderr.String(), "neocode-gateway:")
47+
}
48+
}

docs/gateway-compatibility.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Gateway 兼容性与弃用策略
2+
3+
本文定义 Gateway 对外契约的版本兼容规则,适用于方法、字段、错误码与发布资产。
4+
5+
## 1. 兼容性分层
6+
7+
1. Stable(稳定层):默认向后兼容,不做破坏性改动。
8+
2. Experimental(实验层):允许演进,但必须有显式标注与迁移说明。
9+
10+
当前分层:
11+
12+
1. Stable Core:`gateway.authenticate``gateway.ping``gateway.bindStream``gateway.run``gateway.compact``gateway.cancel``gateway.listSessions``gateway.loadSession``gateway.resolvePermission``gateway.event`
13+
2. Experimental:`wake.openUrl`
14+
15+
## 2. 字段弃用生命周期(必须遵守)
16+
17+
### 2.1 标准流程
18+
19+
1. **v1.2 标记 Deprecated**
20+
字段继续可用;文档、日志、响应元信息中标记 `deprecated: true`(或等效说明)。
21+
2. **v1.3 兼容保留期**
22+
新客户端 SHOULD 停止依赖该字段;服务端保持兼容读取/写出策略。
23+
3. **v1.4 正式移除**
24+
字段从请求/响应契约中删除;若客户端仍发送,返回可诊断错误(通常 `invalid_frame``unsupported_action`,视场景而定)。
25+
26+
### 2.2 示例
27+
28+
若字段 `params.legacy_x` 计划移除:
29+
30+
1. v1.2:文档标记 Deprecated,并在 release notes 给迁移路径。
31+
2. v1.3:继续接受 `legacy_x`,但服务端优先使用新字段。
32+
3. v1.4:拒绝 `legacy_x`,返回明确错误与替代字段提示。
33+
34+
## 3. 破坏性变更门禁
35+
36+
以下变更 MUST 走 RFC 流程并通过灰度窗口:
37+
38+
1. 删除 Stable 方法。
39+
2. 修改 Stable 方法必填字段语义。
40+
3. 修改稳定 `gateway_code` 含义。
41+
4. 改变资产命名规则(下载 URL / checksum 路径)。
42+
43+
## 4. 双产物发布兼容承诺
44+
45+
1. `neocode`:保留现有主入口行为。
46+
2. `neocode-gateway`:仅承载网关服务语义。
47+
3. 同参条件下,`neocode gateway``neocode-gateway` MUST 行为等价(参数归一化、错误语义、关键日志字段)。
48+
49+
## 5. 回滚原则
50+
51+
1. 升级失败时 SHOULD 先回滚二进制版本,再恢复配置。
52+
2. 回滚版本 MUST 与当前稳定协议兼容(至少同主版本)。
53+
3. 回滚步骤必须在发布说明中提供可执行命令与验证点(`/healthz``/rpc` 最小请求)。

0 commit comments

Comments
 (0)