Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mode=silent #28396

Merged
merged 6 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,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
viceice marked this conversation as resolved.
Show resolved Hide resolved

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.
viceice marked this conversation as resolved.
Show resolved Hide resolved

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
3 changes: 2 additions & 1 deletion lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PlatformId } from '../constants';
import type { LogLevelRemap } from '../logger/types';
import type { CustomManager } from '../modules/manager/custom/types';
import type { HostRule } from '../types';
import type { GitNoVerifyOption } from '../util/git/types';
import type { GitNoVerifyOption, LongCommitSha } from '../util/git/types';
import type { MergeConfidence } from '../util/merge-confidence/types';

export type RenovateConfigStage =
Expand Down Expand Up @@ -224,6 +224,7 @@ export interface RenovateConfig
useBaseBranchConfig?: UseBaseBranchConfigType;
baseBranch?: string;
defaultBranch?: string;
defaultBranchSha?: LongCommitSha;
branchList?: string[];
description?: string | string[];
force?: RenovateConfig;
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
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