22// Verifies that all commits in a pull request are GPG-signed.
33// Posts a one-time VerificationBot comment if unverified commits are found.
44
5- // Configuration via environment variables
5+ // Sanitizes string input to prevent injection (uses Unicode property escape per Biome lint)
6+ function sanitizeString ( input ) {
7+ if ( typeof input !== 'string' ) return '' ;
8+ return input . replace ( / \p{ Cc} / gu, '' ) . trim ( ) ;
9+ }
10+
11+ // Validates URL format and returns fallback if invalid
12+ function sanitizeUrl ( input , fallback ) {
13+ const cleaned = sanitizeString ( input ) ;
14+ return / ^ h t t p s ? : \/ \/ [ ^ \s ] + $ / i. test ( cleaned ) ? cleaned : fallback ;
15+ }
16+
17+ // Configuration via environment variables (sanitized)
618const CONFIG = {
7- BOT_NAME : process . env . BOT_NAME || 'VerificationBot' ,
8- BOT_LOGIN : process . env . BOT_LOGIN || 'github-actions' ,
9- COMMENT_MARKER : process . env . COMMENT_MARKER || '[commit-verification-bot]' ,
10- SIGNING_GUIDE_URL : process . env . SIGNING_GUIDE_URL ||
11- 'https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md' ,
12- README_URL : process . env . README_URL ||
13- 'https://github.com/hiero-ledger/hiero-sdk-python/blob/main/README.md' ,
14- DISCORD_URL : process . env . DISCORD_URL ||
15- 'https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md' ,
16- TEAM_NAME : process . env . TEAM_NAME || 'Hiero Python SDK Team' ,
19+ BOT_NAME : sanitizeString ( process . env . BOT_NAME ) || 'VerificationBot' ,
20+ BOT_LOGIN : sanitizeString ( process . env . BOT_LOGIN ) || 'github-actions' ,
21+ COMMENT_MARKER : sanitizeString ( process . env . COMMENT_MARKER ) || '[commit-verification-bot]' ,
22+ SIGNING_GUIDE_URL : sanitizeUrl (
23+ process . env . SIGNING_GUIDE_URL ,
24+ 'https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md'
25+ ) ,
26+ README_URL : sanitizeUrl (
27+ process . env . README_URL ,
28+ 'https://github.com/hiero-ledger/hiero-sdk-python/blob/main/README.md'
29+ ) ,
30+ DISCORD_URL : sanitizeUrl (
31+ process . env . DISCORD_URL ,
32+ 'https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md'
33+ ) ,
34+ TEAM_NAME : sanitizeString ( process . env . TEAM_NAME ) || 'Hiero Python SDK Team' ,
1735 MAX_PAGES : ( ( ) => {
1836 const parsed = Number . parseInt ( process . env . MAX_PAGES ?? '5' , 10 ) ;
1937 return Number . isInteger ( parsed ) && parsed > 0 ? parsed : 5 ;
2038 } ) ( ) ,
2139} ;
2240
23- // Sanitizes string input to prevent injection (uses Unicode property escape per Biome lint)
24- function sanitizeString ( input ) {
25- if ( typeof input !== 'string' ) return '' ;
26- return input . replace ( / \p{ Cc} / gu, '' ) . trim ( ) ;
27- }
28-
2941// Validates PR number is a positive integer
3042function validatePRNumber ( prNumber ) {
3143 const num = parseInt ( prNumber , 10 ) ;
@@ -38,15 +50,29 @@ async function getCommitVerificationStatus(github, owner, repo, prNumber) {
3850
3951 const commits = [ ] ;
4052 let page = 0 ;
41- for await ( const response of github . paginate . iterator (
42- github . rest . pulls . listCommits ,
43- { owner, repo, pull_number : prNumber , per_page : 100 }
44- ) ) {
45- commits . push ( ...response . data ) ;
46- if ( ++ page >= CONFIG . MAX_PAGES ) {
47- console . log ( `[${ CONFIG . BOT_NAME } ] Reached MAX_PAGES (${ CONFIG . MAX_PAGES } ) limit` ) ;
48- break ;
53+ let truncated = false ;
54+
55+ try {
56+ for await ( const response of github . paginate . iterator (
57+ github . rest . pulls . listCommits ,
58+ { owner, repo, pull_number : prNumber , per_page : 100 }
59+ ) ) {
60+ commits . push ( ...response . data ) ;
61+ if ( ++ page >= CONFIG . MAX_PAGES ) {
62+ truncated = true ;
63+ console . warn ( `[${ CONFIG . BOT_NAME } ] Reached MAX_PAGES (${ CONFIG . MAX_PAGES } ) limit` ) ;
64+ break ;
65+ }
4966 }
67+ } catch ( error ) {
68+ console . error ( `[${ CONFIG . BOT_NAME } ] Failed to list commits` , {
69+ owner,
70+ repo,
71+ prNumber,
72+ status : error ?. status ,
73+ message : error ?. message ,
74+ } ) ;
75+ throw error ;
5076 }
5177
5278 const unverifiedCommits = commits . filter (
@@ -55,9 +81,16 @@ async function getCommitVerificationStatus(github, owner, repo, prNumber) {
5581
5682 console . log ( `[${ CONFIG . BOT_NAME } ] Found ${ commits . length } total, ${ unverifiedCommits . length } unverified` ) ;
5783
84+ // Fail-closed: if truncated and no unverified found, treat as potentially unverified
85+ const unverifiedCount = truncated && unverifiedCommits . length === 0
86+ ? 1
87+ : unverifiedCommits . length ;
88+
5889 return {
5990 total : commits . length ,
60- unverified : unverifiedCommits . length ,
91+ unverified : unverifiedCount ,
92+ unverifiedCommits,
93+ truncated,
6194 } ;
6295}
6396
@@ -74,34 +107,63 @@ async function hasExistingBotComment(github, owner, repo, prNumber) {
74107 ] ) ;
75108
76109 let page = 0 ;
77- for await ( const response of github . paginate . iterator (
78- github . rest . issues . listComments ,
79- { owner, repo, issue_number : prNumber , per_page : 100 }
80- ) ) {
81- // Early return if marker found
82- if ( response . data . some ( comment =>
83- botLogins . has ( comment . user ?. login ) &&
84- typeof comment . body === 'string' &&
85- comment . body . includes ( CONFIG . COMMENT_MARKER )
110+ try {
111+ for await ( const response of github . paginate . iterator (
112+ github . rest . issues . listComments ,
113+ { owner, repo, issue_number : prNumber , per_page : 100 }
86114 ) ) {
87- console . log ( `[${ CONFIG . BOT_NAME } ] Existing bot comment: true` ) ;
88- return true ;
89- }
90- if ( ++ page >= CONFIG . MAX_PAGES ) {
91- console . log ( `[${ CONFIG . BOT_NAME } ] Reached MAX_PAGES (${ CONFIG . MAX_PAGES } ) limit` ) ;
92- break ;
115+ // Early return if marker found
116+ if ( response . data . some ( comment =>
117+ botLogins . has ( comment . user ?. login ) &&
118+ typeof comment . body === 'string' &&
119+ comment . body . includes ( CONFIG . COMMENT_MARKER )
120+ ) ) {
121+ console . log ( `[${ CONFIG . BOT_NAME } ] Existing bot comment: true` ) ;
122+ return true ;
123+ }
124+ if ( ++ page >= CONFIG . MAX_PAGES ) {
125+ // Fail-safe: assume comment exists to prevent duplicates
126+ console . warn (
127+ `[${ CONFIG . BOT_NAME } ] Reached MAX_PAGES (${ CONFIG . MAX_PAGES } ) limit; assuming existing comment to avoid duplicates`
128+ ) ;
129+ return true ;
130+ }
93131 }
132+ } catch ( error ) {
133+ console . error ( `[${ CONFIG . BOT_NAME } ] Failed to list comments` , {
134+ owner,
135+ repo,
136+ prNumber,
137+ status : error ?. status ,
138+ message : error ?. message ,
139+ } ) ;
140+ throw error ;
94141 }
95142
96143 console . log ( `[${ CONFIG . BOT_NAME } ] Existing bot comment: false` ) ;
97144 return false ;
98145}
99146
100- // Builds the verification failure comment
101- function buildVerificationComment ( commitsUrl ) {
147+ // Builds the verification failure comment with unverified commit details
148+ function buildVerificationComment ( commitsUrl , unverifiedCommits = [ ] ) {
149+ // Build list of unverified commits (show first 10 max)
150+ const maxDisplay = 10 ;
151+ const commitList = unverifiedCommits . slice ( 0 , maxDisplay ) . map ( c => {
152+ const sha = c . sha ?. substring ( 0 , 7 ) || 'unknown' ;
153+ const msg = sanitizeString ( c . commit ?. message ?. split ( '\n' ) [ 0 ] || 'No message' ) . substring ( 0 , 50 ) ;
154+ return `- \`${ sha } \` ${ msg } ` ;
155+ } ) . join ( '\n' ) ;
156+
157+ const moreCommits = unverifiedCommits . length > maxDisplay
158+ ? `\n- ...and ${ unverifiedCommits . length - maxDisplay } more`
159+ : '' ;
160+
102161 return `${ CONFIG . COMMENT_MARKER }
103162Hi, this is ${ CONFIG . BOT_NAME } .
104- Your pull request cannot be merged as it has **unverified commits**.
163+ Your pull request cannot be merged as it has **${ unverifiedCommits . length } unverified commit(s)**:
164+
165+ ${ commitList } ${ moreCommits }
166+
105167View your commit verification status: [Commits Tab](${ sanitizeString ( commitsUrl ) } ).
106168
107169To achieve verified status, please read:
@@ -118,15 +180,15 @@ From the ${CONFIG.TEAM_NAME}`;
118180}
119181
120182// Posts verification failure comment on the PR with error handling
121- async function postVerificationComment ( github , owner , repo , prNumber , commitsUrl ) {
183+ async function postVerificationComment ( github , owner , repo , prNumber , commitsUrl , unverifiedCommits ) {
122184 console . log ( `[${ CONFIG . BOT_NAME } ] Posting verification failure comment...` ) ;
123185
124186 try {
125187 await github . rest . issues . createComment ( {
126188 owner,
127189 repo,
128190 issue_number : prNumber ,
129- body : buildVerificationComment ( commitsUrl ) ,
191+ body : buildVerificationComment ( commitsUrl , unverifiedCommits ) ,
130192 } ) ;
131193 console . log ( `[${ CONFIG . BOT_NAME } ] Comment posted on PR #${ prNumber } ` ) ;
132194 return true ;
@@ -164,7 +226,7 @@ async function main({ github, context }) {
164226
165227 try {
166228 // Get commit verification status
167- const { total, unverified } = await getCommitVerificationStatus ( github , owner , repo , prNumber ) ;
229+ const { total, unverified, unverifiedCommits } = await getCommitVerificationStatus ( github , owner , repo , prNumber ) ;
168230
169231 // All commits verified - success
170232 if ( unverified === 0 ) {
@@ -182,7 +244,7 @@ async function main({ github, context }) {
182244 console . log ( `[${ CONFIG . BOT_NAME } ] Bot already commented. Skipping duplicate.` ) ;
183245 } else {
184246 const commitsUrl = `https://github.com/${ owner } /${ repo } /pull/${ prNumber } /commits` ;
185- await postVerificationComment ( github , owner , repo , prNumber , commitsUrl ) ;
247+ await postVerificationComment ( github , owner , repo , prNumber , commitsUrl , unverifiedCommits ) ;
186248 }
187249
188250 return { success : false , unverifiedCount : unverified } ;
0 commit comments