Skip to content
49 changes: 31 additions & 18 deletions src/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
})
);
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Comment on lines +153 to +154
Copy link
Contributor

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 🚀

Copy link
Member Author

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 😉

*/
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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
244 changes: 238 additions & 6 deletions test/content.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are pretty sweet 💯 should we also add a case with array options?

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"
				}
			]
		}
	}
]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilliamBergamin This is a fascinating suggestion! 🧠 ✨

Right now we have a blocks array but that's at the top-level and doesn't promise we recurse as expected 🤓

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!

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
});
Expand Down