diff --git a/.github/workflows/approve-preset.yml b/.github/workflows/approve-preset.yml index 73fde4ed61..1e4b1367d3 100644 --- a/.github/workflows/approve-preset.yml +++ b/.github/workflows/approve-preset.yml @@ -94,10 +94,13 @@ jobs: - name: Comment result if: always() && hashFiles('report.md') != '' - env: - GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: gh issue comment "$ISSUE_NUMBER" --body-file report.md + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GH_BOT_TOKEN }} + script: | + const fs = require('node:fs') + const {upsertIssueComment} = require('./src/issue-comment.js') + await upsertIssueComment({github, context, body: fs.readFileSync('report.md', 'utf8')}) - name: Close issue if: steps.publish.outcome == 'success' diff --git a/.github/workflows/check-preset.yml b/.github/workflows/check-preset.yml index 635f2ecd6f..bb9a65adb0 100644 --- a/.github/workflows/check-preset.yml +++ b/.github/workflows/check-preset.yml @@ -70,10 +70,13 @@ jobs: - name: Post result if: always() && hashFiles('report.md') != '' - env: - GH_TOKEN: ${{ github.token }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: gh issue comment "$ISSUE_NUMBER" --body-file report.md + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GH_BOT_TOKEN }} + script: | + const fs = require('node:fs') + const {upsertIssueComment} = require('./src/issue-comment.js') + await upsertIssueComment({github, context, body: fs.readFileSync('report.md', 'utf8')}) - name: Update issue title if: steps.validate.outcome == 'success' diff --git a/package.json b/package.json index e09e70d63c..457e4b10c3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "test": "node --test tests/*.test.js", - "lint": "node --check src/presets.js && node --check src/record.js && node --check src/migrate-database.js && node --check src/database.js && node --check src/issue.js && node --check src/build-site.js && node --check src/statistics.js && node --check src/approval-queue.js && node --check src/comment-command.js && node --check src/verify-approval.js && node --check src/workflow-queue.js && node --check gh-pages-template/assets/js/app.js", + "lint": "node --check src/presets.js && node --check src/record.js && node --check src/migrate-database.js && node --check src/database.js && node --check src/issue.js && node --check src/issue-comment.js && node --check src/build-site.js && node --check src/statistics.js && node --check src/approval-queue.js && node --check src/comment-command.js && node --check src/verify-approval.js && node --check src/workflow-queue.js && node --check gh-pages-template/assets/js/app.js", "test:ci": "node --test --experimental-test-coverage --test-reporter=junit --test-reporter=lcov --test-reporter-destination=junit.xml --test-reporter-destination=lcov.info tests/*.test.js" } } diff --git a/src/issue-comment.js b/src/issue-comment.js new file mode 100644 index 0000000000..4483474cc9 --- /dev/null +++ b/src/issue-comment.js @@ -0,0 +1,25 @@ +'use strict'; + +const MARKER = ''; +const LEGACY_REPORT = /^(?:Preset (?:request|replacement) validated for |Preset validation failed: |Approval stopped: )/; + +async function upsertIssueComment({ github, context, body }) { + const issue = { ...context.repo, issue_number: context.issue.number }; + const { data: actor } = await github.rest.users.getAuthenticated(); + const comments = await github.paginate(github.rest.issues.listComments, { ...issue, per_page: 100 }); + const existing = comments.findLast(comment => comment.user?.id === actor.id && + (comment.body?.includes(MARKER) || LEGACY_REPORT.test(comment.body || ''))); + const markedBody = body.trimEnd() + '\n\n' + MARKER; + + if (existing) { + if (existing.body !== markedBody) { + await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body: markedBody }); + } + return existing.id; + } + + const { data: created } = await github.rest.issues.createComment({ ...issue, body: markedBody }); + return created.id; +} + +module.exports = { MARKER, upsertIssueComment }; diff --git a/tests/issue-comment.test.js b/tests/issue-comment.test.js new file mode 100644 index 0000000000..8b2049c5d1 --- /dev/null +++ b/tests/issue-comment.test.js @@ -0,0 +1,69 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { MARKER, upsertIssueComment } = require('../src/issue-comment'); + +function fixture(initialComments = []) { + const comments = initialComments.map(comment => ({ ...comment })); + const actions = []; + const github = { + paginate: async (_endpoint, params) => { + assert.equal(params.issue_number, 6); + return comments; + }, + rest: { + users: { getAuthenticated: async () => ({ data: { id: 42 } }) }, + issues: { + listComments() {}, + createComment: async ({ body }) => { + actions.push('create'); + const created = { id: 100, user: { id: 42 }, body }; + comments.push(created); + return { data: created }; + }, + updateComment: async ({ comment_id, body }) => { + actions.push('update'); + const comment = comments.find(item => item.id === comment_id); + comment.body = body; + return { data: comment }; + } + } + } + }; + const context = { repo: { owner: 'LizardByte', repo: 'PresetDB' }, issue: { number: 6 } }; + return { github, context, comments, actions }; +} + +test('approval replaces the bot validation comment instead of creating a second one', async () => { + const state = fixture([{ id: 1, user: { id: 99 }, body: MARKER + '\nOther user comment' }]); + await upsertIssueComment({ ...state, body: 'Status: awaiting maintainer review\n' }); + await upsertIssueComment({ ...state, body: 'Status: approved and saved\n' }); + + assert.deepEqual(state.actions, ['create', 'update']); + assert.equal(state.comments.length, 2); + assert.equal(state.comments[0].body, MARKER + '\nOther user comment'); + assert.match(state.comments[1].body, /Status: approved and saved/); + assert.ok(state.comments[1].body.endsWith(MARKER)); +}); + +test('an older bot report is reused and an unchanged result is not posted again', async () => { + const state = fixture([{ id: 9, user: { id: 42 }, body: 'Preset request validated for GameDB game 1164.' }]); + const body = 'Preset request validated for GameDB game 1164.\n\n- Status: approved and saved\n'; + await upsertIssueComment({ ...state, body }); + await upsertIssueComment({ ...state, body }); + + assert.deepEqual(state.actions, ['update']); + assert.equal(state.comments.length, 1); + assert.ok(state.comments[0].body.endsWith(MARKER)); +}); +test('the next check after an issue edit updates the original status comment', async () => { + const state = fixture(); + await upsertIssueComment({ ...state, body: 'Preset validation failed: Invalid command\n' }); + await upsertIssueComment({ ...state, body: 'Preset request validated for GameDB game 1164.\n' }); + + assert.deepEqual(state.actions, ['create', 'update']); + assert.equal(state.comments.length, 1); + assert.match(state.comments[0].body, /Preset request validated/); + assert.doesNotMatch(state.comments[0].body, /validation failed/); +});