Repository: slackapi/slack-github-action (js)

Issue:
Improve error reporting when action fail to parse correctly the payload
### Description

In case of a payload parse error, we get the message `Invalid input! Failed to parse contents of the provided payload.` which is not very specific. It would be great to have a better message that highlight which line is the source of the error.

### What type of issue is this? (place an `x` in one of the `[ ]`)
- [ ] bug
- [x] enhancement (feature request)
- [ ] question
- [ ] documentation related
- [ ] example code related
- [ ] testing related
- [ ] discussion

### Requirements (place an `x` in each of the `[ ]`)
* [x] I've read and understood the [Contributing guidelines](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) and have done my best effort to follow them.
* [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
* [x] I've searched for any related issues and avoided creating a duplicate issue.

---

### Bug Report

When the payload is malformed, it result in a parse error message which does not guide us on where to look for to find the problem. It would be great if we could at least have the line where the first error happen.

#### Reproducible in:

package version: latest

node version:

OS version(s):

#### Steps to reproduce:

1. Send a malformed payload
2. Figure out why it is malformed

#### Expected result:

Display the line of the first error found. I understand there is a difficulty with the optimistic detection of the file format (yaml vs json), but it is likely that a yaml file parse with less error when there is a malformed payload with the yaml parser than with the json parser.

#### Actual result:

Just say: `Invalid input! Failed to parse contents of the provided payload.`


Pull request:
fix: include cause of parsing errors in action output logs
### Summary

This PR includes the cause of parsing errors in action output logs to fix #359.

### Requirements

* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) and have done my best effort to follow them.
* [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).

Patch:
diff --git a/src/content.js b/src/content.js
index 6594a32..e9506a0 100644
--- a/src/content.js
+++ b/src/content.js
@@ -55,6 +55,7 @@ export default class Content {
    * @returns {Content} - the parsed JSON payload to use in requests.
    */
   getContentPayload(config) {
+    const errors = [];
     if (!config.inputs.payload) {
       throw new SlackError(
         config.core,
@@ -71,8 +72,7 @@ export default class Content {
       return /** @type {Content} */ (content);
     } catch (error) {
       if (error instanceof Error) {
-        config.core.debug("Failed to parse input payload as YAML");
-        config.core.debug(error.message);
+        errors.push(error);
       }
     }
     try {
@@ -90,11 +90,12 @@ export default class Content {
       }
       return JSON.parse(trimmed);
     } catch (/** @type {any} */ error) {
+      errors.unshift(error);
       throw new SlackError(
         config.core,
         "Invalid input! Failed to parse contents of the provided payload",
         {
-          cause: error,
+          cause: { values: errors },
         },
       );
     }
@@ -140,7 +141,7 @@ export default class Content {
         config.core,
         "Invalid input! Failed to parse contents of the provided payload file",
         {
-          cause: error,
+          cause: { values: [error] },
         },
       );
     }
diff --git a/src/errors.js b/src/errors.js
index b2d4ce0..3d0d87b 100644
--- a/src/errors.js
+++ b/src/errors.js
@@ -1,12 +1,17 @@
 import core from "@actions/core";
 
+/**
+ * @typedef Cause
+ * @property {Error[]} [values] - Caught exceptions.
+ */
+
 /**
  * SlackError is a custom error wrapper for known errors of Slack GitHub Action.
  */
 export default class SlackError extends Error {
   /**
    * @typedef Options
-   * @property {Error} [cause] - thrown exception of this error.
+   * @property {Cause} [cause] - Reason for an error.
    */
 
   /**
diff --git a/src/index.js b/src/index.js
index fcdf227..ff3b701 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,13 +6,21 @@ import send from "./send.js";
  * from the send.js file for testing purposes.
  */
 try {
-  send(core);
+  await send(core);
 } catch (error) {
   if (error instanceof Error) {
     core.error(error.message);
-    core.debug(`${error.name} cause: ${error.cause}`);
-    core.debug(`${error.name} stack: ${error.stack}`);
+    /** @type {import('./errors.js').Cause} */
+    const causes = /** @type {any} */ (error.cause);
+    if (causes?.values) {
+      for (const cause of causes.values) {
+        core.info(`${cause.stack}`);
+      }
+    } else {
+      core.info(`${error.stack}`);
+    }
   } else {
     core.error(`${error}`);
   }
+  throw error;
 }


Test patch:
diff --git a/test/content.spec.js b/test/content.spec.js
index be279d4..30a3728 100644
--- a/test/content.spec.js
+++ b/test/content.spec.js
@@ -1,6 +1,7 @@
 import path from "node:path";
 import core from "@actions/core";
 import { assert } from "chai";
+import { YAMLException } from "js-yaml";
 import Config from "../src/config.js";
 import Content from "../src/content.js";
 import SlackError from "../src/errors.js";
@@ -190,6 +191,11 @@ describe("content", () => {
             err.message,
             "Invalid input! Failed to parse contents of the provided payload",
           );
+          assert.isDefined(err.cause?.values);
+          assert.equal(err.cause.values.length, 2);
+          const [jsonError, yamlError] = err.cause.values;
+          assert.isTrue(jsonError instanceof SyntaxError);
+          assert.isTrue(yamlError instanceof YAMLException);
         } else {
           assert.fail("Failed to throw a SlackError", err);
         }
@@ -334,8 +340,10 @@ describe("content", () => {
             err.message,
             "Invalid input! Failed to parse contents of the provided payload file",
           );
+          assert.isDefined(err.cause?.values);
+          assert.equal(err.cause.values.length, 1);
           assert.include(
-            err.cause.message,
+            err.cause.values[0].message,
             "Invalid input! Failed to parse file extension unknown.md",
           );
         } else {
@@ -343,5 +351,52 @@ describe("content", () => {
         }
       }
     });
+
+    it("fails if invalid JSON exists in the input payload", async () => {
+      mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
+      mocks.fs.readFileSync
+        .withArgs(path.resolve("example.json"), "utf-8")
+        .returns(`{
+            "message": "a truncated file without an end`);
+      try {
+        await send(mocks.core);
+        assert.fail("Failed to throw for invalid JSON");
+      } catch (err) {
+        if (err instanceof SlackError) {
+          assert.include(
+            err.message,
+            "Invalid input! Failed to parse contents of the provided payload file",
+          );
+          assert.isDefined(err.cause?.values);
+          assert.equal(err.cause.values.length, 1);
+          assert.isTrue(err.cause.values[0] instanceof SyntaxError);
+        } else {
+          assert.fail("Failed to throw a SlackError", err);
+        }
+      }
+    });
+
+    it("fails if invalid YAML exists in the input payload", async () => {
+      mocks.core.getInput.withArgs("payload-file-path").returns("example.yaml");
+      mocks.fs.readFileSync
+        .withArgs(path.resolve("example.yaml"), "utf-8")
+        .returns(`- "message": "assigned": "values"`);
+      try {
+        await send(mocks.core);
+        assert.fail("Failed to throw for invalid YAML");
+      } catch (err) {
+        if (err instanceof SlackError) {
+          assert.include(
+            err.message,
+            "Invalid input! Failed to parse contents of the provided payload file",
+          );
+          assert.isDefined(err.cause?.values);
+          assert.equal(err.cause.values.length, 1);
+          assert.isTrue(err.cause.values[0] instanceof YAMLException);
+        } else {
+          assert.fail("Failed to throw a SlackError", err);
+        }
+      }
+    });
   });
 });