Skip to content

Commit 4f3cf1a

Browse files
committed
fix(changelog): make co-authors first-class authors; keep all contributors
Addresses the Copilot review on PR #39: - Co-authors overwrite (utils.js): instead of storing co-authors in a per-author field that a later commit could overwrite, every `Co-authored-by` trailer now becomes a first-class entry in the authors map (keyed by login). This drops co-authors no more, regardless of how many commits an author has, and works whether or not Slack IDs are fetched. getCommitCoauthors now returns { login, name, email }. - getAuthorsWithSlackIds no longer drops contributors. Restored to the documented contract (action.yml / PR description): return every author and attach `slackId` only when it can be resolved, rather than filtering out anyone without an org email or Slack match. Rebuilt dist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvWuyXuTp7HFAExnfjG91b
1 parent 9ea71f8 commit 4f3cf1a

5 files changed

Lines changed: 137 additions & 144 deletions

File tree

dist/index.js

Lines changed: 42 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/getAuthorsWithSlackIds.js

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -92,79 +92,55 @@ async function fetchGithubOrgUsers({ githubToken, repo }) {
9292
}
9393

9494
/**
95-
* Create mapping from GitHub usernames to org member info (name + verified @apify.com email).
95+
* Create mapping from GitHub usernames to @apify.com emails.
9696
*
9797
* @param {{ githubToken: string, repo?: { owner: string; repo: string } }}
9898
*
99-
* @returns {Promise<{ [login: string]: { name: string, email: string | null } }>}
99+
* @returns {Promise<{ [login: string]: string }>}
100100
*/
101-
async function getGitHubLoginToOrgInfoMap({ githubToken, repo }) {
101+
async function getGitHubLoginToEmailMap({ githubToken, repo }) {
102102
core.info('Trying to fetch @apify.com email addresses for Apify org members');
103103
const githubOrgUsers = await fetchGithubOrgUsers({ githubToken, repo });
104104

105105
core.info(`Fetched ${githubOrgUsers.length} Apify org members`);
106106

107-
return githubOrgUsers.reduce((acc, { login, name, organizationVerifiedDomainEmails }) => {
108-
acc[login] = {
109-
name,
110-
email: organizationVerifiedDomainEmails.length > 0 ? organizationVerifiedDomainEmails[0] : null,
111-
};
107+
return githubOrgUsers.reduce((acc, { login, organizationVerifiedDomainEmails }) => {
108+
acc[login] = organizationVerifiedDomainEmails.length > 0 ? organizationVerifiedDomainEmails[0] : null;
112109
return acc;
113110
}, {});
114111
}
115112

116113
/**
117-
* Resolve every contributor login (commit author + coauthors) across all commits to a Slack ID.
114+
* Enhance authors with Slack IDs matching their email addresses.
118115
*
119-
* Logins not present in the GitHub org collaborators list, or without a verified @apify.com email,
120-
* or without a matching Slack user, are filtered out. This naturally excludes bot accounts and
121-
* external contributors, and includes everyone who shipped via a `Co-authored-by:` trailer.
116+
* If the whole function fails, or some emails cannot be matched, the original authors are returned.
122117
*
123118
* @param {string} githubToken
124119
* @param {string} slackToken
125-
* @param {{ name: string, email: string, login: string, coauthors?: string[] }[]} authors
120+
* @param {array<{ name: string, email: string, login: string }>} authors
126121
* @param {{ owner: string; repo: string }} repo
127122
*
128-
* @returns {Promise<array<{ login: string, name: string, email: string, slackId: string }>>}
123+
* @returns {Promise<array<{ name: string, email: string, login: string, slackId?: string }>>}
129124
*/
130125
async function getAuthorsWithSlackIds(githubToken, slackToken, authors, repo = undefined) {
131126
if (!authors.length) {
132127
core.info('No authors to fetch Slack IDs for');
133128
return authors;
134129
}
135130

136-
const githubLoginToOrgInfo = await getGitHubLoginToOrgInfoMap({ githubToken, repo });
131+
const githubLoginToEmailMap = await getGitHubLoginToEmailMap({ githubToken, repo });
137132
const emailToSlackIdMap = await getEmailToSlackIdMap(slackToken);
138133

139-
// Union of every login mentioned anywhere across the release (primary authors + coauthors).
140-
const contributorLogins = new Set();
141-
for (const author of authors) {
142-
if (author.login) {
143-
contributorLogins.add(author.login);
144-
}
145-
for (const coauthor of author.coauthors ?? []) {
146-
contributorLogins.add(coauthor);
147-
}
148-
}
134+
return authors.map((author) => {
135+
const slackId = emailToSlackIdMap[githubLoginToEmailMap[author.login] || author.email];
149136

150-
const result = [];
151-
for (const login of contributorLogins) {
152-
const orgInfo = githubLoginToOrgInfo[login];
153-
if (!orgInfo?.email) {
154-
// Not an org member, or org member without a verified @apify.com email. Filtered out.
155-
continue;
156-
}
157-
158-
const slackId = emailToSlackIdMap[orgInfo.email];
159137
if (!slackId) {
160-
core.warning(`Slack ID not found for ${login} (${orgInfo.email})`);
161-
continue;
138+
core.warning(`Slack ID not found for ${author.name} (${author.login} / ${author.email})`);
139+
return author;
162140
}
163141

164-
result.push({ login, name: orgInfo.name, email: orgInfo.email, slackId });
165-
}
166-
167-
return result;
142+
return { ...author, slackId };
143+
});
168144
}
169145

170146
module.exports = {

src/utils.js

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,12 @@ async function getChangelogFromPullRequestCommits(octokit, scopes, context) {
130130
const { login } = commit.author;
131131
commitMessages.push(message);
132132

133-
const coauthors = getCommitCoauthors(message);
134-
authors.set(login || author.email, { login, ...author, coauthors }); // We want each author only once
133+
// Each contributor (the commit author and every `Co-authored-by` trailer) becomes a changelog
134+
// author. Keyed by login (falling back to email) so everyone appears exactly once.
135+
authors.set(login || author.email, { login, ...author });
136+
for (const coauthor of getCommitCoauthors(message)) {
137+
authors.set(coauthor.login || coauthor.email, coauthor);
138+
}
135139
}
136140

137141
const { changelog, includedPrNumbers } = await prepareChangeLog(commitMessages, scopes);
@@ -184,8 +188,12 @@ async function getChangelogFromCompareBranches(octokit, context, baseBranch, hea
184188
const { login } = commit.author;
185189
commitMessages.push(message);
186190

187-
const coauthors = getCommitCoauthors(message);
188-
authors.set(login || author.email, { login, ...author, coauthors }); // We want each author only once
191+
// Each contributor (the commit author and every `Co-authored-by` trailer) becomes a changelog
192+
// author. Keyed by login (falling back to email) so everyone appears exactly once.
193+
authors.set(login || author.email, { login, ...author });
194+
for (const coauthor of getCommitCoauthors(message)) {
195+
authors.set(coauthor.login || coauthor.email, coauthor);
196+
}
189197
}
190198
}
191199

@@ -350,49 +358,53 @@ const COAUTHORED_BY_REGEX = /^Co-authored-by: (?<name>.+?) <(?<email>.+?)@(?<ema
350358
const GITHUB_LOGIN_REGEX = /^[a-zA-Z0-9-]+$/i;
351359

352360
/**
353-
* Parses the `Co-authored-by` trailers from commit messages.
361+
* Parses the `Co-authored-by` trailers from a commit message.
354362
*
355363
* @param {string} commitMessage
356-
* @returns {string[]} list of github logins of the commit co-authors
364+
* @returns {{ login: string, name: string, email: string }[]} the commit co-authors, de-duplicated by login
357365
*/
358366
function getCommitCoauthors(commitMessage) {
359367
/** @type {RegExpExecArray | null} */
360-
let coauthor = null;
361-
const coauthors = new Set();
368+
let match = null;
369+
const coauthorsByLogin = new Map();
362370

363-
// Reset the regex with `g`
371+
// The regex is global (`g` flag), so reset its state before iterating to keep this function idempotent.
364372
COAUTHORED_BY_REGEX.lastIndex = 0;
365373

366374
// eslint-disable-next-line no-cond-assign
367-
while ((coauthor = COAUTHORED_BY_REGEX.exec(commitMessage)) != null) {
368-
const { name, email, emailDomain } = coauthor.groups;
375+
while ((match = COAUTHORED_BY_REGEX.exec(commitMessage)) != null) {
376+
const { name, email, emailDomain } = match.groups;
377+
const trimmedName = name && name.trim();
369378

379+
let login = null;
370380
if (emailDomain === 'users.noreply.github.com' && email && email.includes('+')) {
371381
const [, maybeLogin] = email.split('+');
372382

373383
if (GITHUB_LOGIN_REGEX.test(maybeLogin)) {
374-
coauthors.add(maybeLogin);
375-
376-
continue;
384+
login = maybeLogin;
377385
}
378386
}
379387

380-
const trimmedName = name && name.trim();
381-
382-
if (trimmedName && GITHUB_LOGIN_REGEX.test(trimmedName)) {
383-
coauthors.add(trimmedName);
388+
if (!login && trimmedName && GITHUB_LOGIN_REGEX.test(trimmedName)) {
389+
login = trimmedName;
390+
}
384391

392+
if (!login) {
393+
// eslint-disable-next-line no-console
394+
console.warn(`WARNING: could not parse the login from the "Co-authored-by" trailer of a commit message`, {
395+
name,
396+
email: `${email}@${emailDomain}`,
397+
});
385398
continue;
386399
}
387400

388-
// eslint-disable-next-line no-console
389-
console.warn(`WARNING: could not parse the login from the "Co-authored-by" trailer of a commit message`, {
390-
name,
391-
email: `${email}@${emailDomain}`,
392-
});
401+
// Keep the first occurrence of each login.
402+
if (!coauthorsByLogin.has(login)) {
403+
coauthorsByLogin.set(login, { login, name: trimmedName, email: `${email}@${emailDomain}` });
404+
}
393405
}
394406

395-
return Array.from(coauthors);
407+
return Array.from(coauthorsByLogin.values());
396408
}
397409

398410
module.exports = {

0 commit comments

Comments
 (0)