From d0ebf9b728003a1faff03ef21bad6608b902a09e Mon Sep 17 00:00:00 2001 From: Agustin Groh Date: Fri, 16 Jan 2026 12:38:00 -0300 Subject: [PATCH] chore(policy):SP-3822 mark policy threads as fixed when policy check pass --- CHANGELOG.md | 7 +- codescantask/package-lock.json | 4 +- codescantask/package.json | 2 +- .../policies/copyleft-policy-check.ts | 3 +- .../policies/dep-track-policy-check.ts | 1 + codescantask/policies/policy-check.ts | 105 ++++++++++++++---- .../policies/undeclared-policy-check.ts | 6 +- codescantask/task.json | 2 +- .../tests/copyleftPolicySuite.test.ts | 2 +- .../tests/undeclaredPolicySuite.test.ts | 2 +- vss-extension-dev.json | 2 +- vss-extension.json | 2 +- 12 files changed, 102 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38cc406..7fd0590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.5.0] - 2026-01-19 +### Added +- Mark policy threads as fixed when policy checks pass (copyleft, undeclared, and dependency track) + ## [1.4.0] - 2025-11-27 ### Changed - Updated security permission documentation @@ -39,4 +43,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.1.0]: https://github.com/scanoss/ado-code-scan/compare/v1.0.3...v1.1.0 [1.2.0]: https://github.com/scanoss/ado-code-scan/compare/v1.1.0...v1.2.0 [1.3.0]: https://github.com/scanoss/ado-code-scan/compare/v1.2.0...v1.3.0 -[1.4.0]: https://github.com/scanoss/ado-code-scan/compare/v1.3.0...v1.4.0 \ No newline at end of file +[1.4.0]: https://github.com/scanoss/ado-code-scan/compare/v1.3.0...v1.4.0 +[1.5.0]: https://github.com/scanoss/ado-code-scan/compare/v1.4.0...v1.5.0 \ No newline at end of file diff --git a/codescantask/package-lock.json b/codescantask/package-lock.json index 8ce2d8b..7fe43b4 100644 --- a/codescantask/package-lock.json +++ b/codescantask/package-lock.json @@ -1,12 +1,12 @@ { "name": "azure-devops-integration", - "version": "0.21.68", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "azure-devops-integration", - "version": "0.21.68", + "version": "1.5.0", "license": "ISC", "dependencies": { "axios": "^1.7.2", diff --git a/codescantask/package.json b/codescantask/package.json index 8e89173..265a1fa 100644 --- a/codescantask/package.json +++ b/codescantask/package.json @@ -1,6 +1,6 @@ { "name": "azure-devops-integration", - "version": "1.4.0", + "version": "1.5.0", "description": "", "main": "index.js", "scripts": { diff --git a/codescantask/policies/copyleft-policy-check.ts b/codescantask/policies/copyleft-policy-check.ts index 83aef21..a1dffdc 100644 --- a/codescantask/policies/copyleft-policy-check.ts +++ b/codescantask/policies/copyleft-policy-check.ts @@ -98,7 +98,8 @@ export class CopyleftPolicyCheck extends PolicyCheck { const results = tl.execSync(EXECUTABLE, args); if (results.code === 0) { - await this.success('### :white_check_mark: Policy Pass \n #### Not copyleft Licenses were found', undefined); + await this.success('### :white_check_mark: Policy Pass \n #### No copyleft licenses were found', undefined); + await this.resolvePolicyThreads(); return; } diff --git a/codescantask/policies/dep-track-policy-check.ts b/codescantask/policies/dep-track-policy-check.ts index 14c95df..b53ce59 100644 --- a/codescantask/policies/dep-track-policy-check.ts +++ b/codescantask/policies/dep-track-policy-check.ts @@ -256,6 +256,7 @@ export class DepTrackPolicyCheck extends PolicyCheck { successMessage += this.getUploadConfigurationHelp(); } await this.success(successMessage, undefined); + await this.resolvePolicyThreads(); return; } if (results.code === 1) { diff --git a/codescantask/policies/policy-check.ts b/codescantask/policies/policy-check.ts index ab32460..73f62d1 100644 --- a/codescantask/policies/policy-check.ts +++ b/codescantask/policies/policy-check.ts @@ -33,6 +33,19 @@ export enum PR_STATUS { pending = 'pending', } +/** + * @See: https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-threads/update?view=azure-devops-rest-7.1#commentthreadstatus + * */ +export enum THREAD_STATUS { + active = 'active', + pending = 'pending', + fixed = 'fixed', + wontFix = 'wontFix', + closed = 'closed', + byDesign = 'byDesign', + unknown = 'unknown' +} + export abstract class PolicyCheck { protected checkName: string; private readonly accessToken: string | undefined; @@ -72,7 +85,6 @@ export abstract class PolicyCheck { if (text) { await this.addCommentToPR(`${this.checkName} Results`, text); } - } protected async updatePRStatus(state: PR_STATUS, description: string){ @@ -113,23 +125,31 @@ export abstract class PolicyCheck { } } + protected async getPreviousThreads(): Promise { + if (this.buildReason && this.buildReason !== 'PullRequest') return []; + try { + const apiUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads?api-version=6.0`; + const response = await axios.get(apiUrl, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.accessToken}` + } + }); + return response.data.value || []; + } + catch (error: any) { + tl.error(`Failed to get previous threads: ${error.message}`); + return []; + } + } + /** * Deletes existing SCANOSS comments for this check type from the PR * Identifies comments by the SCANOSS marker and check name in the content */ private async deletePreviousComments(title: string):Promise { - if (this.buildReason && this.buildReason !== 'PullRequest') return; - try { - const apiUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads?api-version=6.0`; - - const response = await axios.get(apiUrl, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.accessToken}` - } - }); - - const threads = response.data.value || []; + try{ + const threads = await this.getPreviousThreads(); const scanossMarker = `SCANOSS - ${title}`; tl.debug(`Looking for threads with marker: ${scanossMarker}`); for (const thread of threads) { @@ -158,7 +178,7 @@ export abstract class PolicyCheck { } } - protected async addCommentToPR(title: string, content: string, threadStatus: string = 'pending') { + protected async addCommentToPR(title: string, content: string, threadStatus: THREAD_STATUS = THREAD_STATUS.pending) { if (this.buildReason && this.buildReason !== 'PullRequest') return; try { // Delete previous comments for this check type @@ -188,15 +208,7 @@ export abstract class PolicyCheck { // Update the thread status using PATCH endpoint const threadId = response.data.id; if (threadId) { - const patchUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads/${threadId}?api-version=7.1`; - await axios.patch(patchUrl, { - status: threadStatus - }, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.accessToken}` - } - }); + await this.updateThreadStatus(threadId, threadStatus); tl.debug(`Thread ${threadId} status updated to: ${threadStatus}`); } } catch (error: any) { @@ -216,5 +228,50 @@ export abstract class PolicyCheck { tl.command('artifact.upload', { artifactname: artifactName }, tempFilePath); } - + + /** + * Updates the status of a pull request thread. + * + * @param threadId - The ID of the thread to update + * @param threadStatus - The new status to set (e.g., closed, active) + * @see https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-threads/update?view=azure-devops-rest-7.1 + */ + protected async updateThreadStatus(threadId: string, threadStatus: THREAD_STATUS) { + const patchUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads/${threadId}?api-version=7.1`; + await axios.patch(patchUrl, { + status: threadStatus + }, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.accessToken}` + } + }); + tl.debug(`Thread ${threadId} status updated to: ${threadStatus}`); + } + + /** + * Resolve previous SCANOSS policy threads when the policy check passes. + * + * Searches through all PR threads for comments containing the SCANOSS marker + * for this check type. When found, marks the thread as fixed to indicate the + * policy violation has been resolved. + */ + protected async resolvePolicyThreads(): Promise { + const threads = await this.getPreviousThreads(); + const scanossMarker = `SCANOSS - ${this.checkName}`; + for (const thread of threads) { + if (thread.comments && thread.comments.length > 0) { + for (const comment of thread.comments) { + if (comment.content && comment.content.includes(scanossMarker)) { + try { + await this.updateThreadStatus(thread.id, THREAD_STATUS.fixed); + } catch (error: any) { + tl.warning(`Failed to resolve thread ${thread.id}: ${error.message}`); + } + break; + } + } + } + } + } } \ No newline at end of file diff --git a/codescantask/policies/undeclared-policy-check.ts b/codescantask/policies/undeclared-policy-check.ts index cdd02f3..abb2199 100644 --- a/codescantask/policies/undeclared-policy-check.ts +++ b/codescantask/policies/undeclared-policy-check.ts @@ -21,7 +21,7 @@ THE SOFTWARE. */ -import { PolicyCheck } from './policy-check'; +import {PolicyCheck} from './policy-check'; import {DEBUG, EXECUTABLE, OUTPUT_FILEPATH, REPO_DIR, RUNTIME_CONTAINER, SCANOSS_SETTINGS} from '../app.input'; import * as tl from 'azure-pipelines-task-lib'; import * as fs from 'fs'; @@ -76,7 +76,9 @@ export class UndeclaredPolicyCheck extends PolicyCheck { const results = tl.execSync(EXECUTABLE, args); if (results.code === 0) { - await this.success('### :white_check_mark: Policy Pass \n #### Not undeclared components were found', undefined); + tl.debug('No undeclared components were found'); + await this.success('### :white_check_mark: Policy Pass \n #### No undeclared components were found', undefined); + await this.resolvePolicyThreads(); return; } diff --git a/codescantask/task.json b/codescantask/task.json index 654d2e2..cbe18b6 100644 --- a/codescantask/task.json +++ b/codescantask/task.json @@ -9,7 +9,7 @@ "author": "SCANOSS", "version": { "Major": 1, - "Minor": 4, + "Minor": 5, "Patch": 0 }, "instanceNameFormat": "SCANOSS Code Scan", diff --git a/codescantask/tests/copyleftPolicySuite.test.ts b/codescantask/tests/copyleftPolicySuite.test.ts index 13ed4d6..b463f1c 100644 --- a/codescantask/tests/copyleftPolicySuite.test.ts +++ b/codescantask/tests/copyleftPolicySuite.test.ts @@ -184,7 +184,7 @@ describe('CopyleftPolicyCheck', () => { assert(summary !== undefined, 'Summary should not be undefined'); // Add your assertions here assert.equal(sanitize(summary),sanitize(`### :white_check_mark: Policy Pass - #### Not copyleft Licenses were found`)); + #### No copyleft licenses were found`)); }); it('Copyleft policy explicit licenses', async function () { diff --git a/codescantask/tests/undeclaredPolicySuite.test.ts b/codescantask/tests/undeclaredPolicySuite.test.ts index 9a1c9a8..dd07653 100644 --- a/codescantask/tests/undeclaredPolicySuite.test.ts +++ b/codescantask/tests/undeclaredPolicySuite.test.ts @@ -145,7 +145,7 @@ describe('Undeclared Policy Check Suite', () => { assert(summary !== undefined, 'Summary should not be undefined'); assert.equal(sanitize(summary),sanitize(`### :white_check_mark: Policy Pass - #### Not undeclared components were found`)); + #### No undeclared components were found`)); }); it("should add '--debug' flag to build arguments when DEBUG is enabled", function() { diff --git a/vss-extension-dev.json b/vss-extension-dev.json index 42814c7..ebd2d7f 100644 --- a/vss-extension-dev.json +++ b/vss-extension-dev.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "scanoss-code-scan-dev", "name": "SCANOSS Code Scan DEV", - "version": "0.21.68", + "version": "0.21.71", "publisher": "SCANOSS", "public": false, "targets": [ diff --git a/vss-extension.json b/vss-extension.json index 7cf0e72..0dd7cc7 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "scanoss-code-scan", "name": "SCANOSS Code Scan", - "version": "1.4.0", + "version": "1.5.0", "publisher": "SCANOSS", "public": true, "targets": [