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
5 changes: 0 additions & 5 deletions .changeset/fix-release-tag-shallow.md

This file was deleted.

8 changes: 1 addition & 7 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
10 changes: 7 additions & 3 deletions .github/workflows/changeset-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
pull-requests: write
id-token: write
steps:
# fetch-depth:0:tag-packages 需 HEAD~1 做版本 diff;浅克隆会误触发全量 tag 回退路径
# fetch-depth:0:tag-packages 需 HEAD~1 做版本 diff;浅克隆时 resolvePackagesNeedingTags 才回退 from-existing
- uses: actions/checkout@v5
with:
fetch-depth: 0
Expand All @@ -47,18 +47,22 @@ 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;未配置则回退 GITHUB_TOKEN(需上述勾选)。
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
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(空状态文件亦信任,禁止扫全仓 tag)
publish: npm run ci-release-packages
title: "chore(release): version packages"
commit: "chore(release): version packages"
createGithubReleases: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SFMC_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
16 changes: 15 additions & 1 deletion docs/dev/npm-publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 未创建。workflow 已声明 `pull-requests: write` 仍不够。)

任选其一:

1. **推荐(零 Secret)**:Repo Settings → Actions → General → Workflow permissions → 勾选 *Allow GitHub Actions to create and approve pull requests*。
2. **PAT**:写入 Secret **`CHANGESETS_GITHUB_TOKEN`**(能开本仓 PR);workflow 优先用该 token,未配置时回退 `GITHUB_TOKEN`。

### 本地一键发版(`npm-run-all2`)

当前 pre mode 请用:
Expand All @@ -53,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;仅显式 `--from-existing` / `SFMC_TAG_FROM_EXISTING=1`(或浅克隆无法解析父提交)才只收录已有 tag。空的 `.sfmc-release-tags.json` 表示本轮无目标,下游不得回退扫全仓。 |
| `publish-packages` | 仅 npm publish 包装 |

需要:本机已登录 `gh`、npm(或 `NPM_TOKEN`),且对 `origin` 有推送权限。
Expand Down Expand Up @@ -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 权限)

## 模块包

Expand Down
9 changes: 3 additions & 6 deletions remote-controller/src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
* stdout bare + 落盘 `<SFMC_ROOT>/.sfmc/logs/remote-controller.log`。
*/
import { createNodeServiceLogger } from "@sfmc-bds/sdk/logs";
import { logFile, resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = resolveRuntimeRoot(resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."));
import { logFile } from "@sfmc-bds/sdk/node/config";
import { PROJECT_ROOT } from "./project-root.js";

export const log = createNodeServiceLogger({
source: "remote-controller",
logPath: logFile(ROOT, "remote-controller"),
logPath: logFile(PROJECT_ROOT, "remote-controller"),
});

process.on("exit", () => log.close());
12 changes: 12 additions & 0 deletions remote-controller/src/project-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* project-root.ts — 仓根唯一解析点(DRY/DIP:log 勿各自上溯)
*/

import { resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));

/** SFMC_ROOT > 相对 remote-controller/src 上溯两级 */
export const PROJECT_ROOT: string = resolveRuntimeRoot(resolve(HERE, "..", ".."));
7 changes: 7 additions & 0 deletions tools/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @sfmc-bds/tools

## 0.2.0-beta.1

### Patch Changes

- 44ca909: 修复发版编排:浅克隆不再误打全量 tag;CHANGELOG 摘录正则;release-tags 状态 DRY
- tag-packages 默认 HEAD~1 版本 diff(不再因 CI=true 强制 from-existing);空 `.sfmc-release-tags.json` 下游一律信任,禁止回退扫全仓 tag

## 0.2.0-beta.0

### Minor Changes
Expand Down
31 changes: 17 additions & 14 deletions tools/changeset-github-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
packageDirFor,
packageTagName,
readReleaseTagsState,
resolveReleaseTagEntries,
run,
} from "./lib/changeset-release.mjs";

Expand All @@ -25,21 +26,23 @@ const forceLatest = process.argv.includes("--latest");

function loadTargets() {
const state = readReleaseTagsState();
if (state && state.tags.length > 0) {
return state.tags.map((t) => ({
name: t.name,
version: t.version,
tag: t.tag,
}));
}
const out = [];
for (const p of listPublishablePackages()) {
const tag = packageTagName(p.name, p.version);
if (gitCapture(["tag", "-l", tag])) {
out.push({ name: p.name, version: p.version, tag });
const entries = resolveReleaseTagEntries(state, () => {
/* 仅状态文件缺失时回退:当前版本对应 tag 已在本地的可发包 */
console.warn("[changeset] 无 .sfmc-release-tags.json,回退扫描本地 name@version tag");
const out = [];
for (const p of listPublishablePackages()) {
const tag = packageTagName(p.name, p.version);
if (gitCapture(["tag", "-l", tag])) {
out.push({ name: p.name, version: p.version, tag });
}
}
}
return out;
return out;
});
return entries.map((t) => ({
name: t.name,
version: t.version,
tag: t.tag,
}));
}

function isPrereleaseVersion(version) {
Expand Down
21 changes: 11 additions & 10 deletions tools/changeset-push.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
git,
gitCapture,
readReleaseTagsState,
resolveReleaseTagEntries,
} from "./lib/changeset-release.mjs";

const tagsOnly =
Expand All @@ -26,23 +27,23 @@ if (!tagsOnly) {
console.log("[changeset] CI/tags-only:跳过分支推送");
}

/** @type {string[]} */
let tags = [];
const state = readReleaseTagsState();
if (state && state.tags.length > 0) {
tags = state.tags.map((t) => t.tag).filter(Boolean);
}

if (tags.length === 0) {
/* 回退:推送本机尚未在 origin 的 name@version 风格 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) tags.push(tag);
if (!remote) out.push({ tag });
}
}
return out;
});

const tags = entries.map((t) => t.tag).filter(Boolean);

if (tags.length === 0) {
console.log("[changeset] 无待推送 tag,跳过");
Expand Down
39 changes: 39 additions & 0 deletions tools/changeset-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { describe, it } from "node:test";
import {
packageTagName,
extractChangelogNotes,
listPendingChangesetFiles,
resolveReleaseTagEntries,
RELEASE_TAGS_STATE,
PRE_JSON,
ROOT,
Expand Down Expand Up @@ -46,3 +48,40 @@ describe("release paths (DRY)", () => {
assert.equal(path.basename(RELEASE_TAGS_STATE), ".sfmc-release-tags.json");
});
});

describe("resolveReleaseTagEntries (LSP)", () => {
it("trusts empty tags array — does not call fallback", () => {
let called = false;
const out = resolveReleaseTagEntries({ tags: [], createdAt: "t" }, () => {
called = true;
return [{ tag: "should-not-appear" }];
});
assert.deepEqual(out, []);
assert.equal(called, false);
});

it("uses fallback only when state is missing", () => {
const fb = [{ tag: "@sfmc-bds/tools@0.2.0-beta.1" }];
const out = resolveReleaseTagEntries(null, () => fb);
assert.equal(out, fb);
});

it("returns state.tags when present", () => {
const tags = [{ name: "@sfmc-bds/tools", version: "0.2.0-beta.1", tag: "@sfmc-bds/tools@0.2.0-beta.1" }];
const out = resolveReleaseTagEntries({ tags, createdAt: "t" }, () => []);
assert.equal(out, tags);
});
});

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));
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`);
}
});
});
13 changes: 7 additions & 6 deletions tools/changeset-tag.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* 为当前可发包创建 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
* SFMC_TAG_FROM_EXISTING=1 # 同上
*
* CI=true 且未传 --create 时默认 --from-existing(changeset publish 已建 tag,避免浅克隆误打全量)。
* HEAD~1 不可用时由 resolvePackagesNeedingTags 安全回退到 from-existing(勿因 CI=true 强制)。
* CI 已 fetch-depth:0 时应走版本 diff,避免误收录仓内历史全量 name@version。
*/
import path from "node:path";
import {
Expand All @@ -19,9 +21,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) {
Expand All @@ -30,6 +30,7 @@ if (needed.length === 0) {
? "[changeset] 无已存在的 name@version tag 可收录,跳过"
: "[changeset] 无包版本变化,跳过打 tag"
);
/* 空数组仍写入:下游须信任「本轮无 tag」,禁止回退扫全仓 */
writeReleaseTagsState([]);
process.exit(0);
}
Expand Down
24 changes: 23 additions & 1 deletion tools/lib/changeset-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,21 @@ 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 一致,LSP)。
*/
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));
}

Expand Down Expand Up @@ -75,6 +84,19 @@ export function readReleaseTagsState() {
}
}

/**
* LSP:状态文件存在时一律信任(含 `tags: []` = 本轮无目标);
* 仅文件缺失 / 无法解析时调用 fallback(禁止把空数组当成「无状态」再扫全仓 tag)。
* @template T
* @param {ReleaseTagsState | null} state
* @param {() => T[]} fallback
* @returns {T[] | ReleaseTagEntry[]}
*/
export function resolveReleaseTagEntries(state, fallback) {
if (state) return state.tags;
return fallback();
}

/** @param {ReleaseTagEntry[]} tags */
export function writeReleaseTagsState(tags) {
const state = {
Expand Down
2 changes: 1 addition & 1 deletion tools/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down