diff --git a/src/changelog.spec.js b/src/changelog.spec.js index 04b57d3..7dab2c7 100644 --- a/src/changelog.spec.js +++ b/src/changelog.spec.js @@ -212,6 +212,13 @@ describe("Changelog", () => { name: "User Bot", }, }, + "https://api.github.com/apps/copilot-swe-agent": { + body: { + name: "Copilot SWE Agent", + slug: "copilot-swe-agent", + html_url: "https://github.com/apps/copilot-swe-agent", + }, + }, }; fetch.__setMockResponses(usersCache); }); @@ -222,6 +229,10 @@ describe("Changelog", () => { }); const testCommits = [ + { + commitSHA: "a0000005", + githubIssue: { user: { login: "copilot", html_url: "https://github.com/apps/copilot-swe-agent" } }, + }, { commitSHA: "a0000004", githubIssue: { user: { login: "test-user-1" } }, @@ -236,6 +247,11 @@ describe("Changelog", () => { const committers = await changelog.getCommitters(testCommits); expect(committers).toEqual([ + { + name: "Copilot", + slug: "copilot-swe-agent", + html_url: "https://github.com/apps/copilot-swe-agent", + }, { login: "test-user-1", html_url: "https://github.com/test-user-1", diff --git a/src/changelog.ts b/src/changelog.ts index 332d8d6..5bb46f4 100644 --- a/src/changelog.ts +++ b/src/changelog.ts @@ -5,7 +5,7 @@ import progressBar from "./progress-bar"; import { Configuration } from "./configuration"; import findPullRequestId from "./find-pull-request-id"; import * as Git from "./git"; -import GithubAPI, { GitHubUserResponse } from "./github-api"; +import GithubAPI, { GitHubContributor } from "./github-api"; import { CommitInfo, Release } from "./interfaces"; import MarkdownRenderer from "./markdown-renderer"; @@ -124,17 +124,19 @@ export default class Changelog { return Git.listCommits(from, to); } - private async getCommitters(commits: CommitInfo[]): Promise { - const committers: { [id: string]: GitHubUserResponse } = {}; + private async getCommitters(commits: CommitInfo[]): Promise { + const committers: { [id: string]: GitHubContributor } = {}; for (const commit of commits) { const issue = commit.githubIssue; - const login = issue && issue.user && issue.user.login; + const user = issue && issue.user; + const login = user && user.login; // If a list of `ignoreCommitters` is provided in the lerna.json config // check if the current committer should be kept or not. const shouldKeepCommiter = login && !this.ignoreCommitter(login); + if (login && shouldKeepCommiter && !committers[login]) { - committers[login] = await this.github.getUserData(login); + committers[login] = this.sanitizeCommitter(await this.github.getUserData(user)); } } @@ -145,6 +147,15 @@ export default class Changelog { return this.config.ignoreCommitters.some((c: string) => c === login || login.indexOf(c) > -1); } + private sanitizeCommitter(contributor: GitHubContributor) { + // Response for Copilot is "Copilot SWE Agent" - but we prefer "Copilot" + if (contributor.name === "Copilot SWE Agent") { + contributor.name = "Copilot"; + } + + return contributor; + } + private toCommitInfos(commits: Git.CommitListItem[]): CommitInfo[] { return commits.map(commit => { const { sha, refName, summary: message, date } = commit; diff --git a/src/functional/__snapshots__/markdown-full.spec.js.snap b/src/functional/__snapshots__/markdown-full.spec.js.snap index 85181dd..6043a5d 100644 --- a/src/functional/__snapshots__/markdown-full.spec.js.snap +++ b/src/functional/__snapshots__/markdown-full.spec.js.snap @@ -112,7 +112,7 @@ exports[`createMarkdown > single project > outputs correct changelog 1`] = ` * This is the commit title for the issue (#8) ([@bot-user](https://github.com/bot-user)) #### Committers: 2 -- Bot User ([@bot-user](https://github.com/bot-user)) +- Bot User [Bot] ([@bot-user](https://github.com/bot-user)) - Han Solo ([@han-solo](https://github.com/han-solo)) @@ -175,7 +175,7 @@ exports[`createMarkdown > single tags > outputs correct changelog 1`] = ` * This is the commit title for the issue (#8) ([@bot-user](https://github.com/bot-user)) #### Committers: 2 -- Bot User ([@bot-user](https://github.com/bot-user)) +- Bot User [Bot] ([@bot-user](https://github.com/bot-user)) - Han Solo ([@han-solo](https://github.com/han-solo)) diff --git a/src/functional/markdown-full.spec.js b/src/functional/markdown-full.spec.js index b709b98..56827d4 100644 --- a/src/functional/markdown-full.spec.js +++ b/src/functional/markdown-full.spec.js @@ -163,6 +163,7 @@ const usersCache = { "https://api.github.com/users/luke": { body: { login: "luke", + type: "User", html_url: "https://github.com/luke", name: "Luke Skywalker", }, @@ -170,6 +171,7 @@ const usersCache = { "https://api.github.com/users/princess-leia": { body: { login: "princess-leia", + type: "User", html_url: "https://github.com/princess-leia", name: "Princess Leia Organa", }, @@ -177,6 +179,7 @@ const usersCache = { "https://api.github.com/users/vader": { body: { login: "vader", + type: "User", html_url: "https://github.com/vader", name: "Darth Vader", }, @@ -184,6 +187,7 @@ const usersCache = { "https://api.github.com/users/gtarkin": { body: { login: "gtarkin", + type: "User", html_url: "https://github.com/gtarkin", name: "Governor Tarkin", }, @@ -191,6 +195,7 @@ const usersCache = { "https://api.github.com/users/han-solo": { body: { login: "han-solo", + type: "User", html_url: "https://github.com/han-solo", name: "Han Solo", }, @@ -198,6 +203,7 @@ const usersCache = { "https://api.github.com/users/chewbacca": { body: { login: "chewbacca", + type: "User", html_url: "https://github.com/chewbacca", name: "Chwebacca", }, @@ -205,6 +211,7 @@ const usersCache = { "https://api.github.com/users/rd-d2": { body: { login: "rd-d2", + type: "User", html_url: "https://github.com/rd-d2", name: "R2-D2", }, @@ -212,6 +219,7 @@ const usersCache = { "https://api.github.com/users/c-3po": { body: { login: "c-3po", + type: "User", html_url: "https://github.com/c-3po", name: "C-3PO", }, @@ -327,7 +335,7 @@ describe("createMarkdown", () => { describe("ignore config", () => { it("ignores PRs from bot users even if they were not the (merge) committer", async () => { - git.changedPaths.mockImplementation((sha) => { + git.changedPaths.mockImplementation(sha => { return listOfPackagesForEachCommit[sha]; }); git.lastTag.mockImplementation(() => "v8.0.0"); @@ -351,7 +359,7 @@ describe("createMarkdown", () => { describe("single tags", () => { it("outputs correct changelog", async () => { - git.changedPaths.mockImplementation((sha) => listOfPackagesForEachCommit[sha]); + git.changedPaths.mockImplementation(sha => listOfPackagesForEachCommit[sha]); git.lastTag.mockImplementation(() => "v8.0.0"); git.listCommits.mockImplementation(() => listOfCommits); git.listTagNames.mockImplementation(() => listOfTags); @@ -371,7 +379,7 @@ describe("createMarkdown", () => { describe("multiple tags", () => { it("outputs correct changelog", async () => { - git.changedPaths.mockImplementation((sha) => listOfPackagesForEachCommit[sha]); + git.changedPaths.mockImplementation(sha => listOfPackagesForEachCommit[sha]); git.lastTag.mockImplementation(() => "v8.0.0"); git.listCommits.mockImplementation(() => [ { @@ -424,7 +432,7 @@ describe("createMarkdown", () => { describe("single project", () => { it("outputs correct changelog", async () => { - git.changedPaths.mockImplementation((sha) => listOfFileForEachCommit[sha]); + git.changedPaths.mockImplementation(sha => listOfFileForEachCommit[sha]); git.lastTag.mockImplementation(() => "v8.0.0"); git.listCommits.mockImplementation(() => listOfCommits); git.listTagNames.mockImplementation(() => listOfTags); @@ -449,7 +457,7 @@ describe("createMarkdown", () => { documentation_url: "https://developer.github.com/v3", }; beforeEach(async () => { - git.changedPaths.mockImplementation((sha) => listOfFileForEachCommit[sha]); + git.changedPaths.mockImplementation(sha => listOfFileForEachCommit[sha]); git.lastTag.mockImplementation(() => "v8.0.0"); git.listCommits.mockImplementation(() => listOfCommits); git.listTagNames.mockImplementation(() => listOfTags); diff --git a/src/github-api.spec.ts b/src/github-api.spec.ts index 842b4e6..365edc5 100644 --- a/src/github-api.spec.ts +++ b/src/github-api.spec.ts @@ -17,12 +17,15 @@ describe("github api", function () { }; expect(github.getBaseIssueUrl("foo")).toEqual(`https://github.host.com/foo/issues/`); - github.getUserData("foo"); + github.getUserData({ login: "foo", html_url: "" }); expect(fetchedUrl).toEqual(`https://api.github.host.com/users/foo`); github.getIssueData("foo", "2"); expect(fetchedUrl).toEqual(`https://api.github.host.com/repos/foo/issues/2`); + github.getUserData({ login: "Copilot", html_url: "https://github.com/apps/copilot-swe-agent" }); + expect(fetchedUrl).toEqual(`https://api.github.host.com/apps/copilot-swe-agent`); + delete process.env.GITHUB_DOMAIN; process.env.GITHUB_API_URL = "https://api.github.host2.com"; @@ -36,7 +39,7 @@ describe("github api", function () { }; expect(github.getBaseIssueUrl("foo")).toEqual(`https://github.com/foo/issues/`); - github.getUserData("foo"); + github.getUserData({ login: "foo", html_url: "" }); expect(fetchedUrl).toEqual(`https://api.github.host2.com/users/foo`); github.getIssueData("foo", "2"); diff --git a/src/github-api.ts b/src/github-api.ts index 45af424..b517b4f 100644 --- a/src/github-api.ts +++ b/src/github-api.ts @@ -3,12 +3,22 @@ const path = require("path"); import ConfigurationError from "./configuration-error"; import fetch from "./fetch"; -export interface GitHubUserResponse { - login: string; +interface GitHubContributorBase { name: string; html_url: string; } +export interface GithubUserInfo extends GitHubContributorBase { + login: string; + type: string; // "Bot" | "User" +} + +export interface GithubAppInfo extends GitHubContributorBase { + slug: string; +} + +export type GitHubContributor = GithubUserInfo | GithubAppInfo; + export interface GitHubIssueResponse { number: number; title: string; @@ -18,10 +28,7 @@ export interface GitHubIssueResponse { labels: Array<{ name: string; }>; - user: { - login: string; - html_url: string; - }; + user: GithubUserInfo; } export interface Options { @@ -54,9 +61,18 @@ export default class GithubAPI { return this._fetch(`${prefix}/repos/${repo}/issues/${issue}`); } - public async getUserData(login: string): Promise { + public async getUserData(userInfo: Pick): Promise { + let login = userInfo.login; + let path = "users"; + + // github API itself does not tell if contributor is an app. Best guess was to + // check the `html_url` for the `/apps/` segment + if (userInfo.html_url && userInfo.html_url.includes("/apps/")) { + path = "apps"; + login = userInfo.html_url.split("/").pop() as string; + } const prefix = process.env.GITHUB_API_URL || `https://api.${this.github}`; - return this._fetch(`${prefix}/users/${login}`); + return await this._fetch(`${prefix}/${path}/${login}`); } private async _fetch(url: string): Promise { diff --git a/src/interfaces.ts b/src/interfaces.ts index 1452cc6..a612496 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,4 +1,4 @@ -import { GitHubIssueResponse, GitHubUserResponse } from "./github-api"; +import { GitHubIssueResponse, GitHubContributor } from "./github-api"; export interface CommitInfo { commitSHA: string; @@ -15,5 +15,5 @@ export interface Release { name: string; date: string; commits: CommitInfo[]; - contributors?: GitHubUserResponse[]; + contributors?: GitHubContributor[]; } diff --git a/src/markdown-renderer.spec.ts b/src/markdown-renderer.spec.ts index 1659fe9..4ef3c8c 100644 --- a/src/markdown-renderer.spec.ts +++ b/src/markdown-renderer.spec.ts @@ -1,3 +1,4 @@ +import type { GithubAppInfo, GithubUserInfo } from "./github-api"; import { CommitInfo, Release } from "./interfaces"; import MarkdownRenderer from "./markdown-renderer"; @@ -120,12 +121,14 @@ describe("MarkdownRenderer", () => { const user1 = { login: "hzoo", name: "", + type: "User", html_url: "https://github.com/hzoo", }; const user2 = { login: "Turbo87", name: "Tobias Bieniek", + type: "User", html_url: "https://github.com/Turbo87", }; @@ -140,6 +143,7 @@ describe("MarkdownRenderer", () => { const result = renderer().renderContributor({ login: "foo", name: "", + type: "User", html_url: "http://github.com/foo", }); @@ -150,11 +154,23 @@ describe("MarkdownRenderer", () => { const result = renderer().renderContributor({ login: "foo", name: "Foo Bar", + type: "User", html_url: "http://github.com/foo", }); expect(result).toEqual("Foo Bar ([@foo](http://github.com/foo))"); }); + + it(`renders Copilot`, () => { + const result = renderer().renderContributor({ + login: "Copilot", + name: "Copilot", + slug: "copilot-swe-agent", + html_url: "https://github.com/apps/copilot-swe-agent", + } as GithubAppInfo); + + expect(result).toEqual("Copilot [Bot] ([@copilot-swe-agent](https://github.com/apps/copilot-swe-agent))"); + }); }); describe("groupByCategory", () => { @@ -230,7 +246,7 @@ describe("MarkdownRenderer", () => { categories: [":rocket: New Feature"], }, ], - contributors: [{ name: "Henry", login: "hzoo", html_url: "http://hzoo.com" }], + contributors: [{ name: "Henry", login: "hzoo", type: "User", html_url: "http://hzoo.com" }], }; const options = { categories: [":rocket: New Feature"], diff --git a/src/markdown-renderer.ts b/src/markdown-renderer.ts index 61544d3..4139eed 100644 --- a/src/markdown-renderer.ts +++ b/src/markdown-renderer.ts @@ -1,4 +1,4 @@ -import { GitHubUserResponse } from "./github-api"; +import { GitHubContributor, type GithubAppInfo, type GithubUserInfo } from "./github-api"; import { CommitInfo, Release } from "./interfaces"; const UNRELEASED_TAG = "___unreleased___"; @@ -114,16 +114,19 @@ export default class MarkdownRenderer { } } - public renderContributorList(contributors: GitHubUserResponse[]) { + public renderContributorList(contributors: GitHubContributor[]) { const renderedContributors = contributors.map(contributor => `- ${this.renderContributor(contributor)}`).sort(); return `#### Committers: ${contributors.length}\n${renderedContributors.join("\n")}`; } - public renderContributor(contributor: GitHubUserResponse): string { - const userNameAndLink = `[@${contributor.login}](${contributor.html_url})`; + public renderContributor(contributor: GitHubContributor): string { + const userName = (contributor as GithubAppInfo).slug ?? (contributor as GithubUserInfo).login; + const userNameAndLink = `[@${userName}](${contributor.html_url})`; + if (contributor.name) { - return `${contributor.name} (${userNameAndLink})`; + const name = contributor.name + (!("type" in contributor) ? " [Bot]" : ""); + return `${name} (${userNameAndLink})`; } else { return userNameAndLink; } diff --git a/tests/basic-acceptance.test.js b/tests/basic-acceptance.test.js index dbc48f2..e9dbf7f 100644 --- a/tests/basic-acceptance.test.js +++ b/tests/basic-acceptance.test.js @@ -19,7 +19,7 @@ describe.skipIf(!process.env.GITHUB_AUTH)("command line interface", () => { #### :rocket: Enhancement * \`github-changelog\` - * [#33](https://github.com/embroider-build/github-changelog/pull/33) support github enterpise url detection and env vars ([@patricklx](https://github.com/patricklx)) + * [#33](https://github.com/release-plan/github-changelog/pull/33) support github enterpise url detection and env vars ([@patricklx](https://github.com/patricklx)) #### Committers: 1 - Patrick Pircher ([@patricklx](https://github.com/patricklx)) @@ -29,10 +29,10 @@ describe.skipIf(!process.env.GITHUB_AUTH)("command line interface", () => { #### :house: Internal * \`github-changelog\` - * [#29](https://github.com/embroider-build/github-changelog/pull/29) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions)) + * [#29](https://github.com/release-plan/github-changelog/pull/29) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions)) #### Committers: 1 - - [@github-actions[bot]](https://github.com/apps/github-actions)" + - GitHub Actions [Bot] ([@github-actions](https://github.com/apps/github-actions))" `); }); @@ -46,15 +46,15 @@ describe.skipIf(!process.env.GITHUB_AUTH)("command line interface", () => { #### :rocket: Enhancement * \`github-changelog\` - * [#33](https://github.com/embroider-build/github-changelog/pull/33) support github enterpise url detection and env vars ([@patricklx](https://github.com/patricklx)) + * [#33](https://github.com/release-plan/github-changelog/pull/33) support github enterpise url detection and env vars ([@patricklx](https://github.com/patricklx)) #### :house: Internal * \`github-changelog\` - * [#29](https://github.com/embroider-build/github-changelog/pull/29) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions)) + * [#29](https://github.com/release-plan/github-changelog/pull/29) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions)) #### Committers: 2 - - Patrick Pircher ([@patricklx](https://github.com/patricklx)) - - [@github-actions[bot]](https://github.com/apps/github-actions)" + - GitHub Actions [Bot] ([@github-actions](https://github.com/apps/github-actions)) + - Patrick Pircher ([@patricklx](https://github.com/patricklx))" `); }); });