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: add hermit binary source #16259

Merged
Show file tree
Hide file tree
Changes from 9 commits
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
2 changes: 2 additions & 0 deletions docs/usage/self-hosted-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ Supported tools for dynamic install are:
- `jb`
- `npm`

If all projects are managed by Hermit, you can tell Renovate to use the tooling versions specified in each project via Hermit by setting `binarySource=hermit`.

Tools not on this list fall back to `binarySource=global`.

## cacheDir
Expand Down
2 changes: 1 addition & 1 deletion lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const options: RenovateOptions[] = [
'Controls how third-party tools like npm or Gradle are called: directly, via Docker sidecar containers, or via dynamic install.',
globalOnly: true,
type: 'string',
allowedValues: ['global', 'docker', 'install'],
allowedValues: ['global', 'docker', 'install', 'hermit'],
default: 'global',
},
{
Expand Down
2 changes: 1 addition & 1 deletion lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export interface RepoGlobalConfig {
allowPostUpgradeCommandTemplating?: boolean;
allowScripts?: boolean;
allowedPostUpgradeCommands?: string[];
binarySource?: 'docker' | 'global' | 'install';
binarySource?: 'docker' | 'global' | 'install' | 'hermit';
customEnvVariables?: Record<string, string>;
dockerChildPrefix?: string;
dockerImagePrefix?: string;
Expand Down
71 changes: 71 additions & 0 deletions lib/util/exec/hermit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import tmp, { DirectoryResult } from 'tmp-promise';
import { mockExecAll } from '../../../test/exec-util';

import { GlobalConfig } from '../../config/global';
import { writeLocalFile } from '../fs';
import { findHermitCwd, getHermitEnvs, isHermit } from './hermit';
import type { ExecResult, RawExecOptions } from './types';

describe('util/exec/hermit', () => {
describe('isHermit', () => {
it('should return true when binarySource is hermit', () => {
GlobalConfig.set({ binarySource: 'docker' });
expect(isHermit()).toBeFalse();
GlobalConfig.set({ binarySource: 'hermit' });
expect(isHermit()).toBeTruthy();
});
});

describe('findHermitCwd', () => {
let localDirResult: DirectoryResult;
let localDir: string;

beforeEach(async () => {
localDirResult = await tmp.dir({ unsafeCleanup: true });
localDir = localDirResult.path;

GlobalConfig.set({ localDir });
});

afterEach(async () => {
await localDirResult?.cleanup();
});

it('should find the closest hermit cwd to the given path', async () => {
await writeLocalFile('nested/bin/hermit', 'foo');
await writeLocalFile('bin/hermit', 'bar');

const nestedCwd = 'nested/other/directory';
const localDir = GlobalConfig.get('localDir') ?? '';

expect(await findHermitCwd(nestedCwd)).toBe(`${localDir}/nested/bin`);
expect(await findHermitCwd('nested')).toBe(`${localDir}/nested/bin`);
expect(await findHermitCwd('')).toBe(`${localDir}/bin`);
expect(await findHermitCwd('other/directory')).toBe(`${localDir}/bin`);
});

it('should throw error when hermit cwd is not found', async () => {
const err = new Error('hermit not found for other/directory');

await expect(findHermitCwd('other/directory')).rejects.toThrow(err);
});
});

describe('getHermitEnvs', () => {
it('should return hermit environment variables when hermit env returns successfully', async () => {
await writeLocalFile('bin/hermit', 'bar');
mockExecAll({
stdout: `GOBIN=/usr/src/app/repository-a/.hermit/go/bin
PATH=/usr/src/app/repository-a/bin
`,
} as ExecResult);

const resp = await getHermitEnvs({} as RawExecOptions);

expect(resp).toStrictEqual({
GOBIN: '/usr/src/app/repository-a/.hermit/go/bin',
PATH: '/usr/src/app/repository-a/bin',
});
});
});
});
51 changes: 51 additions & 0 deletions lib/util/exec/hermit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os from 'os';
import upath from 'upath';
import { GlobalConfig } from '../../config/global';
import { logger } from '../../logger';
import { findUpLocal } from '../fs';
import { rawExec } from './common';
import type { RawExecOptions } from './types';

export function isHermit(): boolean {
const { binarySource } = GlobalConfig.get();
return binarySource === 'hermit';
}

export async function findHermitCwd(cwd: string): Promise<string> {
const hermitFile = await findUpLocal('bin/hermit', upath.join(cwd));

if (hermitFile === null) {
throw new Error(`hermit not found for ${cwd}`);
}

return upath.join(GlobalConfig.get('localDir'), upath.dirname(hermitFile));
}

export async function getHermitEnvs(
rawOptions: RawExecOptions
): Promise<Record<string, string>> {
const cwd = rawOptions.cwd?.toString() ?? '';
const hermitCwd = await findHermitCwd(cwd);
logger.debug({ cwd, hermitCwd }, 'fetching hermit environment variables');
// with -r will output the raw unquoted environment variables to consume
const hermitEnvResp = await rawExec('./hermit env -r', {
...rawOptions,
cwd: hermitCwd,
});

const lines = hermitEnvResp.stdout.split(os.EOL);

const out = {} as Record<string, string>;

for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '') {
continue;
}
const equalIndex = trimmedLine.indexOf('=');
const name = trimmedLine.substring(0, equalIndex);
out[name] = trimmedLine.substring(equalIndex + 1);
}

return out;
}
54 changes: 54 additions & 0 deletions lib/util/exec/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { exec as cpExec, envMock } from '../../../test/exec-util';
import { mockedFunction } from '../../../test/util';
import { GlobalConfig } from '../../config/global';
import type { RepoGlobalConfig } from '../../config/types';
import { TEMPORARY_ERROR } from '../../constants/error-messages';
import * as dockerModule from './docker';
import { getHermitEnvs } from './hermit';
import type { ExecOptions, RawExecOptions, VolumeOption } from './types';
import { exec } from '.';

const getHermitEnvsMock = mockedFunction(getHermitEnvs);

jest.mock('./hermit', () => ({
...jest.requireActual('./hermit'),
getHermitEnvs: jest.fn(),
}));
jest.mock('../../modules/datasource');

interface TestInput {
Expand All @@ -15,6 +23,7 @@ interface TestInput {
outCmd: string[];
outOpts: RawExecOptions[];
adminConfig?: Partial<RepoGlobalConfig>;
hermitEnvs?: Record<string, string>;
}

describe('util/exec/index', () => {
Expand Down Expand Up @@ -710,6 +719,46 @@ describe('util/exec/index', () => {
},
},
],

[
'Hermit',
{
processEnv: {
...envMock.basic,
CUSTOM_KEY: 'CUSTOM_VALUE',
PATH: '/home/user-a/bin;/usr/local/bin',
},
inCmd,
inOpts: {
cwd,
},
outCmd: [inCmd],
outOpts: [
{
cwd,
encoding,
env: {
...envMock.basic,
CUSTOM_KEY: 'CUSTOM_OVERRIDEN_VALUE',
GOBIN: '/usr/src/app/repository-a/.hermit/go/bin',
PATH: '/usr/src/app/repository-a/bin/;/home/user-a/bin;/usr/local/bin;',
},
timeout: 900000,
maxBuffer: 10485760,
},
],
hermitEnvs: {
GOBIN: '/usr/src/app/repository-a/.hermit/go/bin',
PATH: '/usr/src/app/repository-a/bin/;/home/user-a/bin;/usr/local/bin;',
},
adminConfig: {
customEnvVariables: {
CUSTOM_KEY: 'CUSTOM_OVERRIDEN_VALUE',
},
binarySource: 'hermit',
},
},
],
];

test.each(testInputs)('%s', async (_msg, testOpts) => {
Expand All @@ -720,6 +769,7 @@ describe('util/exec/index', () => {
outCmd: outCommand,
outOpts,
adminConfig = {} as any,
hermitEnvs,
} = testOpts;

process.env = procEnv;
Expand All @@ -733,6 +783,10 @@ describe('util/exec/index', () => {
return Promise.resolve({ stdout: '', stderr: '' });
});
GlobalConfig.set({ ...globalConfig, localDir: cwd, ...adminConfig });
if (hermitEnvs !== undefined) {
getHermitEnvsMock.mockResolvedValue(hermitEnvs);
}

await exec(cmd as string, inOpts);

expect(actualCmd).toEqual(outCommand);
Expand Down
11 changes: 11 additions & 0 deletions lib/util/exec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { generateInstallCommands, isDynamicInstall } from './buildpack';
import { rawExec } from './common';
import { generateDockerCommand, removeDockerContainer } from './docker';
import { getChildProcessEnv } from './env';
import { getHermitEnvs, isHermit } from './hermit';
import type {
DockerOptions,
ExecOptions,
Expand Down Expand Up @@ -141,6 +142,16 @@ async function prepareRawExec(
...(await generateInstallCommands(opts.toolConstraints)),
...rawCommands,
];
} else if (isHermit()) {
const hermitEnvVars = await getHermitEnvs(rawOptions);
logger.debug(
{ hermitEnvVars },
'merging hermit environment variables into the execution options'
);
rawOptions.env = {
...rawOptions.env,
...hermitEnvVars,
};
}

return { rawCommands, rawOptions };
Expand Down