From 58d8e91c23cdb05db07271d57a9e0b31fa747c03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:49:18 +0000 Subject: [PATCH 1/4] Initial plan From 5b837e833d055a235d8fba9dd36ec28235571968 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:52:46 +0000 Subject: [PATCH 2/4] Add prefix and suffix inputs to wrap comment body Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> --- README.md | 20 ++++++++++++++++++ __tests__/config.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++ action.yml | 6 ++++++ src/config.ts | 18 ++++++++++++++-- 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 581ad8c..0fd5383 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,18 @@ with: path: path-to-comment-contents.txt ``` +### Wrap file output in a code block + +Use `prefix` and `suffix` to wrap the comment body with arbitrary text, such as triple backticks for a code block. + +````yaml +uses: marocchino/sticky-pull-request-comment@v3 +with: + path: path-to-comment-contents.txt + prefix: "```" + suffix: "```" +```` + ### Delete the previous comment and add a comment at the end ```yaml @@ -222,6 +234,14 @@ For more detailed information about permissions, you can read from the link belo **Optional** Comment message +### `prefix` + +**Optional** Text to prepend to the comment body (before `message` or `path` content). Useful for opening a code block with ` ``` `. + +### `suffix` + +**Optional** Text to append to the comment body (after `message` or `path` content). Useful for closing a code block with ` ``` `. + ### `path` **Optional** Path to file containing comment message diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index b44a433..2861452 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -210,4 +210,49 @@ describe("getBody", () => { expect(await config.getBody()).toBe("") expect(core.setFailed).toHaveBeenCalledWith("glob error") }) + + test("wraps message with prefix and suffix", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "hello there" + if (name === "prefix") return "```" + if (name === "suffix") return "```" + return "" + }) + expect(await config.getBody()).toBe("```\nhello there\n```") + }) + + test("wraps file content with prefix and suffix", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "prefix") return "```" + if (name === "suffix") return "```" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), + }) + expect(await config.getBody()).toBe("```\nhi there\n\n```") + }) + + test("applies only prefix when suffix is not provided", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "hello there" + if (name === "prefix") return "```" + return "" + }) + expect(await config.getBody()).toBe("```\nhello there") + }) + + test("applies only suffix when prefix is not provided", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "hello there" + if (name === "suffix") return "```" + return "" + }) + expect(await config.getBody()).toBe("hello there\n```") + }) }) diff --git a/action.yml b/action.yml index d14ce06..6cd7166 100644 --- a/action.yml +++ b/action.yml @@ -54,6 +54,12 @@ inputs: message: description: "comment message" required: false + prefix: + description: "text to prepend to the message (e.g. ` ``` ` to open a code block)" + required: false + suffix: + description: "text to append to the message (e.g. ` ``` ` to close a code block)" + required: false path: description: "glob path to file(s) containing comment message" required: false diff --git a/src/config.ts b/src/config.ts index bc71ddf..0c17390 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,13 +50,17 @@ export async function getBody(): Promise { const followSymbolicLinks = core.getBooleanInput("follow_symbolic_links", { required: true, }) + const prefixInput = core.getInput("prefix", {required: false}) + const suffixInput = core.getInput("suffix", {required: false}) + + let body: string if (pathInput && pathInput.length > 0) { try { const globber = await create(pathInput.join("\n"), { followSymbolicLinks, matchDirectories: false, }) - return (await globber.glob()).map(path => readFileSync(path, "utf-8")).join("\n") + body = (await globber.glob()).map(path => readFileSync(path, "utf-8")).join("\n") } catch (error) { if (error instanceof Error) { core.setFailed(error.message) @@ -64,6 +68,16 @@ export async function getBody(): Promise { return "" } } else { - return core.getInput("message", {required: false}) + body = core.getInput("message", {required: false}) } + + if (prefixInput || suffixInput) { + const parts: string[] = [] + if (prefixInput) parts.push(prefixInput) + parts.push(body) + if (suffixInput) parts.push(suffixInput) + return parts.join("\n") + } + + return body } From 981267b215887a42bb68ece6c9a48b4f55cfe0e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:07:07 +0000 Subject: [PATCH 3/4] feat: support combining path and message inputs using $path placeholder Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> --- __tests__/config.test.ts | 57 ++++++++++++++++++++++++++++++++++++++++ action.yml | 4 +-- src/config.ts | 12 +++++++-- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index 2861452..0dd6304 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -255,4 +255,61 @@ describe("getBody", () => { }) expect(await config.getBody()).toBe("hello there\n```") }) + + test("embeds file content in message when $path placeholder is used", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "```\n$path\n```" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), + }) + expect(await config.getBody()).toBe("```\nhi there\n\n```") + }) + + test("replaces all $path occurrences in message with file content", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "$path\n---\n$path" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), + }) + expect(await config.getBody()).toBe("hi there\n\n---\nhi there\n") + }) + + test("uses file content as body when path is provided but message has no $path placeholder", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "no placeholder here" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), + }) + expect(await config.getBody()).toBe("hi there\n") + }) + + test("embeds multiple files content in message when $path placeholder is used", async () => { + const {config, core} = await loadConfig() + vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/*"]) + vi.mocked(core.getInput).mockImplementation(name => { + if (name === "message") return "```\n$path\n```" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi + .fn() + .mockResolvedValue([ + resolve("__tests__/assets/result"), + resolve("__tests__/assets/result2"), + ]), + }) + expect(await config.getBody()).toBe("```\nhi there\n\nhey there\n\n```") + }) }) diff --git a/action.yml b/action.yml index 6cd7166..f796b90 100644 --- a/action.yml +++ b/action.yml @@ -52,7 +52,7 @@ inputs: default: "OUTDATED" required: false message: - description: "comment message" + description: "comment message. When used together with path, use `$path` as a placeholder in the message where the file content should be inserted." required: false prefix: description: "text to prepend to the message (e.g. ` ``` ` to open a code block)" @@ -61,7 +61,7 @@ inputs: description: "text to append to the message (e.g. ` ``` ` to close a code block)" required: false path: - description: "glob path to file(s) containing comment message" + description: "glob path to file(s) containing comment message. When message is also provided and contains `$path`, the file content is embedded at that placeholder position." required: false ignore_empty: description: "Indicates whether to ignore missing or empty messages" diff --git a/src/config.ts b/src/config.ts index 0c17390..e69eb02 100644 --- a/src/config.ts +++ b/src/config.ts @@ -52,6 +52,7 @@ export async function getBody(): Promise { }) const prefixInput = core.getInput("prefix", {required: false}) const suffixInput = core.getInput("suffix", {required: false}) + const messageInput = core.getInput("message", {required: false}) let body: string if (pathInput && pathInput.length > 0) { @@ -60,7 +61,14 @@ export async function getBody(): Promise { followSymbolicLinks, matchDirectories: false, }) - body = (await globber.glob()).map(path => readFileSync(path, "utf-8")).join("\n") + const fileContent = (await globber.glob()) + .map(path => readFileSync(path, "utf-8")) + .join("\n") + if (messageInput && messageInput.includes("$path")) { + body = messageInput.replace(/\$path/g, fileContent) + } else { + body = fileContent + } } catch (error) { if (error instanceof Error) { core.setFailed(error.message) @@ -68,7 +76,7 @@ export async function getBody(): Promise { return "" } } else { - body = core.getInput("message", {required: false}) + body = messageInput } if (prefixInput || suffixInput) { From 4bbfc023b86acdf06a58deaa6c9c91e20c06d709 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:19:19 +0000 Subject: [PATCH 4/4] feat: use {path} placeholder and remove prefix/suffix inputs Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> --- README.md | 30 +++++++++----------- __tests__/config.test.ts | 59 +++++----------------------------------- action.yml | 10 ++----- src/config.ts | 23 +++------------- 4 files changed, 26 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 0fd5383..78d7fa7 100644 --- a/README.md +++ b/README.md @@ -112,16 +112,20 @@ with: path: path-to-comment-contents.txt ``` -### Wrap file output in a code block +### Embed file content inside a message template -Use `prefix` and `suffix` to wrap the comment body with arbitrary text, such as triple backticks for a code block. +Use `{path}` as a placeholder in the `message` to insert file content at that position. ````yaml -uses: marocchino/sticky-pull-request-comment@v3 -with: - path: path-to-comment-contents.txt - prefix: "```" - suffix: "```" +- name: Run Test + run: rake test > result.txt +- uses: marocchino/sticky-pull-request-comment@v3 + with: + path: result.txt + message: | + ``` + {path} + ``` ```` ### Delete the previous comment and add a comment at the end @@ -232,19 +236,11 @@ For more detailed information about permissions, you can read from the link belo ### `message` -**Optional** Comment message - -### `prefix` - -**Optional** Text to prepend to the comment body (before `message` or `path` content). Useful for opening a code block with ` ``` `. - -### `suffix` - -**Optional** Text to append to the comment body (after `message` or `path` content). Useful for closing a code block with ` ``` `. +**Optional** Comment message. When used together with `path`, use `{path}` as a placeholder in the message where the file content should be inserted. ### `path` -**Optional** Path to file containing comment message +**Optional** Path to file containing comment message. When `message` is also provided and contains `{path}`, the file content is embedded at that placeholder position. ### `number` diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index 0dd6304..7c25634 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -211,23 +211,11 @@ describe("getBody", () => { expect(core.setFailed).toHaveBeenCalledWith("glob error") }) - test("wraps message with prefix and suffix", async () => { - const {config, core} = await loadConfig() - vi.mocked(core.getInput).mockImplementation(name => { - if (name === "message") return "hello there" - if (name === "prefix") return "```" - if (name === "suffix") return "```" - return "" - }) - expect(await config.getBody()).toBe("```\nhello there\n```") - }) - - test("wraps file content with prefix and suffix", async () => { + test("embeds file content in message when {path} placeholder is used", async () => { const {config, core} = await loadConfig() vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) vi.mocked(core.getInput).mockImplementation(name => { - if (name === "prefix") return "```" - if (name === "suffix") return "```" + if (name === "message") return "```\n{path}\n```" return "" }) mockGlobCreate.mockResolvedValue({ @@ -236,44 +224,11 @@ describe("getBody", () => { expect(await config.getBody()).toBe("```\nhi there\n\n```") }) - test("applies only prefix when suffix is not provided", async () => { - const {config, core} = await loadConfig() - vi.mocked(core.getInput).mockImplementation(name => { - if (name === "message") return "hello there" - if (name === "prefix") return "```" - return "" - }) - expect(await config.getBody()).toBe("```\nhello there") - }) - - test("applies only suffix when prefix is not provided", async () => { - const {config, core} = await loadConfig() - vi.mocked(core.getInput).mockImplementation(name => { - if (name === "message") return "hello there" - if (name === "suffix") return "```" - return "" - }) - expect(await config.getBody()).toBe("hello there\n```") - }) - - test("embeds file content in message when $path placeholder is used", async () => { + test("replaces all {path} occurrences in message with file content", async () => { const {config, core} = await loadConfig() vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) vi.mocked(core.getInput).mockImplementation(name => { - if (name === "message") return "```\n$path\n```" - return "" - }) - mockGlobCreate.mockResolvedValue({ - glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), - }) - expect(await config.getBody()).toBe("```\nhi there\n\n```") - }) - - test("replaces all $path occurrences in message with file content", async () => { - const {config, core} = await loadConfig() - vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) - vi.mocked(core.getInput).mockImplementation(name => { - if (name === "message") return "$path\n---\n$path" + if (name === "message") return "{path}\n---\n{path}" return "" }) mockGlobCreate.mockResolvedValue({ @@ -282,7 +237,7 @@ describe("getBody", () => { expect(await config.getBody()).toBe("hi there\n\n---\nhi there\n") }) - test("uses file content as body when path is provided but message has no $path placeholder", async () => { + test("uses file content as body when path is provided but message has no {path} placeholder", async () => { const {config, core} = await loadConfig() vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"]) vi.mocked(core.getInput).mockImplementation(name => { @@ -295,11 +250,11 @@ describe("getBody", () => { expect(await config.getBody()).toBe("hi there\n") }) - test("embeds multiple files content in message when $path placeholder is used", async () => { + test("embeds multiple files content in message when {path} placeholder is used", async () => { const {config, core} = await loadConfig() vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/*"]) vi.mocked(core.getInput).mockImplementation(name => { - if (name === "message") return "```\n$path\n```" + if (name === "message") return "```\n{path}\n```" return "" }) mockGlobCreate.mockResolvedValue({ diff --git a/action.yml b/action.yml index f796b90..064c6cd 100644 --- a/action.yml +++ b/action.yml @@ -52,16 +52,10 @@ inputs: default: "OUTDATED" required: false message: - description: "comment message. When used together with path, use `$path` as a placeholder in the message where the file content should be inserted." - required: false - prefix: - description: "text to prepend to the message (e.g. ` ``` ` to open a code block)" - required: false - suffix: - description: "text to append to the message (e.g. ` ``` ` to close a code block)" + description: "comment message. When used together with path, use `{path}` as a placeholder in the message where the file content should be inserted." required: false path: - description: "glob path to file(s) containing comment message. When message is also provided and contains `$path`, the file content is embedded at that placeholder position." + description: "glob path to file(s) containing comment message. When message is also provided and contains `{path}`, the file content is embedded at that placeholder position." required: false ignore_empty: description: "Indicates whether to ignore missing or empty messages" diff --git a/src/config.ts b/src/config.ts index e69eb02..24ab94c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,11 +50,8 @@ export async function getBody(): Promise { const followSymbolicLinks = core.getBooleanInput("follow_symbolic_links", { required: true, }) - const prefixInput = core.getInput("prefix", {required: false}) - const suffixInput = core.getInput("suffix", {required: false}) const messageInput = core.getInput("message", {required: false}) - let body: string if (pathInput && pathInput.length > 0) { try { const globber = await create(pathInput.join("\n"), { @@ -64,28 +61,16 @@ export async function getBody(): Promise { const fileContent = (await globber.glob()) .map(path => readFileSync(path, "utf-8")) .join("\n") - if (messageInput && messageInput.includes("$path")) { - body = messageInput.replace(/\$path/g, fileContent) - } else { - body = fileContent + if (messageInput && messageInput.includes("{path}")) { + return messageInput.replace(/\{path\}/g, fileContent) } + return fileContent } catch (error) { if (error instanceof Error) { core.setFailed(error.message) } return "" } - } else { - body = messageInput } - - if (prefixInput || suffixInput) { - const parts: string[] = [] - if (prefixInput) parts.push(prefixInput) - parts.push(body) - if (suffixInput) parts.push(suffixInput) - return parts.join("\n") - } - - return body + return messageInput }