diff --git a/lib/settings.js b/lib/settings.js index fd7ca2693..18f92d21a 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -203,6 +203,19 @@ class Settings { }) }) + // A nop run triggered outside the webhook flow (e.g. the full-sync entrypoint) + // can be missing the check run and repository webhook fields in its payload. + // If either is absent there is no check run to report to, so put the plan in + // the log instead. The full diff can contain config values (e.g. Actions + // variables), so it goes to debug; info gets a summary without them. + if (!payload?.check_run || !payload?.repository) { + // Passed as an object so the logger only serializes it when debug is emitted + this.log.debug({ results: this.results }, 'Dry-run results') + const summary = this.results.map(res => `${res.type} ${res.plugin} ${res.repo}: ${res.action?.msg ?? ''}`).join('\n') + this.log.info(`Dry-run finished with ${this.results.length} planned change(s); the full diff is logged at debug level.\n${summary}`) + return + } + let error = false // Different logic const stats = { diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index e102379a6..f47270b82 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -549,4 +549,61 @@ repository: expect(mockRepoSync).toHaveBeenCalledTimes(1) }) }) // updateRepos - archived repo skipping + + describe('handleResults', () => { + let settings + + beforeEach(() => { + stubContext.octokit.rest.checks = { update: jest.fn().mockResolvedValue({}) } + stubContext.octokit.rest.issues = { createComment: jest.fn().mockResolvedValue({}) } + settings = new Settings(true, stubContext, mockRepo, {}, mockRef) + settings.results = [ + { + type: 'INFO', + plugin: 'Branches', + repo: 'test/test-repo', + endpoint: '', + body: '', + action: { msg: 'Changes found', additions: {}, modifications: { MY_VAR: { value: 'plain-value' } }, deletions: {} } + } + ] + }) + + describe('in nop mode without a check run in the payload (full sync)', () => { + it('logs a summary instead of updating a check run, keeping config values out of info', async () => { + // stubContext.payload only contains `installation`, like a full-sync context + await settings.handleResults() + + expect(stubContext.log.info).toHaveBeenCalledWith(expect.stringContaining('Changes found')) + expect(stubContext.log.info).not.toHaveBeenCalledWith(expect.stringContaining('plain-value')) + expect(stubContext.log.debug).toHaveBeenCalledWith({ results: settings.results }, 'Dry-run results') + expect(stubContext.octokit.rest.checks.update).not.toHaveBeenCalled() + }) + }) + + describe('in nop mode with a check run but no repository in the payload', () => { + it('logs the results instead of updating a check run', async () => { + stubContext.payload.check_run = { id: 42, check_suite: { pull_requests: [{ number: 1 }] } } + + await settings.handleResults() + + expect(stubContext.log.info).toHaveBeenCalledWith(expect.stringContaining('Changes found')) + expect(stubContext.octokit.rest.checks.update).not.toHaveBeenCalled() + }) + }) + + describe('in nop mode with a check run in the payload (webhook flow)', () => { + it('completes the check run', async () => { + stubContext.payload.check_run = { id: 42, check_suite: { pull_requests: [{ number: 1 }] } } + stubContext.payload.repository = { owner: { login: 'test' }, name: 'test-repo' } + + await settings.handleResults() + + expect(stubContext.octokit.rest.checks.update).toHaveBeenCalledWith(expect.objectContaining({ + check_run_id: 42, + status: 'completed' + })) + }) + }) + }) // handleResults }) // Settings Tests