Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 28 additions & 15 deletions dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31064,6 +31064,7 @@ function renderChangelog(heading, changelogs) {
}
function getPullRequestNumber() {
if (["pull_request", "pull_request_target"].includes(context.eventName)) return Number(context.payload.number);
if (context.eventName === "pull_request_review") return Number(context.payload.pull_request?.number);
if (context.eventName === "issue_comment" && context.payload.issue?.pull_request) return Number(context.payload.issue.number);
return 0;
}
Expand Down Expand Up @@ -31269,14 +31270,37 @@ function useGithub(token) {
//#region src/github-event/issue-comment.ts
async function issue_comment(token) {
if (context.eventName !== "issue_comment") return false;
if (context.payload.action !== "edited") return false;
if (context.payload.changes?.body === context.payload.comment?.body) return false;
if (!(await getPrCommentWhitelist()).includes(context.actor)) return false;
if (!context.payload.issue?.pull_request) return false;
const action = context.payload.action;
const confirmLog = context.payload.comment?.body || "";
const isChangelogCommand = action === "created" && confirmLog.trim() === "/changelog";
if (action !== "edited" && !isChangelogCommand) return false;
if (action === "edited" && context.payload.changes?.body === context.payload.comment?.body) return false;
if (!(await getPrCommentWhitelist()).includes(context.actor)) return false;
const prNumber = getPullRequestNumber();
if (isChangelogCommand) {
const { getPullRequestData } = useGithub(token);
return confirmPullRequestChangelog(prNumber, await getPullRequestData(prNumber), token);
}
await confirmChangelog(prNumber, confirmLog, token);
await confirmReleaseLog(prNumber, confirmLog, token);
}
async function confirmPullRequestChangelog(prNumber, prData, token) {
if (prData.head.ref.startsWith("release/")) return false;
let logs = "";
const prLog = extractChangelog(prData.body || "", getInputPkgs());
info(`pr_log: ${JSON.stringify(prLog, null, 2)}`);
Object.keys(prLog).forEach((pkgName) => {
if (!prLog[pkgName].length) return;
logs += `#### ${pkgName}\n`;
prLog[pkgName].forEach((log) => {
logs += `- ${log}\n`;
});
});
if (!logs) return false;
await confirmChangelog(prNumber, `### 📝 更新日志\n\n${logs}\n\n`, token);
return true;
}
async function confirmChangelog(prNumber, log, token) {
if (!log.startsWith("### 📝 更新日志")) return false;
const changelog = extractChangelog(log || "", getInputPkgs());
Expand Down Expand Up @@ -82709,18 +82733,7 @@ async function pull_request_review(token) {
if (!(await getPrCommentWhitelist()).includes(context.actor)) return false;
const prNumber = getPullRequestNumber();
const pullRequestData = context.payload.pull_request;
if (pullRequestData.head.ref.startsWith("release/")) return false;
let logs = "";
const prLog = extractChangelog(pullRequestData.body || "", getInputPkgs());
info(`pr_log: ${JSON.stringify(prLog, null, 2)}`);
Object.keys(prLog).forEach((pkgName) => {
if (!prLog[pkgName].length) return;
logs += `#### ${pkgName}\n`;
prLog[pkgName].forEach((log) => {
logs += `- ${log}\n`;
});
});
if (logs) await confirmChangelog(prNumber, `### 📝 更新日志\n\n${logs}\n\n`, token);
return confirmPullRequestChangelog(prNumber, pullRequestData, token);
}
//#endregion
//#region src/github-event/pull-request-target.ts
Expand Down
44 changes: 41 additions & 3 deletions src/github-event/issue-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,64 @@ export async function issue_comment(token: string) {
if (github.context.eventName !== 'issue_comment') {
return false
}
if (github.context.payload.action !== 'edited') {
if (!github.context.payload.issue?.pull_request) {
return false
}

if (github.context.payload.changes?.body === github.context.payload.comment?.body) {
const action = github.context.payload.action
const confirmLog = github.context.payload.comment?.body || ''
const isChangelogCommand = action === 'created' && confirmLog.trim() === '/changelog'
if (action !== 'edited' && !isChangelogCommand) {
return false
}

if (action === 'edited' && github.context.payload.changes?.body === github.context.payload.comment?.body) {
return false
}
const whitelist = await getPrCommentWhitelist()
if (!whitelist.includes(github.context.actor)) {
return false
}
const confirmLog = github.context.payload.comment?.body || ''

const prNumber = getPullRequestNumber()

if (isChangelogCommand) {
const { getPullRequestData } = useGithub(token)
const prData = await getPullRequestData(prNumber) as PullRequestData
return confirmPullRequestChangelog(prNumber, prData, token)
}

await confirmChangelog(prNumber, confirmLog, token)

await confirmReleaseLog(prNumber, confirmLog, token)
}

export async function confirmPullRequestChangelog(prNumber: number, prData: PullRequestData, token: string) {
if (prData.head.ref.startsWith('release/')) {
return false
}

let logs = ''
const prLog = extractChangelog(prData.body || '', getInputPkgs())
core.info(`pr_log: ${JSON.stringify(prLog, null, 2)}`)
Object.keys(prLog).forEach((pkgName) => {
if (!prLog[pkgName].length) {
return
}
logs += `#### ${pkgName}\n`
prLog[pkgName].forEach((log) => {
logs += `- ${log}\n`
})
})
if (!logs) {
return false
}

const body = `### 📝 更新日志\n\n${logs}\n\n`
await confirmChangelog(prNumber, body, token)
return true
}

export async function confirmChangelog(prNumber: number, log: string, token: string) {
if (!log.startsWith('### 📝 更新日志')) {
return false
Expand Down
29 changes: 3 additions & 26 deletions src/github-event/pull-request-review.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { PullRequestData } from '../types'
import * as core from '@actions/core'
import * as github from '@actions/github'
import { extractChangelog, getInputPkgs, getPrCommentWhitelist, getPullRequestNumber } from '../utils'
import { confirmChangelog } from './issue-comment'
import { getPrCommentWhitelist, getPullRequestNumber } from '../utils'
import { confirmPullRequestChangelog } from './issue-comment'

export async function pull_request_review(token: string) {
if (github.context.eventName !== 'pull_request_review') {
Expand All @@ -20,27 +19,5 @@ export async function pull_request_review(token: string) {
}
const prNumber = getPullRequestNumber()
const pullRequestData = github.context.payload.pull_request as PullRequestData

const isRelease = pullRequestData.head.ref.startsWith('release/')
if (isRelease) {
return false
}
let logs = ''
const prLog = extractChangelog(pullRequestData.body || '', getInputPkgs())
core.info(`pr_log: ${JSON.stringify(prLog, null, 2)}`)
Object.keys(prLog).forEach((pkgName) => {
if (!prLog[pkgName].length) {
return
}
logs += `#### ${pkgName}\n`
prLog[pkgName].forEach((log) => {
logs += `- ${log}\n`
},
)
})
if (logs) {
const body = `### 📝 更新日志\n\n${logs}\n\n`

await confirmChangelog(prNumber, body, token)
}
return confirmPullRequestChangelog(prNumber, pullRequestData, token)
}
3 changes: 3 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ export function getPullRequestNumber() {
if (['pull_request', 'pull_request_target'].includes(github.context.eventName)) {
return Number(github.context.payload.number)
}
if (github.context.eventName === 'pull_request_review') {
return Number(github.context.payload.pull_request?.number)
}
if (github.context.eventName === 'issue_comment' && github.context.payload.issue?.pull_request) {
return Number(github.context.payload.issue.number)
}
Expand Down
17 changes: 17 additions & 0 deletions test/get-pull-request-number.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it, vi } from 'vitest'
import { getPullRequestNumber } from '../src/utils/common'

const mocks = vi.hoisted(() => ({
context: {
eventName: 'pull_request_review',
payload: { pull_request: { number: 31 } },
},
}))

vi.mock('@actions/github', () => ({ context: mocks.context }))

describe('getPullRequestNumber', () => {
it('reads the pull request number from a review event', () => {
expect(getPullRequestNumber()).toBe(31)
})
})
159 changes: 159 additions & 0 deletions test/issue-comment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { PullRequestData } from '../src/types'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { issue_comment } from '../src/github-event/issue-comment'

interface IssueCommentPayload {
action: string
changes?: { body: { from: string } }
comment: { body: string }
issue: { number: number, pull_request?: Record<string, never> }
}

const mocks = vi.hoisted(() => ({
addRemote: vi.fn(),
checkoutBranch: vi.fn(),
checkoutPr: vi.fn(),
cloneRepo: vi.fn(),
context: {
actor: 'maintainer',
eventName: 'issue_comment',
payload: {
action: 'created',
comment: { body: '/changelog' },
issue: { number: 42, pull_request: {} },
} as IssueCommentPayload,
repo: { owner: 'owner', repo: 'repo' },
},
exec: vi.fn(),
extractChangelog: vi.fn(),
getPrCommentWhitelist: vi.fn(),
getPullRequestData: vi.fn(),
isNeedCommit: vi.fn(),
stashPackageChangelog: vi.fn(),
}))

vi.mock('@actions/core', () => ({ info: vi.fn() }))
vi.mock('@actions/exec', () => ({ exec: mocks.exec }))
vi.mock('@actions/github', () => ({ context: mocks.context }))
vi.mock('../src/utils/common', () => ({
checkIsForkPr: () => false,
extractChangelog: mocks.extractChangelog,
extractReleaseLog: vi.fn(),
getConfiguredPackages: () => [],
getInputPkgs: () => ['pkg-a'],
getPrCommentWhitelist: mocks.getPrCommentWhitelist,
getPullRequestNumber: () => 42,
getPullRequestReleaseDirs: vi.fn(),
stashPackageChangelog: mocks.stashPackageChangelog,
}))
vi.mock('../src/utils/git', () => ({
default: () => ({
addRemote: mocks.addRemote,
checkoutBranch: mocks.checkoutBranch,
checkoutPr: mocks.checkoutPr,
cloneRepo: mocks.cloneRepo,
isNeedCommit: mocks.isNeedCommit,
}),
}))
vi.mock('../src/utils/github', () => ({
default: () => ({ getPullRequestData: mocks.getPullRequestData }),
}))

const prData = {
body: '### 📝 更新日志\n\n#### pkg-a\n- feat(Button): add loading state',
head: {
ref: 'feat/loading',
repo: { clone_url: 'https://github.com/owner/repo.git' },
user: { login: 'owner' },
},
number: 42,
} as unknown as PullRequestData

describe('issue_comment', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.context.actor = 'maintainer'
mocks.context.eventName = 'issue_comment'
mocks.context.payload = {
action: 'created',
comment: { body: '/changelog' },
issue: { number: 42, pull_request: {} },
}
mocks.getPrCommentWhitelist.mockResolvedValue(['maintainer'])
mocks.getPullRequestData.mockResolvedValue(prData)
mocks.extractChangelog.mockReturnValue({ 'pkg-a': ['feat(Button): add loading state'] })
mocks.isNeedCommit.mockResolvedValue(false)
})

it('submits the PR body changelog when /changelog is created', async () => {
mocks.context.payload.comment.body = ' /changelog\n'

await expect(issue_comment('token')).resolves.toBe(true)

expect(mocks.getPullRequestData).toHaveBeenCalledWith(42)
expect(mocks.extractChangelog).toHaveBeenNthCalledWith(1, prData.body, ['pkg-a'])
expect(mocks.extractChangelog).toHaveBeenNthCalledWith(
2,
'### 📝 更新日志\n\n#### pkg-a\n- feat(Button): add loading state\n\n\n',
['pkg-a'],
)
expect(mocks.stashPackageChangelog).toHaveBeenCalledWith(
prData,
[],
{ 'pkg-a': ['feat(Button): add loading state'] },
)
expect(mocks.cloneRepo).toHaveBeenCalledOnce()
})

it('ignores unrelated created comments before loading the whitelist', async () => {
mocks.context.payload.comment.body = '/approve'

await expect(issue_comment('token')).resolves.toBe(false)

expect(mocks.getPrCommentWhitelist).not.toHaveBeenCalled()
expect(mocks.getPullRequestData).not.toHaveBeenCalled()
})

it('ignores comments on issues', async () => {
mocks.context.payload.issue = { number: 42 }

await expect(issue_comment('token')).resolves.toBe(false)

expect(mocks.getPrCommentWhitelist).not.toHaveBeenCalled()
})

it('requires a whitelisted actor for /changelog', async () => {
mocks.context.actor = 'contributor'

await expect(issue_comment('token')).resolves.toBe(false)

expect(mocks.getPullRequestData).not.toHaveBeenCalled()
expect(mocks.cloneRepo).not.toHaveBeenCalled()
})

it('does not submit changelogs for release pull requests', async () => {
mocks.getPullRequestData.mockResolvedValue({
...prData,
head: { ...prData.head, ref: 'release/1.0.0' },
})

await expect(issue_comment('token')).resolves.toBe(false)

expect(mocks.extractChangelog).not.toHaveBeenCalled()
expect(mocks.cloneRepo).not.toHaveBeenCalled()
})

it('keeps processing edited changelog confirmations', async () => {
mocks.context.payload = {
action: 'edited',
changes: { body: { from: 'old body' } },
comment: { body: '### 📝 更新日志\n\n#### pkg-a\n- feat(Button): add loading state' },
issue: { number: 42, pull_request: {} },
}

await issue_comment('token')

expect(mocks.cloneRepo).toHaveBeenCalledOnce()
expect(mocks.stashPackageChangelog).toHaveBeenCalledOnce()
})
})