Repository: slackapi/slack-github-action
Pull request #420 (2025-03-27 00:02:43)

fix: require a custom 'api' url to send to instead of absolute urls as a 'method'
### Summary

This PR requires using a custom `api` URL in step inputs to send to URLs that are not "https://slack.com/api/".

Fixes unexpected behaviors noted in https://github.com/slackapi/node-slack-sdk/pull/2176 for steps using a variable `method` for cases of [composite](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action#creating-an-action-metadata-file) action workflows or of variable inputs from outputs of a different step.

No change to functionalities when using as a standalone step!

### Preview

The format shown in the [`README.md`](https://github.com/slackapi/slack-github-action?tab=readme-ov-file#sending-to-a-custom-api-url) is now required to send to a custom `api` URL:

```diff
  - name: Post text to a Slack channel
    uses: slackapi/slack-github-action
    with:
-     method: http://localhost:8080/chat.postMessage
+     api: http://localhost:8080/
+     method: chat.postMessage
      token: ${{ secrets.SLACK_BOT_TOKEN }}
      payload: |
        channel: ${{ secrets.SLACK_CHANNEL_ID }}
        text: "howdy <@channel>!"
```

### 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).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slackapi/slack-github-action/pull/420?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.85%. Comparing base [(`e7edf37`)](https://app.codecov.io/gh/slackapi/slack-github-action/commit/e7edf37c48bfa3d4fa8267a7e432dc349c083ae9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) to head [(`cc8c3c9`)](https://app.codecov.io/gh/slackapi/slack-github-action/commit/cc8c3c9256ab14374c57d852d872873ef7eab5e0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #420   +/-   ##
=======================================
  Coverage   99.85%   99.85%           
=======================================
  Files           7        7           
  Lines         702      703    +1     
=======================================
+ Hits          701      702    +1     
  Misses          1        1           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/slackapi/slack-github-action/pull/420?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
zimeg: @WilliamBergamin @mwbrooks Thank y'all both for the reviews 🙏

These tests were stumping me for a bit longer than I'd like to admit but related work in an adjacent repo brought some great inspiration 😉

> But any chance we could add a unit test that ensures the override configuration sends a request to the correct uri?

This doesn't seem simple without catching actual HTTP requests, but we now test that the expected **arguments** are used in both the `WebClient` constructor and `.apiCall` method.

For now I'm hoping this is alright with the upstream tests we also have:

https://github.com/slackapi/node-slack-sdk/blob/29c1e1aff222a8c40513c55e874c5a9149131f40/packages/web-api/src/WebClient.spec.ts#L534-L546
zimeg: @WilliamBergamin @mwbrooks @hello-ashleyintech Thank y'all all for the ideas and suggestions of this change 🙏 ✨ 

I will merge this now with a follow up to improve error messages before releasing this. Exciting times ahead I hope for unlocking the `api` input!

Patch:
diff --git a/src/client.js b/src/client.js
index e476a88..30ed68a 100644
--- a/src/client.js
+++ b/src/client.js
@@ -24,6 +24,7 @@ export default class Client {
     }
     const client = new config.webapi.WebClient(config.inputs.token, {
       agent: this.proxies(config)?.httpsAgent,
+      allowAbsoluteUrls: false,
       logger: config.logger,
       retryConfig: this.retries(config.inputs.retries),
       slackApiUrl: config.inputs.api || undefined,


Test patch:
diff --git a/test/client.spec.js b/test/client.spec.js
index 58f2dcd..d426c80 100644
--- a/test/client.spec.js
+++ b/test/client.spec.js
@@ -60,36 +60,105 @@ describe("client", () => {
         }
       }
     });
+  });
 
-    it("uses input arguments when constructing the web api client", async () => {
-      const spy = sinon.spy(mocks.webapi, "WebClient");
+  describe("api", async () => {
+    it("uses arguments to send to a slack api method", async () => {
+      const apis = sinon.stub().resolves({ ok: true });
+      const constructors = sinon
+        .stub(mocks.webapi, "WebClient")
+        .returns({ apiCall: apis });
       /**
        * @type {Config}
        */
       const config = {
         content: {
-          values: {},
+          values: {
+            channel: "CHANNELHERE",
+            timestamp: "1234567890.000000",
+          },
         },
         core: core,
         logger: new Logger(core).logger,
         inputs: {
-          api: "http://localhost:8080/api",
           method: "pins.add",
-          retries: "10",
           token: "xoxb-example-002",
         },
         webapi: mocks.webapi,
       };
       await new Client().post(config);
-      assert.isTrue(spy.calledWithNew());
+      assert.isTrue(constructors.calledWithNew());
       assert.isTrue(
-        spy.calledWith("xoxb-example-002", {
+        constructors.calledWith("xoxb-example-002", {
           agent: undefined,
+          allowAbsoluteUrls: false,
+          logger: config.logger,
+          retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes,
+          slackApiUrl: undefined,
+        }),
+      );
+      assert.isTrue(apis.calledOnce);
+      assert.isTrue(
+        apis.calledWith("pins.add", {
+          channel: "CHANNELHERE",
+          timestamp: "1234567890.000000",
+        }),
+      );
+      assert.isTrue(config.core.setOutput.calledWith("ok", true));
+    });
+
+    it("uses arguments to send to a custom api method", async () => {
+      const apis = sinon.stub().resolves({ done: true, response: "Infinite" });
+      const constructors = sinon
+        .stub(mocks.webapi, "WebClient")
+        .returns({ apiCall: apis });
+      /**
+       * @type {Config}
+       */
+      const config = {
+        content: {
+          values: {
+            model: "llama3.2",
+            prompt: "How many sides does a circle have?",
+            stream: false,
+          },
+        },
+        core: core,
+        logger: new Logger(core).logger,
+        inputs: {
+          api: "http://localhost:11434/api/",
+          method: "generate",
+          retries: "10",
+          token: "ollamapassword",
+        },
+        webapi: mocks.webapi,
+      };
+      await new Client().post(config);
+      assert.isTrue(constructors.calledWithNew());
+      assert.isTrue(
+        constructors.calledWith("ollamapassword", {
+          agent: undefined,
+          allowAbsoluteUrls: false,
           logger: config.logger,
           retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes,
-          slackApiUrl: "http://localhost:8080/api",
+          slackApiUrl: "http://localhost:11434/api/",
+        }),
+      );
+      assert.isTrue(apis.calledOnce);
+      assert.isTrue(
+        apis.calledWith("generate", {
+          model: "llama3.2",
+          prompt: "How many sides does a circle have?",
+          stream: false,
         }),
       );
+      assert.isTrue(config.core.setOutput.calledWith("ok", undefined));
+      assert.isTrue(
+        config.core.setOutput.calledWith(
+          "response",
+          JSON.stringify({ done: true, response: "Infinite" }),
+        ),
+      );
     });
   });
 
diff --git a/test/index.spec.js b/test/index.spec.js
index 186db60..e3348b0 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -44,7 +44,7 @@ export class Mock {
   constructor() {
     this.sandbox = sinon.createSandbox();
     this.axios = this.sandbox.stub(axios);
-    this.calls = sinon.stub(webapi.WebClient.prototype, "apiCall");
+    this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall");
     this.core = this.sandbox.stub(core);
     this.fs = this.sandbox.stub(fs);
     this.webapi = {
@@ -66,6 +66,13 @@ export class Mock {
     this.axios.post.resetHistory();
     this.calls.resetHistory();
     this.core.getInput.reset();
+    this.webapi = {
+      WebClient: function () {
+        this.apiCall = () => ({
+          ok: true,
+        });
+      },
+    };
     this.core.getInput.withArgs("errors").returns("false");
     this.core.getInput.withArgs("retries").returns("5");
     process.env.SLACK_TOKEN = "";