Compare commits

...

3 commits
v3 ... main

Author SHA1 Message Date
dependabot[bot]
5a61de79c6
build(deps-dev): Bump @biomejs/biome from 2.4.6 to 2.4.7 (#1666)
Some checks failed
Test / test (push) Has been cancelled
* build(deps-dev): Bump @biomejs/biome from 2.4.6 to 2.4.7

Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.6 to 2.4.7.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.7/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* 🔧 Update config

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: marocchino <marocchino@users.noreply.github.com>
2026-03-14 09:15:02 +09:00
Ross Williams
7cb1e16d25
Add number_force that overrides pull_request number (#1652)
Some checks are pending
Test / test (push) Waiting to run
* Add number_force that overrides pull_request event

* Add number_force input and tests for PR #1652

Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com>

* Delete pullRequestNumber.test.ts; simplify config.test.ts

Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com>

* Rewrite config.test.ts to test real src/config.ts code, not a mock of it

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>
2026-03-13 21:46:05 +09:00
Copilot
14d4f1e429
Move validateExclusiveModes before getBody for fail-fast validation (#1663)
* Initial plan

* feat: refactor option validation to list-based approach and add delete+hide_and_recreate check

Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com>

* feat: add only_update to exclusive modes validation list

Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com>

* refactor: extract validation logic into src/validate.ts

Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com>

* refactor: move validateExclusiveModes before getBody in run()

Co-authored-by: marocchino <128431+marocchino@users.noreply.github.com>

* test: assert getBody is not called when validateExclusiveModes fails

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>
2026-03-13 21:05:41 +09:00
11 changed files with 374 additions and 446 deletions

View file

@ -91,6 +91,19 @@ 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
@ -217,6 +230,10 @@ 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,430 +1,213 @@
import { beforeEach, afterEach, test, expect, vi, describe } from 'vitest'
import {afterEach, describe, expect, test, vi} from "vitest"
import {resolve} from "node:path"
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("")
}
vi.mock("@actions/core", () => ({
getInput: vi.fn().mockReturnValue(""),
getBooleanInput: vi.fn().mockReturnValue(false),
getMultilineInput: vi.fn().mockReturnValue([]),
setFailed: vi.fn(),
}))
vi.mock('../src/config', () => {
return mockConfig
})
const mockContext = vi.hoisted(() => ({
repo: {owner: "marocchino", repo: "sticky-pull-request-comment"},
payload: {} as Record<string, unknown>,
}))
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"
// 모킹된 값 초기화
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("")
})
vi.mock("@actions/github", () => ({
context: mockContext,
}))
const mockGlobCreate = vi.hoisted(() => vi.fn())
vi.mock("@actions/glob", () => ({
create: mockGlobCreate,
}))
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"]
mockContext.payload = {}
mockContext.repo = {owner: "marocchino", repo: "sticky-pull-request-comment"}
mockGlobCreate.mockReset()
})
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
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}
}
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)
})
})
describe("repo", () => {
test("defaults to context.repo", async () => {
const {config} = await loadConfig()
expect(config.repo).toEqual({owner: "marocchino", repo: "sticky-pull-request-comment"})
})
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"})
})
expect(await config.getBody()).toEqual("")
})
test("header", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => (name === "header" ? "my-header" : ""))
})
expect(await config.getBody()).toEqual("")
expect(config.header).toBe("my-header")
})
test("append", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "append")
})
expect(await config.getBody()).toEqual("")
expect(config.append).toBe(true)
})
test("recreate", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "recreate")
})
expect(await config.getBody()).toEqual("")
expect(config.recreate).toBe(true)
})
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
test("deleteOldComment", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "delete")
})
expect(await config.getBody()).toEqual("")
expect(config.deleteOldComment).toBe(true)
})
test("hideOldComment", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide")
})
expect(await config.getBody()).toEqual("")
expect(config.hideOldComment).toBe(true)
})
test("hideAndRecreate", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide_and_recreate")
})
expect(await config.getBody()).toEqual("")
expect(config.hideAndRecreate).toBe(true)
})
test("hideClassify", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getInput).mockImplementation(name => (name === "hide_classify" ? "OFF_TOPIC" : ""))
})
expect(await config.getBody()).toEqual("")
expect(config.hideClassify).toBe("OFF_TOPIC")
})
test("hideDetails", async () => {
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
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide_details")
})
expect(await config.getBody()).toEqual("")
expect(config.hideDetails).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")
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
test("ignoreEmpty", async () => {
const {config} = await loadConfig(({core}) => {
vi.mocked(core.getBooleanInput).mockImplementation(name => name === "ignore_empty")
})
expect(config.ignoreEmpty).toBe(true)
})
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")]),
})
expect(await config.getBody()).toEqual("hi there\n")
expect(await config.getBody()).toBe("hi there\n")
})
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
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"),
]),
})
expect(await config.getBody()).toEqual("hi there\n\nhey there\n")
expect(await config.getBody()).toBe("hi there\n\nhey there\n")
})
test("when not exists return null string", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/not_exists"
mockConfig.getBody.mockResolvedValue("")
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("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("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
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")
})
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

@ -115,8 +115,9 @@ describe("run", () => {
mockConfig.recreate = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and recreate cannot be both set to true",
"delete and recreate cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when deleteOldComment and onlyCreateComment are both true", async () => {
@ -124,8 +125,9 @@ describe("run", () => {
mockConfig.onlyCreateComment = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and only_create cannot be both set to true",
"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 () => {
@ -133,8 +135,9 @@ describe("run", () => {
mockConfig.hideOldComment = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"delete and hide cannot be both set to true",
"delete and hide cannot be set to true simultaneously",
)
expect(mockConfig.getBody).not.toHaveBeenCalled()
})
test("fails when onlyCreateComment and onlyUpdateComment are both true", async () => {
@ -142,8 +145,9 @@ describe("run", () => {
mockConfig.onlyUpdateComment = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"only_create and only_update cannot be both set to true",
"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 () => {
@ -151,8 +155,19 @@ describe("run", () => {
mockConfig.hideAndRecreate = true
const {core} = await runMain()
expect(core.setFailed).toHaveBeenCalledWith(
"hide and hide_and_recreate cannot be both set to true",
"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 () => {

View file

@ -0,0 +1,85 @@
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,6 +72,9 @@ 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.6/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
"files": {
"includes": ["src/**/*.ts"]
},

72
package-lock.json generated
View file

@ -15,7 +15,7 @@
"@octokit/graphql-schema": "^15.26.1"
},
"devDependencies": {
"@biomejs/biome": "2.4.6",
"@biomejs/biome": "2.4.7",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
@ -98,9 +98,9 @@
"license": "MIT"
},
"node_modules/@biomejs/biome": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.6.tgz",
"integrity": "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.7.tgz",
"integrity": "sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@ -114,20 +114,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.4.6",
"@biomejs/cli-darwin-x64": "2.4.6",
"@biomejs/cli-linux-arm64": "2.4.6",
"@biomejs/cli-linux-arm64-musl": "2.4.6",
"@biomejs/cli-linux-x64": "2.4.6",
"@biomejs/cli-linux-x64-musl": "2.4.6",
"@biomejs/cli-win32-arm64": "2.4.6",
"@biomejs/cli-win32-x64": "2.4.6"
"@biomejs/cli-darwin-arm64": "2.4.7",
"@biomejs/cli-darwin-x64": "2.4.7",
"@biomejs/cli-linux-arm64": "2.4.7",
"@biomejs/cli-linux-arm64-musl": "2.4.7",
"@biomejs/cli-linux-x64": "2.4.7",
"@biomejs/cli-linux-x64-musl": "2.4.7",
"@biomejs/cli-win32-arm64": "2.4.7",
"@biomejs/cli-win32-x64": "2.4.7"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.6.tgz",
"integrity": "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.7.tgz",
"integrity": "sha512-Oo0cF5mHzmvDmTXw8XSjhCia8K6YrZnk7aCS54+/HxyMdZMruMO3nfpDsrlar/EQWe41r1qrwKiCa2QDYHDzWA==",
"cpu": [
"arm64"
],
@ -142,9 +142,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.6.tgz",
"integrity": "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.7.tgz",
"integrity": "sha512-I+cOG3sd/7HdFtvDSnF9QQPrWguUH7zrkIMMykM3PtfWU9soTcS2yRb9Myq6MHmzbeCT08D1UmY+BaiMl5CcoQ==",
"cpu": [
"x64"
],
@ -159,9 +159,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.6.tgz",
"integrity": "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.7.tgz",
"integrity": "sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w==",
"cpu": [
"arm64"
],
@ -176,9 +176,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.6.tgz",
"integrity": "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.7.tgz",
"integrity": "sha512-I2NvM9KPb09jWml93O2/5WMfNR7Lee5Latag1JThDRMURVhPX74p9UDnyTw3Ae6cE1DgXfw7sqQgX7rkvpc0vw==",
"cpu": [
"arm64"
],
@ -193,9 +193,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.6.tgz",
"integrity": "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.7.tgz",
"integrity": "sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ==",
"cpu": [
"x64"
],
@ -210,9 +210,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.6.tgz",
"integrity": "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.7.tgz",
"integrity": "sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g==",
"cpu": [
"x64"
],
@ -227,9 +227,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.6.tgz",
"integrity": "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.7.tgz",
"integrity": "sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw==",
"cpu": [
"arm64"
],
@ -244,9 +244,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.6.tgz",
"integrity": "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg==",
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.7.tgz",
"integrity": "sha512-qEpGjSkPC3qX4ycbMUthXvi9CkRq7kZpkqMY1OyhmYlYLnANnooDQ7hDerM8+0NJ+DZKVnsIc07h30XOpt7LtQ==",
"cpu": [
"x64"
],

View file

@ -35,7 +35,7 @@
"@octokit/graphql-schema": "^15.26.1"
},
"devDependencies": {
"@biomejs/biome": "2.4.6",
"@biomejs/biome": "2.4.7",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",

View file

@ -5,7 +5,9 @@ import {create} from "@actions/glob"
import type {ReportedContentClassifiers} from "@octokit/graphql-schema"
export const pullRequestNumber =
context?.payload?.pull_request?.number || +core.getInput("number", {required: false})
+core.getInput("number_force", {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,6 +27,7 @@ import {
repo,
skipUnchanged,
} from "./config"
import {validateBody, validateExclusiveModes} from "./validate"
async function run(): Promise<undefined> {
if (Number.isNaN(pullRequestNumber) || pullRequestNumber < 1) {
@ -35,6 +36,15 @@ async function run(): Promise<undefined> {
}
try {
validateExclusiveModes(
deleteOldComment,
recreate,
onlyCreateComment,
onlyUpdateComment,
hideOldComment,
hideAndRecreate,
)
const body = await getBody()
if (!body && ignoreEmpty) {
@ -42,29 +52,7 @@ async function run(): Promise<undefined> {
return
}
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 (deleteOldComment && onlyCreateComment) {
throw new Error("delete and only_create cannot be both set to true")
}
if (deleteOldComment && hideOldComment) {
throw new Error("delete and hide 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")
}
validateBody(body, deleteOldComment, hideOldComment)
const octokit = github.getOctokit(githubToken)
const previous = await findPreviousComment(octokit, repo, pullRequestNumber, header)

35
src/validate.ts Normal file
View file

@ -0,0 +1,35 @@
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`)
}
}