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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 日常 push / PR 的构建与测试门禁;发布流水线见 release.yml。
name: ci

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

- name: Build
run: dotnet build StreamFrame.slnx -c Release

- name: Test
run: dotnet test StreamFrame.slnx -c Release --no-build
60 changes: 0 additions & 60 deletions .github/workflows/publish-nuget.yml

This file was deleted.

59 changes: 50 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# 打标签 v* 时:构建 + 测试 + 打包,创建 GitHub Release 并附 nupkg/snupkg。
# Release 正文优先从 CHANGELOG.md 提取当前版本段落;找不到时回退到 GitHub 自动生成。
# 打标签 v* 时的一条完整发布流水线:
# 版本号校验 → 构建 → 测试 → GitHub Release → 推 nuget.org
# 推 NuGet 是最后一步且不可撤回,此前任何一步(尤其测试)失败都不会发包。
# workflow_dispatch 手动触发仅做构建+测试演练,不创建 Release、不发包;
# 需要重发某个版本时,在对应 tag 的运行记录上 Re-run 即可(推送带 --skip-duplicate,幂等)。
name: release

on:
push:
tags:
- "v*"
workflow_dispatch:

permissions:
contents: write
contents: write # 创建 GitHub Release
id-token: write # OIDC Trusted Publishing 换取 nuget.org 短时 API key

jobs:
release:
Expand All @@ -23,6 +28,18 @@ jobs:
with:
dotnet-version: "8.0.x"

- name: Verify tag matches csproj versions
if: startsWith(github.ref, 'refs/tags/')
run: |
version="${GITHUB_REF_NAME#v}"
for proj in src/StreamFrame/StreamFrame.csproj src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj; do
csproj_version=$(grep -oPm1 '(?<=<Version>)[^<]+' "$proj")
if [ "$csproj_version" != "$version" ]; then
echo "::error::$proj 的 <Version>=$csproj_version 与 tag v$version 不一致,请先同步版本号再打 tag。"
exit 1
fi
done

- name: Build
run: dotnet build StreamFrame.slnx -c Release

Expand All @@ -34,14 +51,13 @@ jobs:
dotnet pack src/StreamFrame/StreamFrame.csproj -c Release --no-build -o artifacts
dotnet pack src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj -c Release --no-build -o artifacts

- name: Extract tag version
id: tag
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# ---------- 以下步骤仅在 tag 推送时执行 ----------

- name: Extract changelog section for this version
if: startsWith(github.ref, 'refs/tags/')
id: notes
run: |
version="${{ steps.tag.outputs.version }}"
version="${GITHUB_REF_NAME#v}"
# 用 index() 固定字符串匹配版本头,避开正则转义;遇到下一个 "## [" 停止。
awk -v header="## [$version]" '
found == 0 && index($0, header) == 1 { found = 1; next }
Expand All @@ -55,7 +71,7 @@ jobs:
fi

- name: Create Release (from CHANGELOG)
if: steps.notes.outputs.has_notes == 'true'
if: startsWith(github.ref, 'refs/tags/') && steps.notes.outputs.has_notes == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
Expand All @@ -66,7 +82,7 @@ jobs:
artifacts/*.snupkg

- name: Create Release (auto-generated notes)
if: steps.notes.outputs.has_notes != 'true'
if: startsWith(github.ref, 'refs/tags/') && steps.notes.outputs.has_notes != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
Expand All @@ -75,3 +91,28 @@ jobs:
files: |
artifacts/*.nupkg
artifacts/*.snupkg

- name: Check NUGET_USER variable
if: startsWith(github.ref, 'refs/tags/')
run: |
if [ -z "${NUGET_USER}" ]; then
echo "::error::NUGET_USER 为空。Settings → Secrets and variables → Actions → Variables 添加 NUGET_USER(nuget.org 用户名,注意是 Variables 不是 Secrets)。"
exit 1
fi
env:
NUGET_USER: ${{ vars.NUGET_USER }}

- name: NuGet Login (OIDC)
if: startsWith(github.ref, 'refs/tags/')
id: login
uses: NuGet/login@v1
with:
user: ${{ vars.NUGET_USER }}

- name: Push to nuget.org
if: startsWith(github.ref, 'refs/tags/')
run: |
dotnet nuget push "artifacts/*.nupkg" \
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \
--source https://www.nuget.org/api/v2/package \
--skip-duplicate
18 changes: 13 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

## [Unreleased]

### 新增
- 暂无

### 修复
- 暂无
- 主动模式连接失败重试不再泄漏 socket:`ConnectAsync` 失败/取消时立即释放本次尝试创建的 socket(此前每次失败重试泄漏一个,等终结器回收)。
- CHANGELOG 底部版本对比链接补齐 `[1.2.0]` 并修正 `[Unreleased]` 指向(v1.2.0 发版时漏更新)。

### 变更
- 发布管线合并为单条流水线 `release.yml`:tag 触发"版本号校验 → 构建 → 测试 → GitHub Release → 推 nuget.org",测试未通过不会发包(此前发布与推送是两条独立 workflow,测试失败时包仍会被推出)。`workflow_dispatch` 手动触发仅做构建+测试演练,不发布;需重发版本时在对应 tag 的运行记录上 Re-run(推送带 `--skip-duplicate`,幂等)。
- tag 与两个 csproj 的 `<Version>` 一致性在流水线中显式校验,不一致直接失败(此前会静默发出错误版本号的包)。
- 明确 `Start(ct)` 的取消令牌语义(仅约束建立连接阶段):接口 XML 注释与 README 同步说明。

### 新增
- LICENSE 文件(MIT 此前只在 README 与包元数据中声明,仓库内无正文)。
- push / PR 持续集成 `ci.yml`(构建 + 测试),回归不再等到发版才暴露。

## [1.2.0] - 2026-08-27

Expand Down Expand Up @@ -62,7 +69,8 @@
- 发布自动化:`release.yml`(tag 自动建 GitHub Release)、`publish-nuget.yml`(OIDC Trusted Publishing 推送 nuget.org)。
- 单元测试 25 个 + 三场景端到端 demo。

[Unreleased]: https://github.com/CSJ608/StreamFrame/compare/v1.1.0...HEAD
[Unreleased]: https://github.com/CSJ608/StreamFrame/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/CSJ608/StreamFrame/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/CSJ608/StreamFrame/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/CSJ608/StreamFrame/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/CSJ608/StreamFrame/releases/tag/v1.0.0
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Marci

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public interface ICodec<TMessage>

- **客户端/服务端双模式**:`isActive: true` 主动连远端,`false` 被动监听
- **自动重连**:`Connecting → Connected → Retry` 状态机;`GetMessages` 是跨重连的稳定消息流——断线重连后已收消息不丢、枚举不中断
- **启动与停止**:`Start(ct)` 的 `ct` 只约束建立连接阶段(连接/监听重试到它取消为止);连接建立后的收发与自动重连由连接自身管理,取消它不影响已建立的连接——要停止一切请 `DisposeAsync`
- **健壮性**:帧内容解码失败、未完成帧超限、发送失败、接收空闲超时都会判定会话失效并自动重建(不再产生"连接看似存活、消息静默消失"的假活)
- **活性探测(可选)**:TCP KeepAlive 与接收空闲超时,兜底半开连接(对端断电/拔线)
- **事件**:`ConnectionChanged` 状态变化、`FrameError` 帧层诊断、`RawBytesReceived/Sent` 原始字节(HEX 调试)
Expand Down
27 changes: 16 additions & 11 deletions docs/PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 发布指南

StreamFrame 的发布流程:打 `v*` 标签 → GitHub Actions 自动构建、建 GitHub Release、推 nuget.org。
StreamFrame 的发布流程:打 `v*` 标签 → GitHub Actions **单条流水线**(`release.yml`)自动校验版本、构建、测试、创建 GitHub Release、推 nuget.org。测试未通过不会发包。

## 一次性的前置配置

Expand All @@ -11,7 +11,7 @@ StreamFrame 的发布流程:打 `v*` 标签 → GitHub Actions 自动构建、
- **Environment**: 留空
- **Subject Identifier**: `repo:CSJ608/StreamFrame:ref:refs/tags/v*`

> 该 subject 仅信任以 `v` 开头的 tag 推送,防止任意分支推送覆盖包。若为个人账号,nuget.org 会要求验证仓库所有权。
> 该 subject 仅信任以 `v` 开头的 tag 推送,防止任意分支推送覆盖包。若为个人账号,nuget.org 会要求验证仓库所有权。subject 只绑定仓库与 tag ref,与 workflow 文件名无关——调整流水线结构不影响本配置。

### 2. GitHub 仓库变量 `NUGET_USER`(浏览器,一次)
仓库 → Settings → Secrets and variables → Actions → **Variables** → New repository variable:
Expand All @@ -24,25 +24,30 @@ StreamFrame 的发布流程:打 `v*` 标签 → GitHub Actions 自动构建、
# 1. 提交改动并推送
git add -A && git commit -m "..." && git push origin main

# 2. 打版本标签并推送 → 触发 release + publish-nuget 两个 workflow
# 2. 打版本标签并推送 → 触发 release.yml 单条流水线
git tag -a v1.0.1 -m "描述"
git push origin v1.0.1

# 3. 查看两个 workflow 结果
# 3. 查看流水线结果(发布与推送在同一条运行里)
gh run list --workflow release.yml
gh run list --workflow publish-nuget.yml
```

## 两个 workflow 各做什么
> 忘了同步 csproj 版本?流水线第一步就会红掉并提示哪个文件不一致,不会发出错误版本号的包。

| Workflow | 触发 | 作用 |
|---|---|---|
| `release.yml` | `push tag v*` | build + test + pack,创建 GitHub Release 并附 `*.nupkg` / `*.snupkg`;Release 正文从 CHANGELOG 提取当前版本段 |
| `publish-nuget.yml` | `push tag v*` | build + pack,`NuGet/login@v1` 用 OIDC 换短时 API key,推 nuget.org(`--skip-duplicate` 幂等) |
## 发布流水线(release.yml)各阶段做什么

| 阶段 | 说明 |
|---|---|
| 版本校验 | tag 与两个 csproj 的 `<Version>` 必须一致,否则直接失败 |
| 构建 + 测试 | `dotnet build` + `dotnet test`;测试不过,后面的 Release 与发包都不会执行 |
| GitHub Release | 正文从 CHANGELOG 提取当前版本段落(找不到时回退 GitHub 自动生成);附 `*.nupkg` / `*.snupkg` |
| 推 nuget.org | `NuGet/login@v1` 用 OIDC 换短时 API key,推送 `--skip-duplicate` 幂等 |

`workflow_dispatch` 手动触发只跑构建 + 测试(演练用),不创建 Release、不发包(OIDC subject 只信任 tag ref)。需要重发某个版本时,在对应 tag 的运行记录上 **Re-run** 即可。

## 版本号约定

- csproj 中 `<Version>` 与 git tag 需一致(如 `1.0.0` ↔ `v1.0.0`),下次发布手动同步。
- csproj 中 `<Version>` 与 git tag 需一致(如 `1.0.0` ↔ `v1.0.0`),下次发布手动同步;流水线第一步强制校验。
- 两个库(`StreamFrame`、`StreamFrame.Protocols.Xml`)当前都发布为同一版本。

## CHANGELOG 约定
Expand Down
12 changes: 11 additions & 1 deletion src/StreamFrame/Connection/StreamConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,17 @@ private async Task StartAsync(CancellationToken ct)
private async Task<Socket> ConnectAsync(CancellationToken ct)
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(IpAddress, Port, ct).ConfigureAwait(false);
try
{
await socket.ConnectAsync(IpAddress, Port, ct).ConfigureAwait(false);
}
catch
{
// 连接失败/取消同样释放本次尝试的 socket,否则失败重试一次泄漏一个
socket.Dispose();
throw;
}

ConfigureSocket(socket);
return socket;
}
Expand Down
9 changes: 8 additions & 1 deletion src/StreamFrame/IStreamConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ public interface IStreamConnection<TMessage> : IAsyncDisposable
/// </summary>
Action<ReadOnlyMemory<byte>>? RawBytesSent { get; set; }

/// <summary>启动连接(主动连接/被动监听),异常时自动重试直到取消。</summary>
/// <summary>
/// 启动连接(主动连接/被动监听),连接/监听失败时自动重试。
/// </summary>
/// <remarks>
/// <paramref name="ct"/> 仅约束"建立连接"阶段:连接建立前取消它会停止连接/监听重试。
/// 连接建立后的收发与断线自动重连由连接自身管理——取消该 token 不会断开已建立的
/// 连接,也不会停止后续重连;要停止一切请调用 DisposeAsync。
/// </remarks>
void Start(CancellationToken ct);

/// <summary>立即进入重连流程。</summary>
Expand Down
Loading