ci: add GitHub release workflow

This commit is contained in:
coolxll
2026-03-30 15:57:18 +08:00
parent 4ae1362527
commit 9b652fea67
4 changed files with 185 additions and 66 deletions

72
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag, for example v0.1.0"
required: true
permissions:
contents: write
jobs:
test:
name: Test
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run tests
run: go test ./...
build-release:
name: Build And Publish Release
runs-on: windows-latest
needs: test
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
ARCHIVE_NAME: lingma-ipc-proxy_${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}_windows_amd64.zip
CHECKSUM_NAME: lingma-ipc-proxy_${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}_sha256.txt
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build binary
shell: pwsh
run: .\scripts\build.ps1 -Clean
- name: Prepare release assets
shell: pwsh
run: |
$archivePath = Join-Path $PWD $env:ARCHIVE_NAME
$checksumPath = Join-Path $PWD $env:CHECKSUM_NAME
Compress-Archive -Path .\dist\lingma-ipc-proxy.exe -DestinationPath $archivePath -Force
$hash = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant()
"$hash $($env:ARCHIVE_NAME)" | Set-Content -NoNewline $checksumPath
- name: Create or update release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
generate_release_notes: true
files: |
${{ env.ARCHIVE_NAME }}
${{ env.CHECKSUM_NAME }}

View File

@@ -84,6 +84,27 @@ Default output:
dist\lingma-ipc-proxy.exe dist\lingma-ipc-proxy.exe
``` ```
## Release
GitHub Actions can publish a GitHub Release automatically.
Trigger rules:
- push a tag matching `v*`, for example `v0.1.0`
- or run the `Release` workflow manually and pass a tag
Example:
```powershell
git tag v0.1.0
git push origin v0.1.0
```
Release assets:
- `lingma-ipc-proxy_<tag>_windows_amd64.zip`
- `lingma-ipc-proxy_<tag>_sha256.txt`
Direct Go build command: Direct Go build command:
```powershell ```powershell

View File

@@ -84,6 +84,27 @@ cd C:\Workspace\Personal\lingma-ipc-proxy
dist\lingma-ipc-proxy.exe dist\lingma-ipc-proxy.exe
``` ```
## 发布
GitHub Actions 可以自动发布 GitHub Release。
触发方式:
- 推送匹配 `v*` 的 tag例如 `v0.1.0`
- 或手动运行 `Release` workflow并传入一个 tag
示例:
```powershell
git tag v0.1.0
git push origin v0.1.0
```
发布产物:
- `lingma-ipc-proxy_<tag>_windows_amd64.zip`
- `lingma-ipc-proxy_<tag>_sha256.txt`
等价的 Go 构建命令: 等价的 Go 构建命令:
```powershell ```powershell

View File

@@ -1,114 +1,119 @@
package httpapi package httpapi
import ( import "testing"
"strings"
"testing"
)
func TestNormalizeOpenAIRequestKeepsEmulationForToolHistoryWithoutTools(t *testing.T) { func TestNormalizeOpenAIRequestCollectsSystemMessages(t *testing.T) {
s := &Server{cfg: Config{EmulateToolCalls: true}}
req := openAIChatRequest{ req := openAIChatRequest{
Model: "test-model", Model: "test-model",
Messages: []rawMessage{ Messages: []rawMessage{
{ {Role: "system", Content: "You are concise."},
Role: "user", {Role: "user", Content: "Hello"},
Content: "Call ping once, then after the tool result reply FINAL_OK.", {Role: "assistant", Content: "Hi"},
}, {Role: "system", Content: "Answer in Chinese."},
{ {Role: "tool", Content: "ignored"},
Role: "assistant", {Role: "user", Content: []any{
Content: nil, map[string]any{"type": "text", "text": "Follow up"},
ToolCalls: []rawToolCall{ }},
{
ID: "call_1",
Type: "function",
Function: struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}{
Name: "ping",
Arguments: "{\"value\":\"x\"}",
},
},
},
},
{
Role: "tool",
ToolCallID: "call_1",
Content: "pong",
},
}, },
} }
normalized, err := s.normalizeOpenAIRequest(req) normalized, err := normalizeOpenAIRequest(req)
if err != nil { if err != nil {
t.Fatalf("normalizeOpenAIRequest() error = %v", err) t.Fatalf("normalizeOpenAIRequest() error = %v", err)
} }
if !normalized.Emulated { if normalized.Model != "test-model" {
t.Fatalf("expected emulation to stay enabled when tool history exists") t.Fatalf("model = %q", normalized.Model)
} }
if len(normalized.ChatRequest.Messages) != 3 { if normalized.System != "You are concise.\n\nAnswer in Chinese." {
t.Fatalf("message count = %d", len(normalized.ChatRequest.Messages)) t.Fatalf("system = %q", normalized.System)
} }
if normalized.ChatRequest.Messages[1].Role != "assistant" { if len(normalized.Messages) != 3 {
t.Fatalf("assistant message role = %q", normalized.ChatRequest.Messages[1].Role) t.Fatalf("message count = %d", len(normalized.Messages))
} }
if !strings.Contains(normalized.ChatRequest.Messages[1].Text, "json action") || !strings.Contains(normalized.ChatRequest.Messages[1].Text, "\"tool\": \"ping\"") { if normalized.Messages[0].Role != "user" || normalized.Messages[0].Text != "Hello" {
t.Fatalf("assistant tool call was not rewritten into action format: %q", normalized.ChatRequest.Messages[1].Text) t.Fatalf("first message = %+v", normalized.Messages[0])
} }
if normalized.ChatRequest.Messages[2].Role != "user" { if normalized.Messages[1].Role != "assistant" || normalized.Messages[1].Text != "Hi" {
t.Fatalf("tool result role = %q", normalized.ChatRequest.Messages[2].Role) t.Fatalf("second message = %+v", normalized.Messages[1])
} }
if !strings.Contains(normalized.ChatRequest.Messages[2].Text, "pong") { if normalized.Messages[2].Role != "user" || normalized.Messages[2].Text != "Follow up" {
t.Fatalf("tool result was not converted into a follow-up prompt: %q", normalized.ChatRequest.Messages[2].Text) t.Fatalf("third message = %+v", normalized.Messages[2])
} }
} }
func TestNormalizeAnthropicRequestKeepsEmulationForToolHistoryWithoutTools(t *testing.T) { func TestNormalizeOpenAIRequestRejectsMissingUserAndAssistantMessages(t *testing.T) {
s := &Server{cfg: Config{EmulateToolCalls: true}} req := openAIChatRequest{
req := anthropicRequest{
Model: "test-model", Model: "test-model",
Messages: []rawMessage{
{Role: "system", Content: "Only system"},
{Role: "tool", Content: "ignored"},
},
}
_, err := normalizeOpenAIRequest(req)
if err == nil {
t.Fatal("expected error for request without user or assistant messages")
}
}
func TestNormalizeAnthropicRequestExtractsStructuredText(t *testing.T) {
req := anthropicRequest{
Model: "test-model",
System: []any{map[string]any{"type": "text", "text": "System prompt"}},
Messages: []rawMessage{ Messages: []rawMessage{
{ {
Role: "user", Role: "user",
Content: []any{ Content: []any{
map[string]any{"type": "text", "text": "Use ping, then after the tool result reply FINAL_OK."}, map[string]any{"type": "text", "text": "Hello"},
}, },
}, },
{ {
Role: "assistant", Role: "assistant",
Content: []any{ Content: []any{
map[string]any{"type": "tool_use", "id": "call_1", "name": "ping", "input": map[string]any{"value": "x"}}, map[string]any{"type": "text", "text": "Hi"},
}, },
}, },
{ {
Role: "user", Role: "metadata",
Content: []any{ Content: []any{
map[string]any{"type": "tool_result", "tool_use_id": "call_1", "content": []any{map[string]any{"type": "text", "text": "pong"}}}, map[string]any{"type": "text", "text": "ignored"},
}, },
}, },
}, },
} }
normalized, err := s.normalizeAnthropicRequest(req) normalized, err := normalizeAnthropicRequest(req)
if err != nil { if err != nil {
t.Fatalf("normalizeAnthropicRequest() error = %v", err) t.Fatalf("normalizeAnthropicRequest() error = %v", err)
} }
if !normalized.Emulated { if normalized.Model != "test-model" {
t.Fatalf("expected emulation to stay enabled when anthropic tool history exists") t.Fatalf("model = %q", normalized.Model)
} }
if len(normalized.ChatRequest.Messages) != 3 { if normalized.System != "System prompt" {
t.Fatalf("message count = %d", len(normalized.ChatRequest.Messages)) t.Fatalf("system = %q", normalized.System)
} }
if normalized.ChatRequest.Messages[1].Role != "assistant" { if len(normalized.Messages) != 2 {
t.Fatalf("assistant message role = %q", normalized.ChatRequest.Messages[1].Role) t.Fatalf("message count = %d", len(normalized.Messages))
} }
if !strings.Contains(normalized.ChatRequest.Messages[1].Text, "json action") || !strings.Contains(normalized.ChatRequest.Messages[1].Text, "\"tool\": \"ping\"") { if normalized.Messages[0].Role != "user" || normalized.Messages[0].Text != "Hello" {
t.Fatalf("assistant tool_use was not rewritten into action format: %q", normalized.ChatRequest.Messages[1].Text) t.Fatalf("first message = %+v", normalized.Messages[0])
} }
if normalized.ChatRequest.Messages[2].Role != "user" { if normalized.Messages[1].Role != "assistant" || normalized.Messages[1].Text != "Hi" {
t.Fatalf("tool result role = %q", normalized.ChatRequest.Messages[2].Role) t.Fatalf("second message = %+v", normalized.Messages[1])
} }
if !strings.Contains(normalized.ChatRequest.Messages[2].Text, "pong") { }
t.Fatalf("tool result was not converted into a follow-up prompt: %q", normalized.ChatRequest.Messages[2].Text)
func TestNormalizeAnthropicRequestRejectsEmptyMessages(t *testing.T) {
req := anthropicRequest{
Model: "test-model",
Messages: []rawMessage{
{Role: "user", Content: ""},
{Role: "assistant", Content: nil},
},
}
_, err := normalizeAnthropicRequest(req)
if err == nil {
t.Fatal("expected error for request without usable messages")
} }
} }