From 9847ef80d1e94d887507b84560cf1036b70d2f73 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 17:53:31 -0700 Subject: [PATCH 1/3] fix: make npm deprecation reruns idempotent --- scripts/deprecate-release.js | 52 ++++++++++++++++++++++++-- test/scripts/deprecate-release.test.ts | 34 +++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/scripts/deprecate-release.js b/scripts/deprecate-release.js index 622869c1a..211c2e608 100644 --- a/scripts/deprecate-release.js +++ b/scripts/deprecate-release.js @@ -86,7 +86,7 @@ if (options.dryRun) { process.exit(0); } -function runNpmCommand(command, args) { +function runNpmCommand(command, args, { writeOutput = true } = {}) { return new Promise((resolve, reject) => { const interactiveStdio = process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY; @@ -99,12 +99,16 @@ function runNpmCommand(command, args) { child.stdout?.on("data", (chunk) => { const text = chunk.toString(); stdout += text; - process.stdout.write(text); + if (writeOutput) { + process.stdout.write(text); + } }); child.stderr?.on("data", (chunk) => { const text = chunk.toString(); stderr += text; - process.stderr.write(text); + if (writeOutput) { + process.stderr.write(text); + } }); child.on("error", reject); child.on("close", (status) => { @@ -112,9 +116,51 @@ function runNpmCommand(command, args) { }); }); } + +function hasDeprecationValue(value) { + if (value === null || value === undefined || value === "") { + return false; + } + if (Array.isArray(value)) { + return value.some(hasDeprecationValue); + } + if (typeof value === "object") { + return Object.values(value).some(hasDeprecationValue); + } + return true; +} + +async function verifyDeprecation(command, targetSpec) { + const verifyResult = await runNpmCommand( + command, + ["view", targetSpec, "deprecated", "--json"], + { writeOutput: false }, + ); + if (verifyResult.status !== 0) { + return false; + } + + const text = verifyResult.stdout.trim(); + if (!text || text === "null" || text === '""') { + return false; + } + + try { + return hasDeprecationValue(JSON.parse(text)); + } catch { + return true; + } +} + const result = await runNpmCommand(npmCommand, npmArgs); if (result.status !== 0) { const output = `${result.stdout}\n${result.stderr}`; + if (output.includes("E422") && (await verifyDeprecation(npmCommand, spec))) { + console.log( + `Verified existing deprecation for ${spec} after npm returned E422.`, + ); + process.exit(0); + } if ( output.includes("E401") || output.includes("401 Unauthorized") || diff --git a/test/scripts/deprecate-release.test.ts b/test/scripts/deprecate-release.test.ts index ae14a8529..f5f58bb66 100644 --- a/test/scripts/deprecate-release.test.ts +++ b/test/scripts/deprecate-release.test.ts @@ -147,6 +147,40 @@ describe("deprecate-release", () => { } }); + it("treats npm E422 as success when the target is already deprecated", () => { + const tempDir = mkdtempSync(join(tmpdir(), "maestro-deprecate-e422-test-")); + const fakeNpm = join(tempDir, "npm"); + writeFileSync( + fakeNpm, + [ + "#!/usr/bin/env bash", + 'if [[ "$1" == "deprecate" ]]; then', + ' echo "npm error code E422" >&2', + ' echo "npm error 422 Unprocessable Entity - PUT https://registry.npmjs.org/@evalops%2fmaestro - Unprocessable Entity" >&2', + " exit 1", + "fi", + 'if [[ "$1" == "view" ]]; then', + ' echo "[\\"Deprecated release. Upgrade to a supported Maestro version.\\"]"', + " exit 0", + "fi", + "exit 2", + "", + ].join("\n"), + ); + chmodSync(fakeNpm, 0o755); + + try { + const result = runDeprecate({ MAESTRO_NPM_COMMAND: fakeNpm }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + "Verified existing deprecation for @evalops/maestro@>=0.10.8 <=0.10.20 after npm returned E422.", + ); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("forwards stdin to npm so local OTP prompts can be answered", () => { const tempDir = mkdtempSync(join(tmpdir(), "maestro-deprecate-otp-test-")); const fakeNpm = join(tempDir, "npm"); From 7555176d871f58d5c67ba5b8a1f98b9f1577f2f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 18:02:49 -0700 Subject: [PATCH 2/3] chore: rerun release-auth checks From 1e9b50e8b1467dad40aa1d7b59303c12ab6b4b75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 18:08:36 -0700 Subject: [PATCH 3/3] fix: verify full deprecation range on rerun --- scripts/deprecate-release.js | 44 +++++++++++++++++------- test/scripts/deprecate-release.test.ts | 47 +++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/scripts/deprecate-release.js b/scripts/deprecate-release.js index 211c2e608..fa36f8279 100644 --- a/scripts/deprecate-release.js +++ b/scripts/deprecate-release.js @@ -121,35 +121,53 @@ function hasDeprecationValue(value) { if (value === null || value === undefined || value === "") { return false; } - if (Array.isArray(value)) { - return value.some(hasDeprecationValue); - } if (typeof value === "object") { return Object.values(value).some(hasDeprecationValue); } return true; } -async function verifyDeprecation(command, targetSpec) { - const verifyResult = await runNpmCommand( +function countJsonValues(value, predicate = () => true) { + if (Array.isArray(value)) { + return value.filter(predicate).length; + } + if (value && typeof value === "object") { + return Object.values(value).filter(predicate).length; + } + return predicate(value) ? 1 : 0; +} + +async function readNpmJson(command, targetSpec, field) { + const result = await runNpmCommand( command, - ["view", targetSpec, "deprecated", "--json"], + ["view", targetSpec, field, "--json"], { writeOutput: false }, ); - if (verifyResult.status !== 0) { - return false; + if (result.status !== 0) { + return undefined; } - const text = verifyResult.stdout.trim(); - if (!text || text === "null" || text === '""') { - return false; + const text = result.stdout.trim(); + if (!text) { + return undefined; } try { - return hasDeprecationValue(JSON.parse(text)); + return JSON.parse(text); } catch { - return true; + return text; + } +} + +async function verifyDeprecation(command, targetSpec) { + const versions = await readNpmJson(command, targetSpec, "version"); + const deprecations = await readNpmJson(command, targetSpec, "deprecated"); + const versionCount = countJsonValues(versions, (value) => Boolean(value)); + if (versionCount === 0) { + return false; } + + return countJsonValues(deprecations, hasDeprecationValue) >= versionCount; } const result = await runNpmCommand(npmCommand, npmArgs); diff --git a/test/scripts/deprecate-release.test.ts b/test/scripts/deprecate-release.test.ts index f5f58bb66..a4e175ac4 100644 --- a/test/scripts/deprecate-release.test.ts +++ b/test/scripts/deprecate-release.test.ts @@ -160,7 +160,11 @@ describe("deprecate-release", () => { " exit 1", "fi", 'if [[ "$1" == "view" ]]; then', - ' echo "[\\"Deprecated release. Upgrade to a supported Maestro version.\\"]"', + ' if [[ "$3" == "version" ]]; then', + ' echo "[\\"0.10.8\\",\\"0.10.9\\"]"', + " else", + ' echo "[\\"Deprecated release. Upgrade to a supported Maestro version.\\",\\"Deprecated release. Upgrade to a supported Maestro version.\\"]"', + " fi", " exit 0", "fi", "exit 2", @@ -181,6 +185,47 @@ describe("deprecate-release", () => { } }); + it("keeps npm E422 fatal when only part of a range is deprecated", () => { + const tempDir = mkdtempSync( + join(tmpdir(), "maestro-deprecate-partial-e422-test-"), + ); + const fakeNpm = join(tempDir, "npm"); + writeFileSync( + fakeNpm, + [ + "#!/usr/bin/env bash", + 'if [[ "$1" == "deprecate" ]]; then', + ' echo "npm error code E422" >&2', + ' echo "npm error 422 Unprocessable Entity - PUT https://registry.npmjs.org/@evalops%2fmaestro - Unprocessable Entity" >&2', + " exit 1", + "fi", + 'if [[ "$1" == "view" ]]; then', + ' if [[ "$3" == "version" ]]; then', + ' echo "[\\"0.10.8\\",\\"0.10.9\\"]"', + " else", + ' echo "[\\"Deprecated release. Upgrade to a supported Maestro version.\\"]"', + " fi", + " exit 0", + "fi", + "exit 2", + "", + ].join("\n"), + ); + chmodSync(fakeNpm, 0o755); + + try { + const result = runDeprecate({ MAESTRO_NPM_COMMAND: fakeNpm }); + + expect(result.status).toBe(1); + expect(result.stdout).not.toContain( + "Verified existing deprecation for @evalops/maestro@>=0.10.8 <=0.10.20 after npm returned E422.", + ); + expect(result.stderr).toContain("npm error code E422"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("forwards stdin to npm so local OTP prompts can be answered", () => { const tempDir = mkdtempSync(join(tmpdir(), "maestro-deprecate-otp-test-")); const fakeNpm = join(tempDir, "npm");