ci: add GitHub release workflow
This commit is contained in:
72
.github/workflows/release.yml
vendored
Normal file
72
.github/workflows/release.yml
vendored
Normal 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 }}
|
||||||
21
README.md
21
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user