Skip to content

Commit

Permalink
feat: mode=silent (#28396)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins authored May 6, 2024
1 parent abc61d6 commit 654c447
Show file tree
Hide file tree
Showing 19 changed files with 197 additions and 15 deletions.
18 changes: 18 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2323,6 +2323,24 @@ This works because Renovate will add a "renovate/stability-days" pending status

Add to this object if you wish to define rules that apply only to minor updates.

## mode

This configuration option was created primarily for use with Mend's hosted app, but can also be useful for some self-hosted use cases.

It enables a new `silent` mode to allow repos to be scanned for updates _and_ for users to be able to request such updates be opened in PRs _on demand_ through the Mend UI, without needing the Dependency Dashboard issue in the repo.

Although similar, the options `mode=silent` and `dryRun` can be used together.
When both are configured, `dryRun` takes precedence, so for example PRs won't be created.

Configuring `silent` mode is quite similar to `dryRun=lookup` except:

- It will bypass onboarding checks (unlike when performing a dry run on a non-onboarded repo) similar to `requireConfig=optional`
- It can create branches/PRs if `checkedBranches` is set
- It will keep any existing branches up-to-date (e.g. ones created previously using `checkedBranches`)

When in `silent` mode, Renovate does not create issues (such as Dependency Dashboard, or due to config errors) or Config Migration PRs, even if enabled.
It also does not prune/close any which already exist.

## npmToken

See [Private npm module support](./getting-started/private-packages.md) for details on how this is used.
Expand Down
7 changes: 7 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { getVersioningList } from '../../modules/versioning';
import type { RenovateOptions } from '../types';

const options: RenovateOptions[] = [
{
name: 'mode',
description: 'Mode of operation.',
type: 'string',
default: 'full',
allowedValues: ['full', 'silent'],
},
{
name: 'allowedHeaders',
description:
Expand Down
7 changes: 7 additions & 0 deletions lib/workers/repository/config-migration/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ describe('workers/repository/config-migration/index', () => {
});
});

it('does nothing when in silent mode', async () => {
await configMigration({ ...config, mode: 'silent' }, []);
expect(MigratedDataFactory.getAsync).toHaveBeenCalledTimes(0);
expect(checkConfigMigrationBranch).toHaveBeenCalledTimes(0);
expect(ensureConfigMigrationPr).toHaveBeenCalledTimes(0);
});

it('does nothing when config migration is disabled', async () => {
await configMigration({ ...config, configMigration: false }, []);
expect(MigratedDataFactory.getAsync).toHaveBeenCalledTimes(0);
Expand Down
7 changes: 7 additions & 0 deletions lib/workers/repository/config-migration/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RenovateConfig } from '../../../config/types';
import { logger } from '../../../logger';
import { checkConfigMigrationBranch } from './branch';
import { MigratedDataFactory } from './branch/migrated-data';
import { ensureConfigMigrationPr } from './pr';
Expand All @@ -8,6 +9,12 @@ export async function configMigration(
branchList: string[],
): Promise<void> {
if (config.configMigration) {
if (config.mode === 'silent') {
logger.debug(
'Config migration issues are not created, updated or closed when mode=silent',
);
return;
}
const migratedConfigData = await MigratedDataFactory.getAsync();
const migrationBranch = await checkConfigMigrationBranch(
config,
Expand Down
11 changes: 11 additions & 0 deletions lib/workers/repository/dependency-dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ describe('workers/repository/dependency-dashboard', () => {
logger.getProblems.mockReturnValue([]);
});

it('does nothing if mode=silent', async () => {
const branches: BranchConfig[] = [];
config.mode = 'silent';
await dependencyDashboard.ensureDependencyDashboard(config, branches);
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
expect(platform.ensureIssue).toHaveBeenCalledTimes(0);

// same with dry run
await dryRun(branches, platform, 0, 0);
});

it('do nothing if dependencyDashboard is disabled', async () => {
const branches: BranchConfig[] = [];
await dependencyDashboard.ensureDependencyDashboard(config, branches);
Expand Down
6 changes: 6 additions & 0 deletions lib/workers/repository/dependency-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ export async function ensureDependencyDashboard(
packageFiles: Record<string, PackageFile[]> = {},
): Promise<void> {
logger.debug('ensureDependencyDashboard()');
if (config.mode === 'silent') {
logger.debug(
'Dependency Dashboard issue is not created, updated or closed when mode=silent',
);
return;
}
// legacy/migrated issue
const reuseTitle = 'Update Dependencies (Renovate Bot)';
const branches = allBranches.filter(
Expand Down
14 changes: 14 additions & 0 deletions lib/workers/repository/error-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ describe('workers/repository/error-config', () => {
GlobalConfig.reset();
});

it('returns if mode is silent', async () => {
config.mode = 'silent';

const res = await raiseConfigWarningIssue(
config,
new Error(CONFIG_VALIDATION),
);

expect(res).toBeUndefined();
expect(logger.debug).toHaveBeenCalledWith(
'Config warning issues are not created, updated or closed when mode=silent',
);
});

it('creates issues', async () => {
const expectedBody = `There are missing credentials for the authentication-required feature. As a precaution, Renovate will pause PRs until it is resolved.
Expand Down
6 changes: 6 additions & 0 deletions lib/workers/repository/error-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ async function raiseWarningIssue(
initialBody: string,
error: Error,
): Promise<void> {
if (config.mode === 'silent') {
logger.debug(
`Config warning issues are not created, updated or closed when mode=silent`,
);
return;
}
let body = initialBody;
if (error.validationSource) {
body += `Location: \`${error.validationSource}\`\n`;
Expand Down
2 changes: 1 addition & 1 deletion lib/workers/repository/init/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('workers/repository/init/index', () => {
it('runs', async () => {
apis.initApis.mockResolvedValue(partial<_apis.WorkerPlatformConfig>());
onboarding.checkOnboardingBranch.mockResolvedValueOnce({});
config.getRepoConfig.mockResolvedValueOnce({});
config.getRepoConfig.mockResolvedValueOnce({ mode: 'silent' });
merge.mergeRenovateConfig.mockResolvedValueOnce({});
secrets.applySecretsToConfig.mockReturnValueOnce(
partial<RenovateConfig>(),
Expand Down
5 changes: 5 additions & 0 deletions lib/workers/repository/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export async function initRepo(
await initializeCaches(config as WorkerPlatformConfig);
config = await getRepoConfig(config);
setRepositoryLogLevelRemaps(config.logLevelRemap);
if (config.mode === 'silent') {
logger.info(
'Repository is running with mode=silent and will not make Issues or PRs by default',
);
}
checkIfConfigured(config);
warnOnUnsupportedOptions(config);
config = applySecretsToConfig(config);
Expand Down
12 changes: 12 additions & 0 deletions lib/workers/repository/init/merge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ describe('workers/repository/init/merge', () => {
});
});

it('uses onboarding config if silent', async () => {
scm.getFileList.mockResolvedValue([]);
migrateAndValidate.migrateAndValidate.mockResolvedValue({
warnings: [],
errors: [],
});
config.mode = 'silent';
config.repository = 'some-org/some-repo';
const res = await mergeRenovateConfig(config);
expect(res).toBeDefined();
});

it('throws error if misconfigured', async () => {
scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']);
fs.readLocalFile.mockResolvedValue('{}');
Expand Down
15 changes: 14 additions & 1 deletion lib/workers/repository/init/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { readLocalFile } from '../../../util/fs';
import * as hostRules from '../../../util/host-rules';
import * as queue from '../../../util/http/queue';
import * as throttle from '../../../util/http/throttle';
import { getOnboardingConfig } from '../onboarding/branch/config';
import { getDefaultConfigFileName } from '../onboarding/branch/create';
import {
getOnboardingConfigFromCache,
getOnboardingFileNameFromCache,
Expand Down Expand Up @@ -58,7 +60,7 @@ export async function detectConfigFile(): Promise<string | null> {
export async function detectRepoFileConfig(): Promise<RepoFileConfig> {
const cache = getCache();
let { configFileName } = cache;
if (configFileName) {
if (is.nonEmptyString(configFileName)) {
let configFileRaw: string | null;
try {
configFileRaw = await platform.getRawFile(configFileName);
Expand Down Expand Up @@ -89,6 +91,7 @@ export async function detectRepoFileConfig(): Promise<RepoFileConfig> {

if (!configFileName) {
logger.debug('No renovate config file found');
cache.configFileName = '';
return {};
}
cache.configFileName = configFileName;
Expand Down Expand Up @@ -171,6 +174,16 @@ export async function mergeRenovateConfig(
if (config.requireConfig !== 'ignored') {
repoConfig = await detectRepoFileConfig();
}
if (!repoConfig.configFileParsed && config.mode === 'silent') {
logger.debug(
'When mode=silent and repo has no config file, we use the onboarding config as repo config',
);
const configFileName = getDefaultConfigFileName(config);
repoConfig = {
configFileName,
configFileParsed: await getOnboardingConfig(config),
};
}
const configFileParsed = repoConfig?.configFileParsed || {};
if (is.nonEmptyArray(returnConfig.extends)) {
configFileParsed.extends = [
Expand Down
5 changes: 5 additions & 0 deletions lib/workers/repository/onboarding/branch/check.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ describe('workers/repository/onboarding/branch/check', () => {
onboarding: true,
});

it('returns true if in silent mode', async () => {
const res = await isOnboarded({ ...config, mode: 'silent' });
expect(res).toBeTrue();
});

it('skips normal onboarding check if onboardingCache is valid', async () => {
cache.getCache.mockReturnValueOnce({
onboardingBranchCache: {
Expand Down
6 changes: 6 additions & 0 deletions lib/workers/repository/onboarding/branch/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export async function isOnboarded(config: RenovateConfig): Promise<boolean> {
logger.debug('isOnboarded()');
const title = `Action required: Add a Renovate config`;

// Repo is onboarded if in silent mode
if (config.mode === 'silent') {
logger.debug('Silent mode enabled so repo is considered onboarded');
return true;
}

// Repo is onboarded if global config is bypassing onboarding and does not require a
// configuration file.
// The repo is considered "not onboarded" if:
Expand Down
19 changes: 12 additions & 7 deletions lib/workers/repository/onboarding/branch/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,27 @@ import { getOnboardingConfigContents } from './config';

const defaultConfigFile = configFileNames[0];

export async function createOnboardingBranch(
export function getDefaultConfigFileName(
config: Partial<RenovateConfig>,
): Promise<string | null> {
): string {
// TODO #22198
const configFile = configFileNames.includes(config.onboardingConfigFileName!)
? config.onboardingConfigFileName
return configFileNames.includes(config.onboardingConfigFileName!)
? config.onboardingConfigFileName!
: defaultConfigFile;
}

export async function createOnboardingBranch(
config: Partial<RenovateConfig>,
): Promise<string | null> {
logger.debug('createOnboardingBranch()');
const configFile = getDefaultConfigFileName(config);
// TODO #22198
const contents = await getOnboardingConfigContents(config, configFile!);
const contents = await getOnboardingConfigContents(config, configFile);
logger.debug('Creating onboarding branch');

const commitMessageFactory = new OnboardingCommitMessageFactory(
config,
configFile!,
configFile,
);
let commitMessage = commitMessageFactory.create().toString();

Expand All @@ -51,7 +56,7 @@ export async function createOnboardingBranch(
{
type: 'addition',
// TODO #22198
path: configFile!,
path: configFile,
contents,
},
],
Expand Down
9 changes: 9 additions & 0 deletions lib/workers/repository/process/deprecated.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ describe('workers/repository/process/deprecated', () => {
await expect(raiseDeprecationWarnings(config, {})).resolves.not.toThrow();
});

it('returns if in silent mode', async () => {
const config: RenovateConfig = {
repoIsOnboarded: true,
suppressNotifications: [],
mode: 'silent',
};
await expect(raiseDeprecationWarnings(config, {})).resolves.not.toThrow();
});

it('raises deprecation warnings', async () => {
const config: RenovateConfig = {
repoIsOnboarded: true,
Expand Down
6 changes: 6 additions & 0 deletions lib/workers/repository/process/deprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export async function raiseDeprecationWarnings(
if (config.suppressNotifications?.includes('deprecationWarningIssues')) {
return;
}
if (config.mode === 'silent') {
logger.debug(
`Deprecation warning issues are not created, updated or closed when mode=silent`,
);
return;
}
for (const [manager, files] of Object.entries(packageFiles)) {
const deprecatedPackages: Record<
string,
Expand Down
36 changes: 36 additions & 0 deletions lib/workers/repository/update/branch/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,42 @@ describe('workers/repository/update/branch/index', () => {
});
});

it('returns if branch does not exist and in silent mode', async () => {
getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({
...updatedPackageFiles,
});
npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({
artifactErrors: [],
updatedArtifacts: [],
});
scm.branchExists.mockResolvedValue(false);
GlobalConfig.set({ ...adminConfig });
config.mode = 'silent';
expect(await branchWorker.processBranch(config)).toEqual({
branchExists: false,
prNo: undefined,
result: 'needs-approval',
});
});

it('returns if branch needs dependencyDashboardApproval', async () => {
getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({
...updatedPackageFiles,
});
npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({
artifactErrors: [],
updatedArtifacts: [],
});
scm.branchExists.mockResolvedValue(false);
GlobalConfig.set({ ...adminConfig });
config.dependencyDashboardApproval = true;
expect(await branchWorker.processBranch(config)).toEqual({
branchExists: false,
prNo: undefined,
result: 'needs-approval',
});
});

it('returns if pr creation limit exceeded and branch exists', async () => {
getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({
...updatedPackageFiles,
Expand Down
21 changes: 15 additions & 6 deletions lib/workers/repository/update/branch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,21 @@ export async function processBranch(
result: 'pending',
};
}
// istanbul ignore if
if (!branchExists && config.dependencyDashboardApproval) {
if (dependencyDashboardCheck) {
logger.debug(`Branch ${config.branchName} is approved for creation`);
} else {
logger.debug(`Branch ${config.branchName} needs approval`);
if (!branchExists) {
if (config.mode === 'silent' && !dependencyDashboardCheck) {
logger.debug(
`Branch ${config.branchName} creation is disabled because mode=silent`,
);
return {
branchExists,
prNo: branchPr?.number,
result: 'needs-approval',
};
}
if (config.dependencyDashboardApproval && !dependencyDashboardCheck) {
logger.debug(
`Branch ${config.branchName} creation is disabled because dependencyDashboardApproval=true`,
);
return {
branchExists,
prNo: branchPr?.number,
Expand Down

0 comments on commit 654c447

Please sign in to comment.