mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
fix(release): never credit the repository owner in release notes
This commit is contained in:
1 parent
028179b37f
commit
de789afafb
4 files changed
+50
-6
No files matched your search
@@ -145,6 +145,8 @@ The published body is rendered, not copied:
|
||||
| `#N`, an issue fixed by a direct push; a commit; a bot author | as written |
|
||||
| `#N`, a discussion or no such number | as written, with a warning |
|
||||
|
||||
The repository owner and any login in `RELEASE_CREDIT_EXCLUDE` (comma separated) are treated like bots: never named in an item or in the list.
|
||||
|
||||
A `## 贡献者` list of the credited authors closes the notes. A `#N` counts only at a line start or after whitespace, `(`, `(`, `、`, `,` or `,`, and never in code, an HTML comment or a link target. Write colours such as `#333` in a code span.
|
||||
|
||||
A lookup GitHub refuses, or cannot answer after retries, fails `publish` with a `release_credits.*` code. Preview with `node scripts/studio-release-notes.mjs release-notes/studio/X.Y.Z.md /tmp/notes.md`; it reads `GH_TOKEN`, else `gh auth token`.
|
||||
|
||||
@@ -34,6 +34,14 @@ export function isBotAccount(login, type) {
|
||||
return name.endsWith("[bot]") || botLogins.has(name);
|
||||
}
|
||||
|
||||
// Accounts left uncredited like bots: the repository owner, plus the logins in
|
||||
// RELEASE_CREDIT_EXCLUDE (comma separated). Compared case-insensitively.
|
||||
export function creditExclusions(repository = defaultRepository, env = process.env) {
|
||||
const extra = String(env.RELEASE_CREDIT_EXCLUDE || "").split(",");
|
||||
const names = [repository.split("/")[0], ...extra].map((name) => name.trim().toLowerCase());
|
||||
return new Set(names.filter(Boolean));
|
||||
}
|
||||
|
||||
export function tokenFromEnvironment(env = process.env, runGh = defaultRunGh) {
|
||||
const token = env.GH_TOKEN || env.GITHUB_TOKEN;
|
||||
if (token) return token;
|
||||
@@ -71,21 +79,23 @@ const refQuery = `query($owner: String!, $name: String!, $number: Int!) {
|
||||
}
|
||||
}`;
|
||||
|
||||
function person(author) {
|
||||
// `bot` is true for every account that is never credited, excluded humans included.
|
||||
function person(author, excluded) {
|
||||
if (!author) return { login: null, bot: true };
|
||||
return { login: author.login, bot: isBotAccount(author.login, author.__typename) };
|
||||
const bot = isBotAccount(author.login, author.__typename) || excluded.has(author.login.toLowerCase());
|
||||
return { login: author.login, bot };
|
||||
}
|
||||
|
||||
// An issue's fixers are the merged pull requests GitHub links to it as closing
|
||||
// it: the one that closed it, and any linked by a closing keyword or by hand.
|
||||
// A mere mention is not a fix, so cross-references are not read.
|
||||
function issueFixes(issue) {
|
||||
function issueFixes(issue, excluded) {
|
||||
const closer = issue.timelineItems?.nodes?.[0]?.closer;
|
||||
const candidates = [...(issue.closedByPullRequestsReferences?.nodes || [])];
|
||||
if (closer?.__typename === "PullRequest") candidates.push(closer);
|
||||
const fixes = new Map();
|
||||
for (const pull of candidates) {
|
||||
if (pull?.merged && !fixes.has(pull.number)) fixes.set(pull.number, { number: pull.number, ...person(pull.author) });
|
||||
if (pull?.merged && !fixes.has(pull.number)) fixes.set(pull.number, { number: pull.number, ...person(pull.author, excluded) });
|
||||
}
|
||||
return [...fixes.values()].sort((a, b) => a.number - b.number);
|
||||
}
|
||||
@@ -116,6 +126,7 @@ export function githubRefLookup({
|
||||
fetchImpl = fetch,
|
||||
sleep = defaultSleep,
|
||||
attempts = 4,
|
||||
excluded = creditExclusions(repository),
|
||||
} = {}) {
|
||||
const [owner, name] = repository.split("/");
|
||||
const headers = { "Content-Type": "application/json", "User-Agent": "reasonix-release-credits" };
|
||||
@@ -148,8 +159,8 @@ export function githubRefLookup({
|
||||
const payload = await query(ref);
|
||||
const repositoryNode = payload.data?.repository;
|
||||
const node = repositoryNode?.issueOrPullRequest;
|
||||
if (node?.__typename === "PullRequest") return { kind: "pull", ...person(node.author) };
|
||||
if (node?.__typename === "Issue") return { kind: "issue", fixes: issueFixes(node) };
|
||||
if (node?.__typename === "PullRequest") return { kind: "pull", ...person(node.author, excluded) };
|
||||
if (node?.__typename === "Issue") return { kind: "issue", fixes: issueFixes(node, excluded) };
|
||||
const errors = payload.errors || [];
|
||||
if (repositoryNode && errors.every((error) => error.type === "NOT_FOUND")) {
|
||||
return { kind: repositoryNode.discussion ? "discussion" : "missing" };
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
contributorLogins,
|
||||
CreditError,
|
||||
CreditErrorCode,
|
||||
creditExclusions,
|
||||
creditSuffix,
|
||||
githubRefLookup,
|
||||
isBotAccount,
|
||||
@@ -216,3 +217,25 @@ test("the token comes from the environment, then gh, else a coded error", () =>
|
||||
{ code: CreditErrorCode.noToken },
|
||||
);
|
||||
});
|
||||
|
||||
test("the repository owner and excluded logins are left as written, a community author is credited", async () => {
|
||||
const { fetchImpl } = fakeGitHub({
|
||||
1: pull("Owner"),
|
||||
2: pull("alice"),
|
||||
3: issue([linked(30, "owner"), linked(31, "alice")]),
|
||||
4: pull("helper"),
|
||||
});
|
||||
const lookup = githubRefLookup({ repository: "owner/r", fetchImpl });
|
||||
const credits = new Map([
|
||||
[1, await lookup(1)],
|
||||
[2, await lookup(2)],
|
||||
[3, await lookup(3)],
|
||||
]);
|
||||
assert.equal(creditSuffix(credits.get(1)), "");
|
||||
assert.equal(creditSuffix(credits.get(2)), " by @alice");
|
||||
assert.equal(creditSuffix(credits.get(3)), " fixed in #30 and #31 by @alice");
|
||||
assert.deepEqual(contributorLogins([1, 2, 3], credits), ["alice"]);
|
||||
const excluding = githubRefLookup({ repository: "owner/r", fetchImpl, excluded: creditExclusions("owner/r", { RELEASE_CREDIT_EXCLUDE: " Helper, ," }) });
|
||||
assert.equal(creditSuffix(await excluding(4)), "");
|
||||
assert.deepEqual([...creditExclusions("o/r", {})], ["o"]);
|
||||
});
|
||||
@@ -80,3 +80,11 @@ test("a number naming a discussion or nothing stays plain and only warns", async
|
||||
);
|
||||
assert.equal(renderStudioNotes(source, credits), source);
|
||||
});
|
||||
|
||||
test("an item whose only author is excluded renders plain and lists no contributor", () => {
|
||||
const credits = new Map([
|
||||
[1, { kind: "pull", login: "owner", bot: true }],
|
||||
[2, { kind: "issue", fixes: [{ number: 9, login: "owner", bot: true }] }],
|
||||
]);
|
||||
assert.equal(renderStudioNotes("摘要。\n\n## 修复\n\n- 一 #1\n- 二 #2\n", credits), "摘要。\n\n## 修复\n\n- 一 #1\n- 二 #2 fixed in #9\n");
|
||||
});
|
||||
Reference in new issue
Block a user