-
Notifications
You must be signed in to change notification settings - Fork 182
fix: parse provided payloads before replacing templated variables #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ab3f13a
3265f17
c247b99
5f75caa
2d93133
5c48fcf
de0912a
3a520d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 {Record<string, unknown>} | ||
| */ | ||
| const out = {}; | ||
| for (const [k, v] of Object.entries(input)) { | ||
| out[k] = this.templatize(v); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid use of recursion 🥇 |
||
| } | ||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,16 +84,120 @@ 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("message: Served ${{ env.NUMBER }} from ${{ github.apiUrl }}"); | ||
| mocks.core.getInput.withArgs("payload").returns(` | ||
| channel: C0123456789 | ||
| reply_broadcast: false | ||
| message: Served \${{ env.NUMBER }} items | ||
| blocks: | ||
| - type: section | ||
| text: | ||
| type: mrkdwn | ||
| text: "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" | ||
| - type: divider | ||
| - type: section | ||
| block_id: selector | ||
| text: | ||
| type: mrkdwn | ||
| 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 = ` | ||
| -fri | ||
| -sat | ||
| -sun`; | ||
| process.env.NUMBER = 12; | ||
| const config = new Config(mocks.core); | ||
| process.env.DETAILS = undefined; | ||
| process.env.NUMBER = undefined; | ||
| const expected = { | ||
| message: "Served 12 from https://api.github.com", | ||
| channel: "C0123456789", | ||
| reply_broadcast: false, | ||
| message: "Served 12 items", | ||
| blocks: [ | ||
| { | ||
| type: "section", | ||
| text: { | ||
| type: "mrkdwn", | ||
| text: "Served 12 items on: \n-fri\n-sat\n-sun", | ||
| }, | ||
| }, | ||
| { | ||
| type: "divider", | ||
| }, | ||
| { | ||
| type: "section", | ||
| block_id: "selector", | ||
| text: { | ||
| type: "mrkdwn", | ||
| 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); | ||
| }); | ||
|
|
@@ -252,19 +356,147 @@ 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests are pretty sweet 💯 should we also add a case with But this might be out of scope for this project because these elements mainly appear in interactive components like static multi-selects Example [
{
"type": "section",
"block_id": "section678",
"text": {
"type": "mrkdwn",
"text": "Pick items from the list"
},
"accessory": {
"action_id": "text1234",
"type": "multi_static_select",
"placeholder": {
"type": "plain_text",
"text": "Select items"
},
"options": [
{
"text": {
"type": "plain_text",
"text": "*this is plain_text text*"
},
"value": "value-0"
},
{
"text": {
"type": "plain_text",
"text": "*this is plain_text text*"
},
"value": "value-2"
}
]
}
}
]
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @WilliamBergamin This is a fascinating suggestion! 🧠 ✨ Right now we have a I hadn't thought about sending interactive blocks from a GitHub Action but I'm now so curious about how this might connect with the same app listening for events in the workspace otherwise!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A similar example is added in 5c48fcf to confirm the templated replacement behavior 🚀 |
||
| .withArgs(path.resolve("example.json"), "utf-8") | ||
| .returns(`{ | ||
| "message": "Served $\{\{ env.NUMBER }} from $\{\{ github.apiUrl }}" | ||
| "channel": "C0123456789", | ||
| "reply_broadcast": false, | ||
| "message": "Served \${{ env.NUMBER }} items", | ||
| "blocks": [ | ||
| { | ||
| "type": "section", | ||
| "text": { | ||
| "type": "mrkdwn", | ||
| "text": "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" | ||
| } | ||
| }, | ||
| { | ||
| "type": "divider" | ||
| }, | ||
| { | ||
| "type": "section", | ||
| "block_id": "selector", | ||
| "text": { | ||
| "type": "mrkdwn", | ||
| "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 = ` | ||
| -fri | ||
| -sat | ||
| -sun`; | ||
| process.env.NUMBER = 12; | ||
| const config = new Config(mocks.core); | ||
| process.env.DETAILS = undefined; | ||
| process.env.NUMBER = undefined; | ||
| const expected = { | ||
| message: "Served 12 from https://api.github.com", | ||
| channel: "C0123456789", | ||
| reply_broadcast: false, | ||
| message: "Served 12 items", | ||
| blocks: [ | ||
| { | ||
| type: "section", | ||
| text: { | ||
| type: "mrkdwn", | ||
| text: "Served 12 items on: \n-fri\n-sat\n-sun", | ||
| }, | ||
| }, | ||
| { | ||
| type: "divider", | ||
| }, | ||
| { | ||
| type: "section", | ||
| block_id: "selector", | ||
| text: { | ||
| type: "mrkdwn", | ||
| 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); | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sweet 💯 I like the simplified arguments, makes it easier to test 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamBergamin Haha I started writing all possible types but also found that testing the actual behavior is more useful here 😉