mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
feat(config)!: parse JSON5/YAML self-hosted admin config (#12644)
Adds support for alternative admin config file formats. BREAKING CHANGE: Renovate will now fail if RENOVATE_CONFIG_FILE is specified without a file extension
This commit is contained in:
@@ -20,6 +20,10 @@ Options which have `"globalOnly": true` are reserved only for bot global configu
|
||||
You can override default configuration using a configuration file, with default name `config.js` in the working directory.
|
||||
If you need an alternate location or name, set it in the environment variable `RENOVATE_CONFIG_FILE`.
|
||||
|
||||
**Note:** `RENOVATE_CONFIG_FILE` must be provided with an explicit file extension.
|
||||
For example `RENOVATE_CONFIG_FILE=myconfig.js` or `RENOVATE_CONFIG_FILE=myconfig.json` and not `RENOVATE_CONFIG_FILE=myconfig`.
|
||||
If none is provided, or the file type is invalid, Renovate will fail.
|
||||
|
||||
Using a configuration file gives you very granular configuration options.
|
||||
For instance, you can override most settings at the global (file), repository, or package level.
|
||||
e.g. apply one set of labels for `backend/package.json` and a different set of labels for `frontend/package.json` in the same repository.
|
||||
|
||||
+8
-11
@@ -2,14 +2,16 @@
|
||||
// istanbul ignore file
|
||||
import { dequal } from 'dequal';
|
||||
import { readFile } from 'fs-extra';
|
||||
import JSON5 from 'json5';
|
||||
import { configFileNames } from './config/app-strings';
|
||||
import { massageConfig } from './config/massage';
|
||||
import { migrateConfig } from './config/migration';
|
||||
import type { RenovateConfig } from './config/types';
|
||||
import { validateConfig } from './config/validation';
|
||||
import { logger } from './logger';
|
||||
import { getConfig as getFileConfig } from './workers/global/config/parse/file';
|
||||
import {
|
||||
getConfig as getFileConfig,
|
||||
getParsedContent,
|
||||
} from './workers/global/config/parse/file';
|
||||
|
||||
let returnVal = 0;
|
||||
|
||||
@@ -52,22 +54,17 @@ type PackageJson = {
|
||||
(name) => name !== 'package.json'
|
||||
)) {
|
||||
try {
|
||||
const rawContent = await readFile(file, 'utf8');
|
||||
logger.info(`Validating ${file}`);
|
||||
const parsedContent = await getParsedContent(file);
|
||||
try {
|
||||
let jsonContent: RenovateConfig;
|
||||
if (file.endsWith('.json5')) {
|
||||
jsonContent = JSON5.parse(rawContent);
|
||||
} else {
|
||||
jsonContent = JSON.parse(rawContent);
|
||||
}
|
||||
await validate(file, jsonContent);
|
||||
logger.info(`Validating ${file}`);
|
||||
await validate(file, parsedContent);
|
||||
} catch (err) {
|
||||
logger.info({ err }, `${file} is not valid Renovate config`);
|
||||
returnVal = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
// file does not exist
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// comment
|
||||
"token": "abcdefg",
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# comment
|
||||
token: abcdefg
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import { DirectoryResult, dir } from 'tmp-promise';
|
||||
import upath from 'upath';
|
||||
import { logger } from '../../../../logger';
|
||||
import customConfig from './__fixtures__/file';
|
||||
import * as file from './file';
|
||||
|
||||
@@ -16,12 +17,17 @@ describe('workers/global/config/parse/file', () => {
|
||||
});
|
||||
|
||||
describe('.getConfig()', () => {
|
||||
it('parses custom config file', async () => {
|
||||
const configFile = upath.resolve(__dirname, './__fixtures__/file.js');
|
||||
it.each([
|
||||
['custom config file with extension', 'file.js'],
|
||||
['JSON5 config file', 'config.json5'],
|
||||
['YAML config file', 'config.yaml'],
|
||||
])('parses %s', async (fileType, filePath) => {
|
||||
const configFile = upath.resolve(__dirname, './__fixtures__/', filePath);
|
||||
expect(
|
||||
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile })
|
||||
).toEqual(customConfig);
|
||||
});
|
||||
|
||||
it('migrates', async () => {
|
||||
const configFile = upath.resolve(__dirname, './__fixtures__/file2.js');
|
||||
const res = await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
|
||||
@@ -33,12 +39,10 @@ describe('workers/global/config/parse/file', () => {
|
||||
expect(await file.getConfig({})).toBeDefined();
|
||||
});
|
||||
|
||||
it('fatal error and exit if error in parsing config.js', async () => {
|
||||
const mockProcessExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
const configFile = upath.resolve(tmp.path, './file3.js');
|
||||
const fileContent = `module.exports = {
|
||||
it.each([
|
||||
[
|
||||
'config.js',
|
||||
`module.exports = {
|
||||
"platform": "github",
|
||||
"token":"abcdef",
|
||||
"logFileLevel": "warn",
|
||||
@@ -48,13 +52,23 @@ describe('workers/global/config/parse/file', () => {
|
||||
"extends": ["config:base"],
|
||||
},
|
||||
"repositories": [ "test/test" ],
|
||||
};`;
|
||||
fs.writeFileSync(configFile, fileContent, { encoding: 'utf8' });
|
||||
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
|
||||
fs.unlinkSync(configFile);
|
||||
});
|
||||
};`,
|
||||
],
|
||||
['config.json5', `"invalid":`],
|
||||
['config.yaml', `invalid: -`],
|
||||
])(
|
||||
'fatal error and exit if error in parsing %s',
|
||||
async (fileName, fileContent) => {
|
||||
const mockProcessExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementationOnce(() => undefined as never);
|
||||
const configFile = upath.resolve(tmp.path, fileName);
|
||||
fs.writeFileSync(configFile, fileContent, { encoding: 'utf8' });
|
||||
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
fs.unlinkSync(configFile);
|
||||
}
|
||||
);
|
||||
|
||||
it('fatal error and exit if custom config file does not exist', async () => {
|
||||
const mockProcessExit = jest
|
||||
@@ -66,5 +80,20 @@ describe('workers/global/config/parse/file', () => {
|
||||
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['invalid config file type', './file.txt'],
|
||||
['missing config file type', './file'],
|
||||
])('fatal error and exit if %s', async (fileType, filePath) => {
|
||||
const mockProcessExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementationOnce(() => undefined as never);
|
||||
const configFile = upath.resolve(tmp.path, filePath);
|
||||
fs.writeFileSync(configFile, `{"token": "abc"}`, { encoding: 'utf8' });
|
||||
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
expect(logger.fatal).toHaveBeenCalledWith('Unsupported file type');
|
||||
fs.unlinkSync(configFile);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
import { load } from 'js-yaml';
|
||||
import JSON5 from 'json5';
|
||||
import upath from 'upath';
|
||||
import { migrateConfig } from '../../../../config/migration';
|
||||
import type { AllConfig } from '../../../../config/types';
|
||||
import type { AllConfig, RenovateConfig } from '../../../../config/types';
|
||||
import { logger } from '../../../../logger';
|
||||
import { readFile } from '../../../../util/fs';
|
||||
|
||||
export async function getParsedContent(file: string): Promise<RenovateConfig> {
|
||||
switch (upath.extname(file)) {
|
||||
case '.yaml':
|
||||
case '.yml':
|
||||
return load(await readFile(file, 'utf8'), {
|
||||
json: true,
|
||||
}) as RenovateConfig;
|
||||
case '.json5':
|
||||
case '.json':
|
||||
return JSON5.parse(await readFile(file, 'utf8'));
|
||||
case '.js': {
|
||||
const tmpConfig = await import(file);
|
||||
return tmpConfig.default ? tmpConfig.default : tmpConfig;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported file type');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> {
|
||||
let configFile = env.RENOVATE_CONFIG_FILE || 'config';
|
||||
let configFile = env.RENOVATE_CONFIG_FILE || 'config.js';
|
||||
if (!upath.isAbsolute(configFile)) {
|
||||
configFile = `${process.cwd()}/${configFile}`;
|
||||
logger.debug('Checking for config file in ' + configFile);
|
||||
}
|
||||
logger.debug('Checking for config file in ' + configFile);
|
||||
let config: AllConfig = {};
|
||||
try {
|
||||
const tmpConfig = await import(configFile);
|
||||
config = tmpConfig.default ? tmpConfig.default : tmpConfig;
|
||||
config = await getParsedContent(configFile);
|
||||
} catch (err) {
|
||||
// istanbul ignore if
|
||||
if (err instanceof SyntaxError || err instanceof TypeError) {
|
||||
logger.fatal(`Could not parse config file \n ${err.stack}`);
|
||||
process.exit(1);
|
||||
} else if (err.message === 'Unsupported file type') {
|
||||
logger.fatal(err.message);
|
||||
process.exit(1);
|
||||
} else if (env.RENOVATE_CONFIG_FILE) {
|
||||
logger.fatal('No custom config file found on disk');
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user