From c5b8c341a905f8f2d0b2f7930de2ab098e487f55 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 05:59:11 +0000 Subject: [PATCH] =?UTF-8?q?fix(release):=20push=20=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E6=80=81=20DRY=20=E5=AF=B9=E9=BD=90=20+=20build-publishable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - push-release 缺失态回退改用 listUnpushedExistingVersionTags(与 gh-release 共用现版本 tag 候选,LSP/DRY) - changeset-release 经 build-publishable 从 NPM_PUBLISH_PACKAGES 构建,去掉 YAML 手抄 workspace 列表(OCP) Co-authored-by: Shiroha --- .changeset/push-fallback-build-dry.md | 5 ++++ .github/workflows/changeset-release.yml | 11 ++------- docs/dev/npm-publish.md | 3 ++- package.json | 1 + tools/build-publishable.mjs | 23 ++++++++++++++++++ tools/changeset-push.mjs | 16 ++++--------- tools/changeset-release.test.mjs | 31 +++++++++++++++++++++++++ tools/lib/changeset-release.mjs | 14 +++++++++++ tools/lib/npm-publish-packages.mjs | 23 ++++++++++++++++++ 9 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 .changeset/push-fallback-build-dry.md create mode 100644 tools/build-publishable.mjs diff --git a/.changeset/push-fallback-build-dry.md b/.changeset/push-fallback-build-dry.md new file mode 100644 index 00000000..e639fe26 --- /dev/null +++ b/.changeset/push-fallback-build-dry.md @@ -0,0 +1,5 @@ +--- +"@sfmc-bds/tools": patch +--- + +发版编排:push 缺失态回退复用 listUnpushedExistingVersionTags;changeset-release 经 build-publishable 对齐 NPM_PUBLISH_PACKAGES diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml index fe03188e..5d5b4ce8 100644 --- a/.github/workflows/changeset-release.yml +++ b/.github/workflows/changeset-release.yml @@ -37,15 +37,8 @@ jobs: - run: npm ci --no-audit --no-fund - name: build publishable workspaces - run: | - npm run build --workspace @sfmc-bds/sdk - npm run build --workspace @sfmc-bds/eslint-plugin --if-present - npm run build --workspace @sfmc-bds/bds-tools --if-present - npm run build --workspace @sfmc-bds/db-server --if-present - npm run build --workspace @sfmc-bds/qq-bridge --if-present - npm run build --workspace @sfmc-bds/cli --if-present - npm run build --workspace @sfmc-bds/tools --if-present - npm run build --workspace @sfmc-bds/sfmc --if-present + # DRY:包清单权威在 tools/lib/npm-publish-packages.mjs,勿在此再抄 workspace 列表 + run: node tools/build-publishable.mjs # Version PR 需要「创建 PR」权限。仅用 GITHUB_TOKEN 且仓库关闭 # Settings→Actions→General→「Allow GitHub Actions to create and approve pull requests」时, diff --git a/docs/dev/npm-publish.md b/docs/dev/npm-publish.md index 23f18f4d..094900d6 100644 --- a/docs/dev/npm-publish.md +++ b/docs/dev/npm-publish.md @@ -66,7 +66,8 @@ npm run prerelease-packages |------|--------| | `prerelease-packages` | **现在**(pre/beta) | | `release-packages` | 仅 `changeset pre exit` 之后(latest) | -| `ci-release-packages` | CI 专用(无交互)。`tag-packages` 默认按 `HEAD~1` 版本 diff;仅显式 `--from-existing` / `SFMC_TAG_FROM_EXISTING=1`(或浅克隆无法解析父提交)才只收录已有 tag。空的 `.sfmc-release-tags.json` 表示本轮无目标,下游不得回退扫全仓。 | +| `ci-release-packages` | CI 专用(无交互)。`tag-packages` 默认按 `HEAD~1` 版本 diff;仅显式 `--from-existing` / `SFMC_TAG_FROM_EXISTING=1`(或浅克隆无法解析父提交)才只收录已有 tag。空的 `.sfmc-release-tags.json` 表示本轮无目标,下游不得回退扫全仓。push / gh-release 缺失态分别回退 `listUnpushedExistingVersionTags` / `listPackagesWithExistingVersionTags`。 | +| `build:publishable` | 按 `NPM_PUBLISH_PACKAGES` 构建可发包(`changeset-release` workflow 使用;勿在 YAML 再抄 workspace 列表)。 | | `publish-packages` | 仅 npm publish 包装 | 需要:本机已登录 `gh`、npm(或 `NPM_TOKEN`),且对 `origin` 有推送权限。 diff --git a/package.json b/package.json index b84989d0..1dd856a9 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "eslint-plugin:build": "npm run build --workspace @sfmc-bds/eslint-plugin", "eslint-plugin:test": "npm run test --workspace @sfmc-bds/eslint-plugin", "pack:verify": "node tools/pack-verify.mjs", + "build:publishable": "node tools/build-publishable.mjs", "changeset": "changeset", "version-packages": "changeset version", "publish-packages": "node tools/changeset-publish.mjs", diff --git a/tools/build-publishable.mjs b/tools/build-publishable.mjs new file mode 100644 index 00000000..8994cad2 --- /dev/null +++ b/tools/build-publishable.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env node +/** + * 按 NPM_PUBLISH_PACKAGES 构建全部可发包(changeset-release / 本地发版共用)。 + * 勿在 workflow YAML 里再抄一份 workspace 列表(DRY/OCP)。 + */ +import { spawnSync } from "node:child_process"; +import { listPublishableBuildOrder } from "./lib/npm-publish-packages.mjs"; +import { ROOT } from "./lib/changeset-release.mjs"; + +const order = listPublishableBuildOrder(); +for (const name of order) { + console.log(`[build-publishable] npm run build --workspace ${name} --if-present`); + const r = spawnSync("npm", ["run", "build", "--workspace", name, "--if-present"], { + cwd: ROOT, + stdio: "inherit", + shell: process.platform === "win32", + }); + if (r.status !== 0) { + process.exit(r.status === null ? 1 : r.status); + } +} + +console.log(`[build-publishable] ok — ${order.length} workspaces`); diff --git a/tools/changeset-push.mjs b/tools/changeset-push.mjs index 2151b9bf..0cde5fcc 100644 --- a/tools/changeset-push.mjs +++ b/tools/changeset-push.mjs @@ -6,6 +6,7 @@ import { git, gitCapture, + listUnpushedExistingVersionTags, readReleaseTagsState, resolveReleaseTagEntries, } from "./lib/changeset-release.mjs"; @@ -28,19 +29,10 @@ if (!tagsOnly) { } const state = readReleaseTagsState(); +/* DRY/LSP:缺失态与 gh-release 共用「当前可发包 + 当前版本 tag」候选,再过滤未推送 */ const entries = resolveReleaseTagEntries(state, () => { - /* 回退:仅当状态文件缺失时,推送本机尚未在 origin 的 name@version 风格 tag */ - console.warn("[changeset] 无 .sfmc-release-tags.json,回退扫描本地未推送 tag"); - /** @type {{ tag: string }[]} */ - const out = []; - const local = gitCapture(["tag", "-l", "@sfmc-bds/*"]) - .split(/\r?\n/) - .filter(Boolean); - for (const tag of local) { - const remote = gitCapture(["ls-remote", "--tags", "origin", `refs/tags/${tag}`]); - if (!remote) out.push({ tag }); - } - return out; + console.warn("[changeset] 无 .sfmc-release-tags.json,回退为未推送的当前 name@version tag"); + return listUnpushedExistingVersionTags(); }); const tags = entries.map((t) => t.tag).filter(Boolean); diff --git a/tools/changeset-release.test.mjs b/tools/changeset-release.test.mjs index 6fe2296c..0d022c85 100644 --- a/tools/changeset-release.test.mjs +++ b/tools/changeset-release.test.mjs @@ -8,11 +8,13 @@ import { extractChangelogNotes, listPendingChangesetFiles, listPackagesWithExistingVersionTags, + listUnpushedExistingVersionTags, resolveReleaseTagEntries, RELEASE_TAGS_STATE, PRE_JSON, ROOT, } from "./lib/changeset-release.mjs"; +import { listPublishableBuildOrder, NPM_PUBLISH_PACKAGES } from "./lib/npm-publish-packages.mjs"; import path from "node:path"; import fs from "node:fs"; import os from "node:os"; @@ -81,6 +83,35 @@ describe("resolveReleaseTagEntries (LSP)", () => { assert.equal(e.tag, packageTagName(e.name, e.version)); } }); + + it("push missing-state fallback is listUnpushedExistingVersionTags (DRY/LSP)", () => { + /* 契约:changeset-push 不得再硬编码 @sfmc-bds/* 全量扫描 */ + const allExisting = listPackagesWithExistingVersionTags(); + const existingTags = new Set(allExisting.map((e) => e.tag)); + /** @type {ReturnType | undefined} */ + let fb; + const viaResolver = resolveReleaseTagEntries(null, () => { + fb = listUnpushedExistingVersionTags(); + return fb; + }); + assert.equal(viaResolver, fb); + assert.ok(Array.isArray(fb)); + for (const e of fb) { + assert.equal(e.tag, packageTagName(e.name, e.version)); + assert.ok(existingTags.has(e.tag), "unpushed 必须是 existing 的子集"); + } + }); +}); + +describe("listPublishableBuildOrder (DRY/OCP)", () => { + it("covers every NPM_PUBLISH_PACKAGES entry once; sdk and bds-tools first", () => { + const order = listPublishableBuildOrder(); + const keys = Object.keys(NPM_PUBLISH_PACKAGES); + assert.equal(order.length, keys.length); + assert.deepEqual([...order].sort(), [...keys].sort()); + assert.equal(order[0], "@sfmc-bds/sdk"); + assert.equal(order[1], "@sfmc-bds/bds-tools"); + }); }); describe("listPendingChangesetFiles (pre mode LSP)", () => { diff --git a/tools/lib/changeset-release.mjs b/tools/lib/changeset-release.mjs index bbe9ea63..5cf433d6 100644 --- a/tools/lib/changeset-release.mjs +++ b/tools/lib/changeset-release.mjs @@ -125,6 +125,20 @@ export function listPackagesWithExistingVersionTags(pkgs = listPublishablePackag return out; } +/** + * 当前版本 tag 已在本地、且 origin 尚无同名 tag 的可发包。 + * push-release 缺失态回退权威实现:候选集与 gh-release 共用 + * listPackagesWithExistingVersionTags(DRY/LSP),再叠加未推送过滤。 + * @param {{ name: string, version: string, pkgPath: string }[]} [pkgs] + * @returns {ReleaseTagEntry[]} + */ +export function listUnpushedExistingVersionTags(pkgs = listPublishablePackages()) { + return listPackagesWithExistingVersionTags(pkgs).filter((e) => { + const remote = gitCapture(["ls-remote", "--tags", "origin", `refs/tags/${e.tag}`]); + return !remote; + }); +} + /** * 相对 HEAD~1 版本有变化的可发包;无法解析父提交时安全回退到 listPackagesWithExistingVersionTags。 * @param {{ fromExisting?: boolean }} [opts] diff --git a/tools/lib/npm-publish-packages.mjs b/tools/lib/npm-publish-packages.mjs index 21c3ce04..1b4b7d58 100644 --- a/tools/lib/npm-publish-packages.mjs +++ b/tools/lib/npm-publish-packages.mjs @@ -18,6 +18,29 @@ export const NPM_PUBLISH_PACKAGES = { "@sfmc-bds/sfmc": "sfmc-meta/package.json", }; +/** + * 可发包 build 顺序:优先编译被依赖的包,其余保持清单插入序(OCP:扩包只改表)。 + * sdk / bds-tools 被 cli、tools 等引用,须先于依赖方。 + * @returns {(keyof typeof NPM_PUBLISH_PACKAGES)[]} + */ +export function listPublishableBuildOrder() { + const all = Object.keys(NPM_PUBLISH_PACKAGES); + const priority = ["@sfmc-bds/sdk", "@sfmc-bds/bds-tools"]; + /** @type {string[]} */ + const out = []; + const seen = new Set(); + for (const name of priority) { + if (all.includes(name)) { + out.push(name); + seen.add(name); + } + } + for (const name of all) { + if (!seen.has(name)) out.push(name); + } + return /** @type {(keyof typeof NPM_PUBLISH_PACKAGES)[]} */ (out); +} + /** @returns {keyof typeof NPM_PUBLISH_PACKAGES | null} */ export function resolvePublishPackage(pkg) { if (Object.prototype.hasOwnProperty.call(NPM_PUBLISH_PACKAGES, pkg)) {