From ab3f13a4b2727dd1d4766557795861868b489730 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 30 May 2025 17:03:10 -0700 Subject: [PATCH 1/6] fix: parse provided payloads before replacing templated variables --- src/content.js | 49 +++++++++++++++--------- test/content.spec.js | 91 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/src/content.js b/src/content.js index e9506a09..bace7b34 100644 --- a/src/content.js +++ b/src/content.js @@ -37,6 +37,9 @@ export default class Content { this.values = github.context; break; } + if (config.inputs.payloadTemplated) { + this.values = this.templatize(this.values); + } if (config.inputs.payloadDelimiter) { this.values = flatten(this.values, { delimiter: config.inputs.payloadDelimiter, @@ -63,9 +66,8 @@ export default class Content { ); } try { - const input = this.templatize(config, config.inputs.payload); const content = /** @type {Content} */ ( - yaml.load(input, { + yaml.load(config.inputs.payload, { schema: yaml.JSON_SCHEMA, }) ); @@ -119,18 +121,17 @@ export default class Content { path.resolve(config.inputs.payloadFilePath), "utf-8", ); - const content = this.templatize(config, input); if ( config.inputs.payloadFilePath.endsWith("yaml") || config.inputs.payloadFilePath.endsWith("yml") ) { - const load = yaml.load(content, { + const load = yaml.load(input, { schema: yaml.JSON_SCHEMA, }); return /** @type {Content} */ (load); } if (config.inputs.payloadFilePath.endsWith("json")) { - return JSON.parse(content); + return JSON.parse(input); } throw new SlackError( config.core, @@ -148,20 +149,32 @@ export default class Content { } /** - * Replace templated variables in the provided content if requested. - * @param {Config} config - * @param {string} input - The initial value of the content. - * @returns {string} Content with templatized variables replaced. + * Replace templated variables in the provided content as requested. + * @param {unknown} input - The initial value of the content. + * @returns {unknown} Content with templatized variables replaced. */ - templatize(config, input) { - if (!config.inputs.payloadTemplated) { - return input; + templatize(input) { + if (Array.isArray(input)) { + return input.map((v) => this.templatize(v)); + } + if (input && typeof input === "object") { + /** + * @type {any} + */ + const out = {}; + for (const [k, v] of Object.entries(input)) { + out[k] = this.templatize(v); + } + return out; + } + if (typeof input === "string") { + const template = input.replace(/\$\{\{/g, "{{"); // swap ${{ for {{ + const context = { + env: process.env, + github: github.context, + }; + return markup.up(template, context); } - const template = input.replace(/\$\{\{/g, "{{"); // swap ${{ for {{ - const context = { - env: process.env, - github: github.context, - }; - return markup.up(template, context); + return input; } } diff --git a/test/content.spec.js b/test/content.spec.js index 30a37286..47a663ef 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -87,13 +87,53 @@ describe("content", () => { it("templatizes variables with matching variables", async () => { mocks.core.getInput .withArgs("payload") - .returns("message: Served ${{ env.NUMBER }} from ${{ github.apiUrl }}"); + .returns(` + channel: C0123456789 + reply_broadcast: false + message: Served \${{ env.NUMBER }} from \${{ github.apiUrl }} + blocks: + - type: section + text: + type: mrkdwn + text: "Served \${{ env.NUMBER }} on: \${{ env.DETAILS }}" + - type: divider + - type: section + text: + type: mrkdwn + text: "> From \${{ github.apiUrl }}" + `); mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + process.env.DETAILS = ` +-fri +-sat +-sun` process.env.NUMBER = 12; const config = new Config(mocks.core); + process.env.DETAILS = undefined; process.env.NUMBER = undefined; const expected = { + channel: "C0123456789", + reply_broadcast: false, message: "Served 12 from https://api.github.com", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Served 12 on: \n-fri\n-sat\n-sun" + } + }, + { + type: "divider" + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "> From https://api.github.com" + } + } + ] }; assert.deepEqual(config.content.values, expected); }); @@ -257,14 +297,61 @@ describe("content", () => { mocks.fs.readFileSync .withArgs(path.resolve("example.json"), "utf-8") .returns(`{ - "message": "Served $\{\{ env.NUMBER }} from $\{\{ github.apiUrl }}" + "channel": "C0123456789", + "reply_broadcast": false, + "message": "Served \${{ env.NUMBER }} from \${{ github.apiUrl }}", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Served \${{ env.NUMBER }} on: \${{ env.DETAILS }}" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "> From \${{ github.apiUrl }}" + } + } + ] }`); mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + process.env.DETAILS = ` +-fri +-sat +-sun` process.env.NUMBER = 12; const config = new Config(mocks.core); + process.env.DETAILS = undefined; process.env.NUMBER = undefined; const expected = { + channel: "C0123456789", + reply_broadcast: false, message: "Served 12 from https://api.github.com", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Served 12 on: \n-fri\n-sat\n-sun" + } + }, + { + type: "divider" + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "> From https://api.github.com" + } + } + ] }; assert.deepEqual(config.content.values, expected); }); From c247b9984a5dbdae7c456f597376e58e213f7912 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 30 May 2025 17:07:06 -0700 Subject: [PATCH 2/6] style: run the linter before pushing changes upstream of course --- test/content.spec.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/test/content.spec.js b/test/content.spec.js index 47a663ef..366d47e1 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -85,9 +85,7 @@ describe("content", () => { }); it("templatizes variables with matching variables", async () => { - mocks.core.getInput - .withArgs("payload") - .returns(` + mocks.core.getInput.withArgs("payload").returns(` channel: C0123456789 reply_broadcast: false message: Served \${{ env.NUMBER }} from \${{ github.apiUrl }} @@ -106,7 +104,7 @@ describe("content", () => { process.env.DETAILS = ` -fri -sat --sun` +-sun`; process.env.NUMBER = 12; const config = new Config(mocks.core); process.env.DETAILS = undefined; @@ -120,20 +118,20 @@ describe("content", () => { type: "section", text: { type: "mrkdwn", - text: "Served 12 on: \n-fri\n-sat\n-sun" - } + text: "Served 12 on: \n-fri\n-sat\n-sun", + }, }, { - type: "divider" + type: "divider", }, { type: "section", text: { type: "mrkdwn", - text: "> From https://api.github.com" - } - } - ] + text: "> From https://api.github.com", + }, + }, + ], }; assert.deepEqual(config.content.values, expected); }); @@ -324,7 +322,7 @@ describe("content", () => { process.env.DETAILS = ` -fri -sat --sun` +-sun`; process.env.NUMBER = 12; const config = new Config(mocks.core); process.env.DETAILS = undefined; @@ -338,20 +336,20 @@ describe("content", () => { type: "section", text: { type: "mrkdwn", - text: "Served 12 on: \n-fri\n-sat\n-sun" - } + text: "Served 12 on: \n-fri\n-sat\n-sun", + }, }, { - type: "divider" + type: "divider", }, { type: "section", text: { type: "mrkdwn", - text: "> From https://api.github.com" - } - } - ] + text: "> From https://api.github.com", + }, + }, + ], }; assert.deepEqual(config.content.values, expected); }); From 2d93133884f0e271f88d5239d1936e87b592714b Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 2 Jun 2025 13:30:59 -0700 Subject: [PATCH 3/6] style: prefer stronger typing of a recursed key value record pair Co-authored-by: William Bergamin --- src/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content.js b/src/content.js index bace7b34..2dba1adb 100644 --- a/src/content.js +++ b/src/content.js @@ -159,7 +159,7 @@ export default class Content { } if (input && typeof input === "object") { /** - * @type {any} + * @type {Record} */ const out = {}; for (const [k, v] of Object.entries(input)) { From 5c48fcffb2efad4481b75a86fb16af74af93e16c Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 2 Jun 2025 14:19:29 -0700 Subject: [PATCH 4/6] test: confirm an array of accessorized options has templated variables replaced --- test/content.spec.js | 144 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 130 insertions(+), 14 deletions(-) diff --git a/test/content.spec.js b/test/content.spec.js index 366d47e1..6d38da53 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -88,17 +88,37 @@ describe("content", () => { mocks.core.getInput.withArgs("payload").returns(` channel: C0123456789 reply_broadcast: false - message: Served \${{ env.NUMBER }} from \${{ github.apiUrl }} + message: Served \${{ env.NUMBER }} items blocks: - type: section text: type: mrkdwn - text: "Served \${{ env.NUMBER }} on: \${{ env.DETAILS }}" + text: "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" - type: divider - type: section + block_id: selector text: type: mrkdwn - text: "> From \${{ github.apiUrl }}" + text: Send feedback + accessory: + action_id: response + type: multi_static_select + placeholder: + type: plain_text + text: Select URL + options: + - text: + type: plain_text + text: "\${{ github.apiUrl }}" + value: api + - text: + type: plain_text + text: "\${{ github.serverUrl }}" + value: server + - text: + type: plain_text + text: "\${{ github.graphqlUrl }}" + value: graphql `); mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); process.env.DETAILS = ` @@ -112,13 +132,13 @@ describe("content", () => { const expected = { channel: "C0123456789", reply_broadcast: false, - message: "Served 12 from https://api.github.com", + message: "Served 12 items", blocks: [ { type: "section", text: { type: "mrkdwn", - text: "Served 12 on: \n-fri\n-sat\n-sun", + text: "Served 12 items on: \n-fri\n-sat\n-sun", }, }, { @@ -126,11 +146,43 @@ describe("content", () => { }, { type: "section", + block_id: "selector", text: { type: "mrkdwn", - text: "> From https://api.github.com", + text: "Send feedback" }, - }, + accessory: { + action_id: "response", + type: "multi_static_select", + placeholder: { + type: "plain_text", + text: "Select URL" + }, + options: [ + { + text: { + type: "plain_text", + text: "https://api.github.com" + }, + value: "api" + }, + { + text: { + type: "plain_text", + text: "https://github.com" + }, + value: "server" + }, + { + text: { + type: "plain_text", + text: "https://api.github.com/graphql" + }, + value: "graphql" + } + ] + } + } ], }; assert.deepEqual(config.content.values, expected); @@ -297,13 +349,13 @@ describe("content", () => { .returns(`{ "channel": "C0123456789", "reply_broadcast": false, - "message": "Served \${{ env.NUMBER }} from \${{ github.apiUrl }}", + "message": "Served \${{ env.NUMBER }} items", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", - "text": "Served \${{ env.NUMBER }} on: \${{ env.DETAILS }}" + "text": "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" } }, { @@ -311,9 +363,41 @@ describe("content", () => { }, { "type": "section", + "block_id": "selector", "text": { "type": "mrkdwn", - "text": "> From \${{ github.apiUrl }}" + "text": "Send feedback" + }, + "accessory": { + "action_id": "response", + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select URL" + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "\${{ github.apiUrl }}" + }, + "value": "api" + }, + { + "text": { + "type": "plain_text", + "text": "\${{ github.serverUrl }}" + }, + "value": "server" + }, + { + "text": { + "type": "plain_text", + "text": "\${{ github.graphqlUrl }}" + }, + "value": "graphql" + } + ] } } ] @@ -330,13 +414,13 @@ describe("content", () => { const expected = { channel: "C0123456789", reply_broadcast: false, - message: "Served 12 from https://api.github.com", + message: "Served 12 items", blocks: [ { type: "section", text: { type: "mrkdwn", - text: "Served 12 on: \n-fri\n-sat\n-sun", + text: "Served 12 items on: \n-fri\n-sat\n-sun", }, }, { @@ -344,11 +428,43 @@ describe("content", () => { }, { type: "section", + block_id: "selector", text: { type: "mrkdwn", - text: "> From https://api.github.com", + text: "Send feedback" }, - }, + accessory: { + action_id: "response", + type: "multi_static_select", + placeholder: { + type: "plain_text", + text: "Select URL" + }, + options: [ + { + text: { + type: "plain_text", + text: "https://api.github.com" + }, + value: "api" + }, + { + text: { + type: "plain_text", + text: "https://github.com" + }, + value: "server" + }, + { + text: { + type: "plain_text", + text: "https://api.github.com/graphql" + }, + value: "graphql" + } + ] + } + } ], }; assert.deepEqual(config.content.values, expected); From de0912a6e5c3677c9cd6b5caa0414d36d41aca36 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 2 Jun 2025 14:25:18 -0700 Subject: [PATCH 5/6] test: confirm configuration is required to rpelace templatized variables --- test/content.spec.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/content.spec.js b/test/content.spec.js index 6d38da53..4b64dba6 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -84,6 +84,20 @@ describe("content", () => { assert.deepEqual(config.content.values, expected); }); + it("templatizes variables requires configuration", async () => { + mocks.core.getInput.withArgs("payload").returns(`{ + "message": "this matches an existing variable: \${{ github.apiUrl }}", + "channel": "C0123456789" + } + `); + const config = new Config(mocks.core); + const expected = { + message: "this matches an existing variable: ${{ github.apiUrl }}", + channel: "C0123456789", + }; + assert.deepEqual(config.content.values, expected); + }); + it("templatizes variables with matching variables", async () => { mocks.core.getInput.withArgs("payload").returns(` channel: C0123456789 @@ -342,6 +356,24 @@ describe("content", () => { assert.deepEqual(config.content.values, expected); }); + it("templatizes variables requires configuration", async () => { + mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); + mocks.fs.readFileSync + .withArgs(path.resolve("example.json"), "utf-8") + .returns(`{ + "message": "this matches an existing variable: \${{ github.apiUrl }}", + "channel": "C0123456789" + } + `); + const config = new Config(mocks.core); + const expected = { + message: "this matches an existing variable: ${{ github.apiUrl }}", + channel: "C0123456789", + }; + assert.deepEqual(config.content.values, expected); + }); + + it("templatizes variables with matching variables", async () => { mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); mocks.fs.readFileSync From 3a520d664ef3f8ffa093772e9c9512e38a13632c Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 2 Jun 2025 14:27:42 -0700 Subject: [PATCH 6/6] style: run the linter with fixes instead of letting it error in ci --- test/content.spec.js | 49 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/test/content.spec.js b/test/content.spec.js index 4b64dba6..737f6040 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -163,40 +163,40 @@ describe("content", () => { block_id: "selector", text: { type: "mrkdwn", - text: "Send feedback" + text: "Send feedback", }, accessory: { action_id: "response", type: "multi_static_select", placeholder: { type: "plain_text", - text: "Select URL" + text: "Select URL", }, options: [ { text: { type: "plain_text", - text: "https://api.github.com" + text: "https://api.github.com", }, - value: "api" + value: "api", }, { text: { type: "plain_text", - text: "https://github.com" + text: "https://github.com", }, - value: "server" + value: "server", }, { text: { type: "plain_text", - text: "https://api.github.com/graphql" + text: "https://api.github.com/graphql", }, - value: "graphql" - } - ] - } - } + value: "graphql", + }, + ], + }, + }, ], }; assert.deepEqual(config.content.values, expected); @@ -373,7 +373,6 @@ describe("content", () => { assert.deepEqual(config.content.values, expected); }); - it("templatizes variables with matching variables", async () => { mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); mocks.fs.readFileSync @@ -463,40 +462,40 @@ describe("content", () => { block_id: "selector", text: { type: "mrkdwn", - text: "Send feedback" + text: "Send feedback", }, accessory: { action_id: "response", type: "multi_static_select", placeholder: { type: "plain_text", - text: "Select URL" + text: "Select URL", }, options: [ { text: { type: "plain_text", - text: "https://api.github.com" + text: "https://api.github.com", }, - value: "api" + value: "api", }, { text: { type: "plain_text", - text: "https://github.com" + text: "https://github.com", }, - value: "server" + value: "server", }, { text: { type: "plain_text", - text: "https://api.github.com/graphql" + text: "https://api.github.com/graphql", }, - value: "graphql" - } - ] - } - } + value: "graphql", + }, + ], + }, + }, ], }; assert.deepEqual(config.content.values, expected);