惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Cloudflare Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
博客园 - 司徒正美
V
Visual Studio Blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏
博客园_首页
Blog — PlanetScale
Blog — PlanetScale
博客园 - 聂微东
S
SegmentFault 最新的问题
T
The Blog of Author Tim Ferriss
D
Docker
Vercel News
Vercel News
Recent Announcements
Recent Announcements
Last Week in AI
Last Week in AI
爱范儿
爱范儿
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(changelog): reject bot/app handles as Thanks attribut...
hxy91819 · 2026-05-14 · via Recent Commits to openclaw:main
1-

import { readFileSync } from "node:fs";

1+

import { execFileSync } from "node:child_process";

2+

import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";

3+

import os from "node:os";

4+

import path from "node:path";

25

import { describe, expect, it } from "vitest";

3-

import { findForbiddenChangelogThanks } from "../../scripts/check-changelog-attributions.mjs";

6+

import {

7+

findForbiddenChangelogThanks,

8+

isForbiddenChangelogThanksHandle,

9+

requiresExplicitHumanChangelogThanks,

10+

} from "../../scripts/check-changelog-attributions.mjs";

11+12+

const changelogScriptPath = path.join(process.cwd(), "scripts", "pr-lib", "changelog.sh");

13+14+

function run(cwd: string, command: string, args: string[], env?: NodeJS.ProcessEnv): string {

15+

return execFileSync(command, args, {

16+

cwd,

17+

encoding: "utf8",

18+

env: env ? { ...process.env, ...env } : process.env,

19+

}).trim();

20+

}

21+22+

function createRepoWithPrChangelogDiff(entry: string): string {

23+

const repo = mkdtempSync(path.join(os.tmpdir(), "openclaw-changelog-credit-"));

24+

run(repo, "git", ["init", "-q", "--initial-branch=main"]);

25+

run(repo, "git", ["config", "user.email", "test@example.com"]);

26+

run(repo, "git", ["config", "user.name", "Test User"]);

27+

writeFileSync(repo + "/CHANGELOG.md", "# Changelog\n\n## Unreleased\n\n### Fixes\n\n", "utf8");

28+

run(repo, "git", ["add", "CHANGELOG.md"]);

29+

run(repo, "git", ["commit", "-qm", "seed"]);

30+

const baseSha = run(repo, "git", ["rev-parse", "HEAD"]);

31+

// validate_changelog_entry_for_pr reads origin/main...HEAD, so the test

32+

// fixture needs a real base ref plus a feature-branch changelog diff.

33+

run(repo, "git", ["update-ref", "refs/remotes/origin/main", baseSha]);

34+

run(repo, "git", ["checkout", "-qb", "feature"]);

35+

writeFileSync(

36+

repo + "/CHANGELOG.md",

37+

`# Changelog\n\n## Unreleased\n\n### Fixes\n\n${entry}\n`,

38+

"utf8",

39+

);

40+

run(repo, "git", ["add", "CHANGELOG.md"]);

41+

run(repo, "git", ["commit", "-qm", "add changelog entry"]);

42+

return repo;

43+

}

44+45+

function validateChangelogEntry(repo: string, contrib: string): string {

46+

return run(

47+

repo,

48+

"bash",

49+

[

50+

"-c",

51+

'source "$OPENCLAW_PR_CHANGELOG_SH"; validate_changelog_entry_for_pr 123 "$OPENCLAW_TEST_CONTRIB"',

52+

],

53+

{

54+

OPENCLAW_PR_CHANGELOG_SH: changelogScriptPath,

55+

OPENCLAW_TEST_CONTRIB: contrib,

56+

},

57+

);

58+

}

459560

describe("check-changelog-attributions", () => {

661

it("flags forbidden bot, org, and maintainer thanks attributions", () => {

@@ -9,13 +64,19 @@ describe("check-changelog-attributions", () => {

964

"- Org-owned fix. Thanks @openclaw.",

1065

"- Maintainer-owned fix. Thanks @steipete.",

1166

"- Mixed credit. Thanks @contributor and @OpenClaw.",

67+

"- Bot repair. Thanks @clawsweeper[bot].",

68+

"- Dependency bump. Thanks @dependabot[bot].",

69+

"- App repair. Thanks @app/clawsweeper.",

1270

].join("\n");

13711472

expect(findForbiddenChangelogThanks(content)).toEqual([

1573

{ line: 1, handle: "codex", text: "- Internal cleanup. Thanks @codex." },

1674

{ line: 2, handle: "openclaw", text: "- Org-owned fix. Thanks @openclaw." },

1775

{ line: 3, handle: "steipete", text: "- Maintainer-owned fix. Thanks @steipete." },

1876

{ line: 4, handle: "openclaw", text: "- Mixed credit. Thanks @contributor and @OpenClaw." },

77+

{ line: 5, handle: "clawsweeper[bot]", text: "- Bot repair. Thanks @clawsweeper[bot]." },

78+

{ line: 6, handle: "dependabot[bot]", text: "- Dependency bump. Thanks @dependabot[bot]." },

79+

{ line: 7, handle: "app/clawsweeper", text: "- App repair. Thanks @app/clawsweeper." },

1980

]);

2081

});

2182

@@ -27,6 +88,82 @@ describe("check-changelog-attributions", () => {

2788

).toStrictEqual([]);

2889

});

299091+

it("checks every thanked handle on a changelog line", () => {

92+

expect(

93+

findForbiddenChangelogThanks("- Mixed credit (#123). Thanks @openclaw and @alice."),

94+

).toEqual([

95+

{

96+

line: 1,

97+

handle: "openclaw",

98+

text: "- Mixed credit (#123). Thanks @openclaw and @alice.",

99+

},

100+

]);

101+

});

102+103+

it("uses one attribution predicate for scanner and shell checks", () => {

104+

expect(isForbiddenChangelogThanksHandle("")).toBe(true);

105+

expect(isForbiddenChangelogThanksHandle("null")).toBe(true);

106+

expect(isForbiddenChangelogThanksHandle("app/any-bot")).toBe(true);

107+

expect(isForbiddenChangelogThanksHandle("codex")).toBe(true);

108+

expect(isForbiddenChangelogThanksHandle("openclaw")).toBe(true);

109+

expect(isForbiddenChangelogThanksHandle("steipete")).toBe(true);

110+

expect(isForbiddenChangelogThanksHandle("app/clawsweeper")).toBe(true);

111+

expect(isForbiddenChangelogThanksHandle("clawsweeper")).toBe(true);

112+

expect(isForbiddenChangelogThanksHandle("clawsweeper[bot]")).toBe(true);

113+

expect(isForbiddenChangelogThanksHandle("openclaw-clawsweeper")).toBe(true);

114+

expect(isForbiddenChangelogThanksHandle("openclaw-clawsweeper[bot]")).toBe(true);

115+

expect(isForbiddenChangelogThanksHandle("dependabot[bot]")).toBe(true);

116+

expect(isForbiddenChangelogThanksHandle("dependabot[bot]", { strictBotHandle: true })).toBe(

117+

true,

118+

);

119+

expect(isForbiddenChangelogThanksHandle("alice")).toBe(false);

120+

expect(isForbiddenChangelogThanksHandle("human-clawsweeper-fan")).toBe(false);

121+

expect(

122+

isForbiddenChangelogThanksHandle("human-clawsweeper-fan", { strictBotHandle: true }),

123+

).toBe(false);

124+125+

expect(requiresExplicitHumanChangelogThanks("clawsweeper")).toBe(true);

126+

expect(requiresExplicitHumanChangelogThanks("clawsweeper[bot]")).toBe(true);

127+

expect(requiresExplicitHumanChangelogThanks("dependabot[bot]")).toBe(true);

128+

expect(requiresExplicitHumanChangelogThanks("app/clawsweeper")).toBe(true);

129+

expect(requiresExplicitHumanChangelogThanks("human-clawsweeper-fan")).toBe(false);

130+

expect(requiresExplicitHumanChangelogThanks("steipete")).toBe(false);

131+

expect(requiresExplicitHumanChangelogThanks("")).toBe(false);

132+

});

133+134+

it("requires explicit human thanks for bot PR changelog entries", () => {

135+

const repo = createRepoWithPrChangelogDiff("- Bot repair (#123).");

136+

try {

137+

let output = "";

138+

try {

139+

validateChangelogEntry(repo, "dependabot[bot]");

140+

} catch (error) {

141+

output = String((error as { stdout?: unknown }).stdout ?? error);

142+

}

143+

expect(output).toContain("must include an explicit human Thanks @handle");

144+

} finally {

145+

rmSync(repo, { recursive: true, force: true });

146+

}

147+

});

148+149+

it("accepts explicit human thanks for bot PR changelog entries", () => {

150+

const repo = createRepoWithPrChangelogDiff("- Bot repair (#123). Thanks @alice.");

151+

try {

152+

expect(validateChangelogEntry(repo, "dependabot[bot]")).toContain("explicit thanks");

153+

} finally {

154+

rmSync(repo, { recursive: true, force: true });

155+

}

156+

});

157+158+

it("keeps non-bot forbidden contributors on the no-thanks fallback", () => {

159+

const repo = createRepoWithPrChangelogDiff("- Maintainer repair (#123).");

160+

try {

161+

expect(validateChangelogEntry(repo, "steipete")).toContain("skipping thanks check");

162+

} finally {

163+

rmSync(repo, { recursive: true, force: true });

164+

}

165+

});

166+30167

it("keeps PR changelog gates on the same attribution policy", () => {

31168

const commonLib = readFileSync("scripts/pr-lib/common.sh", "utf8");

32169

const changelogLib = readFileSync("scripts/pr-lib/changelog.sh", "utf8");

@@ -36,10 +173,12 @@ describe("check-changelog-attributions", () => {

3617337174

expect(commonLib).toContain("pr_contributor_allows_human_trailers");

38175

expect(commonLib).toContain("resolve_contributor_coauthor_email");

39-

expect(changelogLib).toContain("node scripts/check-changelog-attributions.mjs CHANGELOG.md");

176+

expect(changelogLib).toContain("changelog_attribution_script");

177+

expect(changelogLib).toContain("--is-forbidden-handle");

178+

expect(changelogLib).toContain("--requires-explicit-human-thanks");

40179

expect(changelogLib).toContain("changelog_thanks_required_for_contributor");

41-

expect(changelogLib).toContain('"app/"*');

42-

expect(changelogLib).toContain('"clawsweeper"');

180+

expect(changelogLib).toContain("changelog_explicit_human_thanks_required_for_contributor");

181+

expect(changelogLib).toContain("Choose the credited original contributor");

43182

expect(gates).toContain("validate_changelog_attribution_policy");

44183

expect(prepareCore).toContain("resolve_contributor_coauthor_email");

45184

expect(mergeLib).toContain("pr_contributor_allows_human_trailers");