Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
[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
4 changes: 2 additions & 2 deletions codescantask/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion codescantask/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azure-devops-integration",
"version": "1.4.0",
"version": "1.5.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion codescantask/policies/copyleft-policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions codescantask/policies/dep-track-policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
105 changes: 81 additions & 24 deletions codescantask/policies/policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -113,23 +125,31 @@ export abstract class PolicyCheck {
}
}

protected async getPreviousThreads(): Promise<any[]> {
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<void> {
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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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<void> {
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;
}
}
}
}
}
}
6 changes: 4 additions & 2 deletions codescantask/policies/undeclared-policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion codescantask/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"author": "SCANOSS",
"version": {
"Major": 1,
"Minor": 4,
"Minor": 5,
"Patch": 0
},
"instanceNameFormat": "SCANOSS Code Scan",
Expand Down
2 changes: 1 addition & 1 deletion codescantask/tests/copyleftPolicySuite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
2 changes: 1 addition & 1 deletion codescantask/tests/undeclaredPolicySuite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion vss-extension-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion vss-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down