diff --git a/lib/logger/__mocks__/index.ts b/lib/logger/__mocks__/index.ts index 1448ced57b093d19db13bd71dce1212760768628..637c95b8cd85e57bcd1dbfe67842454a7594b50c 100644 --- a/lib/logger/__mocks__/index.ts +++ b/lib/logger/__mocks__/index.ts @@ -20,6 +20,6 @@ export const addMeta = jest.fn(); export const removeMeta = jest.fn(); export const levels = jest.fn(); export const addStream = jest.fn(); -export const getErrors = (): any[] => []; +export const getProblems = jest.fn((): any[] => []); export { logger }; diff --git a/lib/logger/__snapshots__/index.spec.ts.snap b/lib/logger/__snapshots__/index.spec.ts.snap index 2390af8dcaae0cafc1a92a5a9e01ef32b54b515e..281280bb59c76a86596da2dd20df84edaa6884d7 100644 --- a/lib/logger/__snapshots__/index.spec.ts.snap +++ b/lib/logger/__snapshots__/index.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`logger saves errors 1`] = ` +exports[`logger saves problems 1`] = ` Array [ Object { "any": "test", @@ -15,6 +15,7 @@ Array [ "logContext": "abc123", "msg": "", "name": "renovate", + "password": "***********", "some": "meta", }, Object { @@ -25,5 +26,12 @@ Array [ "name": "renovate", "some": "meta", }, + Object { + "any": "test", + "level": 40, + "logContext": "abc123", + "msg": "a warning with a **redacted**", + "name": "renovate", + }, ] `; diff --git a/lib/logger/index.spec.ts b/lib/logger/index.spec.ts index cc2c029d5d19dd2dfdf9b4ad8ca5515ff227c94f..79f599a06199b2fde59ec7b80e2bdb2132221e15 100644 --- a/lib/logger/index.spec.ts +++ b/lib/logger/index.spec.ts @@ -1,11 +1,12 @@ import _fs from 'fs-extra'; import { add } from '../util/host-rules'; +import { add as addSecret } from '../util/sanitize'; import { addMeta, addStream, - clearErrors, + clearProblems, getContext, - getErrors, + getProblems, levels, logger, removeMeta, @@ -52,14 +53,17 @@ describe('logger', () => { expect(() => levels('stdout', 'debug')).not.toThrow(); }); - it('saves errors', () => { + it('saves problems', () => { + addSecret('p4$$w0rd'); levels('stdout', 'fatal'); logger.error('some meta'); - logger.error({ some: 'meta' }); + logger.error({ some: 'meta', password: 'super secret' }); logger.error({ some: 'meta' }, 'message'); - expect(getErrors()).toMatchSnapshot(); - clearErrors(); - expect(getErrors()).toHaveLength(0); + logger.warn('a warning with a p4$$w0rd'); + logger.info('ignored'); + expect(getProblems()).toMatchSnapshot(); + clearProblems(); + expect(getProblems()).toHaveLength(0); }); it('should contain path or stream parameters', () => { diff --git a/lib/logger/index.ts b/lib/logger/index.ts index 9ebba02304fa9255c070a1781a9bdac451e80cfe..9e06d13b33ccdf12de9bcfc8f8dd8f760c8aa487 100644 --- a/lib/logger/index.ts +++ b/lib/logger/index.ts @@ -6,7 +6,7 @@ import cmdSerializer from './cmd-serializer'; import configSerializer from './config-serializer'; import errSerializer from './err-serializer'; import { RenovateStream } from './pretty-stdout'; -import { ErrorStream, withSanitizer } from './utils'; +import { BunyanRecord, ProblemStream, withSanitizer } from './utils'; let logContext: string = process.env.LOG_CONTEXT || shortid.generate(); let curMeta = {}; @@ -17,7 +17,7 @@ export interface LogError { msg?: string; } -const errors = new ErrorStream(); +const problems = new ProblemStream(); const stdout: bunyan.Stream = { name: 'stdout', @@ -49,9 +49,9 @@ const bunyanLogger = bunyan.createLogger({ streams: [ stdout, { - name: 'error', - level: 'error' as bunyan.LogLevel, - stream: errors as any, + name: 'problems', + level: 'warn' as bunyan.LogLevel, + stream: problems as any, type: 'raw', }, ].map(withSanitizer), @@ -139,10 +139,10 @@ export function levels(name: string, level: bunyan.LogLevel): void { bunyanLogger.levels(name, level); } -export function getErrors(): any { - return errors.getErrors(); +export function getProblems(): BunyanRecord[] { + return problems.getProblems(); } -export function clearErrors(): void { - return errors.clearErrors(); +export function clearProblems(): void { + return problems.clearProblems(); } diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts index d2208aba0cf4e1689f8a2a29b5b0abd7cfe4f4bb..4cf60954eb3502dc5debcc059c3359ff69225ef7 100644 --- a/lib/logger/utils.ts +++ b/lib/logger/utils.ts @@ -11,8 +11,8 @@ export interface BunyanRecord extends Record<string, any> { const excludeProps = ['pid', 'time', 'v', 'hostname']; -export class ErrorStream extends Stream { - private _errors: BunyanRecord[] = []; +export class ProblemStream extends Stream { + private _problems: BunyanRecord[] = []; readable: boolean; @@ -25,20 +25,20 @@ export class ErrorStream extends Stream { } write(data: BunyanRecord): boolean { - const err = { ...data }; + const problem = { ...data }; for (const prop of excludeProps) { - delete err[prop]; + delete problem[prop]; } - this._errors.push(err); + this._problems.push(problem); return true; } - getErrors(): BunyanRecord[] { - return this._errors; + getProblems(): BunyanRecord[] { + return this._problems; } - clearErrors(): void { - this._errors = []; + clearProblems(): void { + this._problems = []; } } const templateFields = ['prBody']; diff --git a/lib/workers/global/index.spec.ts b/lib/workers/global/index.spec.ts index b79eef90536cccf64c73cd307a2164d7f7da7322..f48ca89c306305cf912f4612a6f48fc90cf582b6 100644 --- a/lib/workers/global/index.spec.ts +++ b/lib/workers/global/index.spec.ts @@ -1,3 +1,5 @@ +import { ERROR, WARN } from 'bunyan'; +import { logger } from '../../../test/util'; import * as _configParser from '../../config'; import { PLATFORM_TYPE_GITHUB, @@ -20,6 +22,7 @@ const limits = _limits; describe('lib/workers/global', () => { beforeEach(() => { jest.resetAllMocks(); + logger.getProblems.mockImplementationOnce(() => []); configParser.parseConfigs = jest.fn(); platform.initPlatform.mockImplementation((input) => Promise.resolve(input)); }); @@ -77,7 +80,36 @@ describe('lib/workers/global', () => { expect(configParser.parseConfigs).toHaveBeenCalledTimes(1); expect(repositoryWorker.renovateRepository).toHaveBeenCalledTimes(0); }); - + it('exits with non-zero when errors are logged', async () => { + configParser.parseConfigs.mockResolvedValueOnce({ + baseDir: '/tmp/base', + cacheDir: '/tmp/cache', + repositories: [], + }); + logger.getProblems.mockReset(); + logger.getProblems.mockImplementationOnce(() => [ + { + level: ERROR, + msg: 'meh', + }, + ]); + await expect(globalWorker.start()).resolves.not.toEqual(0); + }); + it('exits with zero when warnings are logged', async () => { + configParser.parseConfigs.mockResolvedValueOnce({ + baseDir: '/tmp/base', + cacheDir: '/tmp/cache', + repositories: [], + }); + logger.getProblems.mockReset(); + logger.getProblems.mockImplementationOnce(() => [ + { + level: WARN, + msg: 'meh', + }, + ]); + await expect(globalWorker.start()).resolves.toEqual(0); + }); describe('processes platforms', () => { it('github', async () => { configParser.parseConfigs.mockResolvedValueOnce({ diff --git a/lib/workers/global/index.ts b/lib/workers/global/index.ts index 6e353acf06fd8a89914b3ccc9c6ab87ec7790bcd..9a4260469c3d9343d636bc35795ffa1d257d2e92 100644 --- a/lib/workers/global/index.ts +++ b/lib/workers/global/index.ts @@ -1,8 +1,9 @@ import path from 'path'; import is from '@sindresorhus/is'; +import { ERROR } from 'bunyan'; import fs from 'fs-extra'; import * as configParser from '../../config'; -import { getErrors, logger, setMeta } from '../../logger'; +import { getProblems, logger, setMeta } from '../../logger'; import { setUtilConfig } from '../../util'; import * as hostRules from '../../util/host-rules'; import * as repositoryWorker from '../repository'; @@ -76,8 +77,7 @@ export async function start(): Promise<0 | 1> { globalFinalize(config); logger.debug(`Renovate exiting`); } - const loggerErrors = getErrors(); - /* istanbul ignore if */ + const loggerErrors = getProblems().filter((p) => p.level >= ERROR); if (loggerErrors.length) { logger.info( { loggerErrors }, diff --git a/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap b/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap index 41a7139fd43448f24f32d3ae22eb7775fd1559c9..735ce9523c3a6ecbe21354bc4e9927f6a5b0c0ae 100644 --- a/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap +++ b/lib/workers/repository/__snapshots__/dependency-dashboard.spec.ts.snap @@ -1,5 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`workers/repository/master-issue ensureMasterIssue() contains logged problems 1`] = ` +"This issue contains a list of Renovate updates and their statuses. + +## Repository problems + +These problems occurred while renovating this repository. + + - ERROR: everything is broken + - WARN: just a bit + - ERROR: i am a duplicated problem + - ERROR: i am a non-duplicated problem + - WARN: i am a non-duplicated problem + +## Pending Status Checks + +These updates await pending status checks. To force their creation now, check the box below. + + - [ ] <!-- approvePr-branch=branchName1 -->pr1 + +" +`; + exports[`workers/repository/master-issue ensureMasterIssue() open or update Dependency Dashboard when all branches are closed and dependencyDashboardAutoclose is false 1`] = ` "This issue contains a list of Renovate updates and their statuses. diff --git a/lib/workers/repository/dependency-dashboard.spec.ts b/lib/workers/repository/dependency-dashboard.spec.ts index 08d2045db69e3df0cd378b5570371feb9a2ad545..b71cd8922d592d9df7764476017abbbc99f37eef 100644 --- a/lib/workers/repository/dependency-dashboard.spec.ts +++ b/lib/workers/repository/dependency-dashboard.spec.ts @@ -1,6 +1,12 @@ import fs from 'fs'; +import { ERROR, WARN } from 'bunyan'; import { mock } from 'jest-mock-extended'; -import { RenovateConfig, getConfig, platform } from '../../../test/util'; +import { + RenovateConfig, + getConfig, + logger, + platform, +} from '../../../test/util'; import { PLATFORM_TYPE_GITHUB } from '../../constants/platforms'; import { Platform, Pr } from '../../platform'; import { PrState } from '../../types'; @@ -15,7 +21,7 @@ type PrUpgrade = BranchUpgradeConfig; let config: RenovateConfig; beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); config = getConfig(); config.platform = PLATFORM_TYPE_GITHUB; config.errors = []; @@ -31,7 +37,7 @@ async function dryRun( getBranchPrCalls = 0, findPrCalls = 0 ) { - jest.resetAllMocks(); + jest.clearAllMocks(); config.dryRun = true; await dependencyDashboard.ensureMasterIssue(config, branches); expect(platform.ensureIssueClosing).toHaveBeenCalledTimes( @@ -423,5 +429,54 @@ describe('workers/repository/master-issue', () => { // same with dry run await dryRun(branches, platform); }); + + it('contains logged problems', async () => { + const branches: BranchConfig[] = [ + { + ...mock<BranchConfig>(), + prTitle: 'pr1', + upgrades: [ + { ...mock<PrUpgrade>(), depName: 'dep1', repository: 'repo1' }, + ], + res: ProcessBranchResult.Pending, + branchName: 'branchName1', + }, + ]; + logger.getProblems.mockReturnValueOnce([ + { + level: ERROR, + msg: 'everything is broken', + }, + { + level: WARN, + msg: 'just a bit', + }, + { + level: ERROR, + msg: 'i am a duplicated problem', + }, + { + level: ERROR, + msg: 'i am a duplicated problem', + }, + { + level: ERROR, + msg: 'i am a non-duplicated problem', + }, + { + level: WARN, + msg: 'i am a non-duplicated problem', + }, + { + level: WARN, + msg: 'i am an artifact error', + artifactErrors: {}, + }, + ]); + config.dependencyDashboard = true; + await dependencyDashboard.ensureMasterIssue(config, branches); + expect(platform.ensureIssue).toHaveBeenCalledTimes(1); + expect(platform.ensureIssue.mock.calls[0][0].body).toMatchSnapshot(); + }); }); }); diff --git a/lib/workers/repository/dependency-dashboard.ts b/lib/workers/repository/dependency-dashboard.ts index 520ea837ee8444b13c208ddba85a9e0c71a83c5d..17769450bb5699aebd28c5f2877e25304ca055c5 100644 --- a/lib/workers/repository/dependency-dashboard.ts +++ b/lib/workers/repository/dependency-dashboard.ts @@ -1,6 +1,7 @@ import is from '@sindresorhus/is'; +import { nameFromLevel } from 'bunyan'; import { RenovateConfig } from '../../config'; -import { logger } from '../../logger'; +import { getProblems, logger } from '../../logger'; import { Pr, platform } from '../../platform'; import { PrState } from '../../types'; import { BranchConfig, ProcessBranchResult } from '../common'; @@ -22,6 +23,31 @@ function getListItem(branch: BranchConfig, type: string, pr?: Pr): string { return item + ' (' + uniquePackages.join(', ') + ')\n'; } +function appendRepoProblems(config: RenovateConfig, issueBody: string): string { + let newIssueBody = issueBody; + const repoProblems = new Set( + getProblems() + .filter( + (problem) => + problem.repository === config.repository && !problem.artifactErrors + ) + .map( + (problem) => + `${nameFromLevel[problem.level].toUpperCase()}: ${problem.msg}` + ) + ); + if (repoProblems.size) { + newIssueBody += '## Repository problems\n\n'; + newIssueBody += + 'These problems occurred while renovating this repository.\n\n'; + for (const repoProblem of repoProblems) { + newIssueBody += ` - ${repoProblem}\n`; + } + newIssueBody += '\n'; + } + return newIssueBody; +} + export async function ensureMasterIssue( config: RenovateConfig, branches: BranchConfig[] @@ -60,6 +86,9 @@ export async function ensureMasterIssue( if (config.dependencyDashboardHeader?.length) { issueBody += `${config.dependencyDashboardHeader}\n\n`; } + + issueBody = appendRepoProblems(config, issueBody); + const pendingApprovals = branches.filter( (branch) => branch.res === ProcessBranchResult.NeedsApproval ); diff --git a/test/util.ts b/test/util.ts index 4dd6fe791c3aefd6486ce25443037de4df8903c0..844a7e74e2ce93d61251d5b26f8352a2531e60d5 100644 --- a/test/util.ts +++ b/test/util.ts @@ -2,6 +2,7 @@ import crypto from 'crypto'; import { expect } from '@jest/globals'; import { RenovateConfig as _RenovateConfig } from '../lib/config'; import { getConfig } from '../lib/config/defaults'; +import * as _logger from '../lib/logger'; import { platform as _platform } from '../lib/platform'; import * as _env from '../lib/util/exec/env'; import * as _fs from '../lib/util/fs'; @@ -29,6 +30,7 @@ export const git = mocked(_git); export const platform = mocked(_platform); export const env = mocked(_env); export const hostRules = mocked(_hostRules); +export const logger = mocked(_logger); // Required because of isolatedModules export type RenovateConfig = _RenovateConfig;