From a1e64d3d00ece972c5ad7a738b9fd4aac349601b Mon Sep 17 00:00:00 2001 From: Facundo Fernandez Date: Tue, 9 Dec 2025 13:26:30 -0300 Subject: [PATCH 1/2] Add remote wike and refactor --- ai-code-review/ADR/getAdrs.ts | 140 +++++++++++++++++++++++++++- ai-code-review/devOpsWikiService.ts | 4 +- ai-code-review/main.ts | 68 +------------- ai-code-review/task.json | 64 ++++++++++++- 4 files changed, 208 insertions(+), 68 deletions(-) diff --git a/ai-code-review/ADR/getAdrs.ts b/ai-code-review/ADR/getAdrs.ts index 43f8208..0bd34af 100644 --- a/ai-code-review/ADR/getAdrs.ts +++ b/ai-code-review/ADR/getAdrs.ts @@ -1,4 +1,6 @@ import { Repository } from "../repository"; +import tl = require("azure-pipelines-task-lib/task"); +import { DevOpsWikiService, DevOpsWikiOptions } from "../devOpsWikiService"; /** * Retrieves Architecture Decision Records (ADRs) from the repository. @@ -13,7 +15,7 @@ export async function getAdrs( adrsFolderPath: string, adrsExtensions: string[] = [] ): Promise { - let fileExtensions = ['.md', '.txt', '.html']; + let fileExtensions = [".md", ".txt", ".html"]; if (adrsExtensions.length) { fileExtensions = adrsExtensions; } @@ -25,3 +27,139 @@ export async function getAdrs( ); return adrContent; } + +export async function getAllAdrs(localRepo: Repository): Promise { + let adrContent: string[] = []; + + // Read settings from task inputs + // ADRs from local repository + const adrsLocalFolderPath = tl.getInput("adrsLocalFolderPath", false) || "adrs"; + const adrsLocalFileExtensions = tl.getInput("adrsLocalFileExtensions") || ""; + const reviewWithLocalADRs = tl.getBoolInput("reviewWithLocalADRs", false); + const adrRemoteFolderPath = tl.getInput("adrsRemoteFolderPath", false) || "adrs"; + + // ADRs from remote repository + const adrRemoteFileExtensions = tl.getInput("adrsRemoteFileExtensions") || ""; + const reviewWithRemoteADRs = tl.getBoolInput("reviewWithRemoteADRs", false); + const adrRemoteRepositoryUrl = tl.getInput("adrRemoteRepository", false) || ""; + + // ADRs from local DevOps Wiki + const reviewWithLocalWikiADRs = tl.getBoolInput("reviewWithLocalWikiADRs", false); + const adrsLocalWikiPath = tl.getInput("adrsLocalWikiPath", false) || "/"; + const adrsLocalWikiToken = tl.getInput("adrsLocalWikiToken", false) || ""; + const adrsLocalWikiId = tl.getInput("adrsLocalWikiId", false) || ""; + const adrsLocalProjectId = tl.getInput("adrsLocalProjectId", false) || ""; + + // ADRs from remote DevOps Wiki + const adrsRemoteWikiUrl = tl.getInput("adrsRemoteWikiUrl", false) || ""; + const reviewWithRemoteWikiADRs = tl.getBoolInput("reviewWithRemoteWikiADRs", false); + const adrsRemoteWikiPath = tl.getInput("adrsRemoteWikiPath", false) || "/"; + const adrsRemoteWikiToken = tl.getInput("adrsRemoteWikiToken", false) || ""; + const adrsRemoteWikiId = tl.getInput("adrsRemoteWikiId", false) || ""; + const adrsRemoteProjectId = tl.getInput("adrsRemoteProjectId", false) || ""; + + // Get ADRs from local repository + if (reviewWithLocalADRs) { + const adrsExtensions = getArrayFromCSV(adrsLocalFileExtensions); + adrContent = await getAdrs(localRepo, adrsLocalFolderPath, adrsExtensions); + console.info(`Found ${adrContent.length} ADRs to use in the review.`); + } + + if (reviewWithRemoteADRs) { + if (adrRemoteRepositoryUrl.trim() === "") { + tl.setResult( + tl.TaskResult.Failed, + "ADR Remote Repository URL must be provided when 'Review with Remote ADRs' is enabled." + ); + return adrContent; + } + + let remoteRepo = new Repository(undefined, adrRemoteRepositoryUrl); + try { + await remoteRepo.Clone(); + const adrsExtensions = getArrayFromCSV(adrRemoteFileExtensions); + const remoteAdrs = await getAdrs(remoteRepo, adrRemoteFolderPath, adrsExtensions); + adrContent = [...adrContent, ...remoteAdrs]; + console.info(`Found ${remoteAdrs.length} remote ADRs to use in the review.`); + } catch (e) { + tl.setResult(tl.TaskResult.Failed, `Failed to read ADRs from remote repository: ${e}`); + return adrContent; + } + } + + if (reviewWithLocalWikiADRs) { + if (adrsLocalWikiToken.trim() === "") { + tl.setResult( + tl.TaskResult.Failed, + "ADR Local Wiki Token must be provided when 'Review with Local Wiki ADRs' is enabled." + ); + return adrContent; + } + const options: DevOpsWikiOptions = { + token: adrsLocalWikiToken, + wikiId: adrsLocalWikiId && adrsLocalWikiId.trim().length > 0 ? adrsLocalWikiId : undefined, + projectId: + adrsLocalProjectId && adrsLocalProjectId.trim().length > 0 ? adrsLocalProjectId : undefined + }; + const devOpsWikiService = new DevOpsWikiService(options); + try { + const wikiAdrsContent = await devOpsWikiService.getPages(`${adrsLocalWikiPath}`); + console.info(`Found ${wikiAdrsContent.length} ADR pages in the wiki.`); + adrContent = [...adrContent, ...wikiAdrsContent]; + } catch (e) { + tl.setResult(tl.TaskResult.Failed, `Failed to read ADRs from DevOps Wiki: ${e}`); + return adrContent; + } + } + + if (reviewWithRemoteWikiADRs) { + if (adrsRemoteWikiUrl.trim() === "") { + tl.setResult( + tl.TaskResult.Failed, + "ADR Remote Wiki URL must be provided when 'Review with Remote Wiki ADRs' is enabled." + ); + return adrContent; + } + + if (adrsRemoteWikiToken.trim() === "") { + tl.setResult( + tl.TaskResult.Failed, + "ADR Remote Wiki Token must be provided when 'Review with Remote Wiki ADRs' is enabled." + ); + return adrContent; + } + + if (adrsRemoteProjectId.trim() === "") { + tl.setResult( + tl.TaskResult.Failed, + "ADR Remote Project ID must be provided when 'Review with Remote Wiki ADRs' is enabled." + ); + return adrContent; + } + + const options: DevOpsWikiOptions = { + collectionUri: adrsRemoteWikiUrl, + token: adrsRemoteWikiToken, + wikiId: adrsRemoteWikiId, + projectId: adrsRemoteProjectId + }; + const devOpsWikiService = new DevOpsWikiService(options); + try { + const wikiAdrsContent = await devOpsWikiService.getPages(`${adrsRemoteWikiPath}`); + console.info(`Found ${wikiAdrsContent.length} ADR pages in the remote wiki.`); + adrContent = [...adrContent, ...wikiAdrsContent]; + } catch (e) { + tl.setResult(tl.TaskResult.Failed, `Failed to read ADRs from remote DevOps Wiki: ${e}`); + return adrContent; + } + } + + return adrContent; +} + +function getArrayFromCSV(csv: string) { + if (!csv.trim()) { + return []; + } + return csv.split(","); +} diff --git a/ai-code-review/devOpsWikiService.ts b/ai-code-review/devOpsWikiService.ts index 2338171..7013f12 100644 --- a/ai-code-review/devOpsWikiService.ts +++ b/ai-code-review/devOpsWikiService.ts @@ -6,7 +6,7 @@ export interface DevOpsWikiOptions { collectionUri?: string; // e.g. https://dev.azure.com/{organization}/ projectId?: string; // Team Project Id or name wikiId?: string; // Wiki identifier (name or id). Defaults to repo name + ".wiki" - token?: string; // Personal access token or System.AccessToken + token: string; // Personal access token } export class DevOpsWikiService { @@ -17,7 +17,7 @@ export class DevOpsWikiService { private _httpsAgent: Agent; private _headers: HeadersInit; - constructor(options?: DevOpsWikiOptions) { + constructor(options: DevOpsWikiOptions) { this._collectionUri = options?.collectionUri || tl.getVariable("System.TeamFoundationCollectionUri") || ""; this._projectId = diff --git a/ai-code-review/main.ts b/ai-code-review/main.ts index ae33442..fea4fff 100644 --- a/ai-code-review/main.ts +++ b/ai-code-review/main.ts @@ -3,8 +3,7 @@ import { AzureOpenAI } from "openai"; import { ChatCompletion } from "./chatCompletion"; import { Repository } from "./repository"; import { PullRequest } from "./pullrequest"; -import { getAdrs } from "./ADR/getAdrs"; -import { DevOpsWikiService, DevOpsWikiOptions } from "./devOpsWikiService"; +import { getAllAdrs } from "./ADR/getAdrs"; import "@azure/openai/types"; export class Main { @@ -33,16 +32,7 @@ export class Main { const deploymentName = tl.getInput("azureOpenAiDeploymentName", true)!; const apiKey = tl.getInput("azureOpenAiApiKey", true)!; const apiVersion = tl.getInput("azureOpenAiApiVersion", true)!; - const adrsLocalFolderPath = tl.getInput("adrsLocalFolderPath", false) || "adrs"; - const adrsLocalFileExtensions = tl.getInput("adrsLocalFileExtensions") || ""; - const reviewWithLocalADRs = tl.getBoolInput("reviewWithLocalADRs", false); - const adrRemoteFolderPath = tl.getInput("adrsRemoteFolderPath", false) || "adrs"; - const adrRemoteFileExtensions = tl.getInput("adrsRemoteFileExtensions") || ""; - const reviewWithRemoteADRs = tl.getBoolInput("reviewWithRemoteADRs", false); - const adrRemoteRepositoryUrl = tl.getInput("adrRemoteRepository", false) || ""; - const reviewWithLocalWikiADRs = tl.getBoolInput("reviewWithLocalWikiADRs", false); - const adrsLocalWikiPath = tl.getInput("adrsLocalWikiPath", false) || "/"; - const adrsLocalWikiToken = tl.getInput("adrsLocalWikiToken", false) || ""; + const fileExtensions = tl.getInput("fileExtensions", false); const filesToExclude = tl.getInput("fileExcludes", false); const additionalPrompts = tl.getInput("additionalPrompts", false)?.split(","); @@ -76,55 +66,12 @@ export class Main { this._pullRequest = new PullRequest(); let filesToReview = await this._repository.GetChangedFiles(fileExtensions, filesToExclude); console.info(`Found ${filesToReview.length} changed files to review.`); - let adrContent: string[] = []; - if (reviewWithLocalADRs) { - const adrsExtensions = this.getArrayFromCSV(adrsLocalFileExtensions); - adrContent = await getAdrs(this._repository, adrsLocalFolderPath, adrsExtensions); - console.info(`Found ${adrContent.length} ADRs to use in the review.`); - } - if (reviewWithRemoteADRs) { - if (adrRemoteRepositoryUrl.trim() === "") { - tl.setResult( - tl.TaskResult.Failed, - "ADR Remote Repository URL must be provided when 'Review with Remote ADRs' is enabled." - ); - return; - } - let remoteRepo = new Repository(undefined, adrRemoteRepositoryUrl); - try { - await remoteRepo.Clone(); - const adrsExtensions = this.getArrayFromCSV(adrRemoteFileExtensions); - const remoteAdrs = await getAdrs(remoteRepo, adrRemoteFolderPath, adrsExtensions); - adrContent = [...adrContent, ...remoteAdrs]; - console.info(`Found ${remoteAdrs.length} remote ADRs to use in the review.`); - } catch (e) { - tl.setResult(tl.TaskResult.Failed, `Failed to read ADRs from remote repository: ${e}`); - return; - } - } - - if (reviewWithLocalWikiADRs) { - const options: DevOpsWikiOptions = { - token: - adrsLocalWikiToken && adrsLocalWikiToken.trim().length > 0 - ? adrsLocalWikiToken - : undefined - }; - const devOpsWikiService = new DevOpsWikiService(options); - try { - const wikiAdrsContent = await devOpsWikiService.getPages(`${adrsLocalWikiPath}`); - console.info(`Found ${wikiAdrsContent.length} ADR pages in the wiki.`); - adrContent = [...adrContent, ...wikiAdrsContent]; - } catch (e) { - tl.setResult(tl.TaskResult.Failed, `Failed to read ADRs from DevOps Wiki: ${e}`); - return; - } - } + const adrsContent = await getAllAdrs(this._repository); this._chatCompletion = new ChatCompletion( client, - adrContent, + adrsContent, tl.getBoolInput("reviewBugs", true), tl.getBoolInput("reviewPerformance", true), tl.getBoolInput("reviewBestPractices", true), @@ -197,13 +144,6 @@ export class Main { } tl.setResult(tl.TaskResult.Succeeded, "Pull Request reviewed."); } - - static getArrayFromCSV(csv: string) { - if (!csv.trim()) { - return []; - } - return csv.split(","); - } } Main.Main(); diff --git a/ai-code-review/task.json b/ai-code-review/task.json index ec1f12f..933c90b 100644 --- a/ai-code-review/task.json +++ b/ai-code-review/task.json @@ -121,7 +121,22 @@ "required": false, "helpMarkDown": "Path to the wiki pages containing Architecture Decision Records (ADRs)." }, - + { + "name": "adrsLocalWikiId", + "type": "string", + "label": "ADRs Wiki ID", + "defaultValue": "", + "required": false, + "helpMarkDown": "ID of the wiki pages containing Architecture Decision Records (ADRs)." + }, + { + "name": "adrsLocalProjectId", + "type": "string", + "label": "ADRs Wiki Project ID", + "defaultValue": "", + "required": false, + "helpMarkDown": "Project ID of the wiki pages containing Architecture Decision Records (ADRs)." + }, { "name": "adrsLocalWikiToken", "type": "string", @@ -130,6 +145,53 @@ "required": false, "helpMarkDown": "Token to access the wiki pages containing Architecture Decision Records (ADRs)." }, + { + "name": "reviewWithRemoteWikiADRs", + "type": "boolean", + "label": "Check with ADRs", + "defaultValue": false, + "helpMarkDown": "Specify whether to enable checking with Architecture Decision Records (ADRs) during the code review process.\n\n- Set to `true` to perform checks with ADRs.\n- Set to `false` to skip checks with ADRs.\n\nChecking with ADRs helps ensure that the code aligns with architectural decisions. Default value is `false`." + }, + { + "name": "adrsRemoteWikiPath", + "type": "string", + "label": "ADRs Wiki Path", + "defaultValue": "/", + "required": false, + "helpMarkDown": "Path to the wiki pages containing Architecture Decision Records (ADRs)." + }, + { + "name": "adrsRemoteWikiUrl", + "type": "string", + "label": "ADRs Wiki URL", + "defaultValue": "/", + "required": false, + "helpMarkDown": "URL to the wiki pages containing Architecture Decision Records (ADRs)." + }, + { + "name": "adrsRemoteWikiId", + "type": "string", + "label": "ADRs Wiki ID", + "defaultValue": "", + "required": false, + "helpMarkDown": "ID of the wiki pages containing Architecture Decision Records (ADRs)." + }, + { + "name": "adrsRemoteProjectId", + "type": "string", + "label": "ADRs Wiki Project ID", + "defaultValue": "", + "required": false, + "helpMarkDown": "Project ID of the wiki pages containing Architecture Decision Records (ADRs)." + }, + { + "name": "adrsRemoteWikiToken", + "type": "string", + "label": "ADRs Wiki Token", + "defaultValue": "", + "required": false, + "helpMarkDown": "Token to access the wiki pages containing Architecture Decision Records (ADRs)." + }, { "name": "reviewBugs", "type": "boolean", From 7577872387a235c7f4096f71ea474d4268bdb8e3 Mon Sep 17 00:00:00 2001 From: Facundo Fernandez Date: Tue, 9 Dec 2025 16:32:55 -0300 Subject: [PATCH 2/2] Update ADR table instructions --- ai-code-review/chatCompletion.ts | 4 +++- ai-code-review/package-lock.json | 4 ++-- ai-code-review/package.json | 2 +- ai-code-review/task.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- vss-extension.json | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ai-code-review/chatCompletion.ts b/ai-code-review/chatCompletion.ts index 185c681..9abc211 100644 --- a/ai-code-review/chatCompletion.ts +++ b/ai-code-review/chatCompletion.ts @@ -65,7 +65,7 @@ export class ChatCompletion { adrsContent.length > 0 ? `- Consider the following Architecture Decision Records (ADRs) in your review. \n - Create a summary of each ADR and how it impacts the code changes, in table format.\n - - You ALWAYS have to provide an ADR review table even if there are no comments related to ADRs.\n + - You ALWAYS have to provide an ADR REVIEW TABLE even if there are no comments related to ADRs, without exceptions.\n -ADRs review table example:\n | ADR Name | Comments | Files diff related | ADR validation | | --- | --- | --- | --- | @@ -81,6 +81,8 @@ export class ChatCompletion { If it is not possible to determine the relation between ADRs and code changes, respond with '⁉️ Unknown relation between ADRs and code changes.'\n All rows should be related to one ADR only. Cant be related to multiple ADRs. But can have multiple comments related to same ADR in the same row.\n A row cant be related to a unkown or none ADR. A row is always related to an ADR, and have an ADR name. The ADR name cant be empty, n/a or unkown\n + The ADR REVIEW TABLE is MANDATORY in every review, no matter what.\n + ALL ADRs provided must be included in the ADR REVIEW TABLE,excluding ONLY the templates used to create ADRs.\n diff --git a/ai-code-review/package-lock.json b/ai-code-review/package-lock.json index 07ec6ff..4b55c9b 100644 --- a/ai-code-review/package-lock.json +++ b/ai-code-review/package-lock.json @@ -1,12 +1,12 @@ { "name": "swdevflow-code-review", - "version": "1.0.36", + "version": "1.0.39", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "swdevflow-code-review", - "version": "1.0.36", + "version": "1.0.39", "license": "MIT", "dependencies": { "@azure/openai": "2.0.0-beta.2", diff --git a/ai-code-review/package.json b/ai-code-review/package.json index 74bff0a..91436cf 100644 --- a/ai-code-review/package.json +++ b/ai-code-review/package.json @@ -1,6 +1,6 @@ { "name": "swdevflow-code-review", - "version": "1.0.36", + "version": "1.0.39", "description": "", "main": "index.js", "scripts": { diff --git a/ai-code-review/task.json b/ai-code-review/task.json index 933c90b..c3e176d 100644 --- a/ai-code-review/task.json +++ b/ai-code-review/task.json @@ -8,7 +8,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 36 + "Patch": 39 }, "instanceNameFormat": "AI Code Review $(message)", "inputs": [ diff --git a/package-lock.json b/package-lock.json index b31e94b..5448df5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "swdevflow-code-review", - "version": "1.0.36", + "version": "1.0.39", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "swdevflow-code-review", - "version": "1.0.36", + "version": "1.0.39", "dependencies": { "vss-web-extension-sdk": "^5.141.0" } diff --git a/package.json b/package.json index e22e248..cea4e28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swdevflow-code-review", - "version": "1.0.36", + "version": "1.0.39", "description": "AI code review extension for Azure DevOps using Azure OpenAI services", "main": "main.js", "scripts": { diff --git a/vss-extension.json b/vss-extension.json index a844b7b..372ff3f 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "swdevflow-code-review", "publisher": "SWDevflow", - "version": "1.0.36", + "version": "1.0.39", "name": "AI Code Review Task", "description": "AI code review extension to review pull request changes using your own Azure OpenAI endpoints", "public": false,