diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 9588c304..f005608e 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -1,4 +1,4 @@ -name: Auto Increment Version and Tag +name: Semantic Release Version and Tag on: workflow_run: @@ -9,28 +9,17 @@ on: - main workflow_dispatch: concurrency: - group: auto-tag-main + group: semantic-release-main cancel-in-progress: false jobs: - auto-tag: + dry-run: if: > - ( - github.event_name == 'workflow_run' && - github.event.workflow_run.conclusion == 'success' && - contains(github.event.workflow_run.head_commit.message, '[release ci]') - ) - || - ( - github.event_name == 'workflow_dispatch' && - github.ref == 'refs/heads/main' - ) - + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: - contents: write - outputs: - tagged: ${{ steps.create_tag.outcome == 'success' }} + contents: read steps: - name: Checkout code uses: actions/checkout@v6 @@ -38,29 +27,64 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Get next version - id: version - run: | - LATEST_TAG=$(git tag --list "v*" --sort=-v:refname | head -n 1) - LATEST_TAG=${LATEST_TAG:-v0.0.0} - VERSION=${LATEST_TAG#v} - IFS=. read MAJOR MINOR PATCH <<< "$VERSION" - PATCH=$((PATCH+1)) - echo "new_tag=v$MAJOR.$MINOR.$PATCH" >> $GITHUB_OUTPUT - - - name: Create tag + # 手动触发仅用于验证 semantic-release 的版本推导结果,不会真正推送 tag。 + - name: Semantic release dry-run + id: semantic_release + uses: cycjimmy/semantic-release-action@v6 + with: + dry_run: true + ci: false env: - PAT: ${{ secrets.PAT_TOKEN }} - TAG: ${{ steps.version.outputs.new_tag }} + GITHUB_TOKEN: ${{ github.token }} + + - name: Show dry-run result run: | - set -e - git config user.name "GitHub Action" - git config user.email "action@github.com" + echo "published=${{ steps.semantic_release.outputs.new_release_published }}" + echo "last_tag=${{ steps.semantic_release.outputs.last_release_git_tag }}" + echo "next_version=${{ steps.semantic_release.outputs.new_release_version }}" + echo "next_tag=${{ steps.semantic_release.outputs.new_release_git_tag }}" - if git show-ref --tags --verify --quiet "refs/tags/$TAG"; then - echo "Tag $TAG already exists, skipping" - exit 0 + release-tag: + if: > + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' && + contains(github.event.workflow_run.head_commit.message, '[release ci]') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.workflow_run.head_branch }} + + - name: Validate PAT token + run: | + if [ -z "${PAT_TOKEN}" ]; then + echo "::error::PAT_TOKEN is required because a tag created with GITHUB_TOKEN will not trigger publish.yml." + exit 1 fi + env: + PAT_TOKEN: ${{ secrets.PAT_TOKEN }} - git tag -a "$TAG" -m "Auto tag $TAG" - git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG" + # 真实 workflow_run 负责按 Conventional Commits 计算版本并推送 tag。 + - name: Semantic release + id: semantic_release + uses: cycjimmy/semantic-release-action@v6 + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + + - name: Show release result + env: + PUBLISHED: ${{ steps.semantic_release.outputs.new_release_published }} + LAST_TAG: ${{ steps.semantic_release.outputs.last_release_git_tag }} + NEXT_VERSION: ${{ steps.semantic_release.outputs.new_release_version }} + NEXT_TAG: ${{ steps.semantic_release.outputs.new_release_git_tag }} + run: | + echo "published=${PUBLISHED}" + echo "last_tag=${LAST_TAG}" + echo "next_version=${NEXT_VERSION}" + echo "next_tag=${NEXT_TAG}" diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 00000000..cf7e1a2c --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,72 @@ +{ + "branches": [ + "main" + ], + "tagFormat": "v${version}", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "releaseRules": [ + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "perf", + "release": "patch" + }, + { + "type": "refactor", + "release": "patch" + }, + { + "type": "docs", + "release": false + }, + { + "type": "test", + "release": false + }, + { + "type": "chore", + "release": false + }, + { + "type": "build", + "release": false + }, + { + "type": "ci", + "release": false + }, + { + "type": "style", + "release": false + } + ], + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES" + ] + } + } + ], + [ + "@semantic-release/release-notes-generator", + { + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES" + ] + } + } + ] + ] +} diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md index f5efae2b..9ead4d21 100644 --- a/ai-plan/public/README.md +++ b/ai-plan/public/README.md @@ -43,6 +43,10 @@ help the current worktree land on the right recovery documents without scanning - Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog. - Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md` - Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md` +- `semantic-release-versioning` + - Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow. + - Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md` + - Trace: `ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md` ## Worktree To Active Topic Map @@ -63,6 +67,9 @@ help the current worktree land on the right recovery documents without scanning - Branch: `feat/data-repository-persistence` - Worktree hint: `GFramework-data-repository-persistence` - Priority 1: `data-repository-persistence` +- Branch: `feat/semantic-release-versioning` + - Worktree hint: `GFramework` + - Priority 1: `semantic-release-versioning` - Branch: `docs/sdk-update-documentation` - Worktree hint: `GFramework-update-documentation` - Priority 1: `documentation-full-coverage-governance` diff --git a/ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md b/ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md new file mode 100644 index 00000000..51f52ee3 --- /dev/null +++ b/ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md @@ -0,0 +1,67 @@ +# Semantic Release 版本迁移跟踪 + +## 目标 + +将版本管理从固定 `patch + 1` 的自动打 tag 迁移到 `semantic-release`,同时保留现有 `.github/workflows/publish.yml` +的 tag 触发打包、NuGet 发布、GitHub Packages 发布和 GitHub Release 流程。 + +- 用 `cycjimmy/semantic-release-action` 替换 `auto-tag.yml` 的版本判断和打 tag 逻辑 +- 保留 `publish.yml` 的现有发布实现,不重写 NuGet 流程 +- 避免 `semantic-release` 与 `publish.yml` 重复创建 GitHub Release +- 将版本规则固定为 `feat -> minor`、`fix/perf/refactor -> patch`、`BREAKING CHANGE` 或 `! -> major` +- 为手动 `workflow_dispatch` 保留 dry-run 验证入口,先验证最近提交会算出什么版本 + +## 当前恢复点 + +- 恢复点编号:`SEMREL-RP-001` +- 当前阶段:`Phase 1` +- 当前焦点: + - 增加 `.releaserc.json`,仅启用版本分析与 release notes 生成,不启用 GitHub Release 发布插件 + - 将 `auto-tag.yml` 改成 `workflow_run` 真正打 tag、`workflow_dispatch` 只做 dry-run 的双入口 + - 明确 `PAT_TOKEN` 与 `GITHUB_TOKEN` 的职责边界,确保 tag 继续触发 `publish.yml` + +### 已知风险 + +- `GITHUB_TOKEN` 推送 tag 不会再触发另一个 workflow,真实发布仍需要 `PAT_TOKEN` +- `semantic-release` 的版本判断完全依赖 Conventional Commits;不规范提交会直接影响版本计算 +- 当前仓库本地 `dotnet clean/build` 仍受 WSL fallback NuGet 路径影响,验证时需要继续采用已知可用的直接构建命令 + +## 已完成 + +- 已确认当前版本入口为 `.github/workflows/auto-tag.yml`,现状始终执行 `PATCH + 1` +- 已确认当前 `.github/workflows/publish.yml` 由 tag 触发,并负责 `.nupkg` 打包、发布和 GitHub Release +- 已确认最新 tag 为 `v0.0.222` +- 已确认 `v0.0.222..HEAD` 之间存在 `feat(...)` 提交,按目标规则首次 dry-run 预期版本应为 `v0.1.0` +- 已新增 `.releaserc.json`,仅保留 `@semantic-release/commit-analyzer` 与 + `@semantic-release/release-notes-generator`,避免 `semantic-release` 直接创建 GitHub Release +- 已将 `.github/workflows/auto-tag.yml` 重写为: + - `workflow_run` 在 `main` 上、CI 成功且提交消息包含 `[release ci]` 时执行真实打 tag + - `workflow_dispatch` 只执行 dry-run,输出 `last_tag`、`next_version` 与 `next_tag` +- 已明确真实打 tag 仍使用 `PAT_TOKEN`,因为 `GITHUB_TOKEN` 推送的 tag 不会继续触发 `publish.yml` + +## 验证 + +- `git describe --tags --abbrev=0` + - 结果:通过 + - 备注:当前最新 tag 为 `v0.0.222` +- `git log --pretty=format:%h%x09%s v0.0.222..HEAD` + - 结果:通过 + - 备注:最近版本窗口内存在多条 `feat(...)`,后续 dry-run 预期应提升 `minor` +- `dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=` + - 结果:通过 + - 备注:`GFramework.Core.Abstractions` 与 `GFramework.Cqrs.Abstractions` Release 构建通过,`0 warning / 0 error` +- `npx --yes semantic-release --dry-run --no-ci` + - 结果:受阻 + - 备注:当前工作树的本地 tag 历史在 `git fetch --tags` 阶段出现 `would clobber existing tag` 冲突,不能直接作为 dry-run 环境 +- `git clone --branch main --single-branch git@github.com:GeWuYou/GFramework.git /tmp/gframework-semrel-dryrun` + - 结果:通过 + - 备注:已建立干净临时克隆用于 dry-run 验证 +- `npx --yes semantic-release --dry-run --no-ci`(在 `/tmp/gframework-semrel-dryrun`) + - 结果:通过 + - 备注:dry-run 成功识别 `v0.0.222` 为最新 release,并分析 `269` 个提交;按当前规则会提升到下一次 `minor` 发布,预期 tag 为 `v0.1.0` + +## 下一步 + +1. 复核 `workflow_dispatch` dry-run 输出格式是否还需要额外收窄或增加说明 +2. 评估是否要把 `workflow_run` 的 `[release ci]` 门闸改成更显式的 PR label 或 manual approval +3. 若本轮验证通过,按仓库要求创建提交并等待你审阅发版流程细节 diff --git a/ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md b/ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md new file mode 100644 index 00000000..518be71b --- /dev/null +++ b/ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md @@ -0,0 +1,39 @@ +# Semantic Release 版本迁移追踪 + +## 2026-04-26 + +### 阶段:方案落地准备(SEMREL-RP-001) + +- 读取当前 `auto-tag.yml` 与 `publish.yml`,确认最小侵入改法应只替换版本判断与打 tag,保留 tag 触发发布链 +- 核对最近 tag 与提交历史: + - 最新 tag 为 `v0.0.222` + - `v0.0.222..HEAD` 含多条 `feat(...)`,按目标规则首次 dry-run 预期结果为 `v0.1.0` +- 补建本主题的 active tracking / trace 入口,并在 `ai-plan/public/README.md` 中为 + `feat/semantic-release-versioning` 建立 worktree 映射 + +### 阶段:配置落地与验证(SEMREL-RP-001) + +- 新增 `.releaserc.json`,显式固定: + - `feat -> minor` + - `fix/perf/refactor -> patch` + - `docs/test/chore/build/ci/style -> no release` + - `BREAKING CHANGE` / `BREAKING CHANGES` 作为 major 信号 +- 重写 `auto-tag.yml`: + - 保留 `workflow_run` 监听 `CI - Build & Test` + - `workflow_dispatch` 变为 dry-run 入口 + - 真实打 tag 改由 `semantic-release-action` 处理,并要求 `PAT_TOKEN` +- 完成最小构建验证: + - `dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=` + - 结果:通过,`0 warning / 0 error` +- 直接在当前工作树执行 `semantic-release --dry-run` 时命中本地 tag 抓取冲突: + - `git fetch --tags ... would clobber existing tag` + - 结论:当前工作树不适合作为 dry-run 验证环境 +- 改用干净临时克隆 `/tmp/gframework-semrel-dryrun` 再跑 dry-run: + - 成功识别 `v0.0.222` 为最新 release + - 成功分析 `269` 个提交 + - 按当前规则得出下一次应为 `minor` 发布,预期版本窗口从 `0.0.222` 提升到 `0.1.0` + +### 下一步 + +1. 复核变更 diff 并创建提交 +2. 向用户说明新的发版链路与可优化点