From 677b97476b0ebe9a071b4dd9e5624314a0aebc72 Mon Sep 17 00:00:00 2001 From: Artur Ribeiro <153724638+cx-artur-ribeiro@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:16:40 +0100 Subject: [PATCH 1/2] fix: duplicated comments from kics bot on pull requests --- src/commenter.js | 10 ++--- test/commenter.test.js | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 test/commenter.test.js diff --git a/src/commenter.js b/src/commenter.js index df7622c..83a3861 100644 --- a/src/commenter.js +++ b/src/commenter.js @@ -133,17 +133,13 @@ function computeHeaders(flattenedQueries) { async function postPRComment(results, repo, prNumber, octokit, commentWithQueries = false, excludedColumnsForCommentsWithQueries) { const message = createComment(results, commentWithQueries, excludedColumnsForCommentsWithQueries); - const {data: comments} = await octokit.rest.issues.listComments({ + const comments = await octokit.paginate(octokit.rest.issues.listComments, { ...repo, issue_number: prNumber, + per_page: 100, }); - const comment = comments.find((comment) => { - return ( - comment.user.login === "github-actions[bot]" && - comment.body.startsWith("![kics-logo](") - ); - }); + const comment = comments.find((comment) => comment.body?.startsWith("![kics-logo](")); if (comment) { await octokit.rest.issues.updateComment({ diff --git a/test/commenter.test.js b/test/commenter.test.js new file mode 100644 index 0000000..29f4976 --- /dev/null +++ b/test/commenter.test.js @@ -0,0 +1,83 @@ +const test = require('node:test') +const assert = require('node:assert') +const github = require('@actions/github') +const { postPRComment } = require('../src/commenter') + +const repo = { owner: 'o', repo: 'r' } +const results = { + kics_version: 'v0', severity_counters: {}, total_counter: 0, files_scanned: 0, files_parsed: 0, + files_failed_to_scan: 0, queries_total: 0, queries_failed_to_execute: 0, start: 0, end: 0, +} +const kicsBody = '![kics-logo](https://example.com/logo.png)\nold report' + +// Real octokit client (so paginate behaves like in the action) with a fake GitHub API behind fetch. +// Like GitHub, listing comments returns 30 per page unless per_page is set. +function fakeGitHub(comments) { + const calls = [] + const fetch = async (url, init = {}) => { + const u = new URL(url) + const method = (init.method || 'GET').toUpperCase() + calls.push(`${method} ${u.pathname}`) + let data = [] + if (method === 'GET') { + const perPage = Number(u.searchParams.get('per_page') || 30) + const page = Number(u.searchParams.get('page') || 1) + data = comments.slice((page - 1) * perPage, page * perPage) + } + const headers = { 'content-type': 'application/json' } + if (method === 'GET' && comments.length > data.length) { + const perPage = Number(u.searchParams.get('per_page') || 30) + const page = Number(u.searchParams.get('page') || 1) + if (page * perPage < comments.length) { + const next = new URL(u); next.searchParams.set('page', page + 1) + headers.link = `<${next}>; rel="next"` + } + } + return new Response(JSON.stringify(data), { status: method === 'POST' ? 201 : 200, headers }) + } + const octokit = github.getOctokit('token', { request: { fetch } }) + return { octokit, calls } +} + +const human = (i) => ({ id: i, user: { login: 'someone' }, body: `comment ${i}` }) +const run = (octokit) => postPRComment(results, repo, 1, octokit, false, []) + +test('updates the KICS comment when it is buried past the first 30 comments (bot author)', async () => { + const comments = [] + for (let i = 0; i < 40; i++) comments.push(human(i)) + comments.push({ id: 1000, user: { login: 'github-actions[bot]' }, body: kicsBody }) // newest, so on page 2 + const { octokit, calls } = fakeGitHub(comments) + await run(octokit) + assert.ok(calls.includes('PATCH /repos/o/r/issues/comments/1000'), calls.join('\n')) + assert.ok(!calls.some((c) => c.startsWith('POST')), 'must not create a duplicate') +}) + +test('updates the KICS comment posted by a non-bot author', async () => { + const { octokit, calls } = fakeGitHub([{ id: 5, user: { login: 'my-app[bot]' }, body: kicsBody }]) + await run(octokit) + assert.ok(calls.includes('PATCH /repos/o/r/issues/comments/5'), calls.join('\n')) + assert.ok(!calls.some((c) => c.startsWith('POST'))) +}) + +test('creates a comment when none exists', async () => { + const { octokit, calls } = fakeGitHub([human(1)]) + await run(octokit) + assert.ok(calls.includes('POST /repos/o/r/issues/1/comments'), calls.join('\n')) +}) + +test('does not treat a quoted KICS report as the KICS comment', async () => { + const { octokit, calls } = fakeGitHub([{ id: 7, user: { login: 'someone' }, body: '> ' + kicsBody }]) + await run(octokit) + assert.ok(calls.includes('POST /repos/o/r/issues/1/comments'), calls.join('\n')) +}) + +test('finds the KICS comment across several pages of 100+ comments', async () => { + const comments = [] + for (let i = 0; i < 250; i++) comments.push(human(i)) + comments.push({ id: 1000, user: { login: 'github-actions[bot]' }, body: kicsBody }) // on page 3 + const { octokit, calls } = fakeGitHub(comments) + await run(octokit) + assert.equal(calls.filter((c) => c.startsWith('GET')).length, 3, calls.join('\n')) + assert.ok(calls.includes('PATCH /repos/o/r/issues/comments/1000'), calls.join('\n')) + assert.ok(!calls.some((c) => c.startsWith('POST')), 'must not create a duplicate') +}) From f9593995c6e08910ec14b98218d5ab70718b2584 Mon Sep 17 00:00:00 2001 From: Artur Ribeiro <153724638+cx-artur-ribeiro@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:31:28 +0100 Subject: [PATCH 2/2] remove: test, adding in a new PR --- test/commenter.test.js | 83 ------------------------------------------ 1 file changed, 83 deletions(-) delete mode 100644 test/commenter.test.js diff --git a/test/commenter.test.js b/test/commenter.test.js deleted file mode 100644 index 29f4976..0000000 --- a/test/commenter.test.js +++ /dev/null @@ -1,83 +0,0 @@ -const test = require('node:test') -const assert = require('node:assert') -const github = require('@actions/github') -const { postPRComment } = require('../src/commenter') - -const repo = { owner: 'o', repo: 'r' } -const results = { - kics_version: 'v0', severity_counters: {}, total_counter: 0, files_scanned: 0, files_parsed: 0, - files_failed_to_scan: 0, queries_total: 0, queries_failed_to_execute: 0, start: 0, end: 0, -} -const kicsBody = '![kics-logo](https://example.com/logo.png)\nold report' - -// Real octokit client (so paginate behaves like in the action) with a fake GitHub API behind fetch. -// Like GitHub, listing comments returns 30 per page unless per_page is set. -function fakeGitHub(comments) { - const calls = [] - const fetch = async (url, init = {}) => { - const u = new URL(url) - const method = (init.method || 'GET').toUpperCase() - calls.push(`${method} ${u.pathname}`) - let data = [] - if (method === 'GET') { - const perPage = Number(u.searchParams.get('per_page') || 30) - const page = Number(u.searchParams.get('page') || 1) - data = comments.slice((page - 1) * perPage, page * perPage) - } - const headers = { 'content-type': 'application/json' } - if (method === 'GET' && comments.length > data.length) { - const perPage = Number(u.searchParams.get('per_page') || 30) - const page = Number(u.searchParams.get('page') || 1) - if (page * perPage < comments.length) { - const next = new URL(u); next.searchParams.set('page', page + 1) - headers.link = `<${next}>; rel="next"` - } - } - return new Response(JSON.stringify(data), { status: method === 'POST' ? 201 : 200, headers }) - } - const octokit = github.getOctokit('token', { request: { fetch } }) - return { octokit, calls } -} - -const human = (i) => ({ id: i, user: { login: 'someone' }, body: `comment ${i}` }) -const run = (octokit) => postPRComment(results, repo, 1, octokit, false, []) - -test('updates the KICS comment when it is buried past the first 30 comments (bot author)', async () => { - const comments = [] - for (let i = 0; i < 40; i++) comments.push(human(i)) - comments.push({ id: 1000, user: { login: 'github-actions[bot]' }, body: kicsBody }) // newest, so on page 2 - const { octokit, calls } = fakeGitHub(comments) - await run(octokit) - assert.ok(calls.includes('PATCH /repos/o/r/issues/comments/1000'), calls.join('\n')) - assert.ok(!calls.some((c) => c.startsWith('POST')), 'must not create a duplicate') -}) - -test('updates the KICS comment posted by a non-bot author', async () => { - const { octokit, calls } = fakeGitHub([{ id: 5, user: { login: 'my-app[bot]' }, body: kicsBody }]) - await run(octokit) - assert.ok(calls.includes('PATCH /repos/o/r/issues/comments/5'), calls.join('\n')) - assert.ok(!calls.some((c) => c.startsWith('POST'))) -}) - -test('creates a comment when none exists', async () => { - const { octokit, calls } = fakeGitHub([human(1)]) - await run(octokit) - assert.ok(calls.includes('POST /repos/o/r/issues/1/comments'), calls.join('\n')) -}) - -test('does not treat a quoted KICS report as the KICS comment', async () => { - const { octokit, calls } = fakeGitHub([{ id: 7, user: { login: 'someone' }, body: '> ' + kicsBody }]) - await run(octokit) - assert.ok(calls.includes('POST /repos/o/r/issues/1/comments'), calls.join('\n')) -}) - -test('finds the KICS comment across several pages of 100+ comments', async () => { - const comments = [] - for (let i = 0; i < 250; i++) comments.push(human(i)) - comments.push({ id: 1000, user: { login: 'github-actions[bot]' }, body: kicsBody }) // on page 3 - const { octokit, calls } = fakeGitHub(comments) - await run(octokit) - assert.equal(calls.filter((c) => c.startsWith('GET')).length, 3, calls.join('\n')) - assert.ok(calls.includes('PATCH /repos/o/r/issues/comments/1000'), calls.join('\n')) - assert.ok(!calls.some((c) => c.startsWith('POST')), 'must not create a duplicate') -})