From 2e6f14624fd86a3a053b1d0dfb30974f472a60fe Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Thu, 7 May 2026 14:26:25 +0530 Subject: [PATCH 01/10] Add MicrobotsLogAnalyzer custom ADO task support --- .gitignore | 2 + README.md | 4 + .../MicrobotsLogAnalyzerTask/index.js | 184 ++ .../package-lock.json | 1747 +++++++++++++++++ .../MicrobotsLogAnalyzerTask/package.json | 10 + .../MicrobotsLogAnalyzerTask/task.json | 79 + azure-pipelines/vss-extension.json | 33 + docs/azure-pipelines-log-analyzer.md | 77 + .../microbots-log-analyzer.yml | 27 + mkdocs.yml | 1 + 10 files changed, 2164 insertions(+) create mode 100644 azure-pipelines/MicrobotsLogAnalyzerTask/index.js create mode 100644 azure-pipelines/MicrobotsLogAnalyzerTask/package-lock.json create mode 100644 azure-pipelines/MicrobotsLogAnalyzerTask/package.json create mode 100644 azure-pipelines/MicrobotsLogAnalyzerTask/task.json create mode 100644 azure-pipelines/vss-extension.json create mode 100644 docs/azure-pipelines-log-analyzer.md create mode 100644 docs/examples/azure-pipelines/microbots-log-analyzer.yml diff --git a/.gitignore b/.gitignore index 7a440d3..177e7db 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ # Log files *.log +node_modules/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/README.md b/README.md index a28723e..26e0a79 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ The `WritingBot` will read and write the files inside `code` folder based on spe The MicroBots create a containerized environment and mount the specified directory with restricting the permissions to read-only or read/write based on Bot used. It ensures that the AI agents operate within defined boundaries which enhances security and control over code modifications as well as protecting the local environment. +## Azure Pipelines Log Analyzer + +Microbots includes a custom Azure DevOps task, `MicrobotsLogAnalyzer@0`, for analyzing logs with Azure OpenAI models through an Azure Resource Manager Service Connection. See [docs/azure-pipelines-log-analyzer.md](docs/azure-pipelines-log-analyzer.md) to publish and use the task. + #Legal Notice Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft’s Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. \ No newline at end of file diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js new file mode 100644 index 0000000..73b96b8 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -0,0 +1,184 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); +const tl = require("azure-pipelines-task-lib/task"); +const { loginAzureRM } = require("azure-pipelines-tasks-azure-arm-rest/azCliUtility"); + +const DEFAULT_API_VERSION = "2025-03-01-preview"; +const DEFAULT_TIMEOUT_SECONDS = "600"; +const VENV_NAME = "microbots-log-analyzer-venv"; +const VENV_READY_MARKER = ".microbots-venv-ready-v1"; + +function runCommand(command, args, env) { + const result = spawnSync(command, args, { stdio: "inherit", env: env || process.env }); + if (result.error) throw new Error(`Failed to run ${command}: ${result.error.message}`); + if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} -> exit ${result.status}`); +} + +function input(name, required) { + const value = tl.getInput(name, required); + return value ? value.trim() : value; +} + +function resolveLogPath(codebasePath, logFilePath) { + return path.isAbsolute(logFilePath) + ? path.resolve(logFilePath) + : path.resolve(codebasePath, logFilePath); +} + +function getInputs() { + const inputs = { + serviceConnection: input("serviceConnection", true), + deploymentName: input("deploymentName", true), + endpoint: input("endpoint", true), + apiVersion: input("apiVersion", false) || DEFAULT_API_VERSION, + codebasePath: tl.getPathInput("codebasePath", true, true), + logFilePath: input("logFilePath", true), + timeoutSeconds: input("timeoutSeconds", false) || DEFAULT_TIMEOUT_SECONDS, + }; + + validateInputs(inputs); + return inputs; +} + +function validateInputs(inputs) { + if (!fs.existsSync(inputs.codebasePath)) { + throw new Error(`codebasePath does not exist: ${inputs.codebasePath}`); + } + + if (!fs.statSync(inputs.codebasePath).isDirectory()) { + throw new Error(`codebasePath must be a directory: ${inputs.codebasePath}`); + } + + const logPath = resolveLogPath(inputs.codebasePath, inputs.logFilePath); + if (!fs.existsSync(logPath) || !fs.statSync(logPath).isFile()) { + throw new Error(`logFilePath does not exist: ${logPath}`); + } + inputs.logFilePath = logPath; + + try { + const endpoint = new URL(inputs.endpoint); + if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:") throw new Error(); + } catch (_) { + throw new Error(`endpoint must be a valid HTTP or HTTPS URL: ${inputs.endpoint}`); + } + + const timeoutSeconds = Number(inputs.timeoutSeconds); + if (!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds <= 0) { + throw new Error(`timeoutSeconds must be a positive integer: ${inputs.timeoutSeconds}`); + } + inputs.timeoutSeconds = String(timeoutSeconds); +} + +async function loginWithServiceConnection(serviceConnection) { + console.log("##[section]MicrobotsLogAnalyzer: authenticating with Azure service connection"); + const previousAzureOutput = process.env.AZURE_CORE_OUTPUT; + const originalLoc = tl.loc; + + process.env.AZURE_CORE_OUTPUT = "none"; + tl.loc = function loc(key, ...args) { + if (key === "LoginFailed" || key === "ErrorInSettingUpSubscription") return key; + return originalLoc(key, ...args); + }; + + try { + await loginAzureRM(serviceConnection); + } catch (error) { + throw new Error(`Azure service connection login failed for '${serviceConnection}': ${error.message || String(error)}`); + } finally { + tl.loc = originalLoc; + if (previousAzureOutput === undefined) delete process.env.AZURE_CORE_OUTPUT; + else process.env.AZURE_CORE_OUTPUT = previousAzureOutput; + } + + console.log("##[section]MicrobotsLogAnalyzer: Azure authentication complete"); +} + +function venvPythonPath(venvDir) { + return process.platform === "win32" + ? path.join(venvDir, "Scripts", "python.exe") + : path.join(venvDir, "bin", "python"); +} + +function setupVenv() { + const venvRoot = process.env.AGENT_TEMPDIRECTORY + || process.env.PIPELINE_WORKSPACE + || process.env.RUNNER_TEMP + || "/tmp"; + const venvDir = path.join(venvRoot, VENV_NAME); + const python = venvPythonPath(venvDir); + const venvReadyFile = path.join(venvDir, VENV_READY_MARKER); + + if (fs.existsSync(python) && fs.existsSync(venvReadyFile)) { + console.log(`##[section]MicrobotsLogAnalyzer: reusing Python environment at ${venvDir}`); + return python; + } + + if (fs.existsSync(venvDir)) fs.rmSync(venvDir, { recursive: true, force: true }); + + console.log(`##[section]MicrobotsLogAnalyzer: creating Python environment at ${venvDir}`); + runCommand("python3", ["-m", "venv", venvDir]); + console.log("Installing Python dependencies (microbots, Azure identity)..."); + runCommand(python, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"]); + runCommand(python, ["-m", "pip", "install", "--quiet", "microbots[azure_ad]"]); + fs.writeFileSync(venvReadyFile, new Date().toISOString()); + + return python; +} + +function microbotsEnvironment(inputs) { + return Object.assign({}, process.env, { + OPEN_AI_DEPLOYMENT_NAME: inputs.deploymentName, + OPEN_AI_END_POINT: inputs.endpoint, + OPEN_AI_API_VERSION: inputs.apiVersion, + AZURE_OPENAI_ENDPOINT: inputs.endpoint, + AZURE_OPENAI_API_VERSION: inputs.apiVersion, + AZURE_AUTH_METHOD: "azure_ad", + }); +} + +function runLogAnalyzer(python, inputs) { + const script = [ + "import os, sys, textwrap", + "from azure.identity import AzureCliCredential, get_bearer_token_provider", + "from microbots import LogAnalysisBot", + "codebase_path = os.path.abspath(sys.argv[1])", + "log_file_path = sys.argv[2]", + "timeout_seconds = int(sys.argv[3])", + "os.chdir(codebase_path)", + "print(f'MicrobotsLogAnalyzer: analyzing {log_file_path} with deployment {os.environ[\"OPEN_AI_DEPLOYMENT_NAME\"]}', flush=True)", + "print(f'MicrobotsLogAnalyzer: timeout is {timeout_seconds} seconds', flush=True)", + "token_provider = get_bearer_token_provider(AzureCliCredential(), 'https://cognitiveservices.azure.com/.default')", + "bot = LogAnalysisBot(model=f\"azure-openai/{os.environ['OPEN_AI_DEPLOYMENT_NAME']}\", folder_to_mount=codebase_path, token_provider=token_provider)", + "result = bot.run(file_name=log_file_path, timeout_in_seconds=timeout_seconds)", + "message = result.result or result.error or ''", + "print('##[section]MicrobotsLogAnalyzer: LLM analysis')", + "print('============================================================')", + "print('MICROBOTS LOG ANALYSIS')", + "print('============================================================')", + "for paragraph in str(message).splitlines() or ['']:", + " print(textwrap.fill(paragraph, width=125) if paragraph.strip() else '')", + "print('============================================================')", + "sys.exit(0 if result.status else 1)", + ].join("\n"); + + runCommand(python, ["-c", script, inputs.codebasePath, inputs.logFilePath, inputs.timeoutSeconds], microbotsEnvironment(inputs)); +} + +async function run() { + try { + const inputs = getInputs(); + await loginWithServiceConnection(inputs.serviceConnection); + const python = setupVenv(); + runLogAnalyzer(python, inputs); + tl.setResult(tl.TaskResult.Succeeded, "LogAnalysisBot completed"); + } catch (error) { + tl.setResult(tl.TaskResult.Failed, error.message || String(error)); + } finally { + try { spawnSync("az", ["account", "clear"], { stdio: "ignore" }); } catch (_) {} + } +} + +run(); diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/package-lock.json b/azure-pipelines/MicrobotsLogAnalyzerTask/package-lock.json new file mode 100644 index 0000000..db18649 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/package-lock.json @@ -0,0 +1,1747 @@ +{ + "name": "microbots-log-analyzer-task", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "microbots-log-analyzer-task", + "version": "0.1.0", + "dependencies": { + "azure-pipelines-task-lib": "^5.2.4", + "azure-pipelines-tasks-azure-arm-rest": "^3.274.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.9.0.tgz", + "integrity": "sha512-CzE+4PefDSJWj26zU7G1bKchlGRRHMBFreG4tAlGuzyI8hAPiYGobaJvZBgZBf6L63iphX7VH+ityL8VgEQz9Q==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.5.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.5.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.5.2.tgz", + "integrity": "sha512-GkDEL6TYo3HgT3UuqakdgE9PZfc1hMki6+Hwgy1uddb/EauvAKfu85vVhuofRSo22D1xTnWt8Ucwfg4vSCVwvA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.5.tgz", + "integrity": "sha512-ObTeMoNPmq19X3z40et9Xvs4ZoWVeJg43PZMRLG5iwVL+2nCtAerG3YTDItqPp1CfXNwmCXBbg8jn1DOx65c3g==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.5.2", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/azure-devops-node-api": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-15.1.3.tgz", + "integrity": "sha512-YMgxCjQDqBr//vGy658tXTrXAgS2BzIChJ2Mzrq+fzLK+dh42fODWO/kEQMmtp+Rw0jNgEoUA72cHYVBrxrjRw==", + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "2.1.0" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/azure-devops-node-api/node_modules/typed-rest-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.1.0.tgz", + "integrity": "sha512-Nel9aPbgSzRxfs1+4GoSB4wexCF+4Axlk7OSGVQCMa+4fWcyxIsN/YNmkp0xTT2iQzMD98h8yFLav/cNaULmRA==", + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "^6.10.3", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/azure-pipelines-task-lib": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-5.2.10.tgz", + "integrity": "sha512-6Wak5UB+Bs693LkPoE4gZTDNkNL5DDn2buQKMtvqIWh2ZCIJk+tc1nSzroGWntJa7USs8X3cYvdl0RiSsp4/5A==", + "license": "MIT", + "dependencies": { + "adm-zip": "^0.5.10", + "minimatch": "^3.1.5", + "nodejs-file-downloader": "^4.11.1", + "q": "^1.5.1", + "semver": "^5.7.2", + "shelljs": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "node_modules/azure-pipelines-tasks-azure-arm-rest": { + "version": "3.274.0", + "resolved": "https://registry.npmjs.org/azure-pipelines-tasks-azure-arm-rest/-/azure-pipelines-tasks-azure-arm-rest-3.274.0.tgz", + "integrity": "sha512-UDRQ4EMoEaIl5slo19r1xxoRUQqmYXnKZ1NFxx5Bndtnhf3BnLB63B2jCTFSY8OFd0r5j+xUQ1xyuXuYY6hNXQ==", + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.13.1", + "@types/jsonwebtoken": "^8.5.8", + "@types/mocha": "^5.2.7", + "@types/node": "^10.17.0", + "@types/q": "1.5.4", + "async-mutex": "^0.4.0", + "azure-devops-node-api": "^15.1.3", + "azure-pipelines-task-lib": "^5.2.4", + "https-proxy-agent": "^4.0.0", + "jsonwebtoken": "^9.0.3", + "msalv1": "npm:@azure/msal-node@^1.18.4", + "msalv2": "npm:@azure/msal-node@^2.7.0", + "msalv3": "npm:@azure/msal-node@^3.5.3", + "node-fetch": "^2.6.7", + "q": "1.5.1", + "typed-rest-client": "^2.2.0", + "xml2js": "0.6.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "license": "MIT", + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msalv1": { + "name": "@azure/msal-node", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.4.tgz", + "integrity": "sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==", + "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "13.3.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": "10 || 12 || 14 || 16 || 18" + } + }, + "node_modules/msalv1/node_modules/@azure/msal-common": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz", + "integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/msalv1/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/msalv2": { + "name": "@azure/msal-node", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.3.tgz", + "integrity": "sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.16.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/msalv2/node_modules/@azure/msal-common": { + "version": "14.16.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.1.tgz", + "integrity": "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/msalv2/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/msalv3": { + "name": "@azure/msal-node", + "version": "3.8.10", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.10.tgz", + "integrity": "sha512-0Hz7Kx4hs70KZWep/Rd7aw/qOLUF92wUOhn7ZsOuB5xNR/06NL1E2RAI9+UKH1FtvN8nD6mFjH7UKSjv6vOWvQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.17.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/msalv3/node_modules/@azure/msal-common": { + "version": "15.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.17.0.tgz", + "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/msalv3/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodejs-file-downloader": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz", + "integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==", + "license": "ISC", + "dependencies": { + "follow-redirects": "^1.15.6", + "https-proxy-agent": "^5.0.0", + "mime-types": "^2.1.27", + "sanitize-filename": "^1.6.3" + } + }, + "node_modules/nodejs-file-downloader/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/nodejs-file-downloader/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.10.0.tgz", + "integrity": "sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==", + "license": "BSD-3-Clause", + "dependencies": { + "execa": "^5.1.1", + "fast-glob": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/package.json b/azure-pipelines/MicrobotsLogAnalyzerTask/package.json new file mode 100644 index 0000000..2e4cfde --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/package.json @@ -0,0 +1,10 @@ +{ + "name": "microbots-log-analyzer-task", + "version": "0.1.0", + "description": "Azure DevOps custom task that runs the Microbots LogAnalysisBot.", + "main": "index.js", + "dependencies": { + "azure-pipelines-task-lib": "^5.2.4", + "azure-pipelines-tasks-azure-arm-rest": "^3.274.0" + } +} diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json new file mode 100644 index 0000000..6034cf6 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", + "id": "90104301-67ce-42b6-8f74-e520533e6a66", + "name": "MicrobotsLogAnalyzer", + "friendlyName": "Microbots Log Analyzer", + "description": "Run Microbots LogAnalysisBot against a log file using Azure OpenAI. Authenticates via the supplied Azure Resource Manager service connection.", + "category": "Utility", + "author": "Microbots contributors", + "version": { + "Major": 0, + "Minor": 1, + "Patch": 0 + }, + "instanceNameFormat": "Microbots Log Analyzer: $(logFilePath)", + "inputs": [ + { + "name": "serviceConnection", + "type": "connectedService:AzureRM", + "label": "Azure Resource Manager connection", + "required": true, + "helpMarkDown": "Service connection used to obtain a token for Azure OpenAI." + }, + { + "name": "deploymentName", + "type": "string", + "label": "Deployment name", + "required": true, + "helpMarkDown": "Azure OpenAI deployment name used by Microbots." + }, + { + "name": "endpoint", + "type": "string", + "label": "Azure OpenAI endpoint", + "required": true, + "helpMarkDown": "Azure OpenAI resource endpoint, for example https://my-resource.openai.azure.com/." + }, + { + "name": "apiVersion", + "type": "string", + "label": "API version", + "defaultValue": "2025-03-01-preview", + "required": false + }, + { + "name": "codebasePath", + "type": "filePath", + "label": "Codebase path", + "required": true, + "helpMarkDown": "Path to the repository or source folder Microbots can inspect while analyzing the log." + }, + { + "name": "logFilePath", + "type": "string", + "label": "Log file path", + "required": true, + "helpMarkDown": "Path to the log file. (Both Absolute Path and Relative to Codebase Path are supported.)" + }, + { + "name": "timeoutSeconds", + "type": "string", + "label": "Timeout in seconds", + "defaultValue": "600", + "required": false + } + ], + "execution": { + "Node20_1": { + "target": "index.js" + } + }, + "restrictions": { + "commands": { + "mode": "restricted" + }, + "settableVariables": { + "allowed": [] + } + } +} diff --git a/azure-pipelines/vss-extension.json b/azure-pipelines/vss-extension.json new file mode 100644 index 0000000..9cbb5b7 --- /dev/null +++ b/azure-pipelines/vss-extension.json @@ -0,0 +1,33 @@ +{ + "manifestVersion": 1, + "id": "microbots-log-analyzer", + "name": "Microbots Log Analyzer", + "version": "0.1.0", + "publisher": "microsoft", + "targets": [ + { + "id": "Microsoft.VisualStudio.Services" + } + ], + "description": "Azure Pipelines task for running Microbots log analysis with Azure OpenAI.", + "categories": [ + "Azure Pipelines" + ], + "files": [ + { + "path": "MicrobotsLogAnalyzerTask" + } + ], + "contributions": [ + { + "id": "microbots-log-analyzer-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "MicrobotsLogAnalyzerTask" + } + } + ] +} diff --git a/docs/azure-pipelines-log-analyzer.md b/docs/azure-pipelines-log-analyzer.md new file mode 100644 index 0000000..23a4b34 --- /dev/null +++ b/docs/azure-pipelines-log-analyzer.md @@ -0,0 +1,77 @@ +# MicrobotsLogAnalyzer Azure Pipelines Task + +`MicrobotsLogAnalyzer` is an Azure DevOps custom task that runs Microbots `LogAnalysisBot` against a log file. It authenticates to Azure OpenAI through an Azure Resource Manager Service Connection, creates an isolated Python venv on the build agent, installs `microbots[azure_ad]`, and prints the root-cause analysis into the pipeline logs. + +## Prerequisites + +- Azure DevOps organization where you can install custom extensions. +- Azure Resource Manager Service Connection with permission to request tokens for the Azure OpenAI resource. The pipeline must be authorized to use this service connection. +- Azure Pipelines agent with `azure-cli`, `python3`, `pip` and `python3 -m venv` support. +- Azure OpenAI deployment that works with Microbots and is reachable by the service connection. +- Node.js on the machine where you package and publish the extension. + +## Publish The Task + +From a clone of this repository: + +```bash +npm install -g tfx-cli + +cd azure-pipelines/MicrobotsLogAnalyzerTask +npm ci --omit=dev + +cd .. +tfx extension create --manifest-globs vss-extension.json +tfx extension publish --manifest-globs vss-extension.json --token --share-with +``` + +Update the `publisher` value in `vss-extension.json` before running `tfx extension create`. The task folder must contain `node_modules` when the VSIX is created, so run `npm ci --omit=dev` before packaging. + +After publishing, install the extension into the Azure DevOps organization that owns your pipelines. + +When publishing an update, increment both versions before packaging: + +- `azure-pipelines/vss-extension.json` controls the extension version. +- `azure-pipelines/MicrobotsLogAnalyzerTask/task.json` controls the task version shown to Azure Pipelines. + +You can use `tfx extension create --manifest-globs vss-extension.json --rev-version` to increment the extension patch version, but task behavior changes still need a `task.json` version bump. + +## Use It In A Pipeline + +See the complete sample pipeline at [docs/examples/azure-pipelines/microbots-log-analyzer.yml](examples/azure-pipelines/microbots-log-analyzer.yml). + +```yaml +- task: MicrobotsLogAnalyzer@0 + displayName: Analyze build log + inputs: + serviceConnection: my-azure-service-connection + deploymentName: my-azure-openai-deployment + endpoint: https://my-azure-openai-resource.openai.azure.com/ + codebasePath: $(Build.SourcesDirectory) + logFilePath: logs/build.log + timeoutSeconds: 600 +``` + +The log file must exist before `MicrobotsLogAnalyzer@0` runs. Relative `logFilePath` values are resolved from `codebasePath`; absolute paths are also supported. + +## Inputs + +| Input | Required | Default | Description | +|---|---:|---|---| +| `serviceConnection` | Yes | - | Azure Resource Manager Service Connection used for Azure CLI login. | +| `deploymentName` | Yes | - | Azure OpenAI deployment name. | +| `endpoint` | Yes | - | Azure OpenAI endpoint, for example `https://my-resource.openai.azure.com/`. | +| `apiVersion` | No | `2025-03-01-preview` | Azure OpenAI API version passed to Microbots. | +| `codebasePath` | Yes | - | Repository or source folder Microbots can inspect while analyzing the log. | +| `logFilePath` | Yes | - | Log file path. Use an absolute path, or a relative path resolved from `codebasePath`. | +| `timeoutSeconds` | No | `600` | Maximum time for `LogAnalysisBot.run()`. | + +## How It Works + +1. Azure Pipelines runs the task with the `Node20_1` task handler. +2. The task logs in with the supplied Azure Resource Manager Service Connection. +3. The task creates or reuses a virtual environment (`microbots-log-analyzer-venv`). +4. The task installs `microbots[azure_ad]` into that virtual environment. +5. A short Python runner creates `LogAnalysisBot` with `AzureCliCredential`, mounts `codebasePath` as context, passes `logFilePath` to `LogAnalysisBot.run()`, and prints the analysis result. + +The task clears the Azure CLI account at the end of the run. Its task manifest also uses Azure Pipelines command restrictions so analyzed log content cannot set arbitrary pipeline variables. diff --git a/docs/examples/azure-pipelines/microbots-log-analyzer.yml b/docs/examples/azure-pipelines/microbots-log-analyzer.yml new file mode 100644 index 0000000..d742f8e --- /dev/null +++ b/docs/examples/azure-pipelines/microbots-log-analyzer.yml @@ -0,0 +1,27 @@ +trigger: +- none + +jobs: +- job: MicrobotsLogAnalyzer + pool: + vmImage: ubuntu-latest + timeoutInMinutes: 30 + steps: + - checkout: self + + - bash: | + set -euo pipefail + command -v az >/dev/null 2>&1 || { echo '##[error]azure-cli is required on the agent'; exit 1; } + command -v python3 >/dev/null 2>&1 || { echo '##[error]python3 is required on the agent'; exit 1; } + python3 -m venv --help >/dev/null 2>&1 || { echo '##[error]python3 venv support is required'; exit 1; } + displayName: Check MicrobotsLogAnalyzer prerequisites + + - task: MicrobotsLogAnalyzer@0 + displayName: Analyze build log + inputs: + serviceConnection: my-azure-service-connection + deploymentName: my-azure-openai-deployment + endpoint: https://my-azure-openai-resource.openai.azure.com/ + codebasePath: $(Build.SourcesDirectory) + logFilePath: logs/build.log + timeoutSeconds: 600 diff --git a/mkdocs.yml b/mkdocs.yml index 4028ca9..dbc61de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - CopilotBot: copilot-bot.md - "Authentication Setup": authentication.md - "Azure Managed Identity & Service Connection Setup": guides/azure-managed-identity-setup.md + - Azure Pipelines Log Analyzer: azure-pipelines-log-analyzer.md - Tools: - "Custom Tool Integration Walkthrough": blog/guides/tesseract_ocr_tool_use.md - Blogs: From dc7143a2eb5c27517dff2d755917cb4c310e0f94 Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 11:57:06 +0530 Subject: [PATCH 02/10] Moved Microbots script to separate python file --- .../MicrobotsLogAnalyzerTask/index.js | 31 +++---------- .../log_analyzer_runner.py | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index 73b96b8..cfc7f49 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -140,31 +140,12 @@ function microbotsEnvironment(inputs) { } function runLogAnalyzer(python, inputs) { - const script = [ - "import os, sys, textwrap", - "from azure.identity import AzureCliCredential, get_bearer_token_provider", - "from microbots import LogAnalysisBot", - "codebase_path = os.path.abspath(sys.argv[1])", - "log_file_path = sys.argv[2]", - "timeout_seconds = int(sys.argv[3])", - "os.chdir(codebase_path)", - "print(f'MicrobotsLogAnalyzer: analyzing {log_file_path} with deployment {os.environ[\"OPEN_AI_DEPLOYMENT_NAME\"]}', flush=True)", - "print(f'MicrobotsLogAnalyzer: timeout is {timeout_seconds} seconds', flush=True)", - "token_provider = get_bearer_token_provider(AzureCliCredential(), 'https://cognitiveservices.azure.com/.default')", - "bot = LogAnalysisBot(model=f\"azure-openai/{os.environ['OPEN_AI_DEPLOYMENT_NAME']}\", folder_to_mount=codebase_path, token_provider=token_provider)", - "result = bot.run(file_name=log_file_path, timeout_in_seconds=timeout_seconds)", - "message = result.result or result.error or ''", - "print('##[section]MicrobotsLogAnalyzer: LLM analysis')", - "print('============================================================')", - "print('MICROBOTS LOG ANALYSIS')", - "print('============================================================')", - "for paragraph in str(message).splitlines() or ['']:", - " print(textwrap.fill(paragraph, width=125) if paragraph.strip() else '')", - "print('============================================================')", - "sys.exit(0 if result.status else 1)", - ].join("\n"); - - runCommand(python, ["-c", script, inputs.codebasePath, inputs.logFilePath, inputs.timeoutSeconds], microbotsEnvironment(inputs)); + const scriptPath = path.join(__dirname, "log_analyzer_runner.py"); + runCommand( + python, + [scriptPath, inputs.codebasePath, inputs.logFilePath, inputs.timeoutSeconds], + microbotsEnvironment(inputs) + ); } async function run() { diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py new file mode 100644 index 0000000..f575586 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py @@ -0,0 +1,46 @@ +import os +import sys +import textwrap + +from azure.identity import AzureCliCredential, get_bearer_token_provider +from microbots import LogAnalysisBot + + +def main(): + codebase_path = os.path.abspath(sys.argv[1]) + log_file_path = sys.argv[2] + timeout_seconds = int(sys.argv[3]) + + os.chdir(codebase_path) + print( + f"MicrobotsLogAnalyzer: analyzing {log_file_path} with deployment " + f"{os.environ['OPEN_AI_DEPLOYMENT_NAME']}", + flush=True, + ) + print(f"MicrobotsLogAnalyzer: timeout is {timeout_seconds} seconds", flush=True) + + token_provider = get_bearer_token_provider( + AzureCliCredential(), + "https://cognitiveservices.azure.com/.default", + ) + bot = LogAnalysisBot( + model=f"azure-openai/{os.environ['OPEN_AI_DEPLOYMENT_NAME']}", + folder_to_mount=codebase_path, + token_provider=token_provider, + ) + result = bot.run(file_name=log_file_path, timeout_in_seconds=timeout_seconds) + message = result.result or result.error or "" + + print("##[section]MicrobotsLogAnalyzer: LLM analysis") + print("============================================================") + print("MICROBOTS LOG ANALYSIS") + print("============================================================") + for paragraph in str(message).splitlines() or [""]: + print(textwrap.fill(paragraph, width=125) if paragraph.strip() else "") + print("============================================================") + + return 0 if result.status else 1 + + +if __name__ == "__main__": + sys.exit(main()) From 7fc7e6c581261b19b0ad1076bf5741d349fdde23 Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 12:11:33 +0530 Subject: [PATCH 03/10] Removed the redundant (due to token_provider) azure_auth_method ENV --- azure-pipelines/MicrobotsLogAnalyzerTask/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index cfc7f49..c3572d7 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -135,7 +135,6 @@ function microbotsEnvironment(inputs) { OPEN_AI_API_VERSION: inputs.apiVersion, AZURE_OPENAI_ENDPOINT: inputs.endpoint, AZURE_OPENAI_API_VERSION: inputs.apiVersion, - AZURE_AUTH_METHOD: "azure_ad", }); } From 27f27da7538e918ef0701b7e259b25f082c8f26c Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 12:26:48 +0530 Subject: [PATCH 04/10] Add support for max_iterations parameter --- .../MicrobotsLogAnalyzerTask/index.js | 19 ++++++++++++++----- .../log_analyzer_runner.py | 12 +++++++++++- .../MicrobotsLogAnalyzerTask/task.json | 7 +++++++ docs/azure-pipelines-log-analyzer.md | 4 +++- .../microbots-log-analyzer.yml | 1 + 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index c3572d7..11ce19f 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -37,6 +37,7 @@ function getInputs() { codebasePath: tl.getPathInput("codebasePath", true, true), logFilePath: input("logFilePath", true), timeoutSeconds: input("timeoutSeconds", false) || DEFAULT_TIMEOUT_SECONDS, + maxIterations: input("maxIterations", false), }; validateInputs(inputs); @@ -70,6 +71,14 @@ function validateInputs(inputs) { throw new Error(`timeoutSeconds must be a positive integer: ${inputs.timeoutSeconds}`); } inputs.timeoutSeconds = String(timeoutSeconds); + + if (inputs.maxIterations) { + const maxIterations = Number(inputs.maxIterations); + if (!Number.isSafeInteger(maxIterations) || maxIterations <= 0) { + throw new Error(`maxIterations must be a positive integer: ${inputs.maxIterations}`); + } + inputs.maxIterations = String(maxIterations); + } } async function loginWithServiceConnection(serviceConnection) { @@ -140,11 +149,11 @@ function microbotsEnvironment(inputs) { function runLogAnalyzer(python, inputs) { const scriptPath = path.join(__dirname, "log_analyzer_runner.py"); - runCommand( - python, - [scriptPath, inputs.codebasePath, inputs.logFilePath, inputs.timeoutSeconds], - microbotsEnvironment(inputs) - ); + const args = [scriptPath, inputs.codebasePath, inputs.logFilePath, inputs.timeoutSeconds]; + + if (inputs.maxIterations) args.push(inputs.maxIterations); + + runCommand(python, args, microbotsEnvironment(inputs)); } async function run() { diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py index f575586..1e60e7a 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py @@ -10,6 +10,7 @@ def main(): codebase_path = os.path.abspath(sys.argv[1]) log_file_path = sys.argv[2] timeout_seconds = int(sys.argv[3]) + max_iterations = int(sys.argv[4]) if len(sys.argv) > 4 else None os.chdir(codebase_path) print( @@ -18,6 +19,8 @@ def main(): flush=True, ) print(f"MicrobotsLogAnalyzer: timeout is {timeout_seconds} seconds", flush=True) + if max_iterations is not None: + print(f"MicrobotsLogAnalyzer: max iterations is {max_iterations}", flush=True) token_provider = get_bearer_token_provider( AzureCliCredential(), @@ -28,7 +31,14 @@ def main(): folder_to_mount=codebase_path, token_provider=token_provider, ) - result = bot.run(file_name=log_file_path, timeout_in_seconds=timeout_seconds) + run_kwargs = { + "file_name": log_file_path, + "timeout_in_seconds": timeout_seconds, + } + if max_iterations is not None: + run_kwargs["max_iterations"] = max_iterations + + result = bot.run(**run_kwargs) message = result.result or result.error or "" print("##[section]MicrobotsLogAnalyzer: LLM analysis") diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json index 6034cf6..6ca1634 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json @@ -61,6 +61,13 @@ "label": "Timeout in seconds", "defaultValue": "600", "required": false + }, + { + "name": "maxIterations", + "type": "string", + "label": "Maximum iterations", + "required": false, + "helpMarkDown": "Optional maximum number of Microbots iterations." } ], "execution": { diff --git a/docs/azure-pipelines-log-analyzer.md b/docs/azure-pipelines-log-analyzer.md index 23a4b34..51f84c6 100644 --- a/docs/azure-pipelines-log-analyzer.md +++ b/docs/azure-pipelines-log-analyzer.md @@ -50,6 +50,7 @@ See the complete sample pipeline at [docs/examples/azure-pipelines/microbots-log codebasePath: $(Build.SourcesDirectory) logFilePath: logs/build.log timeoutSeconds: 600 + maxIterations: 20 ``` The log file must exist before `MicrobotsLogAnalyzer@0` runs. Relative `logFilePath` values are resolved from `codebasePath`; absolute paths are also supported. @@ -65,6 +66,7 @@ The log file must exist before `MicrobotsLogAnalyzer@0` runs. Relative `logFileP | `codebasePath` | Yes | - | Repository or source folder Microbots can inspect while analyzing the log. | | `logFilePath` | Yes | - | Log file path. Use an absolute path, or a relative path resolved from `codebasePath`. | | `timeoutSeconds` | No | `600` | Maximum time for `LogAnalysisBot.run()`. | +| `maxIterations` | No | LogAnalysisBot default | Maximum number of Microbots iterations. Leave unset to use the default from `LogAnalysisBot.run()`. | ## How It Works @@ -72,6 +74,6 @@ The log file must exist before `MicrobotsLogAnalyzer@0` runs. Relative `logFileP 2. The task logs in with the supplied Azure Resource Manager Service Connection. 3. The task creates or reuses a virtual environment (`microbots-log-analyzer-venv`). 4. The task installs `microbots[azure_ad]` into that virtual environment. -5. A short Python runner creates `LogAnalysisBot` with `AzureCliCredential`, mounts `codebasePath` as context, passes `logFilePath` to `LogAnalysisBot.run()`, and prints the analysis result. +5. A short Python runner creates `LogAnalysisBot` with `AzureCliCredential`, mounts `codebasePath` as context, passes `logFilePath`, optional `maxIterations`, and `timeoutSeconds` to `LogAnalysisBot.run()`, and prints the analysis result. The task clears the Azure CLI account at the end of the run. Its task manifest also uses Azure Pipelines command restrictions so analyzed log content cannot set arbitrary pipeline variables. diff --git a/docs/examples/azure-pipelines/microbots-log-analyzer.yml b/docs/examples/azure-pipelines/microbots-log-analyzer.yml index d742f8e..0e354e4 100644 --- a/docs/examples/azure-pipelines/microbots-log-analyzer.yml +++ b/docs/examples/azure-pipelines/microbots-log-analyzer.yml @@ -25,3 +25,4 @@ jobs: codebasePath: $(Build.SourcesDirectory) logFilePath: logs/build.log timeoutSeconds: 600 + maxIterations: 20 From c44d0b1d5e686c3aae76b0fada3d6abea887c9ba Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 12:32:37 +0530 Subject: [PATCH 05/10] Removed API Version Default and added it as required param --- azure-pipelines/MicrobotsLogAnalyzerTask/index.js | 3 +-- azure-pipelines/MicrobotsLogAnalyzerTask/task.json | 4 ++-- docs/azure-pipelines-log-analyzer.md | 3 ++- docs/examples/azure-pipelines/microbots-log-analyzer.yml | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index 11ce19f..3db7813 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -6,7 +6,6 @@ const { spawnSync } = require("child_process"); const tl = require("azure-pipelines-task-lib/task"); const { loginAzureRM } = require("azure-pipelines-tasks-azure-arm-rest/azCliUtility"); -const DEFAULT_API_VERSION = "2025-03-01-preview"; const DEFAULT_TIMEOUT_SECONDS = "600"; const VENV_NAME = "microbots-log-analyzer-venv"; const VENV_READY_MARKER = ".microbots-venv-ready-v1"; @@ -33,7 +32,7 @@ function getInputs() { serviceConnection: input("serviceConnection", true), deploymentName: input("deploymentName", true), endpoint: input("endpoint", true), - apiVersion: input("apiVersion", false) || DEFAULT_API_VERSION, + apiVersion: input("apiVersion", true), codebasePath: tl.getPathInput("codebasePath", true, true), logFilePath: input("logFilePath", true), timeoutSeconds: input("timeoutSeconds", false) || DEFAULT_TIMEOUT_SECONDS, diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json index 6ca1634..2952205 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json @@ -38,8 +38,8 @@ "name": "apiVersion", "type": "string", "label": "API version", - "defaultValue": "2025-03-01-preview", - "required": false + "required": true, + "helpMarkDown": "Azure OpenAI API version, for example 2025-03-01-preview." }, { "name": "codebasePath", diff --git a/docs/azure-pipelines-log-analyzer.md b/docs/azure-pipelines-log-analyzer.md index 51f84c6..d195f54 100644 --- a/docs/azure-pipelines-log-analyzer.md +++ b/docs/azure-pipelines-log-analyzer.md @@ -47,6 +47,7 @@ See the complete sample pipeline at [docs/examples/azure-pipelines/microbots-log serviceConnection: my-azure-service-connection deploymentName: my-azure-openai-deployment endpoint: https://my-azure-openai-resource.openai.azure.com/ + apiVersion: 2025-03-01-preview codebasePath: $(Build.SourcesDirectory) logFilePath: logs/build.log timeoutSeconds: 600 @@ -62,7 +63,7 @@ The log file must exist before `MicrobotsLogAnalyzer@0` runs. Relative `logFileP | `serviceConnection` | Yes | - | Azure Resource Manager Service Connection used for Azure CLI login. | | `deploymentName` | Yes | - | Azure OpenAI deployment name. | | `endpoint` | Yes | - | Azure OpenAI endpoint, for example `https://my-resource.openai.azure.com/`. | -| `apiVersion` | No | `2025-03-01-preview` | Azure OpenAI API version passed to Microbots. | +| `apiVersion` | Yes | - | Azure OpenAI API version passed to Microbots, for example `2025-03-01-preview`. | | `codebasePath` | Yes | - | Repository or source folder Microbots can inspect while analyzing the log. | | `logFilePath` | Yes | - | Log file path. Use an absolute path, or a relative path resolved from `codebasePath`. | | `timeoutSeconds` | No | `600` | Maximum time for `LogAnalysisBot.run()`. | diff --git a/docs/examples/azure-pipelines/microbots-log-analyzer.yml b/docs/examples/azure-pipelines/microbots-log-analyzer.yml index 0e354e4..9b8e8a6 100644 --- a/docs/examples/azure-pipelines/microbots-log-analyzer.yml +++ b/docs/examples/azure-pipelines/microbots-log-analyzer.yml @@ -22,6 +22,7 @@ jobs: serviceConnection: my-azure-service-connection deploymentName: my-azure-openai-deployment endpoint: https://my-azure-openai-resource.openai.azure.com/ + apiVersion: 2025-03-01-preview codebasePath: $(Build.SourcesDirectory) logFilePath: logs/build.log timeoutSeconds: 600 From 9049dd8f823d2a46725c151266ff57181c498391 Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 13:31:46 +0530 Subject: [PATCH 06/10] Only allow HTTPS endpoints for Azure OpenAI --- azure-pipelines/MicrobotsLogAnalyzerTask/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index 3db7813..0ac88f5 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -60,9 +60,9 @@ function validateInputs(inputs) { try { const endpoint = new URL(inputs.endpoint); - if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:") throw new Error(); + if (endpoint.protocol !== "https:") throw new Error(); } catch (_) { - throw new Error(`endpoint must be a valid HTTP or HTTPS URL: ${inputs.endpoint}`); + throw new Error(`endpoint must be a valid HTTPS URL: ${inputs.endpoint}`); } const timeoutSeconds = Number(inputs.timeoutSeconds); From 4aede03ad2a9cd5981809da59f3c048f25645457 Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 13:40:03 +0530 Subject: [PATCH 07/10] Added ServiceConnection as an Alias and renamed to AzureSubscription --- azure-pipelines/MicrobotsLogAnalyzerTask/index.js | 8 +++++++- azure-pipelines/MicrobotsLogAnalyzerTask/task.json | 7 +++++-- docs/azure-pipelines-log-analyzer.md | 4 ++-- docs/examples/azure-pipelines/microbots-log-analyzer.yml | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index 0ac88f5..cb8c9fd 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -21,6 +21,12 @@ function input(name, required) { return value ? value.trim() : value; } +function azureSubscriptionInput() { + const value = input("azureSubscription", false) || input("serviceConnection", false); + if (!value) throw new Error("azureSubscription is required"); + return value; +} + function resolveLogPath(codebasePath, logFilePath) { return path.isAbsolute(logFilePath) ? path.resolve(logFilePath) @@ -29,7 +35,7 @@ function resolveLogPath(codebasePath, logFilePath) { function getInputs() { const inputs = { - serviceConnection: input("serviceConnection", true), + serviceConnection: azureSubscriptionInput(), deploymentName: input("deploymentName", true), endpoint: input("endpoint", true), apiVersion: input("apiVersion", true), diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json index 2952205..b7142c0 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/task.json +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json @@ -14,9 +14,12 @@ "instanceNameFormat": "Microbots Log Analyzer: $(logFilePath)", "inputs": [ { - "name": "serviceConnection", + "name": "azureSubscription", + "aliases": [ + "serviceConnection" + ], "type": "connectedService:AzureRM", - "label": "Azure Resource Manager connection", + "label": "Azure Subscription", "required": true, "helpMarkDown": "Service connection used to obtain a token for Azure OpenAI." }, diff --git a/docs/azure-pipelines-log-analyzer.md b/docs/azure-pipelines-log-analyzer.md index d195f54..d20c762 100644 --- a/docs/azure-pipelines-log-analyzer.md +++ b/docs/azure-pipelines-log-analyzer.md @@ -44,7 +44,7 @@ See the complete sample pipeline at [docs/examples/azure-pipelines/microbots-log - task: MicrobotsLogAnalyzer@0 displayName: Analyze build log inputs: - serviceConnection: my-azure-service-connection + azureSubscription: my-azure-service-connection deploymentName: my-azure-openai-deployment endpoint: https://my-azure-openai-resource.openai.azure.com/ apiVersion: 2025-03-01-preview @@ -60,7 +60,7 @@ The log file must exist before `MicrobotsLogAnalyzer@0` runs. Relative `logFileP | Input | Required | Default | Description | |---|---:|---|---| -| `serviceConnection` | Yes | - | Azure Resource Manager Service Connection used for Azure CLI login. | +| `azureSubscription` | Yes | - | Azure Resource Manager service connection used for Azure CLI login. Alias: `serviceConnection`. | | `deploymentName` | Yes | - | Azure OpenAI deployment name. | | `endpoint` | Yes | - | Azure OpenAI endpoint, for example `https://my-resource.openai.azure.com/`. | | `apiVersion` | Yes | - | Azure OpenAI API version passed to Microbots, for example `2025-03-01-preview`. | diff --git a/docs/examples/azure-pipelines/microbots-log-analyzer.yml b/docs/examples/azure-pipelines/microbots-log-analyzer.yml index 9b8e8a6..8385fbb 100644 --- a/docs/examples/azure-pipelines/microbots-log-analyzer.yml +++ b/docs/examples/azure-pipelines/microbots-log-analyzer.yml @@ -19,7 +19,7 @@ jobs: - task: MicrobotsLogAnalyzer@0 displayName: Analyze build log inputs: - serviceConnection: my-azure-service-connection + azureSubscription: my-azure-service-connection deploymentName: my-azure-openai-deployment endpoint: https://my-azure-openai-resource.openai.azure.com/ apiVersion: 2025-03-01-preview From 6e0d4d04ab5b7ab5376ce6092e6e245a5e6182dd Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 13:54:41 +0530 Subject: [PATCH 08/10] Update OPEN_AI ENV to AZURE_OPENAI as per PR-140 --- azure-pipelines/MicrobotsLogAnalyzerTask/index.js | 4 +--- .../MicrobotsLogAnalyzerTask/log_analyzer_runner.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index cb8c9fd..13ab9e0 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -144,9 +144,7 @@ function setupVenv() { function microbotsEnvironment(inputs) { return Object.assign({}, process.env, { - OPEN_AI_DEPLOYMENT_NAME: inputs.deploymentName, - OPEN_AI_END_POINT: inputs.endpoint, - OPEN_AI_API_VERSION: inputs.apiVersion, + AZURE_OPENAI_DEPLOYMENT_NAME: inputs.deploymentName, AZURE_OPENAI_ENDPOINT: inputs.endpoint, AZURE_OPENAI_API_VERSION: inputs.apiVersion, }); diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py index 1e60e7a..2cffba8 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py @@ -15,7 +15,7 @@ def main(): os.chdir(codebase_path) print( f"MicrobotsLogAnalyzer: analyzing {log_file_path} with deployment " - f"{os.environ['OPEN_AI_DEPLOYMENT_NAME']}", + f"{os.environ['AZURE_OPENAI_DEPLOYMENT_NAME']}", flush=True, ) print(f"MicrobotsLogAnalyzer: timeout is {timeout_seconds} seconds", flush=True) @@ -27,7 +27,7 @@ def main(): "https://cognitiveservices.azure.com/.default", ) bot = LogAnalysisBot( - model=f"azure-openai/{os.environ['OPEN_AI_DEPLOYMENT_NAME']}", + model=f"azure-openai/{os.environ['AZURE_OPENAI_DEPLOYMENT_NAME']}", folder_to_mount=codebase_path, token_provider=token_provider, ) From 63eaafaaf7d1508bcea472939c466db7983602f9 Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Mon, 18 May 2026 17:04:11 +0530 Subject: [PATCH 09/10] Add Unit Tests for Microbots LogAnalyzer Custom ADO Task --- .github/workflows/test.yml | 4 + .../MicrobotsLogAnalyzerTask/index.js | 22 +- .../MicrobotsLogAnalyzerTask/package.json | 3 + .../test/index.test.js | 487 ++++++++++++++++++ 4 files changed, 514 insertions(+), 2 deletions(-) create mode 100644 azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1158451..ae69f81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,6 +87,10 @@ jobs: run: | pip install -e . + - name: Run Azure Pipelines task unit tests + if: matrix.test-type == 'unit' + run: npm test --prefix azure-pipelines/MicrobotsLogAnalyzerTask + - name: Build Docker images for integration tests if: matrix.test-type != 'unit' run: | diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js index 13ab9e0..7f936fc 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/index.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -11,9 +11,27 @@ const VENV_NAME = "microbots-log-analyzer-venv"; const VENV_READY_MARKER = ".microbots-venv-ready-v1"; function runCommand(command, args, env) { - const result = spawnSync(command, args, { stdio: "inherit", env: env || process.env }); + const result = spawnSync(command, args, { + stdio: ["ignore", "pipe", "pipe"], + env: env || process.env, + encoding: "utf8", + }); + + if (result.stdout) { + if (process.stdout && typeof process.stdout.write === "function") process.stdout.write(result.stdout); + else console.log(result.stdout.trimEnd()); + } + if (result.stderr) { + if (process.stderr && typeof process.stderr.write === "function") process.stderr.write(result.stderr); + else console.error(result.stderr.trimEnd()); + } + if (result.error) throw new Error(`Failed to run ${command}: ${result.error.message}`); - if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} -> exit ${result.status}`); + if (result.status !== 0) { + const output = [result.stderr, result.stdout].filter(Boolean).join("\n").trim(); + const details = output ? `: ${output.split(/\r?\n/).slice(-10).join("\n")}` : ""; + throw new Error(`${command} ${args.join(" ")} -> exit ${result.status}${details}`); + } } function input(name, required) { diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/package.json b/azure-pipelines/MicrobotsLogAnalyzerTask/package.json index 2e4cfde..76308fe 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/package.json +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "description": "Azure DevOps custom task that runs the Microbots LogAnalysisBot.", "main": "index.js", + "scripts": { + "test": "node --test test/*.test.js" + }, "dependencies": { "azure-pipelines-task-lib": "^5.2.4", "azure-pipelines-tasks-azure-arm-rest": "^3.274.0" diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js b/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js new file mode 100644 index 0000000..56247f5 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js @@ -0,0 +1,487 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const childProcess = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +const taskDir = path.resolve(__dirname, ".."); + +function loadTask(options = {}) { + const source = fs.readFileSync(path.join(taskDir, "index.js"), "utf8").replace( + /run\(\);\s*$/, + `module.exports = { + runCommand, + input, + azureSubscriptionInput, + resolveLogPath, + getInputs, + validateInputs, + loginWithServiceConnection, + venvPythonPath, + setupVenv, + microbotsEnvironment, + runLogAnalyzer, + run, + };` + ); + + const calls = { + spawnSync: [], + loginAzureRM: [], + setResult: [], + rmSync: [], + writeFileSync: [], + }; + const events = []; + + const taskLib = options.taskLib || { + inputs: options.inputs || {}, + pathInputs: options.pathInputs || {}, + TaskResult: { Succeeded: "Succeeded", Failed: "Failed" }, + getInput(name, required) { + const value = this.inputs[name]; + if (required && (value === undefined || value === null || value === "")) { + throw new Error(`${name} is required`); + } + return value; + }, + getPathInput(name, required) { + const value = this.pathInputs[name] ?? this.inputs[name]; + if (required && (value === undefined || value === null || value === "")) { + throw new Error(`${name} is required`); + } + return value; + }, + loc(key, ...args) { + return [key, ...args].join(" "); + }, + setResult(result, message) { + calls.setResult.push({ result, message }); + events.push({ name: "setResult", result }); + }, + }; + + const mockFs = options.fs || fs; + const mockProcess = options.process || { env: {}, platform: "linux" }; + const mockSpawnSync = options.spawnSync || ((command, args, spawnOptions) => { + calls.spawnSync.push({ command, args, options: spawnOptions }); + events.push({ name: "spawnSync", command, args }); + return { status: 0 }; + }); + const mockLoginAzureRM = options.loginAzureRM || (async (serviceConnection) => { + calls.loginAzureRM.push(serviceConnection); + events.push({ name: "loginAzureRM", serviceConnection }); + }); + + const module = { exports: {} }; + const context = { + Date, + Error, + URL, + __dirname: taskDir, + console: options.console || { log() {}, warn() {}, error() {} }, + module, + exports: module.exports, + process: mockProcess, + require(name) { + if (name === "fs") return mockFs; + if (name === "path") return path; + if (name === "child_process") return { spawnSync: mockSpawnSync }; + if (name === "azure-pipelines-task-lib/task") return taskLib; + if (name === "azure-pipelines-tasks-azure-arm-rest/azCliUtility") { + return { loginAzureRM: mockLoginAzureRM }; + } + return require(name); + }, + }; + + vm.runInNewContext(source, context, { filename: path.join(taskDir, "index.js") }); + return { task: module.exports, calls, events, taskLib, process: mockProcess }; +} + +function makeProjectWithLog() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "microbots-task-test-")); + const logDir = path.join(root, "logs"); + fs.mkdirSync(logDir); + fs.writeFileSync(path.join(logDir, "build.log"), "error log"); + return root; +} + +function mockCompletedLinuxVenv(tempDir = "/tmp") { + const venvDir = path.join(tempDir, "microbots-log-analyzer-venv"); + const python = path.join(venvDir, "bin", "python"); + const marker = path.join(venvDir, ".microbots-venv-ready-v1"); + const existing = new Set([python, marker]); + const mockFs = { + existsSync(filePath) { return existing.has(filePath) || fs.existsSync(filePath); }, + statSync(filePath) { return fs.statSync(filePath); }, + rmSync() {}, + writeFileSync() {}, + }; + + return { mockFs, python }; +} + +function writeFile(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); +} + +function mockMicrobotsEnvironment(options = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "microbots-runner-test-")); + const mockModules = path.join(root, "mocks"); + const recordPath = path.join(root, "record.json"); + + writeFile(path.join(mockModules, "azure", "__init__.py"), ""); + writeFile(path.join(mockModules, "azure", "identity.py"), ` +class AzureCliCredential: + pass + +def get_bearer_token_provider(credential, scope): + def token_provider(): + return "mock-token" + token_provider.scope = scope + token_provider.credential_type = type(credential).__name__ + return token_provider +`); + writeFile(path.join(mockModules, "microbots.py"), ` +import json +import os +from types import SimpleNamespace + +class LogAnalysisBot: + def __init__(self, model, folder_to_mount, token_provider): + self.model = model + self.folder_to_mount = folder_to_mount + self.token_provider = token_provider + + def run(self, **kwargs): + record = { + "model": self.model, + "folder_to_mount": self.folder_to_mount, + "token_provider_scope": getattr(self.token_provider, "scope", None), + "token_provider_credential_type": getattr(self.token_provider, "credential_type", None), + "token": self.token_provider(), + "run_kwargs": kwargs, + } + with open(os.environ["MICROBOTS_MOCK_RECORD"], "w", encoding="utf-8") as record_file: + json.dump(record, record_file) + return SimpleNamespace( + status=os.environ["MICROBOTS_MOCK_STATUS"] == "true", + result=os.environ.get("MICROBOTS_MOCK_RESULT") or None, + error=os.environ.get("MICROBOTS_MOCK_ERROR") or None, + ) +`); + + const env = Object.assign({}, process.env, { + AZURE_OPENAI_DEPLOYMENT_NAME: options.deploymentName || "gpt-test", + MICROBOTS_MOCK_RECORD: recordPath, + MICROBOTS_MOCK_STATUS: options.status === false ? "false" : "true", + MICROBOTS_MOCK_RESULT: Object.hasOwn(options, "result") ? options.result : "Root cause found", + MICROBOTS_MOCK_ERROR: Object.hasOwn(options, "error") ? options.error : "", + PYTHONPATH: mockModules + (process.env.PYTHONPATH ? path.delimiter + process.env.PYTHONPATH : ""), + }); + + return { env, recordPath }; +} + +async function runTaskWithMockServiceConnectionAndMockMicrobots(options = {}) { + const codebasePath = makeProjectWithLog(); + const { mockFs, python } = mockCompletedLinuxVenv(); + const logFilePath = path.join(codebasePath, "logs", "build.log"); + const inputs = Object.assign({ + azureSubscription: "service-id", + deploymentName: "gpt-test", + endpoint: "https://example.openai.azure.com/", + apiVersion: "2025-03-01-preview", + codebasePath, + logFilePath: "logs/build.log", + timeoutSeconds: "600", + maxIterations: "12", + }, options.inputs || {}); + let runnerResult; + let runnerRecord; + let runnerRecordPath; + let loaded; + + loaded = loadTask({ + fs: mockFs, + process: { + env: Object.assign({ AGENT_TEMPDIRECTORY: "/tmp" }, options.processEnv || {}), + platform: "linux", + }, + inputs, + spawnSync(command, args, spawnOptions) { + loaded.calls.spawnSync.push({ command, args, options: spawnOptions }); + loaded.events.push({ name: "spawnSync", command, args }); + if (Array.from(args || [])[0] === path.join(taskDir, "log_analyzer_runner.py")) { + const mockMicrobots = mockMicrobotsEnvironment(Object.assign({ + deploymentName: inputs.deploymentName, + result: "Root cause found", + }, options.mockMicrobots || {})); + runnerRecordPath = mockMicrobots.recordPath; + runnerResult = childProcess.spawnSync("python", Array.from(args), { + env: Object.assign({}, spawnOptions.env, mockMicrobots.env), + encoding: "utf8", + }); + runnerRecord = fs.existsSync(runnerRecordPath) + ? JSON.parse(fs.readFileSync(runnerRecordPath, "utf8")) + : null; + return { + status: runnerResult.status, + stdout: runnerResult.stdout, + stderr: runnerResult.stderr, + }; + } + return { status: 0 }; + }, + }); + + await loaded.task.run(); + return { ...loaded, codebasePath, logFilePath, python, runnerResult, runnerRecord }; +} + +test("Input Parameter azureSubscription has serviceConnection as alias", () => { + const primary = loadTask({ inputs: { azureSubscription: " primary " } }); + assert.equal(primary.task.azureSubscriptionInput(), "primary"); + + const alias = loadTask({ inputs: { serviceConnection: " alias " } }); + assert.equal(alias.task.azureSubscriptionInput(), "alias"); + + const missing = loadTask(); + assert.throws(() => missing.task.azureSubscriptionInput(), /azureSubscription is required/); +}); + +test("Valid Inputs Resolve Correctly: log path and numeric values", () => { + const { task } = loadTask(); + const codebasePath = makeProjectWithLog(); + const inputs = { + codebasePath, + logFilePath: "logs/build.log", + endpoint: "https://example.openai.azure.com/", + timeoutSeconds: " 600 ", + maxIterations: "20", + }; + + task.validateInputs(inputs); + + assert.equal(inputs.logFilePath, path.join(codebasePath, "logs", "build.log")); + assert.equal(inputs.timeoutSeconds, "600"); + assert.equal(inputs.maxIterations, "20"); +}); + +test("Invalid Inputs Are Rejected: endpoint, timeout, and maxIterations", () => { + const { task } = loadTask(); + const codebasePath = makeProjectWithLog(); + const validInputs = { + codebasePath, + logFilePath: "logs/build.log", + endpoint: "https://example.openai.azure.com/", + timeoutSeconds: "600", + }; + + assert.throws( + () => task.validateInputs({ ...validInputs, endpoint: "http://example.openai.azure.com/" }), + /valid HTTPS URL/ + ); + assert.throws( + () => task.validateInputs({ ...validInputs, timeoutSeconds: "0" }), + /timeoutSeconds must be a positive integer/ + ); + assert.throws( + () => task.validateInputs({ ...validInputs, maxIterations: "-1" }), + /maxIterations must be a positive integer/ + ); +}); + +test("End To End Flow Works: ServiceConnection Login and LogAnalysisBot Output is Displayed", async () => { + const { + calls, + events, + codebasePath, + logFilePath, + python, + runnerResult, + runnerRecord, + } = await runTaskWithMockServiceConnectionAndMockMicrobots({ + processEnv: { + AGENT_TEMPDIRECTORY: "/tmp", + AZURE_OPENAI_DEPLOYMENT_NAME: "stale-deployment", + AZURE_OPENAI_ENDPOINT: "https://stale.openai.azure.com/", + AZURE_OPENAI_API_VERSION: "stale-version", + KEEP_ME: "yes", + }, + mockMicrobots: { result: "The deployment returned analysis." }, + }); + + assert.deepEqual(calls.setResult, [{ result: "Succeeded", message: "LogAnalysisBot completed" }]); + const loginIndex = events.findIndex((event) => event.name === "loginAzureRM"); + const runnerIndex = events.findIndex((event) => ( + event.name === "spawnSync" && event.args[0] === path.join(taskDir, "log_analyzer_runner.py") + )); + assert.notEqual(loginIndex, -1); + assert.notEqual(runnerIndex, -1); + assert.equal(events[loginIndex].serviceConnection, "service-id"); + assert.ok(loginIndex < runnerIndex); + + const runnerCall = calls.spawnSync.find((call) => ( + call.args[0] === path.join(taskDir, "log_analyzer_runner.py") + )); + assert.ok(runnerCall); + assert.equal(runnerResult.status, 0, runnerResult.stderr); + assert.match(runnerResult.stdout, /The deployment returned analysis\./); + assert.equal(runnerCall.command, python); + assert.deepEqual(Array.from(runnerCall.args).slice(1), [ + codebasePath, + logFilePath, + "600", + "12", + ]); + assert.equal(runnerCall.options.env.KEEP_ME, "yes"); + assert.equal(runnerCall.options.env.AZURE_OPENAI_DEPLOYMENT_NAME, "gpt-test"); + assert.equal(runnerCall.options.env.AZURE_OPENAI_ENDPOINT, "https://example.openai.azure.com/"); + assert.equal(runnerCall.options.env.AZURE_OPENAI_API_VERSION, "2025-03-01-preview"); + assert.equal(runnerRecord.model, "azure-openai/gpt-test"); + assert.equal(runnerRecord.token_provider_scope, "https://cognitiveservices.azure.com/.default"); + assert.deepEqual(runnerRecord.run_kwargs, { + file_name: logFilePath, + timeout_in_seconds: 600, + max_iterations: 12, + }); + + const logoutCall = calls.spawnSync.at(-1); + assert.equal(logoutCall.command, "az"); + assert.deepEqual(Array.from(logoutCall.args), ["account", "clear"]); + assert.equal(logoutCall.options.stdio, "ignore"); +}); + +test("Existing Python Environment Is Reused Only After A Completed Setup", () => { + const tempDir = path.join(path.parse(process.cwd()).root, "tmp"); + const venvDir = path.join(tempDir, "microbots-log-analyzer-venv"); + const python = path.join(venvDir, "bin", "python"); + const marker = path.join(venvDir, ".microbots-venv-ready-v1"); + const exists = new Set([ + python, + marker, + ]); + const mockFs = { + existsSync(filePath) { return exists.has(filePath); }, + rmSync() { throw new Error("rmSync should not be called"); }, + writeFileSync() { throw new Error("writeFileSync should not be called"); }, + }; + const { task, calls } = loadTask({ fs: mockFs, process: { env: { AGENT_TEMPDIRECTORY: tempDir }, platform: "linux" } }); + + assert.equal(task.setupVenv(), python); + assert.equal(calls.spawnSync.length, 0); +}); + +test("Incomplete Python Environment Is Deleted And Rebuilt", () => { + const tempDir = path.join(path.parse(process.cwd()).root, "tmp"); + const venvDir = path.join(tempDir, "microbots-log-analyzer-venv"); + const python = path.join(venvDir, "bin", "python"); + const marker = path.join(venvDir, ".microbots-venv-ready-v1"); + const existing = new Set([venvDir]); + const mockFs = { + existsSync(filePath) { return existing.has(filePath); }, + rmSync(filePath, options) { existing.delete(filePath); calls.rmSync.push({ filePath, options }); }, + writeFileSync(filePath, value) { calls.writeFileSync.push({ filePath, value }); }, + }; + const calls = { rmSync: [], writeFileSync: [] }; + const loaded = loadTask({ + fs: mockFs, + process: { env: { AGENT_TEMPDIRECTORY: tempDir }, platform: "linux" }, + }); + + assert.equal(loaded.task.setupVenv(), python); + assert.equal(calls.rmSync[0].filePath, venvDir); + assert.equal(calls.rmSync[0].options.recursive, true); + assert.equal(calls.rmSync[0].options.force, true); + assert.deepEqual(loaded.calls.spawnSync.map((call) => [call.command, Array.from(call.args)]), [ + ["python3", ["-m", "venv", venvDir]], + [python, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"]], + [python, ["-m", "pip", "install", "--quiet", "microbots[azure_ad]"]], + ]); + assert.equal(calls.writeFileSync[0].filePath, marker); +}); + +test("AzureRM Login Receives ServiceConnection ID", async () => { + const { task, calls, process } = loadTask({ + process: { env: { AZURE_CORE_OUTPUT: "json" }, platform: "linux" }, + }); + + await task.loginWithServiceConnection("service-id"); + + assert.deepEqual(calls.loginAzureRM, ["service-id"]); + assert.equal(process.env.AZURE_CORE_OUTPUT, "json"); +}); + +test("ServiceConnection Login Failures Are Properly Handled And Stop The Analyzer", async () => { + const codebasePath = makeProjectWithLog(); + const { task, calls } = loadTask({ + inputs: { + azureSubscription: "service-id", + deploymentName: "gpt-test", + endpoint: "https://example.openai.azure.com/", + apiVersion: "2025-03-01-preview", + codebasePath, + logFilePath: "logs/build.log", + timeoutSeconds: "600", + }, + loginAzureRM: async () => { throw new Error("authentication failed"); }, + }); + + await task.run(); + + assert.deepEqual(calls.setResult, [{ + result: "Failed", + message: "Azure service connection login failed for 'service-id': authentication failed", + }]); + assert.equal(calls.spawnSync.some((call) => ( + Array.from(call.args || [])[0] === path.join(taskDir, "log_analyzer_runner.py") + )), false); +}); + +test("Python Setup Command Failures Include Error Details", () => { + const spawnError = loadTask({ spawnSync: () => ({ error: new Error("missing") }) }); + assert.throws(() => spawnError.task.runCommand("python3", ["--version"]), /Failed to run python3: missing/); + + const nonZero = loadTask({ spawnSync: () => ({ status: 2, stderr: "venv creation failed" }) }); + assert.throws(() => nonZero.task.runCommand("python3", ["-m", "venv"]), /exit 2: venv creation failed/); +}); + +test("Task Fails With Proper Error Message When LLM Deployment Cannot Be Reached (After Login With ServiceConnection)", async () => { + const { calls, events, runnerResult } = await runTaskWithMockServiceConnectionAndMockMicrobots({ + mockMicrobots: { + status: false, + result: "", + error: "Deployment access failed", + }, + }); + + assert.equal(events.some((event) => event.name === "loginAzureRM"), true); + assert.equal(runnerResult.status, 1); + assert.match(runnerResult.stdout, /Deployment access failed/); + assert.equal(calls.setResult[0].result, "Failed"); + assert.match(calls.setResult[0].message, /exit 1/); + assert.match(calls.setResult[0].message, /Deployment access failed/); +}); + +test("Task Correctly Reports Failures While Analyzing Logs By The LLM", async () => { + const { calls, runnerResult, runnerRecord } = await runTaskWithMockServiceConnectionAndMockMicrobots({ + mockMicrobots: { + status: false, + result: "", + error: "Log analysis timed out", + }, + }); + + assert.equal(runnerResult.status, 1); + assert.equal(runnerRecord.token, "mock-token"); + assert.match(runnerResult.stdout, /Log analysis timed out/); + assert.equal(calls.setResult[0].result, "Failed"); + assert.match(calls.setResult[0].message, /Log analysis timed out/); +}); From 8f3babb424f91c4f4dd2c010d5e88947c039aa3c Mon Sep 17 00:00:00 2001 From: Madhur Aggarwal Date: Tue, 19 May 2026 10:53:30 +0530 Subject: [PATCH 10/10] Added Missing Docker Error Logs and Added Docker as a Prerequisite in Documentations --- .../log_analyzer_runner.py | 28 +++++++++++++++---- .../test/index.test.js | 25 ++++++++++++++++- docs/azure-pipelines-log-analyzer.md | 2 +- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py index 2cffba8..56f197b 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py @@ -6,6 +6,10 @@ from microbots import LogAnalysisBot +def is_docker_access_error(error): + return "docker" in type(error).__module__.lower() or "docker" in str(error).lower() + + def main(): codebase_path = os.path.abspath(sys.argv[1]) log_file_path = sys.argv[2] @@ -26,11 +30,6 @@ def main(): AzureCliCredential(), "https://cognitiveservices.azure.com/.default", ) - bot = LogAnalysisBot( - model=f"azure-openai/{os.environ['AZURE_OPENAI_DEPLOYMENT_NAME']}", - folder_to_mount=codebase_path, - token_provider=token_provider, - ) run_kwargs = { "file_name": log_file_path, "timeout_in_seconds": timeout_seconds, @@ -38,7 +37,24 @@ def main(): if max_iterations is not None: run_kwargs["max_iterations"] = max_iterations - result = bot.run(**run_kwargs) + try: + bot = LogAnalysisBot( + model=f"azure-openai/{os.environ['AZURE_OPENAI_DEPLOYMENT_NAME']}", + folder_to_mount=codebase_path, + token_provider=token_provider, + ) + result = bot.run(**run_kwargs) + except Exception as error: + if not is_docker_access_error(error): + raise + print( + "MicrobotsLogAnalyzer: Docker-compatible daemon was not accessible " + "while starting the Microbots sandbox.", + file=sys.stderr, + ) + print(f"Details: {error}", file=sys.stderr) + return 1 + message = result.result or result.error or "" print("##[section]MicrobotsLogAnalyzer: LLM analysis") diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js b/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js index 56247f5..19d0ea0 100644 --- a/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js @@ -155,6 +155,11 @@ from types import SimpleNamespace class LogAnalysisBot: def __init__(self, model, folder_to_mount, token_provider): + if os.environ.get("MICROBOTS_MOCK_INIT_ERROR") == "docker": + class DockerException(Exception): + pass + DockerException.__module__ = "docker.errors" + raise DockerException("Error while fetching server API version") self.model = model self.folder_to_mount = folder_to_mount self.token_provider = token_provider @@ -183,6 +188,7 @@ class LogAnalysisBot: MICROBOTS_MOCK_STATUS: options.status === false ? "false" : "true", MICROBOTS_MOCK_RESULT: Object.hasOwn(options, "result") ? options.result : "Root cause found", MICROBOTS_MOCK_ERROR: Object.hasOwn(options, "error") ? options.error : "", + MICROBOTS_MOCK_INIT_ERROR: options.initError || "", PYTHONPATH: mockModules + (process.env.PYTHONPATH ? path.delimiter + process.env.PYTHONPATH : ""), }); @@ -216,9 +222,11 @@ async function runTaskWithMockServiceConnectionAndMockMicrobots(options = {}) { }, inputs, spawnSync(command, args, spawnOptions) { + const commandArgs = Array.from(args || []); loaded.calls.spawnSync.push({ command, args, options: spawnOptions }); loaded.events.push({ name: "spawnSync", command, args }); - if (Array.from(args || [])[0] === path.join(taskDir, "log_analyzer_runner.py")) { + + if (commandArgs[0] === path.join(taskDir, "log_analyzer_runner.py")) { const mockMicrobots = mockMicrobotsEnvironment(Object.assign({ deploymentName: inputs.deploymentName, result: "Root cause found", @@ -453,6 +461,21 @@ test("Python Setup Command Failures Include Error Details", () => { assert.throws(() => nonZero.task.runCommand("python3", ["-m", "venv"]), /exit 2: venv creation failed/); }); +test("Runner Reports Docker Sandbox Startup Failures", async () => { + const { calls, events, runnerResult } = await runTaskWithMockServiceConnectionAndMockMicrobots({ + mockMicrobots: { initError: "docker" }, + }); + + assert.equal(events.some((event) => event.name === "loginAzureRM"), true); + assert.equal(events.filter((event) => ( + event.name === "spawnSync" && Array.from(event.args || [])[0] === path.join(taskDir, "log_analyzer_runner.py") + )).length, 1); + assert.equal(runnerResult.status, 1); + assert.match(runnerResult.stderr, /Docker-compatible daemon was not accessible/); + assert.equal(calls.setResult[0].result, "Failed"); + assert.match(calls.setResult[0].message, /Docker-compatible daemon was not accessible/); +}); + test("Task Fails With Proper Error Message When LLM Deployment Cannot Be Reached (After Login With ServiceConnection)", async () => { const { calls, events, runnerResult } = await runTaskWithMockServiceConnectionAndMockMicrobots({ mockMicrobots: { diff --git a/docs/azure-pipelines-log-analyzer.md b/docs/azure-pipelines-log-analyzer.md index d20c762..a66a043 100644 --- a/docs/azure-pipelines-log-analyzer.md +++ b/docs/azure-pipelines-log-analyzer.md @@ -6,7 +6,7 @@ - Azure DevOps organization where you can install custom extensions. - Azure Resource Manager Service Connection with permission to request tokens for the Azure OpenAI resource. The pipeline must be authorized to use this service connection. -- Azure Pipelines agent with `azure-cli`, `python3`, `pip` and `python3 -m venv` support. +- Azure Pipelines agent with `azure-cli`, `python3`, `pip` and `python3 -m venv` support. Microbots uses Docker sandboxing by default, so the agent also needs a reachable Docker-compatible daemon. - Azure OpenAI deployment that works with Microbots and is reachable by the service connection. - Node.js on the machine where you package and publish the extension.