diff --git a/tools/scripts/createCommitChangelog.ts b/tools/scripts/createCommitChangelog.ts index 7bc9016a..3c906573 100644 --- a/tools/scripts/createCommitChangelog.ts +++ b/tools/scripts/createCommitChangelog.ts @@ -8,7 +8,7 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import tokens from './tokens.json'; +import { createCommitChangelog } from './release/CommitChangelog.utils.ts'; const { sha } = yargs(hideBin(process.argv)) .options({ @@ -24,211 +24,9 @@ if (!sha) { throw new Error('Sha of last commit of the previous release has to be passed!'); } -type GithubAuthor = { - name: string; - user: { login: string } | null; -}; - -type GithubPullRequest = { - number: string; - url: string; -}; - -type GithubCommit = { - oid: string; - url: string; - message: string; - authors: { - totalCount: number; - nodes: GithubAuthor[]; - }; - associatedPullRequests: { - nodes: GithubPullRequest[]; - }; -}; - -type CommitQueryResponse = { - data: { - repository: { - ref: { - target: { - history: { - totalCount: number; - pageInfo: { - hasNextPage: boolean; - endCursor: string; - }; - nodes: GithubCommit[]; - }; - }; - }; - }; - }; -}; - -type Commit = { - repoUrl: string; - revision: number; - url: string; - authors: GithubAuthor[]; - title: string; - pullRequest?: GithubPullRequest; -}; - -const fetchCommits = async ( - owner: string, - repo: string, - loadUntilSha: string, - endCursor?: string, -): Promise<{ totalRepoCommitCount: number; commits: GithubCommit[] }> => { - const query = `query ($owner: String!, $name: String!, $afterSha: String) { - repository(owner: $owner, name: $name) { - ref(qualifiedName: "master") { - target { - ... on Commit { - history(first: 100, after: $afterSha) { - totalCount - pageInfo { - hasNextPage - endCursor - } - nodes { - oid - # use "message" instead of "messageHeadline" because GitHub truncates titles if they are too long - message - url - authors(first: 100) { - totalCount - nodes { - name - user { - login - } - } - } - associatedPullRequests(first: 1) { - nodes { - number - url - } - } - } - } - } - } - } - } -}`; - - const variables = { - owner, - name: repo, - afterSha: endCursor, - }; - - const response = (await ( - await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${tokens.githubToken}`, - }, - body: JSON.stringify({ query, variables }), - }) - ).json()) as CommitQueryResponse; - - const repoHistory = response.data.repository.ref.target.history; - const { hasNextPage, endCursor: repoHistoryEndCursor } = repoHistory.pageInfo; - const commits = repoHistory.nodes; - const indexOfOldestCommitToLoad = commits.findIndex((commit) => commit.oid === loadUntilSha); - - const loadedAllCommits = indexOfOldestCommitToLoad !== -1; - if (loadedAllCommits) { - return { totalRepoCommitCount: repoHistory.totalCount, commits: commits.slice(0, indexOfOldestCommitToLoad) }; - } - - if (!hasNextPage) { - throw new Error(`No commit with sha "${loadUntilSha}" found!`); - } - - const loadedCommits = [ - ...commits, - ...(await fetchCommits(owner, repo, loadUntilSha, repoHistoryEndCursor)).commits, - ]; - return { totalRepoCommitCount: repoHistory.totalCount, commits: loadedCommits }; -}; - -const createCommitAuthorCredit = (authors: GithubAuthor[]): string => { - const authorsWithGithubAccount = authors.filter((author) => !!author.user?.login); - - if (!authorsWithGithubAccount.length) { - return `by @${authors[0].name}`; - } - - const commitAuthorsString = authorsWithGithubAccount - .map((author) => author.user!.login) - .reduce((authorCredit, author) => `${authorCredit}, @${author}`); - - return `by @${commitAuthorsString}`; -}; - -const createChangelogCommitLine = (commit: Commit): string => { - try { - const authorCredit = createCommitAuthorCredit(commit.authors); - const revision = `([r${commit.revision}](${commit.url}))`; - - if (!commit.pullRequest) { - return `${revision} ${commit.title} (${authorCredit})`; - } - - const title = commit.title.replace(/(.*) \(#[0-9]+\)$/g, '$1'); // remove the possible pr number from the title (e.g. "my commit title (#420)" => "my commit title") - const prId = commit.pullRequest.number; - const prUrl = commit.pullRequest.url; - const prLink = `[#${prId}](${prUrl})`; - - return `${revision} ${title} (${prLink} ${authorCredit})`; - } catch (e) { - console.log('Unexpected commit format', commit); - throw e; - } -}; - -const getContributors = (commits: Commit[]): string[] => [ - ...new Set( - commits - .map((commit) => commit.authors.map((author) => author.user?.login!).filter((author) => !!author)) - .flat(), - ), -]; - -const createChangelog = async (prevReleaseLastCommitSha: string) => { - const owner = 'Suwayomi'; - const repo = 'Suwayomi-WebUI'; - - const { totalRepoCommitCount: numberOfCommits, commits: githubCommits } = await fetchCommits( - owner, - repo, - prevReleaseLastCommitSha, - ); - - const commits: Commit[] = githubCommits.map((githubCommit, index) => ({ - repoUrl: `https://github.com/${owner}/${repo}`, - revision: numberOfCommits - index, - url: githubCommit.url, - authors: githubCommit.authors.nodes, - title: githubCommit.message.split('\n')[0], - pullRequest: githubCommit.associatedPullRequests.nodes[0], - })); - - const commitChangelogLines = commits.map(createChangelogCommitLine); - commitChangelogLines.forEach((commit) => console.log('-', commit)); - - const contributors = getContributors(commits); - const contributorString = contributors.reduce((authorCredit, author) => `${authorCredit}, @${author}`); - console.log(`\n\nContributors:\n@${contributorString}`); -}; - -createChangelog(sha).catch((error) => { - console.error('Failed due to', error); - process.exit(1); -}); +createCommitChangelog(sha) + .then(console.log) + .catch((error) => { + console.error('Failed due to', error); + process.exit(1); + }); diff --git a/tools/scripts/release/CommitChangelog.utils.ts b/tools/scripts/release/CommitChangelog.utils.ts new file mode 100644 index 00000000..e45a85fb --- /dev/null +++ b/tools/scripts/release/CommitChangelog.utils.ts @@ -0,0 +1,219 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import tokens from '../tokens.json'; + +type GithubAuthor = { + name: string; + user: { login: string } | null; +}; + +type GithubPullRequest = { + number: string; + url: string; +}; + +type GithubCommit = { + oid: string; + url: string; + message: string; + authors: { + totalCount: number; + nodes: GithubAuthor[]; + }; + associatedPullRequests: { + nodes: GithubPullRequest[]; + }; +}; + +type CommitQueryResponse = { + data: { + repository: { + ref: { + target: { + history: { + totalCount: number; + pageInfo: { + hasNextPage: boolean; + endCursor: string; + }; + nodes: GithubCommit[]; + }; + }; + }; + }; + }; +}; + +type Commit = { + repoUrl: string; + revision: number; + url: string; + authors: GithubAuthor[]; + title: string; + pullRequest?: GithubPullRequest; +}; + +const fetchCommits = async ( + owner: string, + repo: string, + loadUntilSha: string, + endCursor?: string, +): Promise<{ totalRepoCommitCount: number; commits: GithubCommit[] }> => { + const query = `query ($owner: String!, $name: String!, $afterSha: String) { + repository(owner: $owner, name: $name) { + ref(qualifiedName: "master") { + target { + ... on Commit { + history(first: 100, after: $afterSha) { + totalCount + pageInfo { + hasNextPage + endCursor + } + nodes { + oid + # use "message" instead of "messageHeadline" because GitHub truncates titles if they are too long + message + url + authors(first: 100) { + totalCount + nodes { + name + user { + login + } + } + } + associatedPullRequests(first: 1) { + nodes { + number + url + } + } + } + } + } + } + } + } +}`; + + const variables = { + owner, + name: repo, + afterSha: endCursor, + }; + + const response = (await ( + await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${tokens.githubToken}`, + }, + body: JSON.stringify({ query, variables }), + }) + ).json()) as CommitQueryResponse; + + const repoHistory = response.data.repository.ref.target.history; + const { hasNextPage, endCursor: repoHistoryEndCursor } = repoHistory.pageInfo; + const commits = repoHistory.nodes; + const indexOfOldestCommitToLoad = commits.findIndex((commit) => commit.oid === loadUntilSha); + + const loadedAllCommits = indexOfOldestCommitToLoad !== -1; + if (loadedAllCommits) { + return { totalRepoCommitCount: repoHistory.totalCount, commits: commits.slice(0, indexOfOldestCommitToLoad) }; + } + + if (!hasNextPage) { + throw new Error(`No commit with sha "${loadUntilSha}" found!`); + } + + const loadedCommits = [ + ...commits, + ...(await fetchCommits(owner, repo, loadUntilSha, repoHistoryEndCursor)).commits, + ]; + return { totalRepoCommitCount: repoHistory.totalCount, commits: loadedCommits }; +}; + +const createCommitAuthorCredit = (authors: GithubAuthor[]): string => { + const authorsWithGithubAccount = authors.filter((author) => !!author.user?.login); + + if (!authorsWithGithubAccount.length) { + return `by @${authors[0].name}`; + } + + const commitAuthorsString = authorsWithGithubAccount + .map((author) => author.user!.login) + .reduce((authorCredit, author) => `${authorCredit}, @${author}`); + + return `by @${commitAuthorsString}`; +}; + +const createChangelogCommitLine = (commit: Commit): string => { + try { + const authorCredit = createCommitAuthorCredit(commit.authors); + const revision = `([r${commit.revision}](${commit.url}))`; + + if (!commit.pullRequest) { + return `${revision} ${commit.title} (${authorCredit})`; + } + + const title = commit.title.replace(/(.*) \(#[0-9]+\)$/g, '$1'); // remove the possible pr number from the title (e.g. "my commit title (#420)" => "my commit title") + const prId = commit.pullRequest.number; + const prUrl = commit.pullRequest.url; + const prLink = `[#${prId}](${prUrl})`; + + return `${revision} ${title} (${prLink} ${authorCredit})`; + } catch (e) { + console.log('Unexpected commit format', commit); + throw e; + } +}; + +const getContributors = (commits: Commit[]): string[] => [ + ...new Set( + commits + .map((commit) => commit.authors.map((author) => author.user?.login!).filter((author) => !!author)) + .flat(), + ), +]; + +export const createCommitChangelog = async (prevReleaseLastCommitSha: string): Promise => { + const owner = 'Suwayomi'; + const repo = 'Suwayomi-WebUI'; + + const { totalRepoCommitCount: numberOfCommits, commits: githubCommits } = await fetchCommits( + owner, + repo, + prevReleaseLastCommitSha, + ); + + const commits: Commit[] = githubCommits.map((githubCommit, index) => ({ + repoUrl: `https://github.com/${owner}/${repo}`, + revision: numberOfCommits - index, + url: githubCommit.url, + authors: githubCommit.authors.nodes, + title: githubCommit.message.split('\n')[0], + pullRequest: githubCommit.associatedPullRequests.nodes[0], + })); + + let changelog = ''; + + const commitChangelogLines = commits.map(createChangelogCommitLine); + commitChangelogLines.forEach((commit) => { + changelog += `- ${commit}\n`; + }); + + const contributors = getContributors(commits); + const contributorString = contributors.reduce((authorCredit, author) => `${authorCredit}, @${author}`); + changelog += `\nContributors:\n@${contributorString}\n`; + + return changelog; +};