From dc185a7de7ee31580512e668fbd0bc0dcad85703 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 05:34:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(ci):=20Version=20PR=20=E6=9D=83?= =?UTF-8?q?=E9=99=90=E5=A4=B1=E8=B4=A5=20+=20bump=20@sfmc-bds/tools@0.2.0-?= =?UTF-8?q?beta.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changeset-release 在 version bump 后因仓库禁止 GITHUB_TOKEN 开 PR 失败。 改用 CHANGESETS_GITHUB_TOKEN(可选)并文档化 Actions 勾选要求; 同时落地 Version 内容(tools beta.1),并修正 listPendingChangesetFiles 在 pre mode 下误把已消费 .md 当待消费(LSP)。 Co-authored-by: Shiroha --- .changeset/pre.json | 8 +------ .github/workflows/changeset-release.yml | 7 +++++- docs/dev/npm-publish.md | 14 +++++++++++ tools/CHANGELOG.md | 31 +++++++++++++++---------- tools/changeset-release.test.mjs | 15 ++++++++++++ tools/lib/changeset-release.mjs | 10 +++++++- tools/package.json | 2 +- 7 files changed, 65 insertions(+), 22 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index c7a833b..8d2fdb7 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -13,12 +13,6 @@ "@sfmc-bds/tools": "0.1.1" }, "changesets": [ - "bds-tools-0-2-beta", - "cli-0-2-beta", - "db-server-0-2-beta", - "qq-bridge-0-1-1-beta", - "sdk-0-2-beta", - "sfmc-meta-0-2-beta", - "tools-0-2-beta" + "fix-release-tag-shallow" ] } diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml index c81f37b..b04e142 100644 --- a/.github/workflows/changeset-release.yml +++ b/.github/workflows/changeset-release.yml @@ -47,6 +47,11 @@ jobs: npm run build --workspace @sfmc-bds/tools --if-present npm run build --workspace @sfmc-bds/sfmc --if-present + # Version PR 需要「创建 PR」权限。若仅用 GITHUB_TOKEN 且仓库关闭 + # Settings→Actions→General→「Allow GitHub Actions to create and approve pull requests」, + # changesets/action 会以 HttpError 失败(分支已推送但 PR 未创建)。 + # 优先使用 PAT Secret CHANGESETS_GITHUB_TOKEN(repo 或 contents+pull_requests); + # 未配置时回退 GITHUB_TOKEN(需上述勾选为开)。 - name: Create Release Pull Request or Publish id: changesets uses: changesets/action@v1 @@ -59,6 +64,6 @@ jobs: commit: "chore(release): version packages" createGithubReleases: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CHANGESETS_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/docs/dev/npm-publish.md b/docs/dev/npm-publish.md index 9222b8d..b909634 100644 --- a/docs/dev/npm-publish.md +++ b/docs/dev/npm-publish.md @@ -39,6 +39,19 @@ 2. PR 合入 `main` 后,[changeset-release.yml](../../.github/workflows/changeset-release.yml) 会开/更新 **Version Packages** PR。 3. 维护者审查并合并 Version PR → CI 跑 `ci-release-packages`(publish + tag + GitHub Release;pre mode → npm **`beta`**)。 +### Version PR 权限(必读) + +`changesets/action` 用 `GITHUB_TOKEN` 开 PR 时,若仓库/组织关闭了 **Allow GitHub Actions to create and approve pull requests**,会报: + +`HttpError: GitHub Actions is not permitted to create or approve pull requests` + +(版本 bump 分支 `changeset-release/main` 可能已推送,但 PR 未创建。) + +任选其一: + +1. **推荐(零 Secret)**:Repo Settings → Actions → General → Workflow permissions → 勾选 *Allow GitHub Actions to create and approve pull requests*(需 `pull-requests: write`,workflow 已声明)。 +2. **PAT**:新建 classic/fine-grained token(至少能开本仓 PR),写入 Secret **`CHANGESETS_GITHUB_TOKEN`**;workflow 优先用该 token,未配置时回退 `GITHUB_TOKEN`。 + ### 本地一键发版(`npm-run-all2`) 当前 pre mode 请用: @@ -95,6 +108,7 @@ npm run pack:verify 1. npm 账号登录并确认 org [sfmc-bds](https://www.npmjs.com/org/sfmc-bds) 2. Granular Access Token(Automation)写入 GitHub Secret `NPM_TOKEN` +3. (可选)若无法开启 Actions「create and approve pull requests」:写入 `CHANGESETS_GITHUB_TOKEN`(见上文 Version PR 权限) ## 模块包 diff --git a/tools/CHANGELOG.md b/tools/CHANGELOG.md index 2f00c81..8fa809f 100644 --- a/tools/CHANGELOG.md +++ b/tools/CHANGELOG.md @@ -1,12 +1,19 @@ -# @sfmc-bds/tools - -## 0.2.0-beta.0 - -### Minor Changes - -- tsc7 双轨入口;sdk-lint / manifest schema / 模块脚手架;zipx DRY;pack:verify 从 NPM_PUBLISH_PACKAGES 派生;ootb 断言对齐。 - -### Patch Changes - -- Updated dependencies - - @sfmc-bds/bds-tools@0.2.0-beta.0 +# @sfmc-bds/tools + +## 0.2.0-beta.1 + +### Patch Changes + +- 44ca909: 修复发版编排:浅克隆不再误打全量 tag;CHANGELOG 摘录正则;release-tags 状态 DRY +- listPendingChangesetFiles:pre mode 下排除已写入 pre.json 的 id,避免误判待消费(LSP) + +## 0.2.0-beta.0 + +### Minor Changes + +- tsc7 双轨入口;sdk-lint / manifest schema / 模块脚手架;zipx DRY;pack:verify 从 NPM_PUBLISH_PACKAGES 派生;ootb 断言对齐。 + +### Patch Changes + +- Updated dependencies + - @sfmc-bds/bds-tools@0.2.0-beta.0 diff --git a/tools/changeset-release.test.mjs b/tools/changeset-release.test.mjs index bafedd0..9f03b9a 100644 --- a/tools/changeset-release.test.mjs +++ b/tools/changeset-release.test.mjs @@ -6,6 +6,7 @@ import { describe, it } from "node:test"; import { packageTagName, extractChangelogNotes, + listPendingChangesetFiles, RELEASE_TAGS_STATE, PRE_JSON, ROOT, @@ -46,3 +47,17 @@ describe("release paths (DRY)", () => { assert.equal(path.basename(RELEASE_TAGS_STATE), ".sfmc-release-tags.json"); }); }); + +describe("listPendingChangesetFiles (pre mode LSP)", () => { + it("excludes ids already recorded in pre.json#changesets", () => { + const pre = JSON.parse(fs.readFileSync(PRE_JSON, "utf8")); + assert.equal(pre.mode, "pre"); + assert.ok(Array.isArray(pre.changesets) && pre.changesets.length > 0); + // pre mode 下已消费的 .md 仍留在磁盘;待消费列表不得再包含它们 + const pending = listPendingChangesetFiles(); + const pendingIds = pending.map((p) => path.basename(p, ".md")); + for (const id of pre.changesets) { + assert.ok(!pendingIds.includes(id), `consumed ${id} must not be pending`); + } + }); +}); diff --git a/tools/lib/changeset-release.mjs b/tools/lib/changeset-release.mjs index 65899a4..98b2753 100644 --- a/tools/lib/changeset-release.mjs +++ b/tools/lib/changeset-release.mjs @@ -25,12 +25,20 @@ export function isPreMode() { return Boolean(pre && pre.mode === "pre"); } -/** 待消费的 changeset 文件(不含 README / config / pre) */ +/** + * 待消费的 changeset 文件(不含 README / config / pre)。 + * pre mode 下 `.md` 会留在磁盘直到 `pre exit`;须排除已写入 `pre.json#changesets` 的 id(与 @changesets getRelevantChangesets 一致)。 + */ export function listPendingChangesetFiles() { if (!fs.existsSync(CHANGESET_DIR)) return []; + const pre = readPreState(); + const consumed = new Set( + pre && pre.mode === "pre" && Array.isArray(pre.changesets) ? pre.changesets : [] + ); return fs .readdirSync(CHANGESET_DIR) .filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md") + .filter((f) => !consumed.has(f.replace(/\.md$/i, ""))) .map((f) => path.join(CHANGESET_DIR, f)); } diff --git a/tools/package.json b/tools/package.json index 668b8a4..38f9582 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "@sfmc-bds/tools", - "version": "0.2.0-beta.0", + "version": "0.2.0-beta.1", "description": "SFMC dev tools - testing before build", "type": "module", "license": "AGPL-3.0-only", From 95a2167456f745017269ef85163d77d1b02708da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 05:35:20 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(release):=20=E7=A6=81=E6=AD=A2=20CI=20f?= =?UTF-8?q?rom-existing=20=E8=BF=87=E6=94=B6=E5=BD=95=20+=20=E7=A9=BA?= =?UTF-8?q?=E5=8F=91=E7=89=88=E6=80=81=E5=85=A8=E9=87=8F=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAJOR/LSP:fetch-depth:0 后 CI 仍默认 --from-existing 会收录全部已有 name@version;空 RELEASE_TAGS_STATE 被 push/gh-release 当缺失并全量扫描。 改为默认 HEAD~1 diff;releaseTagsStateKind 区分 missing/empty/present。 Co-authored-by: Shiroha --- .github/workflows/changeset-release.yml | 2 +- docs/dev/npm-publish.md | 2 +- tools/CHANGELOG.md | 1 + tools/changeset-github-release.mjs | 8 +++++++- tools/changeset-push.mjs | 14 +++++++++----- tools/changeset-release.test.mjs | 15 +++++++++++++++ tools/changeset-tag.mjs | 13 +++++++------ tools/lib/changeset-release.mjs | 14 ++++++++++++++ 8 files changed, 55 insertions(+), 14 deletions(-) diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml index b04e142..8ccd02e 100644 --- a/.github/workflows/changeset-release.yml +++ b/.github/workflows/changeset-release.yml @@ -58,7 +58,7 @@ jobs: with: version: npm run version-packages # CI 不跑交互 changeset/version;只 publish + tag + gh release - # tag-packages 在 CI=true 时默认 --from-existing(publish 已建 tag,避免浅克隆误打全量) + # tag-packages 默认 HEAD~1 diff(本 workflow fetch-depth:0);勿默认 --from-existing publish: npm run ci-release-packages title: "chore(release): version packages" commit: "chore(release): version packages" diff --git a/docs/dev/npm-publish.md b/docs/dev/npm-publish.md index b909634..2b55368 100644 --- a/docs/dev/npm-publish.md +++ b/docs/dev/npm-publish.md @@ -66,7 +66,7 @@ npm run prerelease-packages |------|--------| | `prerelease-packages` | **现在**(pre/beta) | | `release-packages` | 仅 `changeset pre exit` 之后(latest) | -| `ci-release-packages` | CI 专用(无交互)。`CI=true` 时 `tag-packages` 默认 `--from-existing`:只收录 `changeset publish` 已创建的 tag,避免浅克隆误为未发版包打 tag / 建 GH Release。 | +| `ci-release-packages` | CI 专用(无交互)。`tag-packages` 默认按 `HEAD~1` 版本 diff 收录(workflow `fetch-depth: 0`);仅显式 `--from-existing` / 无法解析父提交时才回退「已有 tag」。`RELEASE_TAGS_STATE` 空数组表示本轮无 tag,push/gh-release **不得**回退全量扫描。 | | `publish-packages` | 仅 npm publish 包装 | 需要:本机已登录 `gh`、npm(或 `NPM_TOKEN`),且对 `origin` 有推送权限。 diff --git a/tools/CHANGELOG.md b/tools/CHANGELOG.md index 8fa809f..975dc09 100644 --- a/tools/CHANGELOG.md +++ b/tools/CHANGELOG.md @@ -6,6 +6,7 @@ - 44ca909: 修复发版编排:浅克隆不再误打全量 tag;CHANGELOG 摘录正则;release-tags 状态 DRY - listPendingChangesetFiles:pre mode 下排除已写入 pre.json 的 id,避免误判待消费(LSP) +- tag-packages:CI 不再默认 --from-existing;空 RELEASE_TAGS_STATE 禁止 push/gh 全量回退 ## 0.2.0-beta.0 diff --git a/tools/changeset-github-release.mjs b/tools/changeset-github-release.mjs index b604f9f..66c5364 100644 --- a/tools/changeset-github-release.mjs +++ b/tools/changeset-github-release.mjs @@ -17,6 +17,7 @@ import { packageDirFor, packageTagName, readReleaseTagsState, + releaseTagsStateKind, run, } from "./lib/changeset-release.mjs"; @@ -25,13 +26,18 @@ const forceLatest = process.argv.includes("--latest"); function loadTargets() { const state = readReleaseTagsState(); - if (state && state.tags.length > 0) { + const kind = releaseTagsStateKind(state); + if (kind === "present") { return state.tags.map((t) => ({ name: t.name, version: t.version, tag: t.tag, })); } + if (kind === "empty") { + /* LSP:与 push 一致 — 空态表示本轮无 Release,禁止回退全量 */ + return []; + } const out = []; for (const p of listPublishablePackages()) { const tag = packageTagName(p.name, p.version); diff --git a/tools/changeset-push.mjs b/tools/changeset-push.mjs index 5e56b60..f1991ac 100644 --- a/tools/changeset-push.mjs +++ b/tools/changeset-push.mjs @@ -7,6 +7,7 @@ import { git, gitCapture, readReleaseTagsState, + releaseTagsStateKind, } from "./lib/changeset-release.mjs"; const tagsOnly = @@ -29,12 +30,15 @@ if (!tagsOnly) { /** @type {string[]} */ let tags = []; const state = readReleaseTagsState(); -if (state && state.tags.length > 0) { +const kind = releaseTagsStateKind(state); +if (kind === "present") { tags = state.tags.map((t) => t.tag).filter(Boolean); -} - -if (tags.length === 0) { - /* 回退:推送本机尚未在 origin 的 name@version 风格 tag */ +} else if (kind === "empty") { + /* LSP:tag-packages 已显式写入空列表 = 本轮无 tag,禁止回退全量扫描 */ + console.log("[changeset] 发版态为空(本轮无 tag),跳过推送"); + process.exit(0); +} else { + /* 文件缺失:本地/应急回退 */ const local = gitCapture(["tag", "-l", "@sfmc-bds/*"]) .split(/\r?\n/) .filter(Boolean); diff --git a/tools/changeset-release.test.mjs b/tools/changeset-release.test.mjs index 9f03b9a..408276c 100644 --- a/tools/changeset-release.test.mjs +++ b/tools/changeset-release.test.mjs @@ -7,6 +7,7 @@ import { packageTagName, extractChangelogNotes, listPendingChangesetFiles, + releaseTagsStateKind, RELEASE_TAGS_STATE, PRE_JSON, ROOT, @@ -61,3 +62,17 @@ describe("listPendingChangesetFiles (pre mode LSP)", () => { } }); }); + +describe("releaseTagsStateKind (empty-state LSP)", () => { + it("distinguishes missing / empty / present", () => { + assert.equal(releaseTagsStateKind(null), "missing"); + assert.equal(releaseTagsStateKind({ tags: [], createdAt: "" }), "empty"); + assert.equal( + releaseTagsStateKind({ + tags: [{ name: "@sfmc-bds/tools", version: "0.2.0-beta.1", tag: "@sfmc-bds/tools@0.2.0-beta.1" }], + createdAt: "", + }), + "present" + ); + }); +}); diff --git a/tools/changeset-tag.mjs b/tools/changeset-tag.mjs index 9360dfd..78799f8 100644 --- a/tools/changeset-tag.mjs +++ b/tools/changeset-tag.mjs @@ -3,10 +3,13 @@ * 为当前可发包创建 git tag(与 changeset publish 同格式: name@version)。 * 已存在的 tag 跳过。写入 .sfmc-release-tags.json 供后续 push / gh release 使用。 * - * node tools/changeset-tag.mjs # 按 HEAD~1 版本 diff 打 tag - * node tools/changeset-tag.mjs --from-existing # 仅收录本地已有 name@version(CI publish 之后) + * node tools/changeset-tag.mjs # 按 HEAD~1 版本 diff 打 tag + * node tools/changeset-tag.mjs --from-existing # 仅收录本地已有 name@version + * node tools/changeset-tag.mjs --create # 显式按 diff 创建(与默认相同,保留兼容) * - * CI=true 且未传 --create 时默认 --from-existing(changeset publish 已建 tag,避免浅克隆误打全量)。 + * 默认走 HEAD~1 diff(workflow fetch-depth:0)。仅显式 --from-existing / SFMC_TAG_FROM_EXISTING=1 + * 时用「已有 tag」路径;浅克隆无法解析 HEAD~1 时由 resolvePackagesNeedingTags 安全回退。 + * 切勿在 CI 无条件 from-existing:全量拉 tag 后会过收录未在本轮 bump 的包。 */ import path from "node:path"; import { @@ -19,9 +22,7 @@ import { } from "./lib/changeset-release.mjs"; const fromExisting = - process.argv.includes("--from-existing") || - process.env.SFMC_TAG_FROM_EXISTING === "1" || - (process.env.CI === "true" && !process.argv.includes("--create")); + process.argv.includes("--from-existing") || process.env.SFMC_TAG_FROM_EXISTING === "1"; const needed = resolvePackagesNeedingTags({ fromExisting }); if (needed.length === 0) { diff --git a/tools/lib/changeset-release.mjs b/tools/lib/changeset-release.mjs index 98b2753..f4acd19 100644 --- a/tools/lib/changeset-release.mjs +++ b/tools/lib/changeset-release.mjs @@ -93,6 +93,20 @@ export function writeReleaseTagsState(tags) { return state; } +/** + * 发版态契约(LSP): + * - 文件缺失 → `missing`(允许回退扫描) + * - 文件存在且 tags=[] → `empty`(本轮无 tag,禁止回退全量) + * - 文件存在且有条目 → `present` + * @param {ReleaseTagsState | null} state + * @returns {"missing" | "empty" | "present"} + */ +export function releaseTagsStateKind(state) { + if (!state) return "missing"; + if (!Array.isArray(state.tags) || state.tags.length === 0) return "empty"; + return "present"; +} + /** * 仅收录「当前版本对应 tag 已在本地存在」的可发包。 * 用于:changeset publish 之后的 CI(publish 已建 tag),或浅克隆无法 diff 时的安全回退。