From 50595ee7c1cbf16239ee77660b9d7ebe03a6a3bd Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:59:26 +0100 Subject: [PATCH 1/4] notarytool: be lenient in parsing command output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were seeing failures in our CI on notarytool invocations, as warnings emitted by notarytool were tried to be parsed as JSON. An example for such a warning is the following: ``` ✖ Finalizing package [FAILED: Failed to notarize via notarytool. Failed with unexpected result: warning: unhandled Platform key FamilyDisplayName {"id":"4803438f-a484-493a-acfd-1ef504a1e70b","message":"Processing complete","status":"Accepted"}] ``` This makes it so the parsing can work with such warnings, by finding the valid JSON line in the output and only using that to parse from. The less-hacky way to fix this would be to properly differentiate between stdout and stderr in `spawn.ts`, but I'm unable to test other commands and didn't want to break stuff, so I address the issue for notarytool only here. Furthermore, this makes the assumption that Apple's signing servers will only reply with a single JSON line. This was the case for all my testing queries, but I'm not sure if this holds true universally. Still, I think this is much better than the current situation, which just fails on whatever warning is emitted by notarytool. --- src/notarytool.ts | 36 ++++++++++++----- test/notarytool.test.ts | 85 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 test/notarytool.test.ts diff --git a/src/notarytool.ts b/src/notarytool.ts index 4f48108..b065236 100644 --- a/src/notarytool.ts +++ b/src/notarytool.ts @@ -64,6 +64,30 @@ async function getNotarizationLogs(opts: NotarizeOptions, id: string) { } } +function parseNotarytoolOutput(output: string): any { + const rawOut = output.trim(); + + // Be lenient in parsing the output, as notarytool may output warnings + const jsonOut = rawOut.substring(rawOut.indexOf('{'), rawOut.lastIndexOf('}') + 1); + const nonJsonLines = rawOut + .split('\n') + .filter((line) => !line.trim().startsWith('{') && !line.trim().endsWith('}')); + if (nonJsonLines.length > 0) { + d('notarytool produced some non-JSON output:\n', nonJsonLines.join('\n')); + } + + let parsed: any; + try { + parsed = JSON.parse(jsonOut); + } catch (err) { + throw new Error( + `Could not parse notarytool output: \n\n${rawOut}`, + ); + } + + return parsed; +} + export async function isNotaryToolAvailable(notarytoolPath?: string) { if (notarytoolPath !== undefined) { const result = await spawn(notarytoolPath, ['--version']); @@ -110,16 +134,8 @@ export async function notarizeAndWaitForNotaryTool(opts: NotarizeOptions) { ]; const result = await runNotaryTool(notarizeArgs, opts.notarytoolPath); - const rawOut = result.output.trim(); - - let parsed: any; - try { - parsed = JSON.parse(rawOut); - } catch (err) { - throw new Error( - `Failed to notarize via notarytool. Failed with unexpected result: \n\n${rawOut}`, - ); - } + + const parsed = parseNotarytoolOutput(result.output); let logOutput: undefined | string; if (typeof parsed.id === 'string') { diff --git a/test/notarytool.test.ts b/test/notarytool.test.ts new file mode 100644 index 0000000..71fc4f5 --- /dev/null +++ b/test/notarytool.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; + +describe('parseNotarytoolOutput', () => { + const parseNotarytoolOutput = (output: string): any => { + const rawOut = output.trim(); + + const jsonOut = rawOut.substring(rawOut.indexOf('{'), rawOut.lastIndexOf('}') + 1); + const nonJsonLines = rawOut + .split('\n') + .filter((line) => !line.trim().startsWith('{') && !line.trim().endsWith('}')); + if (nonJsonLines.length > 0) { + console.debug('notarytool produced some non-JSON output:\n', nonJsonLines.join('\n')); + } + + let parsed: any; + try { + parsed = JSON.parse(jsonOut); + } catch (err) { + throw new Error(`Could not parse notarytool output: \n\n${rawOut}`); + } + + return parsed; + }; + + it('parses valid JSON output', () => { + const output = '{"status": "Accepted", "id": "123"}'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: '123', + }); + }); + + it('parses JSON with whitespace', () => { + const output = '\n\n {"status": "Accepted", "id": "456"} \n'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: '456', + }); + }); + + it('parses JSON with warnings before it', () => { + const output = 'Warning: Some warning message\n{"status": "Accepted", "id": "789"}'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: '789', + }); + }); + + it('parses JSON with warnings after it', () => { + const output = '{"status": "Accepted", "id": "abc"}\nWarning: Some warning message'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: 'abc', + }); + }); + + it('parses JSON with warnings before and after it', () => { + const output = + 'Warning: First warning\n{"status": "Invalid", "id": "def"}\nWarning: Second warning'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Invalid', + id: 'def', + }); + }); + + it('throws error for invalid JSON', () => { + const output = 'not json at all'; + expect(() => parseNotarytoolOutput(output)).toThrow( + 'Could not parse notarytool output: \n\nnot json at all', + ); + }); + + it('throws error for incomplete JSON', () => { + const output = '{"status": "Accepted"'; + expect(() => parseNotarytoolOutput(output)).toThrow('Could not parse notarytool output'); + }); + + it('parses nested JSON objects', () => { + const output = '{"status": "Accepted", "data": {"nested": "value"}}'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + data: { nested: 'value' }, + }); + }); +}); From 6cc91a62a3d3476f15d469d0d5b74c828b0c1428 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:18:56 +0100 Subject: [PATCH 2/4] Update src/notarytool.ts Co-authored-by: Kevin Cui --- src/notarytool.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/notarytool.ts b/src/notarytool.ts index b065236..a897992 100644 --- a/src/notarytool.ts +++ b/src/notarytool.ts @@ -67,15 +67,18 @@ async function getNotarizationLogs(opts: NotarizeOptions, id: string) { function parseNotarytoolOutput(output: string): any { const rawOut = output.trim(); - // Be lenient in parsing the output, as notarytool may output warnings - const jsonOut = rawOut.substring(rawOut.indexOf('{'), rawOut.lastIndexOf('}') + 1); - const nonJsonLines = rawOut - .split('\n') - .filter((line) => !line.trim().startsWith('{') && !line.trim().endsWith('}')); - if (nonJsonLines.length > 0) { - d('notarytool produced some non-JSON output:\n', nonJsonLines.join('\n')); + let jsonOut: string = ''; + + for (const line of rawOut.split('\n')) { + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('{') && trimmedLine.endsWith('}')) { + jsonOut = line; + break; + } } + d('notarytool produced output:\n', output); + let parsed: any; try { parsed = JSON.parse(jsonOut); From 1a75fef166866759e23526871cf62272f8b9acac Mon Sep 17 00:00:00 2001 From: Kevin Cui Date: Wed, 12 Nov 2025 18:25:23 +0800 Subject: [PATCH 3/4] fix: code style Signed-off-by: Kevin Cui --- src/notarytool.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/notarytool.ts b/src/notarytool.ts index a897992..b7d7140 100644 --- a/src/notarytool.ts +++ b/src/notarytool.ts @@ -83,9 +83,7 @@ function parseNotarytoolOutput(output: string): any { try { parsed = JSON.parse(jsonOut); } catch (err) { - throw new Error( - `Could not parse notarytool output: \n\n${rawOut}`, - ); + throw new Error(`Could not parse notarytool output: \n\n${rawOut}`); } return parsed; From ed8cd37180ada6284208838b781e0360060300ba Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:53:38 +0100 Subject: [PATCH 4/4] test: use exported parsing function --- src/notarytool.ts | 2 +- test/notarytool.test.ts | 22 +--------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/notarytool.ts b/src/notarytool.ts index b7d7140..649aa57 100644 --- a/src/notarytool.ts +++ b/src/notarytool.ts @@ -64,7 +64,7 @@ async function getNotarizationLogs(opts: NotarizeOptions, id: string) { } } -function parseNotarytoolOutput(output: string): any { +export function parseNotarytoolOutput(output: string): any { const rawOut = output.trim(); let jsonOut: string = ''; diff --git a/test/notarytool.test.ts b/test/notarytool.test.ts index 71fc4f5..fe3a8ce 100644 --- a/test/notarytool.test.ts +++ b/test/notarytool.test.ts @@ -1,27 +1,7 @@ import { describe, expect, it } from 'vitest'; +import { parseNotarytoolOutput } from '../src/notarytool.js'; describe('parseNotarytoolOutput', () => { - const parseNotarytoolOutput = (output: string): any => { - const rawOut = output.trim(); - - const jsonOut = rawOut.substring(rawOut.indexOf('{'), rawOut.lastIndexOf('}') + 1); - const nonJsonLines = rawOut - .split('\n') - .filter((line) => !line.trim().startsWith('{') && !line.trim().endsWith('}')); - if (nonJsonLines.length > 0) { - console.debug('notarytool produced some non-JSON output:\n', nonJsonLines.join('\n')); - } - - let parsed: any; - try { - parsed = JSON.parse(jsonOut); - } catch (err) { - throw new Error(`Could not parse notarytool output: \n\n${rawOut}`); - } - - return parsed; - }; - it('parses valid JSON output', () => { const output = '{"status": "Accepted", "id": "123"}'; expect(parseNotarytoolOutput(output)).toEqual({