diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..19a44b9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml deleted file mode 100644 index 73754da..0000000 --- a/.github/workflows/publish-nuget.yml +++ /dev/null @@ -1,60 +0,0 @@ -# 打标签 v* 时:构建 + 打包,通过 OIDC Trusted Publishing 推送到 nuget.org。 -# -# 前置(一次性,浏览器操作): -# 1. 在 nuget.org 账户的 Manage packages → Trusted publishers 页面, -# 把本仓库注册为 trusted publisher,关联 OIDC document。 -# 2. 在 GitHub 仓库 Settings → Secrets and variables → Actions → Variables -# 添加变量 NUGET_USER = 你的 nuget.org 用户名。 -# (参考:https://learn.microsoft.com/en-us/nuget/quickstart/trusted-publishing) -name: publish-nuget - -on: - push: - tags: - - "v*" - workflow_dispatch: - -permissions: - id-token: write - contents: read - -jobs: - publish: - 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: Pack - run: | - 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: Debug NUGET_USER - run: | - echo "NUGET_USER length: ${#NUGET_USER}" - if [ -z "${NUGET_USER}" ]; then - echo "::error::NUGET_USER is EMPTY. Add it in Settings → Secrets and variables → Actions → Variables (not Secrets)." - fi - env: - NUGET_USER: ${{ vars.NUGET_USER }} - - - name: NuGet Login (OIDC) - id: login - uses: NuGet/login@v1 - with: - user: ${{ vars.NUGET_USER }} - - - name: Push to nuget.org - run: | - dotnet nuget push "artifacts/*.nupkg" \ - --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \ - --source https://www.nuget.org/api/v2/package \ - --skip-duplicate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8182f07..c5dbd9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: @@ -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 '(?<=)[^<]+' "$proj") + if [ "$csproj_version" != "$version" ]; then + echo "::error::$proj 的 =$csproj_version 与 tag v$version 不一致,请先同步版本号再打 tag。" + exit 1 + fi + done + - name: Build run: dotnet build StreamFrame.slnx -c Release @@ -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 } @@ -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 }} @@ -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 }} @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a1f77..25c6dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 的 `` 一致性在流水线中显式校验,不一致直接失败(此前会静默发出错误版本号的包)。 +- 明确 `Start(ct)` 的取消令牌语义(仅约束建立连接阶段):接口 XML 注释与 README 同步说明。 + +### 新增 +- LICENSE 文件(MIT 此前只在 README 与包元数据中声明,仓库内无正文)。 +- push / PR 持续集成 `ci.yml`(构建 + 测试),回归不再等到发版才暴露。 ## [1.2.0] - 2026-08-27 @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20fec01 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 9e16ed8..555d37f 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ public interface ICodec - **客户端/服务端双模式**:`isActive: true` 主动连远端,`false` 被动监听 - **自动重连**:`Connecting → Connected → Retry` 状态机;`GetMessages` 是跨重连的稳定消息流——断线重连后已收消息不丢、枚举不中断 +- **启动与停止**:`Start(ct)` 的 `ct` 只约束建立连接阶段(连接/监听重试到它取消为止);连接建立后的收发与自动重连由连接自身管理,取消它不影响已建立的连接——要停止一切请 `DisposeAsync` - **健壮性**:帧内容解码失败、未完成帧超限、发送失败、接收空闲超时都会判定会话失效并自动重建(不再产生"连接看似存活、消息静默消失"的假活) - **活性探测(可选)**:TCP KeepAlive 与接收空闲超时,兜底半开连接(对端断电/拔线) - **事件**:`ConnectionChanged` 状态变化、`FrameError` 帧层诊断、`RawBytesReceived/Sent` 原始字节(HEX 调试) diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 07f6df2..4d3714d 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -1,6 +1,6 @@ # 发布指南 -StreamFrame 的发布流程:打 `v*` 标签 → GitHub Actions 自动构建、建 GitHub Release、推 nuget.org。 +StreamFrame 的发布流程:打 `v*` 标签 → GitHub Actions **单条流水线**(`release.yml`)自动校验版本、构建、测试、创建 GitHub Release、推 nuget.org。测试未通过不会发包。 ## 一次性的前置配置 @@ -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: @@ -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 的 `` 必须一致,否则直接失败 | +| 构建 + 测试 | `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 中 `` 与 git tag 需一致(如 `1.0.0` ↔ `v1.0.0`),下次发布手动同步。 +- csproj 中 `` 与 git tag 需一致(如 `1.0.0` ↔ `v1.0.0`),下次发布手动同步;流水线第一步强制校验。 - 两个库(`StreamFrame`、`StreamFrame.Protocols.Xml`)当前都发布为同一版本。 ## CHANGELOG 约定 diff --git a/src/StreamFrame/Connection/StreamConnection.cs b/src/StreamFrame/Connection/StreamConnection.cs index 0a1dc01..bf81b16 100644 --- a/src/StreamFrame/Connection/StreamConnection.cs +++ b/src/StreamFrame/Connection/StreamConnection.cs @@ -150,7 +150,17 @@ private async Task StartAsync(CancellationToken ct) private async Task 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; } diff --git a/src/StreamFrame/IStreamConnection.cs b/src/StreamFrame/IStreamConnection.cs index efedce9..d4fc894 100644 --- a/src/StreamFrame/IStreamConnection.cs +++ b/src/StreamFrame/IStreamConnection.cs @@ -54,7 +54,14 @@ public interface IStreamConnection : IAsyncDisposable /// Action>? RawBytesSent { get; set; } - /// 启动连接(主动连接/被动监听),异常时自动重试直到取消。 + /// + /// 启动连接(主动连接/被动监听),连接/监听失败时自动重试。 + /// + /// + /// 仅约束"建立连接"阶段:连接建立前取消它会停止连接/监听重试。 + /// 连接建立后的收发与断线自动重连由连接自身管理——取消该 token 不会断开已建立的 + /// 连接,也不会停止后续重连;要停止一切请调用 DisposeAsync。 + /// void Start(CancellationToken ct); /// 立即进入重连流程。