@@ -33,6 +33,19 @@ export enum PR_STATUS {
3333 pending = 'pending' ,
3434}
3535
36+ /**
37+ * @See : https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-threads/update?view=azure-devops-rest-7.1#commentthreadstatus
38+ * */
39+ export enum THREAD_STATUS {
40+ active = 'active' ,
41+ pending = 'pending' ,
42+ fixed = 'fixed' ,
43+ wontFix = 'wontFix' ,
44+ closed = 'closed' ,
45+ byDesign = 'byDesign' ,
46+ unknown = 'unknown'
47+ }
48+
3649export abstract class PolicyCheck {
3750 protected checkName : string ;
3851 private readonly accessToken : string | undefined ;
@@ -72,7 +85,6 @@ export abstract class PolicyCheck {
7285 if ( text ) {
7386 await this . addCommentToPR ( `${ this . checkName } Results` , text ) ;
7487 }
75-
7688 }
7789
7890 protected async updatePRStatus ( state : PR_STATUS , description : string ) {
@@ -113,23 +125,31 @@ export abstract class PolicyCheck {
113125 }
114126 }
115127
128+ protected async getPreviousThreads ( ) : Promise < any [ ] > {
129+ if ( this . buildReason && this . buildReason !== 'PullRequest' ) return [ ] ;
130+ try {
131+ const apiUrl = `${ this . orgUrl } ${ this . project } /_apis/git/repositories/${ this . repositoryId } /pullRequests/${ this . pullRequestId } /threads?api-version=6.0` ;
132+ const response = await axios . get ( apiUrl , {
133+ headers : {
134+ 'Content-Type' : 'application/json' ,
135+ 'Authorization' : `Bearer ${ this . accessToken } `
136+ }
137+ } ) ;
138+ return response . data . value || [ ] ;
139+ }
140+ catch ( error : any ) {
141+ tl . error ( `Failed to get previous threads: ${ error . message } ` ) ;
142+ return [ ] ;
143+ }
144+ }
145+
116146 /**
117147 * Deletes existing SCANOSS comments for this check type from the PR
118148 * Identifies comments by the SCANOSS marker and check name in the content
119149 */
120150 private async deletePreviousComments ( title : string ) :Promise < void > {
121- if ( this . buildReason && this . buildReason !== 'PullRequest' ) return ;
122- try {
123- const apiUrl = `${ this . orgUrl } ${ this . project } /_apis/git/repositories/${ this . repositoryId } /pullRequests/${ this . pullRequestId } /threads?api-version=6.0` ;
124-
125- const response = await axios . get ( apiUrl , {
126- headers : {
127- 'Content-Type' : 'application/json' ,
128- 'Authorization' : `Bearer ${ this . accessToken } `
129- }
130- } ) ;
131-
132- const threads = response . data . value || [ ] ;
151+ try {
152+ const threads = await this . getPreviousThreads ( ) ;
133153 const scanossMarker = `SCANOSS - ${ title } ` ;
134154 tl . debug ( `Looking for threads with marker: ${ scanossMarker } ` ) ;
135155 for ( const thread of threads ) {
@@ -158,7 +178,7 @@ export abstract class PolicyCheck {
158178 }
159179 }
160180
161- protected async addCommentToPR ( title : string , content : string , threadStatus : string = ' pending' ) {
181+ protected async addCommentToPR ( title : string , content : string , threadStatus : THREAD_STATUS = THREAD_STATUS . pending ) {
162182 if ( this . buildReason && this . buildReason !== 'PullRequest' ) return ;
163183 try {
164184 // Delete previous comments for this check type
@@ -188,15 +208,7 @@ export abstract class PolicyCheck {
188208 // Update the thread status using PATCH endpoint
189209 const threadId = response . data . id ;
190210 if ( threadId ) {
191- const patchUrl = `${ this . orgUrl } ${ this . project } /_apis/git/repositories/${ this . repositoryId } /pullRequests/${ this . pullRequestId } /threads/${ threadId } ?api-version=7.1` ;
192- await axios . patch ( patchUrl , {
193- status : threadStatus
194- } , {
195- headers : {
196- 'Content-Type' : 'application/json' ,
197- 'Authorization' : `Bearer ${ this . accessToken } `
198- }
199- } ) ;
211+ await this . updateThreadStatus ( threadId , threadStatus ) ;
200212 tl . debug ( `Thread ${ threadId } status updated to: ${ threadStatus } ` ) ;
201213 }
202214 } catch ( error : any ) {
@@ -216,5 +228,50 @@ export abstract class PolicyCheck {
216228
217229 tl . command ( 'artifact.upload' , { artifactname : artifactName } , tempFilePath ) ;
218230 }
219-
231+
232+ /**
233+ * Updates the status of a pull request thread.
234+ *
235+ * @param threadId - The ID of the thread to update
236+ * @param threadStatus - The new status to set (e.g., closed, active)
237+ * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-threads/update?view=azure-devops-rest-7.1
238+ */
239+ protected async updateThreadStatus ( threadId : string , threadStatus : THREAD_STATUS ) {
240+ const patchUrl = `${ this . orgUrl } ${ this . project } /_apis/git/repositories/${ this . repositoryId } /pullRequests/${ this . pullRequestId } /threads/${ threadId } ?api-version=7.1` ;
241+ await axios . patch ( patchUrl , {
242+ status : threadStatus
243+ } , {
244+ headers : {
245+ 'Content-Type' : 'application/json' ,
246+ 'Authorization' : `Bearer ${ this . accessToken } `
247+ }
248+ } ) ;
249+ tl . debug ( `Thread ${ threadId } status updated to: ${ threadStatus } ` ) ;
250+ }
251+
252+ /**
253+ * Resolve previous SCANOSS policy threads when the policy check passes.
254+ *
255+ * Searches through all PR threads for comments containing the SCANOSS marker
256+ * for this check type. When found, marks the thread as fixed to indicate the
257+ * policy violation has been resolved.
258+ */
259+ protected async resolvePolicyThreads ( ) : Promise < void > {
260+ const threads = await this . getPreviousThreads ( ) ;
261+ const scanossMarker = `SCANOSS - ${ this . checkName } ` ;
262+ for ( const thread of threads ) {
263+ if ( thread . comments && thread . comments . length > 0 ) {
264+ for ( const comment of thread . comments ) {
265+ if ( comment . content && comment . content . includes ( scanossMarker ) ) {
266+ try {
267+ await this . updateThreadStatus ( thread . id , THREAD_STATUS . fixed ) ;
268+ } catch ( error : any ) {
269+ tl . warning ( `Failed to resolve thread ${ thread . id } : ${ error . message } ` ) ;
270+ }
271+ break ;
272+ }
273+ }
274+ }
275+ }
276+ }
220277}
0 commit comments