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/.gitignore b/.gitignore index 7a440d3..ca0ad18 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ # Log files *.log +node_modules/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] @@ -30,6 +32,7 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg +*.vsix MANIFEST # PyInstaller diff --git a/README.md b/README.md index a28723e..eca4ee0 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 published Azure DevOps Marketplace 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 install and use the task in Azure Pipelines. + #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..99258d0 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/index.js @@ -0,0 +1,218 @@ +"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_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: ["ignore", "inherit", "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 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) + : path.resolve(codebasePath, logFilePath); +} + +function getInputs() { + const inputs = { + serviceConnection: azureSubscriptionInput(), + deploymentName: input("deploymentName", true), + endpoint: input("endpoint", true), + apiVersion: input("apiVersion", true), + codebasePath: tl.getPathInput("codebasePath", true, true), + logFilePath: input("logFilePath", true), + outputFilePath: input("outputFilePath", false), + timeoutSeconds: input("timeoutSeconds", false) || DEFAULT_TIMEOUT_SECONDS, + maxIterations: input("maxIterations", false), + }; + + 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:") throw new Error(); + } catch (_) { + throw new Error(`endpoint must be a valid 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); + + 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); + } + + if (inputs.outputFilePath) { + if (!path.isAbsolute(inputs.outputFilePath)) { + throw new Error(`outputFilePath must be an absolute path: ${inputs.outputFilePath}`); + } + + inputs.outputFilePath = path.resolve(inputs.outputFilePath); + const extension = path.extname(inputs.outputFilePath).toLowerCase(); + if (extension !== ".txt" && extension !== ".md" && extension !== ".log") { + throw new Error(`outputFilePath must end with .txt, .md, or .log: ${inputs.outputFilePath}`); + } + + if (fs.existsSync(inputs.outputFilePath) && fs.statSync(inputs.outputFilePath).isDirectory()) { + throw new Error(`outputFilePath must be a file path, not a directory: ${inputs.outputFilePath}`); + } + } +} + +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, { + AZURE_OPENAI_DEPLOYMENT_NAME: inputs.deploymentName, + AZURE_OPENAI_ENDPOINT: inputs.endpoint, + AZURE_OPENAI_API_VERSION: inputs.apiVersion, + }); +} + +function ensureOutputParentDirectory(outputFilePath) { + if (!outputFilePath) return; + + const outputDirectory = path.dirname(outputFilePath); + if (fs.existsSync(outputDirectory) && !fs.statSync(outputDirectory).isDirectory()) { + throw new Error(`outputFilePath parent must be a directory: ${outputDirectory}`); + } + + fs.mkdirSync(outputDirectory, { recursive: true }); + console.log(`##[section]MicrobotsLogAnalyzer: analysis output will overwrite ${outputFilePath}`); +} + +function runLogAnalyzer(python, inputs) { + const scriptPath = path.join(__dirname, "log_analyzer_runner.py"); + const args = [ + scriptPath, + "--codebase-path", inputs.codebasePath, + "--log-file-path", inputs.logFilePath, + "--timeout-seconds", inputs.timeoutSeconds, + ]; + + if (inputs.outputFilePath) args.push("--output-file", inputs.outputFilePath); + if (inputs.maxIterations) args.push("--max-iterations", inputs.maxIterations); + + ensureOutputParentDirectory(inputs.outputFilePath); + runCommand(python, args, 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/log_analyzer_runner.py b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py new file mode 100644 index 0000000..58d795d --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/log_analyzer_runner.py @@ -0,0 +1,108 @@ +import argparse +import os +import sys +import textwrap + +from azure.identity import AzureCliCredential, get_bearer_token_provider +from microbots import LogAnalysisBot + + +def is_docker_access_error(error): + return "docker" in type(error).__module__.lower() or "docker" in str(error).lower() + + +def parse_args(): + parser = argparse.ArgumentParser(description="Run Microbots LogAnalysisBot.") + parser.add_argument("--codebase-path", required=True) + parser.add_argument("--log-file-path", required=True) + parser.add_argument("--timeout-seconds", required=True, type=int) + parser.add_argument("--output-file") + parser.add_argument("--max-iterations", type=int) + return parser.parse_args() + + +def log(message, *, file=sys.stdout): + print(message, file=file, flush=True) + + +def safe_analysis_line(message): + if message.startswith("##vso[") or message.startswith("##["): + return " " + message + return message + + +def write_text_file(file_path, content): + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "w", encoding="utf-8") as output_file: + output_file.write(content) + + +def main(): + args = parse_args() + codebase_path = os.path.abspath(args.codebase_path) + log_file_path = args.log_file_path + timeout_seconds = args.timeout_seconds + max_iterations = args.max_iterations + + os.chdir(codebase_path) + log( + f"MicrobotsLogAnalyzer: analyzing {log_file_path} with deployment " + f"{os.environ['AZURE_OPENAI_DEPLOYMENT_NAME']}" + ) + log(f"MicrobotsLogAnalyzer: timeout is {timeout_seconds} seconds") + if max_iterations is not None: + log(f"MicrobotsLogAnalyzer: max iterations is {max_iterations}") + if args.output_file: + log(f"MicrobotsLogAnalyzer: analysis output file is {args.output_file}") + + token_provider = get_bearer_token_provider( + AzureCliCredential(), + "https://cognitiveservices.azure.com/.default", + ) + run_kwargs = { + "file_name": log_file_path, + "timeout_in_seconds": timeout_seconds, + } + if max_iterations is not None: + run_kwargs["max_iterations"] = max_iterations + + 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 + log( + "MicrobotsLogAnalyzer: Docker-compatible daemon was not accessible " + "while starting the Microbots sandbox.", + file=sys.stderr, + ) + log(f"Details: {error}", file=sys.stderr) + return 1 + + message = result.result or result.error or "" + if args.output_file: + write_text_file(args.output_file, str(message)) + log(f"MicrobotsLogAnalyzer: wrote analysis output to {args.output_file}") + + log("##[section]MicrobotsLogAnalyzer: LLM analysis") + log("============================================================") + log("MICROBOTS LOG ANALYSIS") + log("============================================================") + for paragraph in str(message).splitlines() or [""]: + if paragraph.strip(): + for line in textwrap.wrap(paragraph, width=125) or [""]: + log(safe_analysis_line(line)) + else: + log("") + log("============================================================") + + return 0 if result.status else 1 + + +if __name__ == "__main__": + sys.exit(main()) 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..76308fe --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/package.json @@ -0,0 +1,13 @@ +{ + "name": "microbots-log-analyzer-task", + "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/task.json b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json new file mode 100644 index 0000000..2c70cf7 --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/task.json @@ -0,0 +1,96 @@ +{ + "$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": 2, + "Patch": 0 + }, + "instanceNameFormat": "Microbots Log Analyzer: $(logFilePath)", + "inputs": [ + { + "name": "azureSubscription", + "aliases": [ + "serviceConnection" + ], + "type": "connectedService:AzureRM", + "label": "Azure Subscription", + "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", + "required": true, + "helpMarkDown": "Azure OpenAI API version, for example 2025-03-01-preview." + }, + { + "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": "outputFilePath", + "type": "string", + "label": "Analysis output file path", + "required": false, + "helpMarkDown": "Optional absolute path to a .txt, .md, or .log file where the LLM analysis result will be written. The file does not need to exist; the task creates missing directories and replaces any existing file contents." + }, + { + "name": "timeoutSeconds", + "type": "string", + "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": { + "Node20_1": { + "target": "index.js" + } + }, + "restrictions": { + "commands": { + "mode": "restricted" + }, + "settableVariables": { + "allowed": [] + } + } +} diff --git a/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js b/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js new file mode 100644 index 0000000..d6d2daa --- /dev/null +++ b/azure-pipelines/MicrobotsLogAnalyzerTask/test/index.test.js @@ -0,0 +1,591 @@ +"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, + ensureOutputParentDirectory, + 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() {}, + readFileSync(filePath, encoding) { return fs.readFileSync(filePath, encoding); }, + mkdirSync(filePath, options) { return fs.mkdirSync(filePath, options); }, + writeFileSync(filePath, value) { return fs.writeFileSync(filePath, value); }, + }; + + 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): + 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 + + 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 : "", + MICROBOTS_MOCK_INIT_ERROR: options.initError || "", + 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) { + const commandArgs = Array.from(args || []); + loaded.calls.spawnSync.push({ command, args, options: spawnOptions }); + loaded.events.push({ name: "spawnSync", command, args }); + + if (commandArgs[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("Optional Output File Path Must Be Absolute Plain Text Path", () => { + 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, outputFilePath: "analysis.md" }), + /outputFilePath must be an absolute path/ + ); + assert.throws( + () => task.validateInputs({ ...validInputs, outputFilePath: "$(Build.ArtifactStagingDirectory)/analysis.md" }), + /outputFilePath must be an absolute path/ + ); + assert.throws( + () => task.validateInputs({ ...validInputs, outputFilePath: path.join(codebasePath, "analysis.json") }), + /outputFilePath must end with .txt, .md, or .log/ + ); + + const inputs = { ...validInputs, outputFilePath: path.join(codebasePath, "out", "analysis.md") }; + task.validateInputs(inputs); + assert.equal(inputs.outputFilePath, path.join(codebasePath, "out", "analysis.md")); + + const logInputs = { ...validInputs, outputFilePath: path.join(codebasePath, "out", "analysis.log") }; + task.validateInputs(logInputs); + assert.equal(logInputs.outputFilePath, path.join(codebasePath, "out", "analysis.log")); +}); + +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), [ + path.join(taskDir, "log_analyzer_runner.py"), + "--codebase-path", codebasePath, + "--log-file-path", logFilePath, + "--timeout-seconds", "600", + "--max-iterations", "12", + ]); + assert.equal(runnerCall.options.stdio[1], "inherit"); + assert.equal(runnerCall.options.stdio[2], "inherit"); + 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 }) }); + assert.throws(() => nonZero.task.runCommand("python3", ["-m", "venv"]), /exit 2/); +}); + +test("Output File Path Can Point To Missing Absolute File", () => { + const { task } = loadTask(); + const codebasePath = makeProjectWithLog(); + const outputFilePath = path.join(codebasePath, "missing", "analysis.txt"); + const inputs = { + codebasePath, + logFilePath: "logs/build.log", + endpoint: "https://example.openai.azure.com/", + timeoutSeconds: "600", + outputFilePath, + }; + + assert.equal(fs.existsSync(outputFilePath), false); + task.validateInputs(inputs); + assert.equal(inputs.outputFilePath, outputFilePath); +}); + +test("Output File Path Creates Directories And Replaces Existing File", async () => { + const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), "microbots-output-test-")); + const outputFilePath = path.join(outputRoot, "nested", "analysis.md"); + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + fs.writeFileSync(outputFilePath, "old analysis", "utf8"); + + const { calls } = await runTaskWithMockServiceConnectionAndMockMicrobots({ + inputs: { outputFilePath }, + mockMicrobots: { result: "New analysis\nwith ##vso[task.setvariable variable=x]unsafe-looking text" }, + }); + + assert.equal( + fs.readFileSync(outputFilePath, "utf8").replace(/\r\n/g, "\n"), + "New analysis\nwith ##vso[task.setvariable variable=x]unsafe-looking text" + ); +}); + +test("Displayed LLM Output Does Not Emit Azure Pipelines Commands", async () => { + const { calls, runnerResult } = await runTaskWithMockServiceConnectionAndMockMicrobots({ + mockMicrobots: { result: "##vso[task.setvariable variable=NotAllowed]spoofed" }, + }); + + assert.equal(runnerResult.status, 0, runnerResult.stderr); + assert.match(runnerResult.stdout, / ##vso\[task\.setvariable variable=NotAllowed\]spoofed/); + assert.equal(calls.setResult[0].result, "Succeeded"); +}); + +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, /exit 1/); +}); + +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/); +}); + +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, /exit 1/); +}); diff --git a/azure-pipelines/vss-extension.json b/azure-pipelines/vss-extension.json new file mode 100644 index 0000000..e8c6a0e --- /dev/null +++ b/azure-pipelines/vss-extension.json @@ -0,0 +1,33 @@ +{ + "manifestVersion": 1, + "id": "microbots-log-analyzer", + "name": "Microbots Log Analyzer", + "version": "0.2.0", + "publisher": "Microbots-log-analyzer", + "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..b0a4387 --- /dev/null +++ b/docs/azure-pipelines-log-analyzer.md @@ -0,0 +1,69 @@ +# 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. See [Azure managed identity setup](guides/azure-managed-identity-setup.md) for service connection and Azure OpenAI RBAC setup. The pipeline must be authorized to use this service connection. +- 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. + +## Use Azure Marketplace Extension + +Install the published Marketplace extension into the Azure DevOps organization that owns your pipelines: + +1. Open the Marketplace listing: + [Microbots Log Analyzer](https://marketplace.visualstudio.com/items?itemName=Microbots-log-analyzer.microbots-log-analyzer) +2. Select **Get it free**. +3. Choose the Azure DevOps organization where your pipelines run. +4. Confirm the extension appears in the organization extension page: + `https://dev.azure.com//_settings/extensions`. If it appears under the `Shared` section you need to install it to you org (requires Azure DevOps Org Admin Access) + +## 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: + azureSubscription: 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 + outputFilePath: $(Build.ArtifactStagingDirectory)/microbots-log-analysis.md + 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. + +`outputFilePath` is optional. When it is provided, it must be an absolute path ending in `.txt`, `.md`, or `.log`. The file does not need to exist; the task creates missing directories and replaces any existing file content with the latest LLM analysis result. + +## Inputs + +| Input | Required | Default | Description | +|---|---:|---|---| +| `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`. | +| `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`. | +| `outputFilePath` | No | - | Absolute `.txt`, `.md`, or `.log` path where the LLM analysis result is written. Missing directories are created, and existing file contents are replaced. | +| `timeoutSeconds` | No | `600` | Maximum time for `LogAnalysisBot.run()`. | +| `maxIterations` | No | `20` | Maximum number of Microbots iterations. Leave unset to use the default from `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`, optional `maxIterations`, and `timeoutSeconds` to `LogAnalysisBot.run()`, and prints the analysis result. +6. If `outputFilePath` is provided, the task writes the LLM analysis result to that file, replacing any existing contents. + +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 run arbitrary logging commands or set 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..b647d54 --- /dev/null +++ b/docs/examples/azure-pipelines/microbots-log-analyzer.yml @@ -0,0 +1,30 @@ +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: + azureSubscription: 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 + outputFilePath: $(Build.ArtifactStagingDirectory)/microbots-log-analysis.md + timeoutSeconds: 600 + maxIterations: 20 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: