From 19d4c1dfe344bd1bfd0344d1eb2b8f6a307a474d Mon Sep 17 00:00:00 2001 From: Jeffrey Sica Date: Wed, 7 Oct 2026 19:56:43 -0500 Subject: [PATCH] fix: treat an issue comment without a body as having no commands GitHub allows an empty issue comment, whose `comment.body` arrives as null. handleIssueComment passed it straight to hasCommand, whose line splitting calls `body.replace(...)`, so the run failed with `TypeError: Cannot read properties of null (reading 'replace')`. A missing or null body is now read as the empty string: no command matches, nothing is called and the run succeeds. The handlers only run after hasCommand has matched, and /meow, the approve plugin and the comment scanners already guard their bodies, so the dispatcher is the only place the crash was reachable. Signed-off-by: Jeffrey Sica --- .../commentWithoutBody.test.ts | 51 +++++++++++++++++++ dist/index.js | 3 +- src/issueComment/handleIssueComment.ts | 3 +- 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 __tests__/issueCommentTest/commentWithoutBody.test.ts diff --git a/__tests__/issueCommentTest/commentWithoutBody.test.ts b/__tests__/issueCommentTest/commentWithoutBody.test.ts new file mode 100644 index 0000000..90f6fb8 --- /dev/null +++ b/__tests__/issueCommentTest/commentWithoutBody.test.ts @@ -0,0 +1,51 @@ +import * as core from '@actions/core' +import { setupServer } from 'msw/node' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +import { handleIssueComment } from '../../src/issueComment/handleIssueComment' +import issueCommentEvent from '../fixtures/issues/issueCommentEvent.json' +import * as utils from '../testUtils' + +const server = setupServer() +let calls: string[] +beforeAll(() => server.listen(utils.failOnUnhandledRequest)) +beforeEach(() => { + utils.setupActionsEnv('/assign /approve /lgtm /hold /kind /help /meow /retest /check-required-labels') + calls = [] + server.events.on('request:start', ({ request }) => { + calls.push(`${request.method} ${request.url}`) + }) +}) +afterEach(() => { + server.resetHandlers() + server.events.removeAllListeners() +}) +afterAll(() => server.close()) + +function withComment(comment: Record | undefined) { + const payload = structuredClone(issueCommentEvent) as Record + if (comment === undefined) + delete payload.comment + else + payload.comment = comment + return new utils.MockContext(payload) +} + +const { body: _body, ...commentWithoutBody } = issueCommentEvent.comment + +describe('an issue comment without a body', () => { + it.each([ + ['a null body', () => withComment({ ...commentWithoutBody, body: null })], + ['no body field', () => withComment(commentWithoutBody)], + ['no comment at all', () => withComment(undefined)], + ])('runs no command and succeeds with %s', async (_name, contextFor) => { + const setFailed = vi.spyOn(core, 'setFailed').mockImplementation(() => {}) + const fetchSpy = vi.spyOn(globalThis, 'fetch') + + await expect(handleIssueComment(contextFor())).resolves.toBeUndefined() + + expect(setFailed).not.toHaveBeenCalled() + expect(fetchSpy).not.toHaveBeenCalled() + expect(calls).toEqual([]) + }) +}) diff --git a/dist/index.js b/dist/index.js index 574f8c0..3022ed8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -46606,7 +46606,8 @@ async function handleIssueComment(context = github_context) { .split(/\s+/) .filter(command => command !== '') .map(command => canonicalCommand(command.toLowerCase())))]; - const commentBody = context.payload.comment?.body; + // GitHub allows an empty comment, whose body arrives as null + const commentBody = context.payload.comment?.body ?? ''; if (commandConfig.length === 0) { setFailed(`please provide a list of space delimited commands / jobs to run. None found`); return; diff --git a/src/issueComment/handleIssueComment.ts b/src/issueComment/handleIssueComment.ts index 4f55833..af0d541 100644 --- a/src/issueComment/handleIssueComment.ts +++ b/src/issueComment/handleIssueComment.ts @@ -113,7 +113,8 @@ export async function handleIssueComment(context: Context = github.context): Pro .filter(command => command !== '') .map(command => canonicalCommand(command.toLowerCase())), )] - const commentBody: string = context.payload.comment?.body + // GitHub allows an empty comment, whose body arrives as null + const commentBody: string = context.payload.comment?.body ?? '' if (commandConfig.length === 0) { core.setFailed(