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>
This commit is contained in:
Ross Williams 2026-03-13 12:46:05 +00:00 committed by GitHub
parent 14d4f1e429
commit 7cb1e16d25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 185 additions and 380 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. 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 ### Read comment from a file
```yaml ```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. **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` ### `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. **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 = { vi.mock("@actions/core", () => ({
pullRequestNumber: 123, getInput: vi.fn().mockReturnValue(""),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, getBooleanInput: vi.fn().mockReturnValue(false),
header: "", getMultilineInput: vi.fn().mockReturnValue([]),
append: false, setFailed: vi.fn(),
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('../src/config', () => { const mockContext = vi.hoisted(() => ({
return mockConfig repo: {owner: "marocchino", repo: "sticky-pull-request-comment"},
}) payload: {} as Record<string, unknown>,
}))
beforeEach(() => { vi.mock("@actions/github", () => ({
// Set up default environment variables for each test context: mockContext,
process.env["GITHUB_REPOSITORY"] = "marocchino/stick-pull-request-comment" }))
process.env["INPUT_NUMBER"] = "123"
process.env["INPUT_APPEND"] = "false" const mockGlobCreate = vi.hoisted(() => vi.fn())
process.env["INPUT_RECREATE"] = "false"
process.env["INPUT_DELETE"] = "false" vi.mock("@actions/glob", () => ({
process.env["INPUT_ONLY_CREATE"] = "false" create: mockGlobCreate,
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("")
})
afterEach(() => { afterEach(() => {
vi.resetModules() mockContext.payload = {}
delete process.env["GITHUB_REPOSITORY"] mockContext.repo = {owner: "marocchino", repo: "sticky-pull-request-comment"}
delete process.env["INPUT_OWNER"] mockGlobCreate.mockReset()
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 () => { async function loadConfig(
process.env["INPUT_OWNER"] = "jin" setup?: (mocks: {core: typeof import("@actions/core")}) => void,
process.env["INPUT_REPO"] = "other" ) {
vi.resetModules()
mockConfig.repo = {owner: "jin", repo: "other"} const core = await import("@actions/core")
// vi.resetModules clears the config module cache but not mock instances,
const config = await import('../src/config') // so reset core back to default values before each test.
expect(config).toMatchObject({ vi.mocked(core.getInput).mockReturnValue("")
pullRequestNumber: expect.any(Number), vi.mocked(core.getBooleanInput).mockReturnValue(false)
repo: {owner: "jin", repo: "other"}, vi.mocked(core.getMultilineInput).mockReturnValue([])
header: "", setup?.({core})
append: false, const config = await import("../src/config")
recreate: false, return {config, core}
deleteOldComment: false, }
hideOldComment: false,
hideAndRecreate: false, describe("pullRequestNumber", () => {
hideClassify: "OUTDATED", test("number_force takes highest priority", async () => {
hideDetails: false, mockContext.payload = {pull_request: {number: 789}}
githubToken: "some-token", const {config} = await loadConfig(({core}) => {
ignoreEmpty: false, vi.mocked(core.getInput).mockImplementation(name => {
skipUnchanged: false 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 () => { test("header", async () => {
process.env["INPUT_HEADER"] = "header" const {config} = await loadConfig(({core}) => {
mockConfig.header = "header" vi.mocked(core.getInput).mockImplementation(name => (name === "header" ? "my-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(await config.getBody()).toEqual("") expect(config.header).toBe("my-header")
}) })
test("append", async () => { test("append", async () => {
process.env["INPUT_APPEND"] = "true" const {config} = await loadConfig(({core}) => {
mockConfig.append = true vi.mocked(core.getBooleanInput).mockImplementation(name => name === "append")
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(await config.getBody()).toEqual("") expect(config.append).toBe(true)
}) })
test("recreate", async () => { test("recreate", async () => {
process.env["INPUT_RECREATE"] = "true" const {config} = await loadConfig(({core}) => {
mockConfig.recreate = true vi.mocked(core.getBooleanInput).mockImplementation(name => name === "recreate")
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(await config.getBody()).toEqual("") expect(config.recreate).toBe(true)
}) })
test("delete", async () => { test("deleteOldComment", async () => {
process.env["INPUT_DELETE"] = "true" const {config} = await loadConfig(({core}) => {
mockConfig.deleteOldComment = true vi.mocked(core.getBooleanInput).mockImplementation(name => name === "delete")
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(await config.getBody()).toEqual("") expect(config.deleteOldComment).toBe(true)
}) })
test("hideOldComment", async () => { test("hideOldComment", async () => {
process.env["INPUT_HIDE"] = "true" const {config} = await loadConfig(({core}) => {
mockConfig.hideOldComment = true vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide")
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(await config.getBody()).toEqual("") expect(config.hideOldComment).toBe(true)
}) })
test("hideAndRecreate", async () => { test("hideAndRecreate", async () => {
process.env["INPUT_HIDE_AND_RECREATE"] = "true" const {config} = await loadConfig(({core}) => {
mockConfig.hideAndRecreate = true vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide_and_recreate")
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(await config.getBody()).toEqual("") expect(config.hideAndRecreate).toBe(true)
}) })
test("hideClassify", async () => { test("hideClassify", async () => {
process.env["INPUT_HIDE_CLASSIFY"] = "OFF_TOPIC" const {config} = await loadConfig(({core}) => {
mockConfig.hideClassify = "OFF_TOPIC" vi.mocked(core.getInput).mockImplementation(name => (name === "hide_classify" ? "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(await config.getBody()).toEqual("") expect(config.hideClassify).toBe("OFF_TOPIC")
}) })
test("hideDetails", async () => { test("hideDetails", async () => {
process.env["INPUT_HIDE_DETAILS"] = "true" const {config} = await loadConfig(({core}) => {
mockConfig.hideDetails = true vi.mocked(core.getBooleanInput).mockImplementation(name => name === "hide_details")
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(await config.getBody()).toEqual("") expect(config.hideDetails).toBe(true)
}) })
describe("path", () => { test("ignoreEmpty", async () => {
test("when exists return content of a file", async () => { const {config} = await loadConfig(({core}) => {
process.env["INPUT_PATH"] = "./__tests__/assets/result" vi.mocked(core.getBooleanInput).mockImplementation(name => name === "ignore_empty")
mockConfig.getBody.mockResolvedValue("hi there\n") })
expect(config.ignoreEmpty).toBe(true)
const config = await import('../src/config') })
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), test("skipUnchanged", async () => {
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, const {config} = await loadConfig(({core}) => {
header: "", vi.mocked(core.getBooleanInput).mockImplementation(name => name === "skip_unchanged")
append: false, })
recreate: false, expect(config.skipUnchanged).toBe(true)
deleteOldComment: false, })
hideOldComment: false,
hideAndRecreate: false, test("githubToken", async () => {
hideClassify: "OUTDATED", const {config} = await loadConfig(({core}) => {
hideDetails: false, vi.mocked(core.getInput).mockImplementation(name => (name === "GITHUB_TOKEN" ? "my-token" : ""))
githubToken: "some-token", })
ignoreEmpty: false, expect(config.githubToken).toBe("my-token")
skipUnchanged: false })
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 () => { test("glob matches multiple files", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/*" const {config, core} = await loadConfig()
mockConfig.getBody.mockResolvedValue("hi there\n\nhey there\n") vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/*"])
mockGlobCreate.mockResolvedValue({
const config = await import('../src/config') glob: vi
expect(config).toMatchObject({ .fn()
pullRequestNumber: expect.any(Number), .mockResolvedValue([
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, resolve("__tests__/assets/result"),
header: "", resolve("__tests__/assets/result2"),
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("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 () => { test("returns empty string when path matches no files", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/not_exists" const {config, core} = await loadConfig()
mockConfig.getBody.mockResolvedValue("") vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/not_exists"])
mockGlobCreate.mockResolvedValue({glob: vi.fn().mockResolvedValue([])})
const config = await import('../src/config') expect(await config.getBody()).toBe("")
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 () => { test("returns empty string and calls setFailed when glob throws", async () => {
process.env["INPUT_MESSAGE"] = "hello there" const {config, core} = await loadConfig()
mockConfig.getBody.mockResolvedValue("hello there") vi.mocked(core.getMultilineInput).mockReturnValue(["__tests__/assets/result"])
mockGlobCreate.mockRejectedValue(new Error("glob error"))
const config = await import('../src/config') expect(await config.getBody()).toBe("")
expect(config).toMatchObject({ expect(core.setFailed).toHaveBeenCalledWith("glob error")
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

@ -72,6 +72,9 @@ inputs:
number: number:
description: "pull request number for push event" description: "pull request number for push event"
required: false required: false
number_force:
description: "pull request number for any event"
required: false
owner: 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." 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 required: false

View file

@ -5,7 +5,9 @@ import {create} from "@actions/glob"
import type {ReportedContentClassifiers} from "@octokit/graphql-schema" import type {ReportedContentClassifiers} from "@octokit/graphql-schema"
export const pullRequestNumber = 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 repo = buildRepo()
export const header = core.getInput("header", {required: false}) export const header = core.getInput("header", {required: false})