From dcafcb2d8bdaf7af37afa305afb10396a840825b Mon Sep 17 00:00:00 2001
From: Roshane Pascual <rtpascual@users.noreply.github.com>
Date: Wed, 3 Apr 2024 13:16:58 -0700
Subject: [PATCH] fix(amplify-cli-core): gracefully handle execa race condition
 (#13692)

---
 .../src/__tests__/hooks/hooksExecutor.test.ts | 36 +++++++++++++++++++
 .../src/hooks/hooksExecutor.ts                | 10 +++++-
 2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/packages/amplify-cli-core/src/__tests__/hooks/hooksExecutor.test.ts b/packages/amplify-cli-core/src/__tests__/hooks/hooksExecutor.test.ts
index 6aca81697fc..e307c606ccf 100644
--- a/packages/amplify-cli-core/src/__tests__/hooks/hooksExecutor.test.ts
+++ b/packages/amplify-cli-core/src/__tests__/hooks/hooksExecutor.test.ts
@@ -178,4 +178,40 @@ describe('hooksExecutioner tests', () => {
       duplicateErrorThrown,
     );
   });
+
+  test('should not exit process if execa fails with exitCode being 0', async () => {
+    const execaMock = execa as jest.Mocked<typeof execa>;
+    (execaMock as any).mockReturnValue({
+      exitCode: 0,
+      errNo: -32,
+      code: 'EPIPE',
+      syscall: 'write',
+      originalMessage: 'write EPIPE',
+      shortMessage: 'Command failed with EPIPE',
+      escapedCommand: 'testCommand',
+      stderr: '',
+      failed: true,
+      timedOut: false,
+      isCanceled: false,
+      killed: false,
+    });
+    const processExitMock = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
+    await executeHooks(HooksMeta.getInstance({ command: 'add', plugin: 'auth' } as CommandLineInput, 'pre'));
+    expect(processExitMock).toBeCalledTimes(0);
+  });
+
+  test('should exit process with exit code 76 if execa fails with exitCode other than 0', async () => {
+    const execaMock = execa as jest.Mocked<typeof execa>;
+    (execaMock as any).mockReturnValue({
+      exitCode: 1,
+      stderr: '',
+      failed: true,
+      timedOut: false,
+      isCanceled: false,
+      killed: false,
+    });
+    const processExitMock = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
+    await executeHooks(HooksMeta.getInstance({ command: 'add', plugin: 'auth' } as CommandLineInput, 'pre'));
+    expect(processExitMock).toBeCalledWith(76);
+  });
 });
diff --git a/packages/amplify-cli-core/src/hooks/hooksExecutor.ts b/packages/amplify-cli-core/src/hooks/hooksExecutor.ts
index 758652bf5db..b0c430719c6 100644
--- a/packages/amplify-cli-core/src/hooks/hooksExecutor.ts
+++ b/packages/amplify-cli-core/src/hooks/hooksExecutor.ts
@@ -86,9 +86,17 @@ const execHelper = async (
         error: errorParameter,
       }),
       stripFinalNewline: false,
+      stdout: 'inherit',
+      // added to do further checks before throwing due to EPIPE error
+      reject: false,
     });
-    childProcess?.stdout?.pipe(process.stdout);
     const childProcessResult = await childProcess;
+
+    // throw if child process ended with anything other than exitCode 0
+    if (childProcessResult && childProcess.exitCode !== 0) {
+      throw childProcessResult;
+    }
+
     if (!childProcessResult?.stdout?.endsWith(EOL)) {
       printer.blankLine();
     }