import type { MockInstance } from 'vitest'; import { getEnvName } from '../../../../config/options/env.ts'; import { getOptions } from '../../../../config/options/index.ts'; import { logger } from '../../../../logger/index.ts'; import { coerceArray } from '../../../../util/array.ts'; import * as env from './env.ts'; import type { ParseConfigOptions } from './types.ts'; describe('workers/global/config/parse/env', () => { describe('.getConfig(env)', () => { it('returns empty env', async () => { await expect(env.getConfig({})).resolves.toEqual({ hostRules: [] }); }); it('supports boolean true', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG_MIGRATION: 'true' }; expect((await env.getConfig(envParam)).configMigration).toBeTrue(); }); it('supports boolean false', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG_MIGRATION: 'false', }; expect((await env.getConfig(envParam)).configMigration).toBeFalse(); }); it('throws exception for invalid boolean value', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG_MIGRATION: 'badvalue', }; await expect(env.getConfig(envParam)).rejects.toThrow( Error( "RENOVATE_CONFIG_MIGRATION was invalid: Error: Invalid boolean value: expected 'true' or 'false', but got 'badvalue'", ), ); }); vi.stubEnv('RENOVATE_CONFIG_MIGRATION', undefined); it('supports list single', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_LABELS: 'a' }; expect((await env.getConfig(envParam)).labels).toEqual(['a']); }); it('supports list multiple', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_LABELS: 'a,b,c' }; expect((await env.getConfig(envParam)).labels).toEqual(['a', 'b', 'c']); }); it('supports list multiple without blank items', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_LABELS: 'a,b,c,' }; expect((await env.getConfig(envParam)).labels).toEqual(['a', 'b', 'c']); }); it('supports string', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_TOKEN: 'a' }; expect((await env.getConfig(envParam)).token).toBe('a'); }); it('coerces string newlines', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_GIT_PRIVATE_KEY: 'abc\\ndef', }; expect((await env.getConfig(envParam)).gitPrivateKey).toBe('abc\ndef'); }); it('supports custom prefixes', async () => { const envParam: NodeJS.ProcessEnv = { ENV_PREFIX: 'FOOBAR_', FOOBAR_TOKEN: 'abc', }; const res = await env.getConfig(envParam); expect(res).toMatchObject({ token: 'abc' }); }); it('supports json', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_LOCK_FILE_MAINTENANCE: '{}', }; expect((await env.getConfig(envParam)).lockFileMaintenance).toEqual({}); }); it('supports arrays of objects', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_HOST_RULES: JSON.stringify([{ foo: 'bar' }]), }; const res = await env.getConfig(envParam); expect(res).toMatchObject({ hostRules: [{ foo: 'bar' }] }); }); it.each` envArg | config ${{ RENOVATE_RECREATE_CLOSED: 'true' }} | ${{ recreateWhen: 'always' }} ${{ RENOVATE_RECREATE_CLOSED: 'false' }} | ${{ recreateWhen: 'auto' }} ${{ RENOVATE_RECREATE_WHEN: 'auto' }} | ${{ recreateWhen: 'auto' }} ${{ RENOVATE_RECREATE_WHEN: 'always' }} | ${{ recreateWhen: 'always' }} ${{ RENOVATE_RECREATE_WHEN: 'never' }} | ${{ recreateWhen: 'never' }} ${{ RENOVATE_BASE_BRANCHES: '["main", "dev"]' }} | ${{ baseBranchPatterns: ['main', 'dev'] }} `('"$envArg" -> $config', async ({ envArg, config }) => { await expect(env.getConfig(envArg)).resolves.toMatchObject(config); }); it('skips misconfigured arrays', async () => { const envName = 'RENOVATE_HOST_RULES'; const val = JSON.stringify('foobar'); const envParam: NodeJS.ProcessEnv = { [envName]: val }; const res = await env.getConfig(envParam); expect(res).toEqual({ hostRules: [] }); expect(logger.debug).toHaveBeenLastCalledWith( { val, envName }, 'Could not parse object array', ); }); it('skips garbage array values', async () => { const envName = 'RENOVATE_HOST_RULES'; const val = '!@#'; const envParam: NodeJS.ProcessEnv = { [envName]: val }; const res = await env.getConfig(envParam); expect(res).toEqual({ hostRules: [] }); expect(logger.debug).toHaveBeenLastCalledWith( { val, envName }, 'Could not parse environment variable', ); }); it('supports GitHub token', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_TOKEN: 'github.com token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ hostRules: [], token: 'github.com token', }); }); it('supports GitHub custom endpoint', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_ENDPOINT: 'a ghe endpoint', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'a ghe endpoint', hostRules: [], }); }); it('supports GitHub custom endpoint and github.com', async () => { const envParam: NodeJS.ProcessEnv = { GITHUB_COM_TOKEN: 'a github.com token', RENOVATE_ENDPOINT: 'a ghe endpoint', RENOVATE_TOKEN: 'a ghe token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'a ghe endpoint', hostRules: [ { hostType: 'github', matchHost: 'github.com', token: 'a github.com token', }, ], token: 'a ghe token', }); }); it('supports GitHub fine-grained PATs', async () => { const envParam: NodeJS.ProcessEnv = { GITHUB_COM_TOKEN: 'github_pat_XXXXXX', RENOVATE_TOKEN: 'a github.com token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ token: 'a github.com token', hostRules: [ { hostType: 'github', matchHost: 'github.com', token: 'github_pat_XXXXXX', }, ], }); }); it('supports RENOVATE_ prefixed github com token', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_GITHUB_COM_TOKEN: 'github_pat_XXXXXX', RENOVATE_TOKEN: 'a github.com token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ token: 'a github.com token', hostRules: [ { hostType: 'github', matchHost: 'github.com', token: 'github_pat_XXXXXX', }, ], }); }); it('GITHUB_COM_TOKEN takes precedence over RENOVATE_GITHUB_COM_TOKEN', async () => { const envParam: NodeJS.ProcessEnv = { GITHUB_COM_TOKEN: 'github_pat_XXXXXX', RENOVATE_GITHUB_COM_TOKEN: 'github_pat_YYYYYY', RENOVATE_TOKEN: 'a github.com token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ token: 'a github.com token', hostRules: [ { hostType: 'github', matchHost: 'github.com', token: 'github_pat_XXXXXX', }, ], }); }); it('supports GitHub custom endpoint and gitlab.com', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_ENDPOINT: 'a ghe endpoint', RENOVATE_TOKEN: 'a ghe token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'a ghe endpoint', hostRules: [], token: 'a ghe token', }); }); it('supports GitLab token', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM: 'gitlab', RENOVATE_TOKEN: 'a gitlab.com token', }; await expect(env.getConfig(envParam)).resolves.toEqual({ hostRules: [], platform: 'gitlab', token: 'a gitlab.com token', }); }); it('supports GitLab custom endpoint', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM: 'gitlab', RENOVATE_TOKEN: 'a gitlab token', RENOVATE_ENDPOINT: 'a gitlab endpoint', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'a gitlab endpoint', hostRules: [], platform: 'gitlab', token: 'a gitlab token', }); }); it('supports Azure DevOps', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM: 'azure', RENOVATE_TOKEN: 'an Azure DevOps token', RENOVATE_ENDPOINT: 'an Azure DevOps endpoint', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'an Azure DevOps endpoint', hostRules: [], platform: 'azure', token: 'an Azure DevOps token', }); }); it('supports Bitbucket token', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM: 'bitbucket', RENOVATE_ENDPOINT: 'a bitbucket endpoint', RENOVATE_USERNAME: 'some-username', RENOVATE_PASSWORD: 'app-password', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'a bitbucket endpoint', hostRules: [], password: 'app-password', platform: 'bitbucket', username: 'some-username', }); }); it('supports Bitbucket username/password', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM: 'bitbucket', RENOVATE_ENDPOINT: 'a bitbucket endpoint', RENOVATE_USERNAME: 'some-username', RENOVATE_PASSWORD: 'app-password', }; await expect(env.getConfig(envParam)).resolves.toEqual({ endpoint: 'a bitbucket endpoint', hostRules: [], password: 'app-password', platform: 'bitbucket', username: 'some-username', }); }); it('merges full config from env', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG: '{"enabled":false,"token":"foo"}', RENOVATE_TOKEN: 'a', }; const config = await env.getConfig(envParam); expect(config.enabled).toBeFalse(); expect(config.token).toBe('a'); }); it('massages converted experimental env vars', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_X_MERGE_CONFIDENCE_API_BASE_URL: 'some-url', // converted RENOVATE_X_MERGE_CONFIDENCE_SUPPORTED_DATASOURCES: '["docker"]', // converted RENOVATE_X_AUTODISCOVER_REPO_SORT: 'alpha', RENOVATE_X_DOCKER_MAX_PAGES: '10', RENOVATE_AUTODISCOVER_REPO_ORDER: 'desc', RENOVATE_X_DELETE_CONFIG_FILE: 'true', RENOVATE_X_S3_ENDPOINT: 'endpoint', RENOVATE_X_S3_PATH_STYLE: 'true', // NOTE that a non-empty string is treated as `true` RENOVATE_X_REPO_CACHE_FORCE_LOCAL: 'enabled', }; const config = await env.getConfig(envParam); expect(config).toMatchObject({ mergeConfidenceEndpoint: 'some-url', mergeConfidenceDatasources: ['docker'], autodiscoverRepoSort: 'alpha', autodiscoverRepoOrder: 'desc', dockerMaxPages: 10, deleteConfigFile: true, s3Endpoint: 'endpoint', s3PathStyle: true, repositoryCacheForceLocal: true, }); }); it('does not migrate empty RENOVATE_X_REPO_CACHE_FORCE_LOCAL', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_X_REPO_CACHE_FORCE_LOCAL: '', }; const config = await env.getConfig(envParam); expect(config.repositoryCacheForceLocal).toBeUndefined(); }); describe('RENOVATE_CONFIG tests', () => { let processExit: MockInstance<(code?: number | string | null) => never>; beforeAll(() => { processExit = vi .spyOn(process, 'exit') .mockImplementation((() => void 0) as never); }); afterAll(() => { processExit.mockRestore(); }); it('crashes', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG: '!@#' }; processExit.mockImplementationOnce(() => { throw new Error('terminate function to simulate process.exit call'); }); await expect(env.getConfig(envParam)).toReject(); expect(processExit).toHaveBeenCalledExactlyOnceWith(1); }); it('migrates RENOVATE_CONFIG', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG: '{"automerge":"any","token":"foo"}', }; const config = await env.getConfig(envParam); expect(logger.warn).toHaveBeenCalledTimes(1); expect(config.automerge).toBeTrue(); }); it('warns if config in RENOVATE_CONFIG is invalid', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG: '{"enabled":"invalid-value","prTitle":"something"}', }; await env.getConfig(envParam); expect(logger.warn).toHaveBeenCalledTimes(2); }); }); describe('migrations', () => { it('renames migrated variables', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_GIT_LAB_AUTOMERGE: 'true', }; const config = await env.getConfig(envParam); expect(config.platformAutomerge).toBe(true); }); }); }); it('has no duplicate env names across options', () => { const options = getOptions(); const envNameToOptions = new Map(); for (const option of options) { const envName = getEnvName(option); if (envName === '') { continue; } const existing = coerceArray(envNameToOptions.get(envName)); existing.push(option.name); envNameToOptions.set(envName, existing); } const duplicates = [...envNameToOptions.entries()] .filter(([, names]) => names.length > 1) .map(([envName, names]) => `${envName}: ${names.join(', ')}`); expect(duplicates).toEqual([]); }); describe('.getEnvName(definition)', () => { it('returns empty', () => { const option: ParseConfigOptions = { name: 'foo', env: false, }; expect(getEnvName(option)).toBe(''); }); it('returns existing env', () => { const option: ParseConfigOptions = { name: 'foo', env: 'FOO', }; expect(getEnvName(option)).toBe('FOO'); }); it('generates RENOVATE_ env', () => { const option: ParseConfigOptions = { name: 'oneTwoThree', }; expect(getEnvName(option)).toBe('RENOVATE_ONE_TWO_THREE'); }); it('dryRun boolean true', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_DRY_RUN: 'true', }; const config = await env.getConfig(envParam); expect(config.dryRun).toBe('full'); }); it('dryRun boolean false', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_DRY_RUN: 'false', }; const config = await env.getConfig(envParam); expect(config.dryRun).toBeUndefined(); }); it('dryRun null', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_DRY_RUN: 'null', }; const config = await env.getConfig(envParam); expect(config.dryRun).toBeUndefined(); }); it('requireConfig boolean true', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_REQUIRE_CONFIG: 'true', }; const config = await env.getConfig(envParam); expect(config.requireConfig).toBe('required'); }); it('requireConfig boolean false', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_REQUIRE_CONFIG: 'false', }; const config = await env.getConfig(envParam); expect(config.requireConfig).toBe('optional'); }); it('platformCommit boolean true', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM_COMMIT: 'true', }; const config = await env.getConfig(envParam); expect(config.platformCommit).toBe('enabled'); }); it('platformCommit boolean false', async () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM_COMMIT: 'false', }; const config = await env.getConfig(envParam); expect(config.platformCommit).toBe('disabled'); }); }); });