From d009d359be736d014deb57a7de5eb8f3cfe6c5ca Mon Sep 17 00:00:00 2001 From: Joob1n Date: Mon, 17 Aug 2026 16:55:24 +0800 Subject: [PATCH 1/2] fix(scripts): say what drifted when the notices check fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--check` compares 15k generated lines byte-for-byte and reports only that they are stale. Whoever hits it is usually reading a CI log for an OS they are not holding, and the suggested command is the fix for only one of the two causes. The check now separates them. A changed dependency closure names the packages on each side, capped at ten with a count. Identical packages whose notice text moved say so and point at the first differing line with both versions quoted — that shape is not a missed regeneration and the suggested command will not explain it. Found while diagnosing a failure this reports in one line: the same packages listed, first difference at line 430, which localizes it to LICENSE content rather than a dependency change. Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J --- scripts/generate-third-party-notices.mjs | 58 +++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/scripts/generate-third-party-notices.mjs b/scripts/generate-third-party-notices.mjs index 24b5ebcbac..0a42a9cd9a 100644 --- a/scripts/generate-third-party-notices.mjs +++ b/scripts/generate-third-party-notices.mjs @@ -333,12 +333,66 @@ function validateAssetNotices() { } } +/** + * Say what drifted, not just that something did. + * + * The comparison is byte-exact over a 15k-line generated file, so "stale" on + * its own leaves whoever hit it — often on a CI runner whose OS they are not + * holding — with nothing to act on. The two shapes worth separating are a + * changed dependency closure, which the suggested command fixes, and identical + * packages whose license bytes moved, which usually means something upstream + * or environmental rather than a missed regeneration. + */ +function describeNoticeDrift(committed, generated) { + const packagesIn = (text) => + new Set( + text + .split('\n') + .filter((line) => line.startsWith('Package: ')) + .map((line) => line.slice('Package: '.length)), + ); + const committedPackages = packagesIn(committed); + const generatedPackages = packagesIn(generated); + const onlyGenerated = [...generatedPackages].filter((name) => !committedPackages.has(name)); + const onlyCommitted = [...committedPackages].filter((name) => !generatedPackages.has(name)); + + const lines = []; + const list = (label, names) => { + if (names.length === 0) return; + const shown = names.slice(0, 10); + lines.push(` ${label} (${names.length}):`); + for (const name of shown) lines.push(` ${name}`); + if (names.length > shown.length) lines.push(` …and ${names.length - shown.length} more`); + }; + list('present in the closure but missing from the committed file', onlyGenerated); + list('committed but no longer in the closure', onlyCommitted); + + if (onlyGenerated.length === 0 && onlyCommitted.length === 0) { + const committedLines = committed.split('\n'); + const generatedLines = generated.split('\n'); + const limit = Math.max(committedLines.length, generatedLines.length); + let index = 0; + while (index < limit && committedLines[index] === generatedLines[index]) index += 1; + lines.push(' the same packages are listed, so the difference is in the notice text itself'); + lines.push(` first difference at line ${index + 1}:`); + lines.push(` committed: ${JSON.stringify(committedLines[index] ?? '')}`); + lines.push(` generated: ${JSON.stringify(generatedLines[index] ?? '')}`); + } + return lines.join('\n'); +} + validateAssetNotices(); const generated = renderNotice(); if (checkOnly) { - if (!existsSync(outputPath) || readFileSync(outputPath, 'utf8') !== generated) { + if (!existsSync(outputPath)) { + throw new Error( + `Production dependency notices are missing at ${outputPath}. Run npm run generate:third-party-notices.`, + ); + } + const committed = readFileSync(outputPath, 'utf8'); + if (committed !== generated) { throw new Error( - 'Production dependency notices are stale. Run npm run generate:third-party-notices.', + `Production dependency notices are stale. Run npm run generate:third-party-notices.\n${describeNoticeDrift(committed, generated)}`, ); } console.log('[third-party-notices] OK — production dependency inventory is current.'); From 1c14c53df72569ac4fcc29e43e869c9e544e879a Mon Sep 17 00:00:00 2001 From: Joob1n Date: Mon, 17 Aug 2026 17:09:24 +0800 Subject: [PATCH 2/2] fix(scripts): keep the drift report correct under CRLF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packagesIn` split on `\n` only, so a CRLF checkout left `\r` on every extracted name. The same package set then reported as both added and removed, and the text-drift branch — the useful half — never ran. The diagnostic would have misdescribed precisely the environment-specific failure it exists to explain. Both sides now split on either ending and the extracted names are trimmed. While there: when every line matches after normalizing, the difference is the endings themselves. That now says so, and points at .gitattributes and the checkout instead of at the dependency closure, because regenerating cannot fix it and this repository already forces LF. Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J --- scripts/generate-third-party-notices.mjs | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/scripts/generate-third-party-notices.mjs b/scripts/generate-third-party-notices.mjs index 0a42a9cd9a..7165bd0375 100644 --- a/scripts/generate-third-party-notices.mjs +++ b/scripts/generate-third-party-notices.mjs @@ -344,12 +344,16 @@ function validateAssetNotices() { * or environmental rather than a missed regeneration. */ function describeNoticeDrift(committed, generated) { + // Split on either ending. A CRLF checkout would otherwise leave `\r` on every + // extracted name, reporting the same packages as both added and removed and + // hiding the text-drift branch below — the diagnostic would misdescribe + // exactly the environment-specific failure it exists to explain. + const linesOf = (text) => text.split(/\r?\n/); const packagesIn = (text) => new Set( - text - .split('\n') + linesOf(text) .filter((line) => line.startsWith('Package: ')) - .map((line) => line.slice('Package: '.length)), + .map((line) => line.slice('Package: '.length).trim()), ); const committedPackages = packagesIn(committed); const generatedPackages = packagesIn(generated); @@ -368,15 +372,26 @@ function describeNoticeDrift(committed, generated) { list('committed but no longer in the closure', onlyCommitted); if (onlyGenerated.length === 0 && onlyCommitted.length === 0) { - const committedLines = committed.split('\n'); - const generatedLines = generated.split('\n'); + const committedLines = linesOf(committed); + const generatedLines = linesOf(generated); const limit = Math.max(committedLines.length, generatedLines.length); let index = 0; while (index < limit && committedLines[index] === generatedLines[index]) index += 1; lines.push(' the same packages are listed, so the difference is in the notice text itself'); - lines.push(` first difference at line ${index + 1}:`); - lines.push(` committed: ${JSON.stringify(committedLines[index] ?? '')}`); - lines.push(` generated: ${JSON.stringify(generatedLines[index] ?? '')}`); + if (index === limit) { + // Every line matches once endings are normalized, so the bytes differ only + // in how the lines end. Naming that is the whole answer — regenerating + // will not help, and the repository already forces LF through + // .gitattributes, so a CRLF checkout means that rule did not take effect. + lines.push(' every line matches once line endings are normalized:'); + lines.push(` committed uses CRLF: ${committed.includes('\r\n')}`); + lines.push(` generated uses CRLF: ${generated.includes('\r\n')}`); + lines.push(' check .gitattributes and the checkout, not the dependency closure'); + } else { + lines.push(` first difference at line ${index + 1}:`); + lines.push(` committed: ${JSON.stringify(committedLines[index] ?? '')}`); + lines.push(` generated: ${JSON.stringify(generatedLines[index] ?? '')}`); + } } return lines.join('\n'); }