Skip to content
Snippets Groups Projects
Unverified Commit 9aa97af5 authored by Nejc Habjan's avatar Nejc Habjan Committed by GitHub
Browse files

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
parent 7c4a71b6
No related branches found
No related tags found
No related merge requests found
...@@ -20,6 +20,10 @@ Options which have `"globalOnly": true` are reserved only for bot global configu ...@@ -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. 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`. 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. 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. 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. 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.
......
...@@ -2,14 +2,16 @@ ...@@ -2,14 +2,16 @@
// istanbul ignore file // istanbul ignore file
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { readFile } from 'fs-extra'; import { readFile } from 'fs-extra';
import JSON5 from 'json5';
import { configFileNames } from './config/app-strings'; import { configFileNames } from './config/app-strings';
import { massageConfig } from './config/massage'; import { massageConfig } from './config/massage';
import { migrateConfig } from './config/migration'; import { migrateConfig } from './config/migration';
import type { RenovateConfig } from './config/types'; import type { RenovateConfig } from './config/types';
import { validateConfig } from './config/validation'; import { validateConfig } from './config/validation';
import { logger } from './logger'; 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; let returnVal = 0;
...@@ -52,22 +54,17 @@ type PackageJson = { ...@@ -52,22 +54,17 @@ type PackageJson = {
(name) => name !== 'package.json' (name) => name !== 'package.json'
)) { )) {
try { try {
const rawContent = await readFile(file, 'utf8'); const parsedContent = await getParsedContent(file);
logger.info(`Validating ${file}`);
try { try {
let jsonContent: RenovateConfig; logger.info(`Validating ${file}`);
if (file.endsWith('.json5')) { await validate(file, parsedContent);
jsonContent = JSON5.parse(rawContent);
} else {
jsonContent = JSON.parse(rawContent);
}
await validate(file, jsonContent);
} catch (err) { } catch (err) {
logger.info({ err }, `${file} is not valid Renovate config`); logger.info({ err }, `${file} is not valid Renovate config`);
returnVal = 1; returnVal = 1;
} }
} catch (err) { } catch (err) {
// file does not exist // file does not exist
continue;
} }
} }
try { try {
......
{
// comment
"token": "abcdefg",
}
---
# comment
token: abcdefg
import fs from 'fs'; import fs from 'fs';
import { DirectoryResult, dir } from 'tmp-promise'; import { DirectoryResult, dir } from 'tmp-promise';
import upath from 'upath'; import upath from 'upath';
import { logger } from '../../../../logger';
import customConfig from './__fixtures__/file'; import customConfig from './__fixtures__/file';
import * as file from './file'; import * as file from './file';
...@@ -16,12 +17,17 @@ describe('workers/global/config/parse/file', () => { ...@@ -16,12 +17,17 @@ describe('workers/global/config/parse/file', () => {
}); });
describe('.getConfig()', () => { describe('.getConfig()', () => {
it('parses custom config file', async () => { it.each([
const configFile = upath.resolve(__dirname, './__fixtures__/file.js'); ['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( expect(
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile }) await file.getConfig({ RENOVATE_CONFIG_FILE: configFile })
).toEqual(customConfig); ).toEqual(customConfig);
}); });
it('migrates', async () => { it('migrates', async () => {
const configFile = upath.resolve(__dirname, './__fixtures__/file2.js'); const configFile = upath.resolve(__dirname, './__fixtures__/file2.js');
const res = await file.getConfig({ RENOVATE_CONFIG_FILE: configFile }); const res = await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
...@@ -33,12 +39,10 @@ describe('workers/global/config/parse/file', () => { ...@@ -33,12 +39,10 @@ describe('workers/global/config/parse/file', () => {
expect(await file.getConfig({})).toBeDefined(); expect(await file.getConfig({})).toBeDefined();
}); });
it('fatal error and exit if error in parsing config.js', async () => { it.each([
const mockProcessExit = jest [
.spyOn(process, 'exit') 'config.js',
.mockImplementation(() => undefined as never); `module.exports = {
const configFile = upath.resolve(tmp.path, './file3.js');
const fileContent = `module.exports = {
"platform": "github", "platform": "github",
"token":"abcdef", "token":"abcdef",
"logFileLevel": "warn", "logFileLevel": "warn",
...@@ -48,13 +52,23 @@ describe('workers/global/config/parse/file', () => { ...@@ -48,13 +52,23 @@ describe('workers/global/config/parse/file', () => {
"extends": ["config:base"], "extends": ["config:base"],
}, },
"repositories": [ "test/test" ], "repositories": [ "test/test" ],
};`; };`,
],
['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' }); fs.writeFileSync(configFile, fileContent, { encoding: 'utf8' });
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile }); await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
expect(mockProcessExit).toHaveBeenCalledWith(1); expect(mockProcessExit).toHaveBeenCalledWith(1);
fs.unlinkSync(configFile); fs.unlinkSync(configFile);
}); }
);
it('fatal error and exit if custom config file does not exist', async () => { it('fatal error and exit if custom config file does not exist', async () => {
const mockProcessExit = jest const mockProcessExit = jest
...@@ -66,5 +80,20 @@ describe('workers/global/config/parse/file', () => { ...@@ -66,5 +80,20 @@ describe('workers/global/config/parse/file', () => {
expect(mockProcessExit).toHaveBeenCalledWith(1); 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);
});
}); });
}); });
import { load } from 'js-yaml';
import JSON5 from 'json5';
import upath from 'upath'; import upath from 'upath';
import { migrateConfig } from '../../../../config/migration'; import { migrateConfig } from '../../../../config/migration';
import type { AllConfig } from '../../../../config/types'; import type { AllConfig, RenovateConfig } from '../../../../config/types';
import { logger } from '../../../../logger'; 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> { 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)) { if (!upath.isAbsolute(configFile)) {
configFile = `${process.cwd()}/${configFile}`; configFile = `${process.cwd()}/${configFile}`;
logger.debug('Checking for config file in ' + configFile);
} }
logger.debug('Checking for config file in ' + configFile);
let config: AllConfig = {}; let config: AllConfig = {};
try { try {
const tmpConfig = await import(configFile); config = await getParsedContent(configFile);
config = tmpConfig.default ? tmpConfig.default : tmpConfig;
} catch (err) { } catch (err) {
// istanbul ignore if // istanbul ignore if
if (err instanceof SyntaxError || err instanceof TypeError) { if (err instanceof SyntaxError || err instanceof TypeError) {
logger.fatal(`Could not parse config file \n ${err.stack}`); logger.fatal(`Could not parse config file \n ${err.stack}`);
process.exit(1); process.exit(1);
} else if (err.message === 'Unsupported file type') {
logger.fatal(err.message);
process.exit(1);
} else if (env.RENOVATE_CONFIG_FILE) { } else if (env.RENOVATE_CONFIG_FILE) {
logger.fatal('No custom config file found on disk'); logger.fatal('No custom config file found on disk');
process.exit(1); process.exit(1);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment