Compare commits

..

No commits in common. "main" and "v2.9.1" have entirely different histories.
main ... v2.9.1

20 changed files with 6739 additions and 3285 deletions

4
.eslintignore Normal file
View file

@ -0,0 +1,4 @@
dist/
lib/
node_modules/
jest.config.js

59
.eslintrc.json Normal file
View file

@ -0,0 +1,59 @@
{
"root": true,
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"i18n-text/no-en": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{"accessibility": "no-public"}
],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": [
"error",
{"allowExpressions": true}
],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}

View file

@ -11,14 +11,15 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- run: yarn install - run: yarn install
- run: | - run: |
set -o pipefail set -o pipefail
mkdir -p ./pr mkdir -p ./pr
echo ${{ github.event.number }} | tee ./pr/number
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "all_result<<$EOF" >> "$GITHUB_ENV" echo "all_result<<$EOF" >> "$GITHUB_ENV"
yarn all >> "$GITHUB_ENV" 2>&1 || true # proceed even if yarn fails yarn all >> "$GITHUB_ENV" 2&>1 || true # proceed even if yarn fails
echo >> "$GITHUB_ENV" # yarn all doesn't necessarily produce a newline echo >> "$GITHUB_ENV" # yarn all doesn't necessarily produce a newline
echo "$EOF" >> "$GITHUB_ENV" echo "$EOF" >> "$GITHUB_ENV"
id: all id: all
@ -28,9 +29,9 @@ jobs:
header: All header: All
message: | message: |
<details open> <details open>
<summary>Output of yarn all</summary> <summary>output of yarn all</summary>
```shell ```
${{ env.all_result }} ${{ env.all_result }}
``` ```
</details> </details>
@ -42,7 +43,3 @@ jobs:
hide_details: true hide_details: true
message: | message: |
The build is over. The build is over.
- name: Lint
run: npm run lint
- name: Format Check
run: npm run format-check

3
.prettierignore Normal file
View file

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

9
.prettierrc.json Normal file
View file

@ -0,0 +1,9 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid"
}

View file

@ -1,6 +1,5 @@
import {getOctokit} from "@actions/github" import {getOctokit} from "@actions/github"
import * as core from "@actions/core" import * as core from "@actions/core"
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { import {
createComment, createComment,
@ -12,8 +11,8 @@ import {
commentsEqual commentsEqual
} from "../src/comment" } from "../src/comment"
vi.mock("@actions/core", () => ({ jest.mock("@actions/core", () => ({
warning: vi.fn() warning: jest.fn()
})) }))
const repo = { const repo = {
@ -69,7 +68,7 @@ it("findPreviousComment", async () => {
} }
] ]
const octokit = getOctokit("github-token") const octokit = getOctokit("github-token")
vi.spyOn(octokit, "graphql").mockResolvedValue({ jest.spyOn(octokit, "graphql").mockResolvedValue({
viewer: authenticatedBotUser, viewer: authenticatedBotUser,
repository: { repository: {
pullRequest: { pullRequest: {
@ -91,8 +90,12 @@ it("findPreviousComment", async () => {
} as any) } as any)
expect(await findPreviousComment(octokit, repo, 123, "")).toBe(comment) expect(await findPreviousComment(octokit, repo, 123, "")).toBe(comment)
expect(await findPreviousComment(octokit, repo, 123, "TypeA")).toBe(commentWithCustomHeader) expect(await findPreviousComment(octokit, repo, 123, "TypeA")).toBe(
expect(await findPreviousComment(octokit, repo, 123, "LegacyComment")).toBe(headerFirstComment) commentWithCustomHeader
)
expect(await findPreviousComment(octokit, repo, 123, "LegacyComment")).toBe(
headerFirstComment
)
expect(octokit.graphql).toBeCalledWith(expect.any(String), { expect(octokit.graphql).toBeCalledWith(expect.any(String), {
after: null, after: null,
number: 123, number: 123,
@ -105,18 +108,22 @@ describe("updateComment", () => {
const octokit = getOctokit("github-token") const octokit = getOctokit("github-token")
beforeEach(() => { beforeEach(() => {
vi.spyOn(octokit, "graphql").mockResolvedValue("") jest.spyOn<any, string>(octokit, "graphql").mockResolvedValue("")
}) })
it("with comment body", async () => { it("with comment body", async () => {
expect(await updateComment(octokit, "456", "hello there", "")).toBeUndefined() expect(
await updateComment(octokit, "456", "hello there", "")
).toBeUndefined()
expect(octokit.graphql).toBeCalledWith(expect.any(String), { expect(octokit.graphql).toBeCalledWith(expect.any(String), {
input: { input: {
id: "456", id: "456",
body: "hello there\n<!-- Sticky Pull Request Comment -->" body: "hello there\n<!-- Sticky Pull Request Comment -->"
} }
}) })
expect(await updateComment(octokit, "456", "hello there", "TypeA")).toBeUndefined() expect(
await updateComment(octokit, "456", "hello there", "TypeA")
).toBeUndefined()
expect(octokit.graphql).toBeCalledWith(expect.any(String), { expect(octokit.graphql).toBeCalledWith(expect.any(String), {
input: { input: {
id: "456", id: "456",
@ -151,21 +158,24 @@ describe("createComment", () => {
const octokit = getOctokit("github-token") const octokit = getOctokit("github-token")
beforeEach(() => { beforeEach(() => {
vi.spyOn(octokit.rest.issues, "createComment") jest
.mockResolvedValue({ data: "<return value>" } as any) .spyOn<any, string>(octokit.rest.issues, "createComment")
.mockResolvedValue("<return value>")
}) })
it("with comment body or previousBody", async () => { it("with comment body or previousBody", async () => {
expect(await createComment(octokit, repo, 456, "hello there", "")).toEqual({ data: "<return value>" }) expect(await createComment(octokit, repo, 456, "hello there", "")).toEqual(
"<return value>"
)
expect(octokit.rest.issues.createComment).toBeCalledWith({ expect(octokit.rest.issues.createComment).toBeCalledWith({
issue_number: 456, issue_number: 456,
owner: "marocchino", owner: "marocchino",
repo: "sticky-pull-request-comment", repo: "sticky-pull-request-comment",
body: "hello there\n<!-- Sticky Pull Request Comment -->" body: "hello there\n<!-- Sticky Pull Request Comment -->"
}) })
expect(await createComment(octokit, repo, 456, "hello there", "TypeA")).toEqual( expect(
{ data: "<return value>" } await createComment(octokit, repo, 456, "hello there", "TypeA")
) ).toEqual("<return value>")
expect(octokit.rest.issues.createComment).toBeCalledWith({ expect(octokit.rest.issues.createComment).toBeCalledWith({
issue_number: 456, issue_number: 456,
owner: "marocchino", owner: "marocchino",
@ -183,7 +193,7 @@ describe("createComment", () => {
it("deleteComment", async () => { it("deleteComment", async () => {
const octokit = getOctokit("github-token") const octokit = getOctokit("github-token")
vi.spyOn(octokit, "graphql").mockReturnValue(undefined as any) jest.spyOn(octokit, "graphql").mockReturnValue(undefined as any)
expect(await deleteComment(octokit, "456")).toBeUndefined() expect(await deleteComment(octokit, "456")).toBeUndefined()
expect(octokit.graphql).toBeCalledWith(expect.any(String), { expect(octokit.graphql).toBeCalledWith(expect.any(String), {
id: "456" id: "456"
@ -193,7 +203,7 @@ it("deleteComment", async () => {
it("minimizeComment", async () => { it("minimizeComment", async () => {
const octokit = getOctokit("github-token") const octokit = getOctokit("github-token")
vi.spyOn(octokit, "graphql").mockReturnValue(undefined as any) jest.spyOn(octokit, "graphql").mockReturnValue(undefined as any)
expect(await minimizeComment(octokit, "456", "OUTDATED")).toBeUndefined() expect(await minimizeComment(octokit, "456", "OUTDATED")).toBeUndefined()
expect(octokit.graphql).toBeCalledWith(expect.any(String), { expect(octokit.graphql).toBeCalledWith(expect.any(String), {
input: { input: {
@ -226,7 +236,7 @@ describe("getBodyOf", () => {
</details> </details>
<!-- Sticky Pull Request CommentTypeA --> <!-- Sticky Pull Request CommentTypeA -->
` `
it.each` test.each`
append | hideDetails | previous | expected append | hideDetails | previous | expected
${false} | ${false} | ${detailsPrevious} | ${undefined} ${false} | ${false} | ${detailsPrevious} | ${undefined}
${true} | ${false} | ${nullPrevious} | ${undefined} ${true} | ${false} | ${nullPrevious} | ${undefined}
@ -243,7 +253,7 @@ describe("getBodyOf", () => {
}) })
describe("commentsEqual", () => { describe("commentsEqual", () => {
it.each([ test.each([
{ {
body: "body", body: "body",
previous: "body\n<!-- Sticky Pull Request Commentheader -->", previous: "body\n<!-- Sticky Pull Request Commentheader -->",

View file

@ -1,28 +1,4 @@
import { beforeEach, afterEach, test, expect, vi, describe } from 'vitest'
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('../src/config', () => {
return mockConfig
})
beforeEach(() => { beforeEach(() => {
// Set up default environment variables for each test
process.env["GITHUB_REPOSITORY"] = "marocchino/stick-pull-request-comment" process.env["GITHUB_REPOSITORY"] = "marocchino/stick-pull-request-comment"
process.env["INPUT_NUMBER"] = "123" process.env["INPUT_NUMBER"] = "123"
process.env["INPUT_APPEND"] = "false" process.env["INPUT_APPEND"] = "false"
@ -38,26 +14,10 @@ beforeEach(() => {
process.env["INPUT_IGNORE_EMPTY"] = "false" process.env["INPUT_IGNORE_EMPTY"] = "false"
process.env["INPUT_SKIP_UNCHANGED"] = "false" process.env["INPUT_SKIP_UNCHANGED"] = "false"
process.env["INPUT_FOLLOW_SYMBOLIC_LINKS"] = "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() jest.resetModules()
delete process.env["GITHUB_REPOSITORY"] delete process.env["GITHUB_REPOSITORY"]
delete process.env["INPUT_OWNER"] delete process.env["INPUT_OWNER"]
delete process.env["INPUT_REPO"] delete process.env["INPUT_REPO"]
@ -83,11 +43,7 @@ afterEach(() => {
test("repo", async () => { test("repo", async () => {
process.env["INPUT_OWNER"] = "jin" process.env["INPUT_OWNER"] = "jin"
process.env["INPUT_REPO"] = "other" process.env["INPUT_REPO"] = "other"
expect(require("../src/config")).toMatchObject({
mockConfig.repo = {owner: "jin", repo: "other"}
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "jin", repo: "other"}, repo: {owner: "jin", repo: "other"},
header: "", header: "",
@ -102,15 +58,11 @@ test("repo", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("header", async () => { test("header", async () => {
process.env["INPUT_HEADER"] = "header" process.env["INPUT_HEADER"] = "header"
mockConfig.header = "header" expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "header", header: "header",
@ -125,15 +77,11 @@ test("header", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("append", async () => { test("append", async () => {
process.env["INPUT_APPEND"] = "true" process.env["INPUT_APPEND"] = "true"
mockConfig.append = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -148,15 +96,11 @@ test("append", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("recreate", async () => { test("recreate", async () => {
process.env["INPUT_RECREATE"] = "true" process.env["INPUT_RECREATE"] = "true"
mockConfig.recreate = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -171,15 +115,11 @@ test("recreate", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("delete", async () => { test("delete", async () => {
process.env["INPUT_DELETE"] = "true" process.env["INPUT_DELETE"] = "true"
mockConfig.deleteOldComment = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -194,15 +134,11 @@ test("delete", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("hideOldComment", async () => { test("hideOldComment", async () => {
process.env["INPUT_HIDE"] = "true" process.env["INPUT_HIDE"] = "true"
mockConfig.hideOldComment = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -217,15 +153,11 @@ test("hideOldComment", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("hideAndRecreate", async () => { test("hideAndRecreate", async () => {
process.env["INPUT_HIDE_AND_RECREATE"] = "true" process.env["INPUT_HIDE_AND_RECREATE"] = "true"
mockConfig.hideAndRecreate = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -240,15 +172,11 @@ test("hideAndRecreate", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("hideClassify", async () => { test("hideClassify", async () => {
process.env["INPUT_HIDE_CLASSIFY"] = "OFF_TOPIC" process.env["INPUT_HIDE_CLASSIFY"] = "OFF_TOPIC"
mockConfig.hideClassify = "OFF_TOPIC" expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -263,15 +191,11 @@ test("hideClassify", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("hideDetails", async () => { test("hideDetails", async () => {
process.env["INPUT_HIDE_DETAILS"] = "true" process.env["INPUT_HIDE_DETAILS"] = "true"
mockConfig.hideDetails = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -286,16 +210,12 @@ test("hideDetails", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
describe("path", () => { describe("path", () => {
test("when exists return content of a file", async () => { test("when exists return content of a file", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/result" process.env["INPUT_PATH"] = "./__tests__/assets/result"
mockConfig.getBody.mockResolvedValue("hi there\n") expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -310,15 +230,12 @@ describe("path", () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("hi there\n") expect(await require("../src/config").getBody()).toEqual("hi there\n")
}) })
test("glob match files", async () => { test("glob match files", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/*" process.env["INPUT_PATH"] = "./__tests__/assets/*"
mockConfig.getBody.mockResolvedValue("hi there\n\nhey there\n") expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -333,15 +250,14 @@ describe("path", () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("hi there\n\nhey there\n") expect(await require("../src/config").getBody()).toEqual(
"hi there\n\nhey there\n"
)
}) })
test("when not exists return null string", async () => { test("when not exists return null string", async () => {
process.env["INPUT_PATH"] = "./__tests__/assets/not_exists" process.env["INPUT_PATH"] = "./__tests__/assets/not_exists"
mockConfig.getBody.mockResolvedValue("") expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -356,16 +272,13 @@ describe("path", () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
}) })
test("message", async () => { test("message", async () => {
process.env["INPUT_MESSAGE"] = "hello there" process.env["INPUT_MESSAGE"] = "hello there"
mockConfig.getBody.mockResolvedValue("hello there") expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -380,15 +293,12 @@ test("message", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("hello there") expect(await require("../src/config").getBody()).toEqual("hello there")
}) })
test("ignore_empty", async () => { test("ignore_empty", async () => {
process.env["INPUT_IGNORE_EMPTY"] = "true" process.env["INPUT_IGNORE_EMPTY"] = "true"
mockConfig.ignoreEmpty = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -403,15 +313,12 @@ test("ignore_empty", async () => {
ignoreEmpty: true, ignoreEmpty: true,
skipUnchanged: false skipUnchanged: false
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })
test("skip_unchanged", async () => { test("skip_unchanged", async () => {
process.env["INPUT_SKIP_UNCHANGED"] = "true" process.env["INPUT_SKIP_UNCHANGED"] = "true"
mockConfig.skipUnchanged = true expect(require("../src/config")).toMatchObject({
const config = await import('../src/config')
expect(config).toMatchObject({
pullRequestNumber: expect.any(Number), pullRequestNumber: expect.any(Number),
repo: {owner: "marocchino", repo: "stick-pull-request-comment"}, repo: {owner: "marocchino", repo: "stick-pull-request-comment"},
header: "", header: "",
@ -426,5 +333,5 @@ test("skip_unchanged", async () => {
ignoreEmpty: false, ignoreEmpty: false,
skipUnchanged: true skipUnchanged: true
}) })
expect(await config.getBody()).toEqual("") expect(await require("../src/config").getBody()).toEqual("")
}) })

View file

@ -1,33 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"files": {
"includes": ["src/**/*.ts"]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"includes": ["vitest.config.ts", "src/**/*.ts", "__tests__/**/*.ts"]
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded",
"bracketSameLine": false,
"quoteStyle": "double",
"bracketSpacing": false
},
"globals": ["jest"]
}
}

4269
dist/index.js generated vendored

File diff suppressed because it is too large Load diff

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

25
dist/licenses.txt generated vendored
View file

@ -573,6 +573,31 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
is-plain-object
MIT
The MIT License (MIT)
Copyright (c) 2014-2017, Jon Schlinkert.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
minimatch minimatch
ISC ISC
The ISC License The ISC License

10
jest.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

View file

@ -6,15 +6,12 @@
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"format": "biome format --write .", "format": "prettier --write **/*.ts",
"format-check": "biome format --write .", "format-check": "prettier --check **/*.ts",
"lint": "biome check .", "lint": "eslint src/**/*.ts",
"lint:fix": "biome check --apply .",
"package": "ncc build --source-map --license licenses.txt", "package": "ncc build --source-map --license licenses.txt",
"test": "vitest run", "test": "jest",
"test:watch": "vitest", "build_test": "tsc && jest",
"coverage": "vitest run --coverage",
"build_test": "tsc && vitest run",
"all": "yarn build && yarn format && yarn lint && yarn package && yarn test" "all": "yarn build && yarn format && yarn lint && yarn package && yarn test"
}, },
"repository": { "repository": {
@ -30,16 +27,24 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@actions/github": "^6.0.1", "@actions/github": "^6.0.0",
"@actions/glob": "^0.5.0", "@actions/glob": "^0.5.0",
"@octokit/graphql-schema": "^15.26.0" "@octokit/graphql-schema": "^14.52.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.4", "@types/jest": "^29.5.14",
"@types/node": "^24.5.2", "@types/node": "^22.10.7",
"@typescript-eslint/parser": "^8.20.0",
"@vercel/ncc": "^0.38.3", "@vercel/ncc": "^0.38.3",
"eslint": "^8.56.0",
"eslint-plugin-github": "^5.1.5",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.2.2",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"typescript": "^5.9.2", "prettier": "3.4.2",
"vitest": "^3.2.4" "ts-jest": "^29.2.5",
"typescript": "^5.7.3"
} }
} }

View file

@ -1,17 +1,17 @@
import * as core from "@actions/core" import * as core from "@actions/core"
import type {GitHub} from "@actions/github/lib/utils" import {
import type {
IssueComment, IssueComment,
ReportedContentClassifiers, ReportedContentClassifiers,
Repository, Repository,
User, User
} from "@octokit/graphql-schema" } from "@octokit/graphql-schema"
import {GitHub} from "@actions/github/lib/utils"
type CreateCommentResponse = Awaited< type CreateCommentResponse = Awaited<
ReturnType<InstanceType<typeof GitHub>["rest"]["issues"]["createComment"]> ReturnType<InstanceType<typeof GitHub>["rest"]["issues"]["createComment"]>
> >
function headerComment(header: string): string { function headerComment(header: String): string {
return `<!-- Sticky Pull Request Comment${header} -->` return `<!-- Sticky Pull Request Comment${header} -->`
} }
@ -30,7 +30,7 @@ export async function findPreviousComment(
repo: string repo: string
}, },
number: number, number: number,
header: string, header: string
): Promise<IssueComment | undefined> { ): Promise<IssueComment | undefined> {
let after = null let after = null
let hasNextPage = true let hasNextPage = true
@ -60,7 +60,7 @@ export async function findPreviousComment(
} }
} }
`, `,
{...repo, after, number}, {...repo, after, number}
) )
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const viewer = data.viewer as User const viewer = data.viewer as User
@ -70,13 +70,14 @@ export async function findPreviousComment(
(node: IssueComment | null | undefined) => (node: IssueComment | null | undefined) =>
node?.author?.login === viewer.login.replace("[bot]", "") && node?.author?.login === viewer.login.replace("[bot]", "") &&
!node?.isMinimized && !node?.isMinimized &&
node?.body?.includes(h), node?.body?.includes(h)
) )
if (target) { if (target) {
return target return target
} }
after = repository.pullRequest?.comments?.pageInfo?.endCursor after = repository.pullRequest?.comments?.pageInfo?.endCursor
hasNextPage = repository.pullRequest?.comments?.pageInfo?.hasNextPage ?? false hasNextPage =
repository.pullRequest?.comments?.pageInfo?.hasNextPage ?? false
} }
return undefined return undefined
} }
@ -86,11 +87,14 @@ export async function updateComment(
id: string, id: string,
body: string, body: string,
header: string, header: string,
previousBody?: string, previousBody?: string
): Promise<void> { ): Promise<void> {
if (!body && !previousBody) return core.warning("Comment body cannot be blank") if (!body && !previousBody)
return core.warning("Comment body cannot be blank")
const rawPreviousBody: string = previousBody ? bodyWithoutHeader(previousBody, header) : "" const rawPreviousBody: String = previousBody
? bodyWithoutHeader(previousBody, header)
: ""
await octokit.graphql( await octokit.graphql(
` `
@ -108,9 +112,9 @@ export async function updateComment(
id, id,
body: previousBody body: previousBody
? bodyWithHeader(`${rawPreviousBody}\n${body}`, header) ? bodyWithHeader(`${rawPreviousBody}\n${body}`, header)
: bodyWithHeader(body, header), : bodyWithHeader(body, header)
}, }
}, }
) )
} }
export async function createComment( export async function createComment(
@ -122,7 +126,7 @@ export async function createComment(
issue_number: number, issue_number: number,
body: string, body: string,
header: string, header: string,
previousBody?: string, previousBody?: string
): Promise<CreateCommentResponse | undefined> { ): Promise<CreateCommentResponse | undefined> {
if (!body && !previousBody) { if (!body && !previousBody) {
core.warning("Comment body cannot be blank") core.warning("Comment body cannot be blank")
@ -132,12 +136,14 @@ export async function createComment(
return await octokit.rest.issues.createComment({ return await octokit.rest.issues.createComment({
...repo, ...repo,
issue_number, issue_number,
body: previousBody ? `${previousBody}\n${body}` : bodyWithHeader(body, header), body: previousBody
? `${previousBody}\n${body}`
: bodyWithHeader(body, header)
}) })
} }
export async function deleteComment( export async function deleteComment(
octokit: InstanceType<typeof GitHub>, octokit: InstanceType<typeof GitHub>,
id: string, id: string
): Promise<void> { ): Promise<void> {
await octokit.graphql( await octokit.graphql(
` `
@ -147,13 +153,13 @@ export async function deleteComment(
} }
} }
`, `,
{id}, {id}
) )
} }
export async function minimizeComment( export async function minimizeComment(
octokit: InstanceType<typeof GitHub>, octokit: InstanceType<typeof GitHub>,
subjectId: string, subjectId: string,
classifier: ReportedContentClassifiers, classifier: ReportedContentClassifiers
): Promise<void> { ): Promise<void> {
await octokit.graphql( await octokit.graphql(
` `
@ -163,27 +169,31 @@ export async function minimizeComment(
} }
} }
`, `,
{input: {subjectId, classifier}}, {input: {subjectId, classifier}}
) )
} }
export function getBodyOf( export function getBodyOf(
previous: {body?: string}, previous: {body: string},
append: boolean, append: boolean,
hideDetails: boolean, hideDetails: boolean
): string | undefined { ): string | undefined {
if (!append) { if (!append) {
return undefined return undefined
} }
if (!hideDetails || !previous.body) { if (!hideDetails) {
return previous.body return previous.body
} }
return previous.body.replace(/(<details.*?)\s*\bopen\b(.*>)/g, "$1$2") return previous.body?.replace(/(<details.*?)\s*\bopen\b(.*>)/g, "$1$2")
} }
export function commentsEqual(body: string, previous: string | undefined, header: string): boolean { export function commentsEqual(
body: string,
previous: string,
header: string
): boolean {
const newBody = bodyWithHeader(body, header) const newBody = bodyWithHeader(body, header)
return newBody === previous return newBody === previous
} }

View file

@ -1,60 +1,63 @@
import {readFileSync} from "node:fs"
import * as core from "@actions/core" import * as core from "@actions/core"
import {ReportedContentClassifiers} from "@octokit/graphql-schema"
import {context} from "@actions/github" import {context} from "@actions/github"
import {readFileSync} from "fs"
import {create} from "@actions/glob" import {create} from "@actions/glob"
import type {ReportedContentClassifiers} from "@octokit/graphql-schema"
export const pullRequestNumber = export const pullRequestNumber =
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 repo = buildRepo()
export const header = core.getInput("header", {required: false}) export const header = core.getInput("header", {required: false})
export const append = core.getBooleanInput("append", {required: true}) export const append = core.getBooleanInput("append", {required: true})
export const hideDetails = core.getBooleanInput("hide_details", { export const hideDetails = core.getBooleanInput("hide_details", {
required: true, required: true
}) })
export const recreate = core.getBooleanInput("recreate", {required: true}) export const recreate = core.getBooleanInput("recreate", {required: true})
export const hideAndRecreate = core.getBooleanInput("hide_and_recreate", { export const hideAndRecreate = core.getBooleanInput("hide_and_recreate", {
required: true, required: true
}) })
export const hideClassify = core.getInput("hide_classify", { export const hideClassify = core.getInput("hide_classify", {
required: true, required: true
}) as ReportedContentClassifiers }) as ReportedContentClassifiers
export const deleteOldComment = core.getBooleanInput("delete", {required: true}) export const deleteOldComment = core.getBooleanInput("delete", {required: true})
export const onlyCreateComment = core.getBooleanInput("only_create", { export const onlyCreateComment = core.getBooleanInput("only_create", {
required: true, required: true
}) })
export const onlyUpdateComment = core.getBooleanInput("only_update", { export const onlyUpdateComment = core.getBooleanInput("only_update", {
required: true, required: true
}) })
export const skipUnchanged = core.getBooleanInput("skip_unchanged", { export const skipUnchanged = core.getBooleanInput("skip_unchanged", {
required: true, required: true
}) })
export const hideOldComment = core.getBooleanInput("hide", {required: true}) export const hideOldComment = core.getBooleanInput("hide", {required: true})
export const githubToken = core.getInput("GITHUB_TOKEN", {required: true}) export const githubToken = core.getInput("GITHUB_TOKEN", {required: true})
export const ignoreEmpty = core.getBooleanInput("ignore_empty", { export const ignoreEmpty = core.getBooleanInput("ignore_empty", {
required: true, required: true
}) })
function buildRepo(): {repo: string; owner: string} { function buildRepo(): {repo: string; owner: string} {
return { return {
owner: core.getInput("owner", {required: false}) || context.repo.owner, owner: core.getInput("owner", {required: false}) || context.repo.owner,
repo: core.getInput("repo", {required: false}) || context.repo.repo, repo: core.getInput("repo", {required: false}) || context.repo.repo
} }
} }
export async function getBody(): Promise<string> { export async function getBody(): Promise<string> {
const pathInput = core.getMultilineInput("path", {required: false}) const pathInput = core.getMultilineInput("path", {required: false})
const followSymbolicLinks = core.getBooleanInput("follow_symbolic_links", { const followSymbolicLinks = core.getBooleanInput("follow_symbolic_links", {
required: true, required: true
}) })
if (pathInput && pathInput.length > 0) { if (pathInput && pathInput.length > 0) {
try { try {
const globber = await create(pathInput.join("\n"), { const globber = await create(pathInput.join("\n"), {
followSymbolicLinks, followSymbolicLinks,
matchDirectories: false, matchDirectories: false
}) })
return (await globber.glob()).map(path => readFileSync(path, "utf-8")).join("\n") return (await globber.glob())
.map(path => readFileSync(path, "utf-8"))
.join("\n")
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
core.setFailed(error.message) core.setFailed(error.message)

View file

@ -1,35 +1,35 @@
import * as core from "@actions/core" import * as core from "@actions/core"
import * as github from "@actions/github" import * as github from "@actions/github"
import {
commentsEqual,
createComment,
deleteComment,
findPreviousComment,
getBodyOf,
minimizeComment,
updateComment,
} from "./comment"
import { import {
append, append,
deleteOldComment,
getBody, getBody,
deleteOldComment,
githubToken, githubToken,
header, header,
hideAndRecreate, hideAndRecreate,
hideClassify, hideClassify,
hideDetails, hideDetails,
hideOldComment, hideOldComment,
ignoreEmpty,
onlyCreateComment,
onlyUpdateComment,
pullRequestNumber, pullRequestNumber,
recreate, recreate,
repo, repo,
ignoreEmpty,
skipUnchanged, skipUnchanged,
onlyCreateComment,
onlyUpdateComment
} from "./config" } from "./config"
import {
createComment,
deleteComment,
findPreviousComment,
getBodyOf,
minimizeComment,
updateComment,
commentsEqual
} from "./comment"
async function run(): Promise<undefined> { async function run(): Promise<undefined> {
if (Number.isNaN(pullRequestNumber) || pullRequestNumber < 1) { if (isNaN(pullRequestNumber) || pullRequestNumber < 1) {
core.info("no pull request numbers given: skip step") core.info("no pull request numbers given: skip step")
return return
} }
@ -59,7 +59,12 @@ async function run(): Promise<undefined> {
} }
const octokit = github.getOctokit(githubToken) const octokit = github.getOctokit(githubToken)
const previous = await findPreviousComment(octokit, repo, pullRequestNumber, header) const previous = await findPreviousComment(
octokit,
repo,
pullRequestNumber,
header
)
core.setOutput("previous_comment_id", previous?.id) core.setOutput("previous_comment_id", previous?.id)
@ -74,7 +79,13 @@ async function run(): Promise<undefined> {
if (onlyUpdateComment) { if (onlyUpdateComment) {
return return
} }
const created = await createComment(octokit, repo, pullRequestNumber, body, header) const created = await createComment(
octokit,
repo,
pullRequestNumber,
body,
header
)
core.setOutput("created_comment_id", created?.data.id) core.setOutput("created_comment_id", created?.data.id)
return return
} }
@ -90,12 +101,12 @@ async function run(): Promise<undefined> {
return return
} }
if (skipUnchanged && commentsEqual(body, previous.body || "", header)) { if (skipUnchanged && commentsEqual(body, previous.body, header)) {
// don't recreate or update if the message is unchanged // don't recreate or update if the message is unchanged
return return
} }
const previousBody = getBodyOf({body: previous.body || ""}, append, hideDetails) const previousBody = getBodyOf(previous, append, hideDetails)
if (recreate) { if (recreate) {
await deleteComment(octokit, previous.id) await deleteComment(octokit, previous.id)
const created = await createComment( const created = await createComment(
@ -104,7 +115,7 @@ async function run(): Promise<undefined> {
pullRequestNumber, pullRequestNumber,
body, body,
header, header,
previousBody, previousBody
) )
core.setOutput("created_comment_id", created?.data.id) core.setOutput("created_comment_id", created?.data.id)
return return
@ -112,7 +123,13 @@ async function run(): Promise<undefined> {
if (hideAndRecreate) { if (hideAndRecreate) {
await minimizeComment(octokit, previous.id, hideClassify) await minimizeComment(octokit, previous.id, hideClassify)
const created = await createComment(octokit, repo, pullRequestNumber, body, header) const created = await createComment(
octokit,
repo,
pullRequestNumber,
body,
header
)
core.setOutput("created_comment_id", created?.data.id) core.setOutput("created_comment_id", created?.data.id)
return return
} }

View file

@ -1,25 +1,12 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true, "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"declaration": false, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"declarationMap": false, "outDir": "./lib" /* Redirect output structure to the directory. */,
"esModuleInterop": true, "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"forceConsistentCasingInFileNames": true, "strict": true /* Enable all strict type-checking options. */,
"lib": ["ES2022"], "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"module": "NodeNext", "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"moduleResolution": "NodeNext",
"newLine": "lf",
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"pretty": true,
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
"target": "ES2022",
"outDir": "./lib",
"rootDir": "./src"
}, },
"exclude": ["node_modules", "**/*.test.ts", "vitest.config.ts"] "exclude": ["node_modules", "**/*.test.ts"]
} }

View file

@ -1,21 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
clearMocks: true,
coverage: {
provider: 'v8',
reporter: ['json', 'lcov', 'text', 'clover'],
exclude: ['/node_modules/'],
},
environment: 'node',
include: ['**/__tests__/**/*.test.ts'],
globals: true,
testTimeout: 10000,
poolOptions: {
threads: {
maxThreads: 10,
},
},
},
});

15
wallaby.js Normal file
View file

@ -0,0 +1,15 @@
module.exports = function(wallaby) {
return {
files: ["src/**/*.js?(x)", "!src/**/*.spec.ts?(x)"],
tests: ["__tests__/**/*.test.ts?(x)"],
env: {
type: "node",
runner: "node"
},
testFramework: "jest",
debug: true
};
};

5144
yarn.lock

File diff suppressed because it is too large Load diff