From c5392150697c6b7545940b855f5d90422c0d24a7 Mon Sep 17 00:00:00 2001 From: Alexey Samoylov Date: Mon, 22 Jun 2026 10:36:55 +0600 Subject: [PATCH] chore(azure): save extension rollout updates (dp-dai) --- DiffPalReviewDevV1/task.json | 4 +-- DiffPalReviewV1/task.json | 4 +-- README.md | 14 +++++--- package-lock.json | 4 +-- package.json | 2 +- scripts/smoke-test.js | 69 +++++++++++++++++++++++++++++++++++- src/index.ts | 51 ++++++++++++++++++++++---- vss-extension.dev.json | 2 +- vss-extension.json | 2 +- 9 files changed, 131 insertions(+), 21 deletions(-) diff --git a/DiffPalReviewDevV1/task.json b/DiffPalReviewDevV1/task.json index 2e06231..e751a28 100644 --- a/DiffPalReviewDevV1/task.json +++ b/DiffPalReviewDevV1/task.json @@ -10,7 +10,7 @@ "version": { "Major": 1, "Minor": 6, - "Patch": 8 + "Patch": 9 }, "minimumAgentVersion": "3.224.0", "instanceNameFormat": "DiffPal Review Dev", @@ -28,7 +28,7 @@ "type": "string", "label": "DiffPal version", "required": false, - "defaultValue": "latest", + "defaultValue": "0.1.31", "helpMarkDown": "npm version or dist-tag for @diffpal/diffpal." }, { diff --git a/DiffPalReviewV1/task.json b/DiffPalReviewV1/task.json index 68365ca..f90a961 100644 --- a/DiffPalReviewV1/task.json +++ b/DiffPalReviewV1/task.json @@ -10,7 +10,7 @@ "version": { "Major": 1, "Minor": 6, - "Patch": 8 + "Patch": 9 }, "minimumAgentVersion": "3.224.0", "instanceNameFormat": "DiffPal Review", @@ -28,7 +28,7 @@ "type": "string", "label": "DiffPal version", "required": false, - "defaultValue": "latest", + "defaultValue": "0.1.31", "helpMarkDown": "npm version or dist-tag for @diffpal/diffpal." }, { diff --git a/README.md b/README.md index 77222aa..7696098 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ VSIX packaging, and Marketplace release flow. The task installs `@diffpal/diffpal` by default and runs `diffpal review ado`. Bring the provider recipe you want to use; the Azure review flow stays the same. -By default it installs `@diffpal/diffpal@latest`, so pin `diffpalVersion` when -you need rollout-safe CLI behavior. +By default it installs `@diffpal/diffpal@0.1.31`, the tested CLI release paired +with this extension. Set `diffpalVersion` only when you need to override that +default rollout. ## Behavior @@ -39,8 +40,8 @@ base/head, and redacted CLI arguments before the review starts. With `feedback: balanced` or `feedback: inline`, DiffPal publishes Azure threads for all findings. Blocking findings stay active; non-blocking findings are -published as closed immediately. Findings without canonical file/line mapping -fall back to non-file PR threads. +published as closed immediately. Findings without canonical file/line mapping to +current PR changes are skipped instead of publishing a broken file thread. ## Examples @@ -66,7 +67,7 @@ steps: - task: DiffPalReview@1 displayName: DiffPal review inputs: - diffpalVersion: latest + diffpalVersion: 0.1.31 profile: ci feedback: balanced env: @@ -74,6 +75,9 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) ``` +When blocking findings are present, the task fails with a human-readable gate +message and preserves the non-zero DiffPal exit code for pipeline control. + ### Blocking gate ```yaml diff --git a/package-lock.json b/package-lock.json index 7bbcc39..34c7e0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@diffpal/azure-devops-extension", - "version": "0.1.30", + "version": "0.1.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@diffpal/azure-devops-extension", - "version": "0.1.30", + "version": "0.1.31", "license": "MIT", "dependencies": { "azure-pipelines-task-lib": "5.2.10" diff --git a/package.json b/package.json index 713ba08..afa8ffe 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,5 @@ "publish:prod": "npm run build \u0026\u0026 tfx extension publish --manifest-globs vss-extension.json", "smoke": "npm run build \u0026\u0026 node scripts/smoke-test.js" }, - "version": "0.1.30" + "version": "0.1.31" } diff --git a/scripts/smoke-test.js b/scripts/smoke-test.js index 5b2bc65..fb84b2a 100644 --- a/scripts/smoke-test.js +++ b/scripts/smoke-test.js @@ -63,10 +63,16 @@ function read(file) { return fs.readFileSync(file, "utf8"); } -function makeFakeDiffPal(file, argvFile) { +function makeFakeDiffPal(file, argvFile, options = {}) { + const exitCode = options.exitCode ?? 0; + const stdout = options.stdout ?? ""; + const stderr = options.stderr ?? ""; writeExecutable(file, `#!/usr/bin/env bash set -euo pipefail printf '%s\\n' "$@" > "${argvFile}" +printf '%b' ${JSON.stringify(stdout)} +printf '%b' ${JSON.stringify(stderr)} >&2 +exit ${exitCode} `); } @@ -134,6 +140,25 @@ function testDefaultInstall() { assert(read(diffpalArgv).includes("review\nado"), "default install did not run diffpal review ado"); } +function testDefaultPinnedVersionIsUsedWhenInputIsUnset() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "diffpal-ado-default-version-")); + const fakeBin = path.join(dir, "bin"); + const npmArgv = path.join(dir, "npm-argv"); + const diffpalArgv = path.join(dir, "diffpal-argv"); + const agentTemp = path.join(dir, "agent-temp"); + fs.mkdirSync(agentTemp, { recursive: true }); + makeFakeNpm(path.join(fakeBin, "npm"), npmArgv, diffpalArgv); + + runHandler("default pinned version", { + AGENT_TEMPDIRECTORY: agentTemp, + INPUT_INSTALL: "true", + PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ""}` + }); + + assert(read(npmArgv).includes("@diffpal/diffpal@0.1.31"), "default install did not request the pinned DiffPal version"); + assert(read(diffpalArgv).includes("review\nado"), "default pinned version did not run diffpal review ado"); +} + function testCustomPathSkipsInstall() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "diffpal-ado-custom-")); const fakeBin = path.join(dir, "bin"); @@ -475,7 +500,47 @@ function testExplainPrintsResolvedContext() { assert(!result.stdout.includes("secret-token-value"), "explain output should redact secrets"); } +function testGateFailureUsesHumanMessageForReviewBlockedExitCode() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "diffpal-ado-gate-blocked-")); + const diffpalArgv = path.join(dir, "diffpal-argv"); + const customDiffPal = path.join(dir, "diffpal"); + makeFakeDiffPal(customDiffPal, diffpalArgv, { + exitCode: 10, + stderr: "review blocked: blocking findings detected: 2\n" + }); + + const result = runHandlerExpectFailure("gate blocked exit code", { + INPUT_INSTALL: "false", + INPUT_DIFFPALPATH: customDiffPal, + INPUT_GATE: "true" + }); + + assert(result.status === 10, `gate blocked exit code should be preserved, got ${result.status}`); + assert(result.stdout.includes("DiffPal code review found blocking issues at or above the high threshold."), "gate failure should be human-readable"); + assert(!result.stdout.includes("diffpal exited with code 10"), "gate failure should not fall back to the generic exit code message"); +} + +function testNonGateFailureStaysGeneric() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "diffpal-ado-generic-failure-")); + const diffpalArgv = path.join(dir, "diffpal-argv"); + const customDiffPal = path.join(dir, "diffpal"); + makeFakeDiffPal(customDiffPal, diffpalArgv, { + exitCode: 4, + stderr: "platform publish failed\n" + }); + + const result = runHandlerExpectFailure("generic failure", { + INPUT_INSTALL: "false", + INPUT_DIFFPALPATH: customDiffPal, + INPUT_GATE: "true" + }); + + assert(result.status === 4, `generic failure exit code should be preserved, got ${result.status}`); + assert(result.stdout.includes("diffpal exited with code 4"), "non-gate failure should keep the generic task message"); +} + testDefaultInstall(); +testDefaultPinnedVersionIsUsedWhenInputIsUnset(); testCustomPathSkipsInstall(); testInstallDisabledUsesPath(); testDefaultInstructionsFileDirectoryIsIgnored(); @@ -492,4 +557,6 @@ testPrContextComputesMergeBase(); testExplicitRangeBypassesGitResolution(); testMissingTargetRefExplainsFetchFailure(); testExplainPrintsResolvedContext(); +testGateFailureUsesHumanMessageForReviewBlockedExitCode(); +testNonGateFailureStaysGeneric(); console.log("Azure DevOps task smoke tests passed"); diff --git a/src/index.ts b/src/index.ts index 9a158e9..f0bfc6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,9 @@ type ReviewRange = { pullRequest: PullRequestContext; }; +const DEFAULT_DIFFPAL_VERSION = "0.1.31"; +const REVIEW_BLOCKED_EXIT_CODE = 10; + function input(name: string): string { return (tl.getInput(name, false) ?? "").trim(); } @@ -233,6 +236,34 @@ function spawnCommand(command: string, args: string[]): Promise { }); } +function spawnCommandWithCapture(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + env: process.env, + shell: false, + stdio: ["ignore", "pipe", "pipe"] + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk: Buffer) => { + const text = chunk.toString("utf8"); + stdout += text; + process.stdout.write(text); + }); + child.stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString("utf8"); + stderr += text; + process.stderr.write(text); + }); + child.on("error", reject); + child.on("close", (code) => resolve({ + code: code ?? 1, + stdout, + stderr + })); + }); +} + function runCommand(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { @@ -257,6 +288,10 @@ function runCommand(command: string, args: string[]): Promise { }); } +function isReviewBlockedFailure(code: number): boolean { + return code === REVIEW_BLOCKED_EXIT_CODE; +} + async function fetchTargetBranch(targetBranch: string): Promise { if (!targetBranch) { return; @@ -349,7 +384,7 @@ async function installDiffPal(version: string): Promise { const installRoot = path.join(tempDir, "diffpal-task"); fs.mkdirSync(installRoot, { recursive: true }); - const packageSpec = `@diffpal/diffpal@${version || "latest"}`; + const packageSpec = `@diffpal/diffpal@${version || DEFAULT_DIFFPAL_VERSION}`; tl.debug(`Installing ${packageSpec} into ${installRoot}`); const code = await spawnCommand(npm, [ "install", @@ -392,7 +427,7 @@ async function resolveDiffPalCommand(): Promise { if (!boolInput("install", true)) { return tl.which(diffpalPath, true); } - return installDiffPal(input("diffpalVersion") || "latest"); + return installDiffPal(input("diffpalVersion") || DEFAULT_DIFFPAL_VERSION); } async function run(): Promise { @@ -431,10 +466,14 @@ async function run(): Promise { } tl.debug(`Running ${command} ${args.join(" ")}`); - const code = await spawnCommand(command, args); - if (code !== 0) { - process.exitCode = code; - tl.setResult(tl.TaskResult.Failed, `diffpal exited with code ${code}`); + const result = await spawnCommandWithCapture(command, args); + if (result.code !== 0) { + process.exitCode = result.code; + if (gate && isReviewBlockedFailure(result.code)) { + tl.setResult(tl.TaskResult.Failed, `DiffPal code review found blocking issues at or above the ${blockOn} threshold.`); + return; + } + tl.setResult(tl.TaskResult.Failed, `diffpal exited with code ${result.code}`); return; } tl.setResult(tl.TaskResult.Succeeded, "DiffPal review completed"); diff --git a/vss-extension.dev.json b/vss-extension.dev.json index 32a83a6..cd625f7 100644 --- a/vss-extension.dev.json +++ b/vss-extension.dev.json @@ -46,5 +46,5 @@ "id": "Microsoft.VisualStudio.Services" } ], - "version": "0.1.30" + "version": "0.1.31" } diff --git a/vss-extension.json b/vss-extension.json index b6ded06..1ed62f6 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -46,5 +46,5 @@ "id": "Microsoft.VisualStudio.Services" } ], - "version": "0.1.30" + "version": "0.1.31" }