diff --git a/DiffPalReviewDevV1/task.json b/DiffPalReviewDevV1/task.json index e751a28..4ab4426 100644 --- a/DiffPalReviewDevV1/task.json +++ b/DiffPalReviewDevV1/task.json @@ -10,7 +10,7 @@ "version": { "Major": 1, "Minor": 6, - "Patch": 9 + "Patch": 10 }, "minimumAgentVersion": "3.224.0", "instanceNameFormat": "DiffPal Review Dev", diff --git a/DiffPalReviewV1/task.json b/DiffPalReviewV1/task.json index f90a961..225dd03 100644 --- a/DiffPalReviewV1/task.json +++ b/DiffPalReviewV1/task.json @@ -10,7 +10,7 @@ "version": { "Major": 1, "Minor": 6, - "Patch": 9 + "Patch": 10 }, "minimumAgentVersion": "3.224.0", "instanceNameFormat": "DiffPal Review", diff --git a/README.md b/README.md index 7696098..756d71b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ steps: 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. +Transient provider failures, including empty or invalid structured review +responses after retries, also fail with a human-readable task message while +preserving DiffPal's exit code. ### Blocking gate diff --git a/package-lock.json b/package-lock.json index 34c7e0e..784637c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@diffpal/azure-devops-extension", - "version": "0.1.31", + "version": "0.1.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@diffpal/azure-devops-extension", - "version": "0.1.31", + "version": "0.1.32", "license": "MIT", "dependencies": { "azure-pipelines-task-lib": "5.2.10" diff --git a/package.json b/package.json index afa8ffe..9d20546 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.31" + "version": "0.1.32" } diff --git a/scripts/smoke-test.js b/scripts/smoke-test.js index fb84b2a..9b259aa 100644 --- a/scripts/smoke-test.js +++ b/scripts/smoke-test.js @@ -520,6 +520,30 @@ function testGateFailureUsesHumanMessageForReviewBlockedExitCode() { assert(!result.stdout.includes("diffpal exited with code 10"), "gate failure should not fall back to the generic exit code message"); } +function testTransientStructuredOutputFailureUsesHumanMessage() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "diffpal-ado-transient-structured-")); + const diffpalArgv = path.join(dir, "diffpal-argv"); + const customDiffPal = path.join(dir, "diffpal"); + makeFakeDiffPal(customDiffPal, diffpalArgv, { + exitCode: 3, + stderr: [ + "diffpal: transient: validate structured output: structured output schema validation error\n", + "structured I/O schema validation error\n", + "extract output JSON: output is empty\n" + ].join("") + }); + + const result = runHandlerExpectFailure("transient structured output failure", { + INPUT_INSTALL: "false", + INPUT_DIFFPALPATH: customDiffPal, + INPUT_GATE: "true" + }); + + assert(result.status === 3, `transient failure exit code should be preserved, got ${result.status}`); + assert(result.stdout.includes("DiffPal review could not complete because the provider returned an empty or invalid structured response after retries."), "transient structured output failure should be human-readable"); + assert(!result.stdout.includes("diffpal exited with code 3"), "transient structured output failure should not use 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"); @@ -558,5 +582,6 @@ testExplicitRangeBypassesGitResolution(); testMissingTargetRefExplainsFetchFailure(); testExplainPrintsResolvedContext(); testGateFailureUsesHumanMessageForReviewBlockedExitCode(); +testTransientStructuredOutputFailureUsesHumanMessage(); testNonGateFailureStaysGeneric(); console.log("Azure DevOps task smoke tests passed"); diff --git a/src/index.ts b/src/index.ts index f0bfc6f..fbe6121 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ type ReviewRange = { }; const DEFAULT_DIFFPAL_VERSION = "0.1.31"; +const TRANSIENT_REVIEW_EXIT_CODE = 3; const REVIEW_BLOCKED_EXIT_CODE = 10; function input(name: string): string { @@ -292,6 +293,31 @@ function isReviewBlockedFailure(code: number): boolean { return code === REVIEW_BLOCKED_EXIT_CODE; } +function isTransientReviewFailure(code: number): boolean { + return code === TRANSIENT_REVIEW_EXIT_CODE; +} + +function hasStructuredOutputFailure(output: string): boolean { + const normalized = output.toLowerCase(); + return normalized.includes("structured output") || + normalized.includes("output is empty") || + normalized.includes("no json object"); +} + +function diffPalFailureMessage(result: CommandResult, gate: boolean, blockOn: string): string { + if (gate && isReviewBlockedFailure(result.code)) { + return `DiffPal code review found blocking issues at or above the ${blockOn} threshold.`; + } + if (isTransientReviewFailure(result.code)) { + const output = `${result.stdout}\n${result.stderr}`; + if (hasStructuredOutputFailure(output)) { + return "DiffPal review could not complete because the provider returned an empty or invalid structured response after retries. Rerun the pipeline or check provider availability, auth, and quota."; + } + return "DiffPal review could not complete because the provider failed transiently after retries. Rerun the pipeline or check provider availability, auth, and quota."; + } + return `diffpal exited with code ${result.code}`; +} + async function fetchTargetBranch(targetBranch: string): Promise { if (!targetBranch) { return; @@ -469,11 +495,7 @@ async function run(): Promise { 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}`); + tl.setResult(tl.TaskResult.Failed, diffPalFailureMessage(result, gate, blockOn)); return; } tl.setResult(tl.TaskResult.Succeeded, "DiffPal review completed"); diff --git a/vss-extension.dev.json b/vss-extension.dev.json index cd625f7..aba8a34 100644 --- a/vss-extension.dev.json +++ b/vss-extension.dev.json @@ -46,5 +46,5 @@ "id": "Microsoft.VisualStudio.Services" } ], - "version": "0.1.31" + "version": "0.1.32" } diff --git a/vss-extension.json b/vss-extension.json index 1ed62f6..34a6a1d 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -46,5 +46,5 @@ "id": "Microsoft.VisualStudio.Services" } ], - "version": "0.1.31" + "version": "0.1.32" }