Compare commits

..

No commits in common. "main" and "v3.0.1" have entirely different histories.
main ... v3.0.1

15 changed files with 784 additions and 1066 deletions

View file

@ -91,19 +91,6 @@ If for some reason, triggering on pr is not possible, you can use push.
This message is from a push.
```
### Override pull request number
Use `number_force` to comment on a different pull request than the one that triggered the event.
```yaml
- uses: marocchino/sticky-pull-request-comment@v2
with:
number_force: 123
message: |
This comment will be posted to PR #123,
regardless of which PR triggered this workflow.
```
### Read comment from a file
```yaml
@ -230,10 +217,6 @@ For more detailed information about permissions, you can read from the link belo
**Optional** Pull request number for push event. Note that this has a **lower priority** than the number of a pull_request event.
### `number_force`
**Optional** Pull request number for any event. Note that this has the **highest priority** and will override the number from a pull_request event.
### `owner`
**Optional** Another repository owner, If not set, the current repository owner is used by default. Note that when you trying changing a repo, be aware that `GITHUB_TOKEN` should also have permission for that repository.

View file

@ -1,213 +1,430 @@
import {afterEach, describe, expect, test, vi} from "vitest"
import {resolve} from "node:path"
import { beforeEach, afterEach, test, expect, vi, describe } from 'vitest'
vi.mock("@actions/core", () => ({
getInput: vi.fn().mockReturnValue(""),
getBooleanInput: vi.fn().mockReturnValue(false),
getMultilineInput: vi.fn().mockReturnValue([]),
setFailed: vi.fn(),
}))
const mockContext = vi.hoisted(() => ({
repo: {owner: "marocchino", repo: "sticky-pull-request-comment"},
payload: {} as Record<string, unknown>,
}))
vi.mock("@actions/github", () => ({
context: mockContext,
}))
const mockGlobCreate = vi.hoisted(() => vi.fn())
vi.mock("@actions/glob", () => ({
create: mockGlobCreate,
}))
afterEach(() => {
mockContext.payload = {}
mockContext.repo = {owner: "marocchino", repo: "sticky-pull-request-comment"}
mockGlobCreate.mockReset()
})
async function loadConfig(
setup?: (mocks: {core: typeof import("@actions/core")}) => void,
) {
vi.resetModules()
const core = await import("@actions/core")
// vi.resetModules clears the config module cache but not mock instances,
// so reset core back to default values before each test.
vi.mocked(core.getInput).mockReturnValue("")
vi.mocked(core.getBooleanInput).mockReturnValue(false)
vi.mocked(core.getMultilineInput).mockReturnValue([])
setup?.({core})
const config = await import("../src/config")
return {config, core}
const mockConfig = {
pullRequestNumber: 123,
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false,
getBody: vi.fn().mockResolvedValue("")
}
describe("pullRequestNumber", () => {
test("number_force takes highest priority", async () => {
mockContext.payload = {pull_request: {number: 789}}
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => {
if (name === "number_force") return "456"
if (name === "number") return "123"
return ""
})
})
expect(config.pullRequestNumber).toBe(456)
})
test("falls back to context.payload.pull_request.number", async () => {
mockContext.payload = {pull_request: {number: 789}}
const {config} = await loadConfig()
expect(config.pullRequestNumber).toBe(789)
})
test("falls back to number input", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => (name === "number" ? "123" : ""))
})
expect(config.pullRequestNumber).toBe(123)
})
vi.mock('../src/config', () => {
return mockConfig
})
describe("repo", () => {
test("defaults to context.repo", async () => {
const {config} = await loadConfig()
expect(config.repo).toEqual({owner: "marocchino", repo: "sticky-pull-request-comment"})
})
beforeEach(() => {
// Set up default environment variables for each test
process.env["GITHUB_REPOSITORY"] = "marocchino/stick-pull-request-comment"
process.env["INPUT_NUMBER"] = "123"
process.env["INPUT_APPEND"] = "false"
process.env["INPUT_RECREATE"] = "false"
process.env["INPUT_DELETE"] = "false"
process.env["INPUT_ONLY_CREATE"] = "false"
process.env["INPUT_ONLY_UPDATE"] = "false"
process.env["INPUT_HIDE"] = "false"
process.env["INPUT_HIDE_AND_RECREATE"] = "false"
process.env["INPUT_HIDE_CLASSIFY"] = "OUTDATED"
process.env["INPUT_HIDE_DETAILS"] = "false"
process.env["INPUT_GITHUB_TOKEN"] = "some-token"
process.env["INPUT_IGNORE_EMPTY"] = "false"
process.env["INPUT_SKIP_UNCHANGED"] = "false"
process.env["INPUT_FOLLOW_SYMBOLIC_LINKS"] = "false"
test("uses owner and repo inputs when provided", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => {
if (name === "owner") return "jin"
if (name === "repo") return "other"
return ""
})
})
expect(config.repo).toEqual({owner: "jin", repo: "other"})
// 모킹된 값 초기화
mockConfig.pullRequestNumber = 123
mockConfig.repo = {owner: "marocchino", repo: "stick-pull-request-comment"}
mockConfig.header = ""
mockConfig.append = false
mockConfig.recreate = false
mockConfig.deleteOldComment = false
mockConfig.hideOldComment = false
mockConfig.hideAndRecreate = false
mockConfig.hideClassify = "OUTDATED"
mockConfig.hideDetails = false
mockConfig.githubToken = "some-token"
mockConfig.ignoreEmpty = false
mockConfig.skipUnchanged = false
mockConfig.getBody.mockResolvedValue("")
})
afterEach(() => {
vi.resetModules()
delete process.env["GITHUB_REPOSITORY"]
delete process.env["INPUT_OWNER"]
delete process.env["INPUT_REPO"]
delete process.env["INPUT_HEADER"]
delete process.env["INPUT_MESSAGE"]
delete process.env["INPUT_NUMBER"]
delete process.env["INPUT_APPEND"]
delete process.env["INPUT_RECREATE"]
delete process.env["INPUT_DELETE"]
delete process.env["INPUT_ONLY_CREATE"]
delete process.env["INPUT_ONLY_UPDATE"]
delete process.env["INPUT_HIDE"]
delete process.env["INPUT_HIDE_AND_RECREATE"]
delete process.env["INPUT_HIDE_CLASSIFY"]
delete process.env["INPUT_HIDE_DETAILS"]
delete process.env["INPUT_GITHUB_TOKEN"]
delete process.env["INPUT_PATH"]
delete process.env["INPUT_IGNORE_EMPTY"]
delete process.env["INPUT_SKIP_UNCHANGED"]
delete process.env["INPUT_FOLLOW_SYMBOLIC_LINKS"]
})
test("repo", async () => {
process.env["INPUT_OWNER"] = "jin"
process.env["INPUT_REPO"] = "other"
mockConfig.repo = {owner: "jin", repo: "other"}
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "jin", repo: "other"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(await config.getBody()).toEqual("")
})
test("header", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => (name === "header" ? "my-header" : ""))
process.env["INPUT_HEADER"] = "header"
mockConfig.header = "header"
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "header",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.header).toBe("my-header")
expect(await config.getBody()).toEqual("")
})
test("append", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "append")
process.env["INPUT_APPEND"] = "true"
mockConfig.append = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: true,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.append).toBe(true)
expect(await config.getBody()).toEqual("")
})
test("recreate", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "recreate")
process.env["INPUT_RECREATE"] = "true"
mockConfig.recreate = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: true,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.recreate).toBe(true)
expect(await config.getBody()).toEqual("")
})
test("deleteOldComment", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "delete")
test("delete", async () => {
process.env["INPUT_DELETE"] = "true"
mockConfig.deleteOldComment = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: true,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.deleteOldComment).toBe(true)
expect(await config.getBody()).toEqual("")
})
test("hideOldComment", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide")
process.env["INPUT_HIDE"] = "true"
mockConfig.hideOldComment = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: true,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.hideOldComment).toBe(true)
expect(await config.getBody()).toEqual("")
})
test("hideAndRecreate", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide_and_recreate")
process.env["INPUT_HIDE_AND_RECREATE"] = "true"
mockConfig.hideAndRecreate = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: true,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.hideAndRecreate).toBe(true)
expect(await config.getBody()).toEqual("")
})
test("hideClassify", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => (name === "hide_classify" ? "OFF_TOPIC" : ""))
process.env["INPUT_HIDE_CLASSIFY"] = "OFF_TOPIC"
mockConfig.hideClassify = "OFF_TOPIC"
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OFF_TOPIC",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.hideClassify).toBe("OFF_TOPIC")
expect(await config.getBody()).toEqual("")
})
test("hideDetails", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide_details")
process.env["INPUT_HIDE_DETAILS"] = "true"
mockConfig.hideDetails = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: true,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(config.hideDetails).toBe(true)
expect(await config.getBody()).toEqual("")
})
test("ignoreEmpty", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "ignore_empty")
})
expect(config.ignoreEmpty).toBe(true)
})
describe("path", () => {
test("when exists return content of a file", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/result"
mockConfig.getBody.mockResolvedValue("hi there\n")
test("skipUnchanged", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "skip_unchanged")
})
expect(config.skipUnchanged).toBe(true)
})
test("githubToken", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => (name === "GITHUB_TOKEN" ? "my-token" : ""))
})
expect(config.githubToken).toBe("my-token")
})
describe("getBody", () => {
test("returns message when no path is provided", async () => {
const {config, core} = await loadConfig()
vi.mocked(core.getInput).mockImplementation(name => (name === "message" ? "hello there" : ""))
expect(await config.getBody()).toBe("hello there")
})
test("returns file content when path exists", async () => {
const {config, core} = await loadConfig()
vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"])
mockGlobCreate.mockResolvedValue({
glob: vi.fn().mockResolvedValue([resolve("__tests__/assets/result")]),
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(await config.getBody()).toBe("hi there\n")
expect(await config.getBody()).toEqual("hi there\n")
})
test("glob matches multiple files", async () => {
const {config, core} = await loadConfig()
vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/*"])
mockGlobCreate.mockResolvedValue({
glob: vi
.fn()
.mockResolvedValue([
resolve("__tests__/assets/result"),
resolve("__tests__/assets/result2"),
]),
test("glob match files", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/*"
mockConfig.getBody.mockResolvedValue("hi there\n\nhey there\n")
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(await config.getBody()).toBe("hi there\n\nhey there\n")
expect(await config.getBody()).toEqual("hi there\n\nhey there\n")
})
test("returns empty string when path matches no files", async () => {
const {config, core} = await loadConfig()
vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/not_exists"])
mockGlobCreate.mockResolvedValue({glob: vi.fn().mockResolvedValue([])})
expect(await config.getBody()).toBe("")
})
test("when not exists return null string", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/not_exists"
mockConfig.getBody.mockResolvedValue("")
test("returns empty string and calls setFailed when glob throws", async () => {
const {config, core} = await loadConfig()
vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"])
mockGlobCreate.mockRejectedValue(new Error("glob error"))
expect(await config.getBody()).toBe("")
expect(core.setFailed).toHaveBeenCalledWith("glob error")
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(await config.getBody()).toEqual("")
})
})
test("message", async () => {
process.env["INPUT_MESSAGE"] = "hello there"
mockConfig.getBody.mockResolvedValue("hello there")
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: false
})
expect(await config.getBody()).toEqual("hello there")
})
test("ignore_empty", async () => {
process.env["INPUT_IGNORE_EMPTY"] = "true"
mockConfig.ignoreEmpty = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: true,
skipUnchanged: false
})
expect(await config.getBody()).toEqual("")
})
test("skip_unchanged", async () => {
process.env["INPUT_SKIP_UNCHANGED"] = "true"
mockConfig.skipUnchanged = true
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "",
append: false,
recreate: false,
deleteOldComment: false,
hideOldComment: false,
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
githubToken: "some-token",
ignoreEmpty: false,
skipUnchanged: true
})
expect(await config.getBody()).toEqual("")
})

View file

@ -1,328 +0,0 @@
import {beforeEach, describe, expect, test, vi} from "vitest"
vi.mock("@actions/core", () => ({
info: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
}))
vi.mock("@actions/github", () => ({
getOctokit: vi.fn().mockReturnValue({}),
}))
vi.mock("../src/comment", () => ({
commentsEqual: vi.fn(),
createComment: vi.fn(),
deleteComment: vi.fn(),
findPreviousComment: vi.fn(),
getBodyOf: vi.fn(),
minimizeComment: vi.fn(),
updateComment: vi.fn(),
}))
const mockConfig = {
append: false,
deleteOldComment: false,
getBody: vi.fn(),
githubToken: "some-token",
header: "",
hideAndRecreate: false,
hideClassify: "OUTDATED",
hideDetails: false,
hideOldComment: false,
ignoreEmpty: false,
onlyCreateComment: false,
onlyUpdateComment: false,
pullRequestNumber: 123,
recreate: false,
repo: {owner: "marocchino", repo: "sticky-pull-request-comment"},
skipUnchanged: false,
}
vi.mock("../src/config", () => mockConfig)
const flushPromises = () => new Promise<void>(resolve => setTimeout(resolve, 0))
async function runMain(
setup?: (mocks: {comment: typeof import("../src/comment")}) => void,
) {
vi.resetModules()
const comment = await import("../src/comment")
vi.mocked(comment.findPreviousComment).mockResolvedValue(null)
vi.mocked(comment.createComment).mockResolvedValue({data: {id: 456}} as any)
vi.mocked(comment.deleteComment).mockResolvedValue(undefined)
vi.mocked(comment.updateComment).mockResolvedValue(undefined)
vi.mocked(comment.minimizeComment).mockResolvedValue(undefined)
vi.mocked(comment.commentsEqual).mockReturnValue(false)
vi.mocked(comment.getBodyOf).mockReturnValue(undefined)
setup?.({comment})
await import("../src/main")
await flushPromises()
const core = await import("@actions/core")
return {comment, core}
}
beforeEach(() => {
mockConfig.append = false
mockConfig.deleteOldComment = false
mockConfig.getBody = vi.fn().mockResolvedValue("test body")
mockConfig.githubToken = "some-token"
mockConfig.header = ""
mockConfig.hideAndRecreate = false
mockConfig.hideClassify = "OUTDATED"
mockConfig.hideDetails = false
mockConfig.hideOldComment = false
mockConfig.ignoreEmpty = false
mockConfig.onlyCreateComment = false
mockConfig.onlyUpdateComment = false
mockConfig.pullRequestNumber = 123
mockConfig.recreate = false
mockConfig.repo = {owner: "marocchino", repo: "sticky-pull-request-comment"}
mockConfig.skipUnchanged = false
})
describe("run", () => {
test("skips step when pullRequestNumber is NaN", async () => {
mockConfig.pullRequestNumber = NaN
const {comment, core} = await runMain()
expect(core.info).toHaveBeenCalledWith("no pull request numbers given: skip step")
expect(comment.findPreviousComment).not.toHaveBeenCalled()
})
test("skips step when pullRequestNumber is less than 1", async () => {
mockConfig.pullRequestNumber = 0
const {comment, core} = await runMain()
expect(core.info).toHaveBeenCalledWith("no pull request numbers given: skip step")
expect(comment.findPreviousComment).not.toHaveBeenCalled()
})
test("skips step when body is empty and ignoreEmpty is true", async () => {
mockConfig.getBody = vi.fn().mockResolvedValue("")
mockConfig.ignoreEmpty = true
const {comment, core} = await runMain()
expect(core.info).toHaveBeenCalledWith("no body given: skip step by ignoreEmpty")
expect(comment.findPreviousComment).not.toHaveBeenCalled()
})
test("fails when body is empty and no delete or hide flags are set", async () => {
mockConfig.getBody = vi.fn().mockResolvedValue("")
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith("Either message or path input is required")
})
test("fails when deleteOldComment and recreate are both true", async () => {
mockConfig.deleteOldComment = true
mockConfig.recreate = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and recreate cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when deleteOldComment and onlyCreateComment are both true", async () => {
mockConfig.deleteOldComment = true
mockConfig.onlyCreateComment = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and only_create cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when deleteOldComment and hideOldComment are both true", async () => {
mockConfig.deleteOldComment = true
mockConfig.hideOldComment = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and hide cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when onlyCreateComment and onlyUpdateComment are both true", async () => {
mockConfig.onlyCreateComment = true
mockConfig.onlyUpdateComment = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"only_create and only_update cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when hideOldComment and hideAndRecreate are both true", async () => {
mockConfig.hideOldComment = true
mockConfig.hideAndRecreate = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"hide and hide_and_recreate cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when deleteOldComment and hideAndRecreate are both true", async () => {
mockConfig.deleteOldComment = true
mockConfig.hideAndRecreate = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and hide_and_recreate cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("deletes previous comment when deleteOldComment is true and previous comment exists", async () => {
mockConfig.deleteOldComment = true
const previous = {id: "existing-id", body: "old body"}
const {comment, core} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
})
expect(core.setOutput).toHaveBeenCalledWith("previous_comment_id", "existing-id")
expect(comment.deleteComment).toHaveBeenCalledWith(expect.anything(), "existing-id")
expect(comment.createComment).not.toHaveBeenCalled()
expect(comment.updateComment).not.toHaveBeenCalled()
})
test("skips delete when deleteOldComment is true but no previous comment exists", async () => {
mockConfig.deleteOldComment = true
const {comment, core} = await runMain()
expect(core.setOutput).toHaveBeenCalledWith("previous_comment_id", undefined)
expect(comment.deleteComment).not.toHaveBeenCalled()
expect(comment.createComment).not.toHaveBeenCalled()
expect(comment.updateComment).not.toHaveBeenCalled()
})
test("Updates previous comment when onlyUpdateComment is true and previous comment exists", async () => {
mockConfig.onlyUpdateComment = true
const previous = {id: "existing-id", body: "old body"}
const {comment, core} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
vi.mocked(comment.getBodyOf).mockReturnValue("previous body content")
})
expect(comment.updateComment).toHaveBeenCalledWith(
expect.anything(),
"existing-id",
"test body",
"",
"previous body content",
)
expect(comment.createComment).not.toHaveBeenCalled()
expect(core.setOutput).toHaveBeenCalledWith("previous_comment_id", "existing-id")
})
test("skips creating comment when onlyUpdateComment is true and no previous comment exists", async () => {
mockConfig.onlyUpdateComment = true
const {comment} = await runMain()
expect(comment.createComment).not.toHaveBeenCalled()
expect(comment.updateComment).not.toHaveBeenCalled()
})
test("creates comment when no previous comment exists", async () => {
const {comment, core} = await runMain()
expect(comment.createComment).toHaveBeenCalledWith(
expect.anything(),
{owner: "marocchino", repo: "sticky-pull-request-comment"},
123,
"test body",
"",
)
expect(core.setOutput).toHaveBeenCalledWith("created_comment_id", 456)
})
test("skips update when onlyCreateComment is true and previous comment exists", async () => {
mockConfig.onlyCreateComment = true
const previous = {id: "existing-id", body: "old body"}
const {comment} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
})
expect(comment.updateComment).not.toHaveBeenCalled()
expect(comment.createComment).not.toHaveBeenCalled()
})
test("minimizes previous comment when hideOldComment is true", async () => {
mockConfig.hideOldComment = true
const previous = {id: "existing-id", body: "old body"}
const {comment} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
})
expect(comment.minimizeComment).toHaveBeenCalledWith(
expect.anything(),
"existing-id",
"OUTDATED",
)
expect(comment.updateComment).not.toHaveBeenCalled()
})
test("skips when hideOldComment is true and no previous comment exists", async () => {
mockConfig.hideOldComment = true
const {comment} = await runMain()
expect(comment.minimizeComment).not.toHaveBeenCalled()
expect(comment.createComment).not.toHaveBeenCalled()
expect(comment.updateComment).not.toHaveBeenCalled()
})
test("skips update when skipUnchanged is true and body is unchanged", async () => {
mockConfig.skipUnchanged = true
const previous = {id: "existing-id", body: "old body"}
const {comment} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
vi.mocked(comment.commentsEqual).mockReturnValue(true)
})
expect(comment.updateComment).not.toHaveBeenCalled()
expect(comment.createComment).not.toHaveBeenCalled()
})
test("deletes and recreates comment when recreate is true", async () => {
mockConfig.recreate = true
const previous = {id: "existing-id", body: "old body"}
const {comment, core} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
vi.mocked(comment.getBodyOf).mockReturnValue("previous body content")
})
expect(comment.deleteComment).toHaveBeenCalledWith(expect.anything(), "existing-id")
expect(comment.createComment).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
"test body",
"",
"previous body content",
)
expect(core.setOutput).toHaveBeenCalledWith("created_comment_id", 456)
})
test("minimizes and recreates comment when hideAndRecreate is true", async () => {
mockConfig.hideAndRecreate = true
const previous = {id: "existing-id", body: "old body"}
const {comment, core} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
})
expect(comment.minimizeComment).toHaveBeenCalledWith(
expect.anything(),
"existing-id",
"OUTDATED",
)
expect(comment.createComment).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
"test body",
"",
)
expect(core.setOutput).toHaveBeenCalledWith("created_comment_id", 456)
})
test("updates existing comment by default", async () => {
const previous = {id: "existing-id", body: "old body"}
const {comment} = await runMain(({comment}) => {
vi.mocked(comment.findPreviousComment).mockResolvedValue(previous as any)
vi.mocked(comment.getBodyOf).mockReturnValue("previous body content")
})
expect(comment.updateComment).toHaveBeenCalledWith(
expect.anything(),
"existing-id",
"test body",
"",
"previous body content",
)
})
})

View file

@ -1,85 +0,0 @@
import {describe, expect, test} from "vitest"
import {validateBody, validateExclusiveModes} from "../src/validate"
describe("validateBody", () => {
test("throws when body is empty and neither delete nor hide is set", () => {
expect(() => validateBody("", false, false)).toThrow(
"Either message or path input is required",
)
})
test("does not throw when body is provided", () => {
expect(() => validateBody("some body", false, false)).not.toThrow()
})
test("does not throw when body is empty but deleteOldComment is true", () => {
expect(() => validateBody("", true, false)).not.toThrow()
})
test("does not throw when body is empty but hideOldComment is true", () => {
expect(() => validateBody("", false, true)).not.toThrow()
})
})
describe("validateExclusiveModes", () => {
test("does not throw when no modes are enabled", () => {
expect(() => validateExclusiveModes(false, false, false, false, false, false)).not.toThrow()
})
test("does not throw when exactly one mode is enabled", () => {
expect(() => validateExclusiveModes(true, false, false, false, false, false)).not.toThrow()
expect(() => validateExclusiveModes(false, true, false, false, false, false)).not.toThrow()
expect(() => validateExclusiveModes(false, false, true, false, false, false)).not.toThrow()
expect(() => validateExclusiveModes(false, false, false, true, false, false)).not.toThrow()
expect(() => validateExclusiveModes(false, false, false, false, true, false)).not.toThrow()
expect(() => validateExclusiveModes(false, false, false, false, false, true)).not.toThrow()
})
test("throws when delete and recreate are both true", () => {
expect(() => validateExclusiveModes(true, true, false, false, false, false)).toThrow(
"delete and recreate cannot be set to true simultaneously",
)
})
test("throws when delete and only_create are both true", () => {
expect(() => validateExclusiveModes(true, false, true, false, false, false)).toThrow(
"delete and only_create cannot be set to true simultaneously",
)
})
test("throws when delete and only_update are both true", () => {
expect(() => validateExclusiveModes(true, false, false, true, false, false)).toThrow(
"delete and only_update cannot be set to true simultaneously",
)
})
test("throws when delete and hide are both true", () => {
expect(() => validateExclusiveModes(true, false, false, false, true, false)).toThrow(
"delete and hide cannot be set to true simultaneously",
)
})
test("throws when delete and hide_and_recreate are both true", () => {
expect(() => validateExclusiveModes(true, false, false, false, false, true)).toThrow(
"delete and hide_and_recreate cannot be set to true simultaneously",
)
})
test("throws when only_create and only_update are both true", () => {
expect(() => validateExclusiveModes(false, false, true, true, false, false)).toThrow(
"only_create and only_update cannot be set to true simultaneously",
)
})
test("throws when hide and hide_and_recreate are both true", () => {
expect(() => validateExclusiveModes(false, false, false, false, true, true)).toThrow(
"hide and hide_and_recreate cannot be set to true simultaneously",
)
})
test("uses Oxford comma when three or more modes are enabled", () => {
expect(() => validateExclusiveModes(true, true, true, false, false, false)).toThrow(
"delete, recreate, and only_create cannot be set to true simultaneously",
)
})
})

View file

@ -72,9 +72,6 @@ inputs:
number:
description: "pull request number for push event"
required: false
number_force:
description: "pull request number for any event"
required: false
owner:
description: "Another repo owner, If not set, the current repo owner is used by default. Note that when you trying changing a repo, be aware that GITHUB_TOKEN should also have permission for that repository."
required: false

View file

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
"files": {
"includes": ["src/**/*.ts"]
},

109
dist/index.js generated vendored
View file

@ -27925,7 +27925,7 @@ function requireUndici () {
var undiciExports = requireUndici();
/* eslint-disable @typescript-eslint/no-explicit-any */
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -27986,7 +27986,7 @@ var MediaTypes;
HttpCodes.GatewayTimeout
];
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -27996,7 +27996,7 @@ var MediaTypes;
});
};
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28006,7 +28006,7 @@ var MediaTypes;
});
};
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28017,7 +28017,7 @@ var MediaTypes;
};
const { access, appendFile, writeFile } = fs.promises;
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28031,7 +28031,7 @@ const { chmod, copyFile, lstat, mkdir, open, readdir, rename, rm, rmdir, stat, s
process.platform === 'win32';
fs__namespace.constants.O_RDONLY;
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28041,7 +28041,7 @@ fs__namespace.constants.O_RDONLY;
});
};
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28053,7 +28053,7 @@ fs__namespace.constants.O_RDONLY;
/* eslint-disable @typescript-eslint/unbound-method */
process.platform === 'win32';
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28063,7 +28063,7 @@ process.platform === 'win32';
});
};
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -28075,7 +28075,7 @@ process.platform === 'win32';
os.platform();
os.arch();
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -29114,7 +29114,7 @@ function requireLib () {
var libExports = requireLib();
var __awaiter$2 = (globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -29150,19 +29150,6 @@ function getProxyFetch(destinationUrl) {
function getApiBaseUrl() {
return process.env['GITHUB_API_URL'] || 'https://api.github.com';
}
function getUserAgentWithOrchestrationId(baseUserAgent) {
var _a;
const orchId = (_a = process.env['ACTIONS_ORCHESTRATION_ID']) === null || _a === void 0 ? void 0 : _a.trim();
if (orchId) {
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_');
const tag = `actions_orchestration_id/${sanitizedId}`;
if (baseUserAgent === null || baseUserAgent === void 0 ? void 0 : baseUserAgent.includes(tag))
return baseUserAgent;
const ua = baseUserAgent ? `${baseUserAgent} ` : '';
return `${ua}${tag}`;
}
return baseUserAgent;
}
function getUserAgent() {
if (typeof navigator === "object" && "userAgent" in navigator) {
@ -33081,11 +33068,6 @@ function getOctokitOptions(token, options) {
if (auth) {
opts.auth = auth;
}
// Orchestration ID
const userAgent = getUserAgentWithOrchestrationId(opts.userAgent);
if (userAgent) {
opts.userAgent = userAgent;
}
return opts;
}
@ -33746,7 +33728,7 @@ function requireBraceExpansion () {
var y = numeric(n[1]);
var width = Math.max(n[0].length, n[1].length);
var incr = n.length == 3
? Math.max(Math.abs(numeric(n[2])), 1)
? Math.abs(numeric(n[2]))
: 1;
var test = lte;
var reverse = y < x;
@ -35126,7 +35108,7 @@ class SearchState {
}
}
var __awaiter$1 = (globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -35135,15 +35117,15 @@ var __awaiter$1 = (globalThis && globalThis.__awaiter) || function (thisArg, _ar
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (globalThis && globalThis.__asyncValues) || function (o) {
var __asyncValues = (undefined && undefined.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __await = (globalThis && globalThis.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); };
var __asyncGenerator = (globalThis && globalThis.__asyncGenerator) || function (thisArg, _arguments, generator) {
var __await = (undefined && undefined.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); };
var __asyncGenerator = (undefined && undefined.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
@ -35337,7 +35319,7 @@ class DefaultGlobber {
}
}
(globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
(undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -35346,7 +35328,7 @@ class DefaultGlobber {
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
(globalThis && globalThis.__asyncValues) || function (o) {
(undefined && undefined.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
@ -35354,7 +35336,7 @@ class DefaultGlobber {
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __awaiter = (globalThis && globalThis.__awaiter) || function (thisArg, _arguments, P, generator) {
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@ -35375,9 +35357,7 @@ function create(patterns, options) {
});
}
const pullRequestNumber = +getInput("number_force", { required: false }) ||
context?.payload?.pull_request?.number ||
+getInput("number", { required: false });
const pullRequestNumber = context?.payload?.pull_request?.number || +getInput("number", { required: false });
const repo = buildRepo();
const header = getInput("header", { required: false });
const append = getBooleanInput("append", { required: true });
@ -35437,47 +35417,40 @@ async function getBody() {
}
}
function validateBody(body, deleteOldComment, hideOldComment) {
if (!deleteOldComment && !hideOldComment && !body) {
throw new Error("Either message or path input is required");
}
}
function validateExclusiveModes(deleteOldComment, recreate, onlyCreateComment, onlyUpdateComment, hideOldComment, hideAndRecreate) {
const exclusiveModes = [
["delete", deleteOldComment],
["recreate", recreate],
["only_create", onlyCreateComment],
["only_update", onlyUpdateComment],
["hide", hideOldComment],
["hide_and_recreate", hideAndRecreate],
];
const enabledModes = exclusiveModes.filter(([, flag]) => flag).map(([name]) => name);
if (enabledModes.length > 1) {
const last = enabledModes[enabledModes.length - 1];
const rest = enabledModes.slice(0, -1);
const joined = enabledModes.length === 2 ? `${rest[0]} and ${last}` : `${rest.join(", ")}, and ${last}`;
throw new Error(`${joined} cannot be set to true simultaneously`);
}
}
async function run() {
if (Number.isNaN(pullRequestNumber) || pullRequestNumber < 1) {
info("no pull request numbers given: skip step");
return;
}
try {
validateExclusiveModes(deleteOldComment, recreate, onlyCreateComment, onlyUpdateComment, hideOldComment, hideAndRecreate);
const body = await getBody();
if (!body && ignoreEmpty) {
info("no body given: skip step by ignoreEmpty");
return;
}
validateBody(body, deleteOldComment, hideOldComment);
if (!deleteOldComment && !hideOldComment && !body) {
throw new Error("Either message or path input is required");
}
if (deleteOldComment && recreate) {
throw new Error("delete and recreate cannot be both set to true");
}
if (onlyCreateComment && onlyUpdateComment) {
throw new Error("only_create and only_update cannot be both set to true");
}
if (hideOldComment && hideAndRecreate) {
throw new Error("hide and hide_and_recreate cannot be both set to true");
}
const octokit = getOctokit(githubToken);
const previous = await findPreviousComment(octokit, repo, pullRequestNumber, header);
setOutput("previous_comment_id", previous?.id);
if (deleteOldComment) {
if (previous) {
await deleteComment(octokit, previous.id);
}
return;
}
if (!previous) {
if (onlyUpdateComment || hideOldComment || deleteOldComment) {
if (onlyUpdateComment) {
return;
}
const created = await createComment(octokit, repo, pullRequestNumber, body, header);
@ -35493,10 +35466,6 @@ async function run() {
await minimizeComment(octokit, previous.id, hideClassify);
return;
}
if (deleteOldComment) {
await deleteComment(octokit, previous.id);
return;
}
if (skipUnchanged && commentsEqual(body, previous.body || "", header)) {
// don't recreate or update if the message is unchanged
return;

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

659
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "sticky-pull-request-comment",
"version": "3.0.4",
"version": "3.0.1",
"private": true,
"description": "Create comment on pull request, if exists update that comment.",
"main": "lib/main.js",
@ -30,20 +30,20 @@
"license": "MIT",
"dependencies": {
"@actions/core": "^3.0.0",
"@actions/github": "^9.1.0",
"@actions/github": "^9.0.0",
"@actions/glob": "^0.6.1",
"@octokit/graphql-schema": "^15.26.1"
},
"devDependencies": {
"@biomejs/biome": "2.4.11",
"@biomejs/biome": "2.4.6",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/node": "^25.6.0",
"@types/node": "^25.5.0",
"js-yaml": "^4.1.0",
"rimraf": "^6.1.3",
"rollup": "^4.60.1",
"typescript": "^6.0.2",
"vitest": "^4.1.4"
"rollup": "^4.59.0",
"typescript": "^5.9.2",
"vitest": "^4.0.16"
}
}

View file

@ -5,7 +5,6 @@ import nodeResolve from "@rollup/plugin-node-resolve"
import typescript from "@rollup/plugin-typescript"
const config = {
context: "globalThis",
input: "src/main.ts",
output: {
exports: "auto",

View file

@ -5,9 +5,7 @@ import {create} from "@actions/glob"
import type {ReportedContentClassifiers} from "@octokit/graphql-schema"
export const pullRequestNumber =
+core.getInput("number_force", {required: false}) ||
context?.payload?.pull_request?.number ||
+core.getInput("number", {required: false})
context?.payload?.pull_request?.number || +core.getInput("number", {required: false})
export const repo = buildRepo()
export const header = core.getInput("header", {required: false})

View file

@ -27,7 +27,6 @@ import {
repo,
skipUnchanged,
} from "./config"
import {validateBody, validateExclusiveModes} from "./validate"
async function run(): Promise<undefined> {
if (Number.isNaN(pullRequestNumber) || pullRequestNumber < 1) {
@ -36,15 +35,6 @@ async function run(): Promise<undefined> {
}
try {
validateExclusiveModes(
deleteOldComment,
recreate,
onlyCreateComment,
onlyUpdateComment,
hideOldComment,
hideAndRecreate,
)
const body = await getBody()
if (!body && ignoreEmpty) {
@ -52,15 +42,36 @@ async function run(): Promise<undefined> {
return
}
validateBody(body, deleteOldComment, hideOldComment)
if (!deleteOldComment && !hideOldComment && !body) {
throw new Error("Either message or path input is required")
}
if (deleteOldComment && recreate) {
throw new Error("delete and recreate cannot be both set to true")
}
if (onlyCreateComment && onlyUpdateComment) {
throw new Error("only_create and only_update cannot be both set to true")
}
if (hideOldComment && hideAndRecreate) {
throw new Error("hide and hide_and_recreate cannot be both set to true")
}
const octokit = github.getOctokit(githubToken)
const previous = await findPreviousComment(octokit, repo, pullRequestNumber, header)
core.setOutput("previous_comment_id", previous?.id)
if (deleteOldComment) {
if (previous) {
await deleteComment(octokit, previous.id)
}
return
}
if (!previous) {
if (onlyUpdateComment || hideOldComment || deleteOldComment) {
if (onlyUpdateComment) {
return
}
const created = await createComment(octokit, repo, pullRequestNumber, body, header)
@ -79,11 +90,6 @@ async function run(): Promise<undefined> {
return
}
if (deleteOldComment) {
await deleteComment(octokit, previous.id)
return
}
if (skipUnchanged && commentsEqual(body, previous.body || "", header)) {
// don't recreate or update if the message is unchanged
return

View file

@ -1,35 +0,0 @@
export function validateBody(
body: string,
deleteOldComment: boolean,
hideOldComment: boolean,
): void {
if (!deleteOldComment && !hideOldComment && !body) {
throw new Error("Either message or path input is required")
}
}
export function validateExclusiveModes(
deleteOldComment: boolean,
recreate: boolean,
onlyCreateComment: boolean,
onlyUpdateComment: boolean,
hideOldComment: boolean,
hideAndRecreate: boolean,
): void {
const exclusiveModes: [string, boolean][] = [
["delete", deleteOldComment],
["recreate", recreate],
["only_create", onlyCreateComment],
["only_update", onlyUpdateComment],
["hide", hideOldComment],
["hide_and_recreate", hideAndRecreate],
]
const enabledModes = exclusiveModes.filter(([, flag]) => flag).map(([name]) => name)
if (enabledModes.length > 1) {
const last = enabledModes[enabledModes.length - 1]
const rest = enabledModes.slice(0, -1)
const joined =
enabledModes.length === 2 ? `${rest[0]} and ${last}` : `${rest.join(", ")}, and ${last}`
throw new Error(`${joined} cannot be set to true simultaneously`)
}
}

View file

@ -17,11 +17,9 @@
"outDir": "./dist",
"pretty": true,
"resolveJsonModule": true,
"rootDir": "./src",
"strict": true,
"strictNullChecks": true,
"target": "ES2022",
"types": ["node"]
"target": "ES2022"
},
"exclude": ["node_modules", "**/*.test.ts", "dist"],
"include": ["src"]