From ae583cebedbfd32256b349cce8ddf5772e77636e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:37:42 +0900 Subject: [PATCH] feat: embed file content in message template via `{path}` placeholder (#1665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add prefix and suffix inputs to wrap comment body Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> * feat: support combining path and message inputs using $path placeholder Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> * feat: use {path} placeholder and remove prefix/suffix inputs Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> * feat: rename placeholder from {path} to {{{content}}} Agent-Logs-Url: https://github.com/marocchino/sticky-pull-request-comment/sessions/72744cd4-264e-4c06-95d4-6fdec3446f90 Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> * fix: when message has no {{{content}}} placeholder, return message as-is Agent-Logs-Url: https://github.com/marocchino/sticky-pull-request-comment/sessions/b6492aa3-c55c-41f7-b43e-12cc3f2c3002 Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> * refactor: simplify {{{content}}} replacement — drop includes guard and regex Agent-Logs-Url: https://github.com/marocchino/sticky-pull-request-comment/sessions/81112a7b-0e6d-4f21-ad36-530af7c0259f Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> * chore: remove accidentally committed dist/src/* and add to .gitignore Agent-Logs-Url: https://github.com/marocchino/sticky-pull-request-comment/sessions/7ced80dd-92a9-46ad-b667-bf5ceaa129ea Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com> --- .gitignore | 1 + README.md | 20 ++++++++++++-- __tests__/config.test.ts | 57 ++++++++++++++++++++++++++++++++++++++++ action.yml | 4 +-- src/config.ts | 13 ++++++--- 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c466de2..44ed79b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __tests__/runner/* lib/* +dist/src/ rollup.config.js # ^^^ compiled output of rollup.config.ts (generated by --configPlugin at build time) # comment out in distribution branches diff --git a/README.md b/README.md index 581ad8c..ca6cc28 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,22 @@ with: path: path-to-comment-contents.txt ``` +### Embed file content inside a message template + +Use `{{{content}}}` as a placeholder in the `message` to insert file content at that position. + +````yaml +- name: Run Test + run: rake test > result.txt +- uses: marocchino/sticky-pull-request-comment@v3 + with: + path: result.txt + message: | + ``` + {{{content}}} + ``` +```` + ### Delete the previous comment and add a comment at the end ```yaml @@ -220,11 +236,11 @@ For more detailed information about permissions, you can read from the link belo ### `message` -**Optional** Comment message +**Optional** Comment message. When used together with `path`, use `{{{content}}}` 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 `{{{content}}}`, the file content is embedded at that placeholder position. ### `number` diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index b44a433..cee11df 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -210,4 +210,61 @@ describe("getBody", () => { expect(await config.getBody()).toBe("") expect(core.setFailed).toHaveBeenCalledWith("glob error") }) + + test("embeds file content in message when {{{content}}} 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{{{content}}}\n```" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), + }) + expect(await config.getBody()).toBe("```\nhi there\n\n```") + }) + + test("replaces {{{content}}} 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 "{{{content}}}\n---\n{{{content}}}" + return "" + }) + mockGlobCreate.mockResolvedValue({ + glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]), + }) + expect(await config.getBody()).toBe("hi there\n\n---\n{{{content}}}") + }) + + test("uses message as body when path is provided but message has no {{{content}}} 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("no placeholder here") + }) + + test("embeds multiple files content in message when {{{content}}} 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{{{content}}}\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 d14ce06..8bfec43 100644 --- a/action.yml +++ b/action.yml @@ -52,10 +52,10 @@ inputs: default: "OUTDATED" required: false message: - description: "comment message" + description: "comment message. When used together with path, use `{{{content}}}` 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" + description: "glob path to file(s) containing comment message. When message is also provided and contains `{{{content}}}`, 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 bc71ddf..d1a4d1d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,20 +50,27 @@ export async function getBody(): Promise { const followSymbolicLinks = core.getBooleanInput("follow_symbolic_links", { required: true, }) + const messageInput = core.getInput("message", {required: false}) + 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") + const fileContent = (await globber.glob()) + .map(path => readFileSync(path, "utf-8")) + .join("\n") + if (messageInput) { + return messageInput.replace("{{{content}}}", fileContent) + } + return fileContent } catch (error) { if (error instanceof Error) { core.setFailed(error.message) } return "" } - } else { - return core.getInput("message", {required: false}) } + return messageInput }