2025-02-09 23:34:40 +01:00
/ *
* 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/.
* /
2025-08-02 23:13:50 +02:00
import 'dotenv/config' ;
2025-02-09 23:34:40 +01:00
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' ,
2025-08-02 23:13:50 +02:00
Authorization : ` Bearer ${ process . env . GITHUB_TOKEN } ` ,
2025-02-09 23:34:40 +01:00
} ,
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
2026-03-10 10:59:38 +01:00
. map ( ( commit ) = >
commit . authors . map ( ( author ) = > author . user ? . login ) . filter ( ( author ) : author is string = > ! ! author ) ,
)
2025-02-09 23:34:40 +01:00
. flat ( ) ,
) ,
] ;
export const createCommitChangelog = async ( prevReleaseLastCommitSha : string ) : Promise < 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 ] ,
} ) ) ;
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 ;
} ;