From f9c4ed7315f742ecaf7b596bce755b28d899ad18 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Mon, 3 Aug 2026 11:24:34 +0200 Subject: [PATCH 1/4] fix(release): fully sanitize npm lookup diagnostics --- scripts/ci/publish-npm-packages.sh | 8 +- scripts/ci/publish-npm-packages.test.ts | 468 ++++++++++++------------ scripts/deno.lock | 4 + 3 files changed, 250 insertions(+), 230 deletions(-) diff --git a/scripts/ci/publish-npm-packages.sh b/scripts/ci/publish-npm-packages.sh index c224f0b830..889be4f968 100755 --- a/scripts/ci/publish-npm-packages.sh +++ b/scripts/ci/publish-npm-packages.sh @@ -168,12 +168,12 @@ sanitize_npm_lookup_output() { printf '%s\n' "$1" \ | sed -E \ -e '/^npm error A complete log of this run can be found in:/d' \ - -e 's#Bearer [A-Za-z0-9._~-]+#Bearer #g' \ + -e 's#Bearer [^[:space:]]+#Bearer #g' \ -e 's#([?&]token=)[^[:space:]&]+#\1#g' \ -e 's#(_authToken=)[^[:space:]]+#\1#g' \ - -e 's#/Users/[^[:space:]]+##g' \ - -e 's#/home/[^[:space:]]+##g' \ - -e 's#/var/folders/[^[:space:]]+##g' + -e 's#(^|[[:space:]"=(])/[[:graph:]]+#\1#g' \ + -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^[:space:]]+#\1#g' \ + -e 's#(^|[[:space:]"=(])\\\\[^[:space:]]+#\1#g' } ensure_package_names_registered() { diff --git a/scripts/ci/publish-npm-packages.test.ts b/scripts/ci/publish-npm-packages.test.ts index 5d8749d3ef..ca0f744600 100644 --- a/scripts/ci/publish-npm-packages.test.ts +++ b/scripts/ci/publish-npm-packages.test.ts @@ -1,4 +1,5 @@ import { assertEquals, assertStringIncludes } from "#std/assert"; +import { describe, it } from "#veryfront/testing/bdd.ts"; const scriptPath = `${Deno.cwd()}/scripts/ci/publish-npm-packages.sh`; const decoder = new TextDecoder(); @@ -15,240 +16,255 @@ async function runBash( }).output(); } -Deno.test("npm gitHead verification tolerates metadata appearing after 120 seconds", async () => { - const stateDir = await Deno.makeTempDir(); - const countFile = `${stateDir}/npm-view-count`; - await Deno.writeTextFile(countFile, "0"); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "npm() {", - ' count="$(cat "$COUNT_FILE")"', - " count=$((count + 1))", - ' printf "%s" "$count" > "$COUNT_FILE"', - ' if [ "$count" -ge 26 ]; then', - ' printf "%s\\n" "$GITHUB_SHA"', - " fi", - "}", - "sleep() { :; }", - 'wait_for_npm_git_head "@veryfront/ext-auth-jwt"', - ].join("\n"), - { - COUNT_FILE: countFile, - GITHUB_SHA: "expected-commit", - VERSION: "0.1.1069", - }, - ); +describe("npm package publishing", () => { + it("tolerates npm gitHead metadata appearing after 120 seconds", async () => { + const stateDir = await Deno.makeTempDir(); + const countFile = `${stateDir}/npm-view-count`; + await Deno.writeTextFile(countFile, "0"); - assertEquals(output.code, 0, decoder.decode(output.stderr)); - assertEquals(await Deno.readTextFile(countFile), "26"); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "npm() {", + ' count="$(cat "$COUNT_FILE")"', + " count=$((count + 1))", + ' printf "%s" "$count" > "$COUNT_FILE"', + ' if [ "$count" -ge 26 ]; then', + ' printf "%s\\n" "$GITHUB_SHA"', + " fi", + "}", + "sleep() { :; }", + 'wait_for_npm_git_head "@veryfront/ext-auth-jwt"', + ].join("\n"), + { + COUNT_FILE: countFile, + GITHUB_SHA: "expected-commit", + VERSION: "0.1.1069", + }, + ); -Deno.test("npm gitHead verification fails fast on a wrong non-empty hash", async () => { - const stateDir = await Deno.makeTempDir(); - const countFile = `${stateDir}/npm-view-count`; - const sleepFile = `${stateDir}/sleep-count`; - await Deno.writeTextFile(countFile, "0"); - await Deno.writeTextFile(sleepFile, "0"); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "npm() {", - ' count="$(cat "$COUNT_FILE")"', - " count=$((count + 1))", - ' printf "%s" "$count" > "$COUNT_FILE"', - ' printf "%s\\n" "wrong-commit"', - "}", - "sleep() {", - ' count="$(cat "$SLEEP_FILE")"', - ' printf "%s" "$((count + 1))" > "$SLEEP_FILE"', - "}", - 'if wait_for_npm_git_head "@veryfront/ext-auth-jwt"; then', - " exit 91", - "fi", - ].join("\n"), - { - COUNT_FILE: countFile, - GITHUB_SHA: "expected-commit", - SLEEP_FILE: sleepFile, - VERSION: "0.1.1069", - }, - ); + assertEquals(output.code, 0, decoder.decode(output.stderr)); + assertEquals(await Deno.readTextFile(countFile), "26"); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); - assertEquals(output.code, 0, decoder.decode(output.stderr)); - assertEquals(await Deno.readTextFile(countFile), "1"); - assertEquals(await Deno.readTextFile(sleepFile), "0"); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + it("fails fast when npm reports a wrong non-empty gitHead", async () => { + const stateDir = await Deno.makeTempDir(); + const countFile = `${stateDir}/npm-view-count`; + const sleepFile = `${stateDir}/sleep-count`; + await Deno.writeTextFile(countFile, "0"); + await Deno.writeTextFile(sleepFile, "0"); -Deno.test("npm release rerun skips a package already published for the commit", async () => { - const stateDir = await Deno.makeTempDir(); - const packageDir = `${stateDir}/package`; - const npmLog = `${stateDir}/npm.log`; - await Deno.mkdir(packageDir); - await Deno.writeTextFile( - `${packageDir}/package.json`, - JSON.stringify({ name: "@veryfront/ext-auth-jwt" }), - ); - await Deno.writeTextFile(npmLog, ""); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "npm() {", - ' printf "%s\\n" "$*" >> "$NPM_LOG"', - ' if [ "$1" = "view" ] && [ "$3" = "gitHead" ]; then', - ' printf "%s\\n" "$GITHUB_SHA"', - " return 0", - " fi", - " return 92", - "}", - "sleep() { return 93; }", - 'release_publish_package_dir "$PACKAGE_DIR"', - ].join("\n"), - { - GITHUB_SHA: "expected-commit", - NPM_LOG: npmLog, - PACKAGE_DIR: packageDir, - VERSION: "0.1.1069", - }, - ); + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "npm() {", + ' count="$(cat "$COUNT_FILE")"', + " count=$((count + 1))", + ' printf "%s" "$count" > "$COUNT_FILE"', + ' printf "%s\\n" "wrong-commit"', + "}", + "sleep() {", + ' count="$(cat "$SLEEP_FILE")"', + ' printf "%s" "$((count + 1))" > "$SLEEP_FILE"', + "}", + 'if wait_for_npm_git_head "@veryfront/ext-auth-jwt"; then', + " exit 91", + "fi", + ].join("\n"), + { + COUNT_FILE: countFile, + GITHUB_SHA: "expected-commit", + SLEEP_FILE: sleepFile, + VERSION: "0.1.1069", + }, + ); - assertEquals(output.code, 0, decoder.decode(output.stderr)); - const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); - assertEquals(calls, [ - "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", - "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", - ]); - assertStringIncludes( - decoder.decode(output.stdout), - "already published for this commit; skipping npm publish", - ); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + assertEquals(output.code, 0, decoder.decode(output.stderr)); + assertEquals(await Deno.readTextFile(countFile), "1"); + assertEquals(await Deno.readTextFile(sleepFile), "0"); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); -Deno.test("npm release preflight rejects an E404 unbootstrapped package name", async () => { - const stateDir = await Deno.makeTempDir(); - const npmLog = `${stateDir}/npm.log`; - await Deno.writeTextFile(npmLog, ""); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "package_names_from_workspace() {", - ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-new"', - "}", - "npm() {", - ' printf "%s\\n" "$*" >> "$NPM_LOG"', - ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', - ' printf "%s\\n" "@veryfront/ext-existing"', - " return 0", - " fi", - ' printf "%s\\n" "npm error code E404" >&2', - ' printf "%s\\n" "npm error 404 Not Found - GET https://registry.npmjs.org/@veryfront%2fext-new - Not found" >&2', - " return 1", - "}", - "run_preflight", - ].join("\n"), - { - GITHUB_SHA: "expected-commit", - NPM_LOG: npmLog, - VERSION: "0.1.1189", - }, + it("skips a package already published for the commit on a release rerun", async () => { + const stateDir = await Deno.makeTempDir(); + const packageDir = `${stateDir}/package`; + const npmLog = `${stateDir}/npm.log`; + await Deno.mkdir(packageDir); + await Deno.writeTextFile( + `${packageDir}/package.json`, + JSON.stringify({ name: "@veryfront/ext-auth-jwt" }), ); + await Deno.writeTextFile(npmLog, ""); - assertEquals(output.code, 1, decoder.decode(output.stderr)); - assertStringIncludes( - decoder.decode(output.stderr), - "@veryfront/ext-new is not registered on npm", - ); - assertStringIncludes( - decoder.decode(output.stderr), - "Publish each package once with a prerelease version and a non-latest dist-tag", - ); - assertEquals( - decoder.decode(output.stderr).includes("npm registry lookup failed"), - false, - ); - const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); - assertEquals(calls, [ - "view @veryfront/ext-existing@* name", - "view @veryfront/ext-new@* name", - ]); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "npm() {", + ' printf "%s\\n" "$*" >> "$NPM_LOG"', + ' if [ "$1" = "view" ] && [ "$3" = "gitHead" ]; then', + ' printf "%s\\n" "$GITHUB_SHA"', + " return 0", + " fi", + " return 92", + "}", + "sleep() { return 93; }", + 'release_publish_package_dir "$PACKAGE_DIR"', + ].join("\n"), + { + GITHUB_SHA: "expected-commit", + NPM_LOG: npmLog, + PACKAGE_DIR: packageDir, + VERSION: "0.1.1069", + }, + ); -Deno.test("npm release preflight reports non-E404 registry lookup failures", async () => { - const stateDir = await Deno.makeTempDir(); - const npmLog = `${stateDir}/npm.log`; - await Deno.writeTextFile(npmLog, ""); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "package_names_from_workspace() {", - ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-flaky"', - "}", - "npm() {", - ' printf "%s\\n" "$*" >> "$NPM_LOG"', - ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', - ' printf "%s\\n" "@veryfront/ext-existing"', - " return 0", - " fi", - ' printf "%s\\n" "npm error code E503" >&2', - ' printf "%s\\n" "npm error 503 Service Unavailable" >&2', - ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users//.npm/_logs/debug.log" >&2', - " return 42", - "}", - "run_preflight", - ].join("\n"), - { - GITHUB_SHA: "expected-commit", - NPM_LOG: npmLog, - VERSION: "0.1.1189", - }, - ); + assertEquals(output.code, 0, decoder.decode(output.stderr)); + const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); + assertEquals(calls, [ + "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", + "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", + ]); + assertStringIncludes( + decoder.decode(output.stdout), + "already published for this commit; skipping npm publish", + ); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); - assertEquals(output.code, 1, decoder.decode(output.stderr)); - const stderr = decoder.decode(output.stderr); - assertStringIncludes( - stderr, - "npm registry lookup failed for @veryfront/ext-flaky (status 42)", - ); - assertStringIncludes(stderr, "npm error code E503"); - assertEquals(stderr.includes("is not registered on npm"), false); - assertEquals( - stderr.includes("Publish each package once with a prerelease version"), - false, - ); - assertEquals(stderr.includes("/Users/"), false); - const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); - assertEquals(calls, [ - "view @veryfront/ext-existing@* name", - "view @veryfront/ext-flaky@* name", - ]); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } + it("rejects an E404 unbootstrapped package name during release preflight", async () => { + const stateDir = await Deno.makeTempDir(); + const npmLog = `${stateDir}/npm.log`; + await Deno.writeTextFile(npmLog, ""); + + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "package_names_from_workspace() {", + ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-new"', + "}", + "npm() {", + ' printf "%s\\n" "$*" >> "$NPM_LOG"', + ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', + ' printf "%s\\n" "@veryfront/ext-existing"', + " return 0", + " fi", + ' printf "%s\\n" "npm error code E404" >&2', + ' printf "%s\\n" "npm error 404 Not Found - GET https://registry.npmjs.org/@veryfront%2fext-new - Not found" >&2', + " return 1", + "}", + "run_preflight", + ].join("\n"), + { + GITHUB_SHA: "expected-commit", + NPM_LOG: npmLog, + VERSION: "0.1.1189", + }, + ); + + assertEquals(output.code, 1, decoder.decode(output.stderr)); + assertStringIncludes( + decoder.decode(output.stderr), + "@veryfront/ext-new is not registered on npm", + ); + assertStringIncludes( + decoder.decode(output.stderr), + "Publish each package once with a prerelease version and a non-latest dist-tag", + ); + assertEquals( + decoder.decode(output.stderr).includes("npm registry lookup failed"), + false, + ); + const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); + assertEquals(calls, [ + "view @veryfront/ext-existing@* name", + "view @veryfront/ext-new@* name", + ]); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); + + it("reports sanitized non-E404 registry lookup failures", async () => { + const stateDir = await Deno.makeTempDir(); + const npmLog = `${stateDir}/npm.log`; + await Deno.writeTextFile(npmLog, ""); + + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "package_names_from_workspace() {", + ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-flaky"', + "}", + "npm() {", + ' printf "%s\\n" "$*" >> "$NPM_LOG"', + ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', + ' printf "%s\\n" "@veryfront/ext-existing"', + " return 0", + " fi", + ' printf "%s\\n" "npm error code E503" >&2', + ' printf "%s\\n" "npm error 503 Service Unavailable" >&2', + ' printf "%s\\n" "npm error auth Bearer fixture-token_~-/+=" >&2', + ' printf "%s\\n" "npm error cache=/tmp/npm-private/cache.log" >&2', + ' printf "%s\\n" "npm error config C:\\\\Users\\\\runner\\\\.npmrc" >&2', + ' printf "%s\\n" "npm error workspace D:/build/private/package" >&2', + ' printf "%s\\n" "npm error share \\\\\\\\server\\\\private\\\\debug.log" >&2', + ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users/runner/.npm/_logs/debug.log" >&2', + " return 42", + "}", + "run_preflight", + ].join("\n"), + { + GITHUB_SHA: "expected-commit", + NPM_LOG: npmLog, + VERSION: "0.1.1189", + }, + ); + + assertEquals(output.code, 1, decoder.decode(output.stderr)); + const stderr = decoder.decode(output.stderr); + assertStringIncludes( + stderr, + "npm registry lookup failed for @veryfront/ext-flaky (status 42)", + ); + assertStringIncludes(stderr, "npm error code E503"); + assertStringIncludes(stderr, "Bearer "); + assertStringIncludes(stderr, "cache="); + assertStringIncludes(stderr, "config "); + assertEquals(stderr.includes("fixture-token"), false); + assertEquals(stderr.includes("/tmp/"), false); + assertEquals(stderr.includes("C:\\"), false); + assertEquals(stderr.includes("D:/"), false); + assertEquals(stderr.includes("\\\\server"), false); + assertEquals(stderr.includes("is not registered on npm"), false); + assertEquals( + stderr.includes("Publish each package once with a prerelease version"), + false, + ); + assertEquals(stderr.includes("/Users/"), false); + const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); + assertEquals(calls, [ + "view @veryfront/ext-existing@* name", + "view @veryfront/ext-flaky@* name", + ]); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); }); diff --git a/scripts/deno.lock b/scripts/deno.lock index 4111744f8d..7f18e98b97 100644 --- a/scripts/deno.lock +++ b/scripts/deno.lock @@ -18,6 +18,7 @@ "jsr:@ts-morph/common@0.27": "0.27.0", "npm:@babel/parser@7.29.2": "7.29.2", "npm:@mdx-js/mdx@3.1.1": "3.1.1", + "npm:es-module-lexer@2.3.1": "2.3.1", "npm:esbuild@0.28.1": "0.28.1" }, "jsr": { @@ -365,6 +366,9 @@ "dequal" ] }, + "es-module-lexer@2.3.1": { + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==" + }, "esast-util-from-estree@2.0.0": { "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", "dependencies": [ From b92efcd4a809ba11ea5521b095b5b759c1fcf757 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Mon, 3 Aug 2026 11:40:52 +0200 Subject: [PATCH 2/4] Preserve quoted path delimiters in release diagnostics Absolute-path redaction previously consumed closing double quotes because each path pattern matched every non-whitespace character. The sanitizer now stops at quote delimiters and regression coverage exercises quoted POSIX, Windows-drive, and UNC paths. Constraint: Registry diagnostics must redact machine-local paths without corrupting the surrounding npm message. Rejected: Rebalance quotes after sanitization | delimiter-aware matching is smaller and preserves the original syntax. Confidence: high Scope-risk: narrow Reversibility: clean Tested: focused release-script BDD suite, format, lint, typecheck, Bash syntax, and git diff check Not-tested: full repository suite --- scripts/ci/publish-npm-packages.sh | 6 +++--- scripts/ci/publish-npm-packages.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/ci/publish-npm-packages.sh b/scripts/ci/publish-npm-packages.sh index 889be4f968..48f40a62be 100755 --- a/scripts/ci/publish-npm-packages.sh +++ b/scripts/ci/publish-npm-packages.sh @@ -171,9 +171,9 @@ sanitize_npm_lookup_output() { -e 's#Bearer [^[:space:]]+#Bearer #g' \ -e 's#([?&]token=)[^[:space:]&]+#\1#g' \ -e 's#(_authToken=)[^[:space:]]+#\1#g' \ - -e 's#(^|[[:space:]"=(])/[[:graph:]]+#\1#g' \ - -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^[:space:]]+#\1#g' \ - -e 's#(^|[[:space:]"=(])\\\\[^[:space:]]+#\1#g' + -e 's#(^|[[:space:]"=(])/[^[:space:]"]+#\1#g' \ + -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^[:space:]"]+#\1#g' \ + -e 's#(^|[[:space:]"=(])\\\\[^[:space:]"]+#\1#g' } ensure_package_names_registered() { diff --git a/scripts/ci/publish-npm-packages.test.ts b/scripts/ci/publish-npm-packages.test.ts index ca0f744600..e314b3b5b7 100644 --- a/scripts/ci/publish-npm-packages.test.ts +++ b/scripts/ci/publish-npm-packages.test.ts @@ -222,9 +222,12 @@ describe("npm package publishing", () => { ' printf "%s\\n" "npm error 503 Service Unavailable" >&2', ' printf "%s\\n" "npm error auth Bearer fixture-token_~-/+=" >&2', ' printf "%s\\n" "npm error cache=/tmp/npm-private/cache.log" >&2', + ' printf "%s\\n" "npm error quoted=\\"/tmp/npm-private/quoted.log\\"" >&2', ' printf "%s\\n" "npm error config C:\\\\Users\\\\runner\\\\.npmrc" >&2', + ' printf "%s\\n" "npm error quoted-win=\\"C:\\\\Users\\\\runner\\\\quoted.log\\"" >&2', ' printf "%s\\n" "npm error workspace D:/build/private/package" >&2', ' printf "%s\\n" "npm error share \\\\\\\\server\\\\private\\\\debug.log" >&2', + ' printf "%s\\n" "npm error quoted-share=\\"\\\\\\\\server\\\\private\\\\quoted.log\\"" >&2', ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users/runner/.npm/_logs/debug.log" >&2', " return 42", "}", @@ -246,7 +249,10 @@ describe("npm package publishing", () => { assertStringIncludes(stderr, "npm error code E503"); assertStringIncludes(stderr, "Bearer "); assertStringIncludes(stderr, "cache="); + assertStringIncludes(stderr, 'quoted=""'); assertStringIncludes(stderr, "config "); + assertStringIncludes(stderr, 'quoted-win=""'); + assertStringIncludes(stderr, 'quoted-share=""'); assertEquals(stderr.includes("fixture-token"), false); assertEquals(stderr.includes("/tmp/"), false); assertEquals(stderr.includes("C:\\"), false); From 37353de6cbbe0f50cf228098e9c2dc68412d5225 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Mon, 3 Aug 2026 11:58:40 +0200 Subject: [PATCH 3/4] Prevent release diagnostics from leaking delimited local paths npm failures can report absolute paths inside quotes, brackets, and file URIs. Sanitize these structured forms before the existing unquoted fallback so complete paths are removed without consuming their delimiters or altering registry URLs. Constraint: The release helper must remain portable across the Bash and sed implementations used locally and in GitHub Actions. Rejected: Replace sed with a new parser dependency | unnecessary dependency and release-path complexity. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep structured path rules ahead of the unquoted fallback so paths containing spaces are redacted as one value. Tested: Focused 5-step BDD suite, bash -n, Deno fmt/lint/check, deno task verify:quick, git diff --check. Not-tested: Live npm diagnostic variants outside the covered POSIX, Windows, UNC, quoted, bracketed, and file URI forms. --- scripts/ci/publish-npm-packages.sh | 7 +++++++ scripts/ci/publish-npm-packages.test.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/scripts/ci/publish-npm-packages.sh b/scripts/ci/publish-npm-packages.sh index 48f40a62be..bec774f160 100755 --- a/scripts/ci/publish-npm-packages.sh +++ b/scripts/ci/publish-npm-packages.sh @@ -171,6 +171,13 @@ sanitize_npm_lookup_output() { -e 's#Bearer [^[:space:]]+#Bearer #g' \ -e 's#([?&]token=)[^[:space:]&]+#\1#g' \ -e 's#(_authToken=)[^[:space:]]+#\1#g' \ + -e 's#"(file://)/[^"]*"#"\1"#g' \ + -e "s#'(file://)/[^']*'#'\1'#g" \ + -e 's#\[(file://)/[^]]*\]#[\1]#g' \ + -e 's#(file://)/[^[:space:]]+#\1#g' \ + -e 's#(^|[[:space:]=(])"((/|[A-Za-z]:[\\/]|\\\\)[^"]*)"#\1""#g' \ + -e "s#(^|[[:space:]=(])'((/|[A-Za-z]:[\\\\/]|\\\\\\\\)[^']*)'#\1''#g" \ + -e 's#\[(/|[A-Za-z]:[\\/]|\\\\)[^]]*\]#[]#g' \ -e 's#(^|[[:space:]"=(])/[^[:space:]"]+#\1#g' \ -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^[:space:]"]+#\1#g' \ -e 's#(^|[[:space:]"=(])\\\\[^[:space:]"]+#\1#g' diff --git a/scripts/ci/publish-npm-packages.test.ts b/scripts/ci/publish-npm-packages.test.ts index e314b3b5b7..ff41f8fbdd 100644 --- a/scripts/ci/publish-npm-packages.test.ts +++ b/scripts/ci/publish-npm-packages.test.ts @@ -223,11 +223,18 @@ describe("npm package publishing", () => { ' printf "%s\\n" "npm error auth Bearer fixture-token_~-/+=" >&2', ' printf "%s\\n" "npm error cache=/tmp/npm-private/cache.log" >&2', ' printf "%s\\n" "npm error quoted=\\"/tmp/npm-private/quoted.log\\"" >&2', + ' printf "%s\\n" "npm error single-posix=\'/tmp/npm-private/single.log\'" >&2', + ' printf "%s\\n" "npm error bracket-posix=[/tmp/npm-private/bracket.log]" >&2', + ' printf "%s\\n" "npm error spaced-posix=\\"/tmp/npm private/spaced.log\\"" >&2', + ' printf "%s\\n" "npm error file-posix=file:///tmp/npm-private/file.log" >&2', + ' printf "%s\\n" "npm error file-windows=file:///C:/Users/runner/file.log" >&2', ' printf "%s\\n" "npm error config C:\\\\Users\\\\runner\\\\.npmrc" >&2', ' printf "%s\\n" "npm error quoted-win=\\"C:\\\\Users\\\\runner\\\\quoted.log\\"" >&2', + ' printf "%s\\n" "npm error single-win=\'C:\\\\Users\\\\CI Runner\\\\single.log\'" >&2', ' printf "%s\\n" "npm error workspace D:/build/private/package" >&2', ' printf "%s\\n" "npm error share \\\\\\\\server\\\\private\\\\debug.log" >&2', ' printf "%s\\n" "npm error quoted-share=\\"\\\\\\\\server\\\\private\\\\quoted.log\\"" >&2', + ' printf "%s\\n" "npm error registry https://registry.npmjs.org/@veryfront%2fext-flaky" >&2', ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users/runner/.npm/_logs/debug.log" >&2', " return 42", "}", @@ -250,9 +257,19 @@ describe("npm package publishing", () => { assertStringIncludes(stderr, "Bearer "); assertStringIncludes(stderr, "cache="); assertStringIncludes(stderr, 'quoted=""'); + assertStringIncludes(stderr, "single-posix=''"); + assertStringIncludes(stderr, "bracket-posix=[]"); + assertStringIncludes(stderr, 'spaced-posix=""'); + assertStringIncludes(stderr, "file-posix=file://"); + assertStringIncludes(stderr, "file-windows=file://"); assertStringIncludes(stderr, "config "); assertStringIncludes(stderr, 'quoted-win=""'); + assertStringIncludes(stderr, "single-win=''"); assertStringIncludes(stderr, 'quoted-share=""'); + assertStringIncludes( + stderr, + "registry https://registry.npmjs.org/@veryfront%2fext-flaky", + ); assertEquals(stderr.includes("fixture-token"), false); assertEquals(stderr.includes("/tmp/"), false); assertEquals(stderr.includes("C:\\"), false); From 993a3ca68249506c7fc390184c0467f3040dce06 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Mon, 3 Aug 2026 12:13:32 +0200 Subject: [PATCH 4/4] Preserve delimiters in npm diagnostic redaction Npm registry diagnostics can wrap fallback token and path values in quotes, parentheses, or comma-delimited text. The sanitizer now stops fallback matches before those delimiters while preserving the existing redaction output shape for credentials and local paths. Constraint: Scoped to PR #3321 npm release diagnostic sanitizer follow-up. Rejected: Rewrite sanitizer away from sed | too broad for a release-script follow-up. Confidence: high Scope-risk: narrow Directive: Keep fallback redaction patterns delimiter-aware when adding new token or path forms. Tested: Focused regression showed RED before the sanitizer change, then passed: deno test --config=scripts/test.deno.json --frozen --allow-all scripts/ci/publish-npm-packages.test.ts. Tested: bash -n scripts/ci/publish-npm-packages.sh; deno fmt --check --config=scripts/test.deno.json scripts/ci/publish-npm-packages.test.ts; deno lint --config=scripts/test.deno.json scripts/ci/publish-npm-packages.test.ts; deno check --config=scripts/test.deno.json --frozen scripts/ci/publish-npm-packages.test.ts; git diff --check. Tested: deno task verify:quick. Not-tested: ./scripts/hooks/pre-push is red before E2E execution because it references missing tests/e2e/playwright.config.ts; deno task test:e2e:playwright is red from mixed Playwright 1.60.0 and 1.59.0 loads; full deno test was interrupted after unrelated hosted/tool/cache network timeout failures. --- scripts/ci/publish-npm-packages.sh | 14 +++++----- scripts/ci/publish-npm-packages.test.ts | 34 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/scripts/ci/publish-npm-packages.sh b/scripts/ci/publish-npm-packages.sh index bec774f160..dabef9d779 100755 --- a/scripts/ci/publish-npm-packages.sh +++ b/scripts/ci/publish-npm-packages.sh @@ -168,19 +168,19 @@ sanitize_npm_lookup_output() { printf '%s\n' "$1" \ | sed -E \ -e '/^npm error A complete log of this run can be found in:/d' \ - -e 's#Bearer [^[:space:]]+#Bearer #g' \ - -e 's#([?&]token=)[^[:space:]&]+#\1#g' \ - -e 's#(_authToken=)[^[:space:]]+#\1#g' \ + -e "s#Bearer [^][[:space:]\"'),]+#Bearer #g" \ + -e "s#([?&]token=)[^][[:space:]\"'),&]+#\1#g" \ + -e "s#(_authToken=)[^][[:space:]\"'),]+#\1#g" \ -e 's#"(file://)/[^"]*"#"\1"#g' \ -e "s#'(file://)/[^']*'#'\1'#g" \ -e 's#\[(file://)/[^]]*\]#[\1]#g' \ - -e 's#(file://)/[^[:space:]]+#\1#g' \ + -e 's#(file://)/[^][[:space:]"),]+#\1#g' \ -e 's#(^|[[:space:]=(])"((/|[A-Za-z]:[\\/]|\\\\)[^"]*)"#\1""#g' \ -e "s#(^|[[:space:]=(])'((/|[A-Za-z]:[\\\\/]|\\\\\\\\)[^']*)'#\1''#g" \ -e 's#\[(/|[A-Za-z]:[\\/]|\\\\)[^]]*\]#[]#g' \ - -e 's#(^|[[:space:]"=(])/[^[:space:]"]+#\1#g' \ - -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^[:space:]"]+#\1#g' \ - -e 's#(^|[[:space:]"=(])\\\\[^[:space:]"]+#\1#g' + -e 's#(^|[[:space:]"=(])/[^][[:space:]"),]+#\1#g' \ + -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^][[:space:]"),]+#\1#g' \ + -e 's#(^|[[:space:]"=(])\\\\[^][[:space:]"),]+#\1#g' } ensure_package_names_registered() { diff --git a/scripts/ci/publish-npm-packages.test.ts b/scripts/ci/publish-npm-packages.test.ts index ff41f8fbdd..8ab7c61a4d 100644 --- a/scripts/ci/publish-npm-packages.test.ts +++ b/scripts/ci/publish-npm-packages.test.ts @@ -221,19 +221,33 @@ describe("npm package publishing", () => { ' printf "%s\\n" "npm error code E503" >&2', ' printf "%s\\n" "npm error 503 Service Unavailable" >&2', ' printf "%s\\n" "npm error auth Bearer fixture-token_~-/+=" >&2', + ' printf "%s\\n" "npm error quoted-bearer=\\"Bearer fixture-token_~-/+=\\"" >&2', + ' printf "%s\\n" "npm error comma-bearer=Bearer fixture-token_~-/+=," >&2', + ' printf "%s\\n" "npm error quoted-url=\\"https://registry.npmjs.org/?token=fixture-token_~-/+=\\"" >&2', + ' printf "%s\\n" "npm error comma-url=https://registry.npmjs.org/?token=fixture-token_~-/+=," >&2', + ' printf "%s\\n" "npm error quoted-auth=\\"_authToken=fixture-token_~-/+=\\"" >&2', + ' printf "%s\\n" "npm error comma-auth=_authToken=fixture-token_~-/+=," >&2', ' printf "%s\\n" "npm error cache=/tmp/npm-private/cache.log" >&2', ' printf "%s\\n" "npm error quoted=\\"/tmp/npm-private/quoted.log\\"" >&2', + ' printf "%s\\n" "npm error paren-posix=(/tmp/npm-private/paren.log)" >&2', + ' printf "%s\\n" "npm error comma-posix=/tmp/npm-private/comma.log," >&2', ' printf "%s\\n" "npm error single-posix=\'/tmp/npm-private/single.log\'" >&2', ' printf "%s\\n" "npm error bracket-posix=[/tmp/npm-private/bracket.log]" >&2', ' printf "%s\\n" "npm error spaced-posix=\\"/tmp/npm private/spaced.log\\"" >&2', ' printf "%s\\n" "npm error file-posix=file:///tmp/npm-private/file.log" >&2', + ' printf "%s\\n" "npm error paren-file=(file:///tmp/npm-private/paren.log)" >&2', + ' printf "%s\\n" "npm error comma-file=file:///tmp/npm-private/comma.log," >&2', ' printf "%s\\n" "npm error file-windows=file:///C:/Users/runner/file.log" >&2', ' printf "%s\\n" "npm error config C:\\\\Users\\\\runner\\\\.npmrc" >&2', ' printf "%s\\n" "npm error quoted-win=\\"C:\\\\Users\\\\runner\\\\quoted.log\\"" >&2', + ' printf "%s\\n" "npm error paren-win=(C:\\\\Users\\\\runner\\\\paren.log)" >&2', + ' printf "%s\\n" "npm error comma-win=C:\\\\Users\\\\runner\\\\comma.log," >&2', ' printf "%s\\n" "npm error single-win=\'C:\\\\Users\\\\CI Runner\\\\single.log\'" >&2', ' printf "%s\\n" "npm error workspace D:/build/private/package" >&2', ' printf "%s\\n" "npm error share \\\\\\\\server\\\\private\\\\debug.log" >&2', ' printf "%s\\n" "npm error quoted-share=\\"\\\\\\\\server\\\\private\\\\quoted.log\\"" >&2', + ' printf "%s\\n" "npm error paren-share=(\\\\\\\\server\\\\private\\\\paren.log)" >&2', + ' printf "%s\\n" "npm error comma-share=\\\\\\\\server\\\\private\\\\comma.log," >&2', ' printf "%s\\n" "npm error registry https://registry.npmjs.org/@veryfront%2fext-flaky" >&2', ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users/runner/.npm/_logs/debug.log" >&2', " return 42", @@ -255,17 +269,37 @@ describe("npm package publishing", () => { ); assertStringIncludes(stderr, "npm error code E503"); assertStringIncludes(stderr, "Bearer "); + assertStringIncludes(stderr, 'quoted-bearer="Bearer "'); + assertStringIncludes(stderr, "comma-bearer=Bearer ,"); + assertStringIncludes( + stderr, + 'quoted-url="https://registry.npmjs.org/?token="', + ); + assertStringIncludes( + stderr, + "comma-url=https://registry.npmjs.org/?token=,", + ); + assertStringIncludes(stderr, 'quoted-auth="_authToken="'); + assertStringIncludes(stderr, "comma-auth=_authToken=,"); assertStringIncludes(stderr, "cache="); assertStringIncludes(stderr, 'quoted=""'); + assertStringIncludes(stderr, "paren-posix=()"); + assertStringIncludes(stderr, "comma-posix=,"); assertStringIncludes(stderr, "single-posix=''"); assertStringIncludes(stderr, "bracket-posix=[]"); assertStringIncludes(stderr, 'spaced-posix=""'); assertStringIncludes(stderr, "file-posix=file://"); + assertStringIncludes(stderr, "paren-file=(file://)"); + assertStringIncludes(stderr, "comma-file=file://,"); assertStringIncludes(stderr, "file-windows=file://"); assertStringIncludes(stderr, "config "); assertStringIncludes(stderr, 'quoted-win=""'); + assertStringIncludes(stderr, "paren-win=()"); + assertStringIncludes(stderr, "comma-win=,"); assertStringIncludes(stderr, "single-win=''"); assertStringIncludes(stderr, 'quoted-share=""'); + assertStringIncludes(stderr, "paren-share=()"); + assertStringIncludes(stderr, "comma-share=,"); assertStringIncludes( stderr, "registry https://registry.npmjs.org/@veryfront%2fext-flaky",