diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index b45d28f04a076b192390746942d76ceaddf612ff..36b803d423aa57cf4a00d41519ca274cc98a9e4b 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -77,6 +77,15 @@ RFC5322-compliant string if you wish to customise the git author for commits. ## gitPrivateKey +This should be an armored private key, e.g. the type you get from running `gpg --export-secret-keys --armor 92066A17F0D1707B4E96863955FEF5171C45FAE5 > private.key`. Replace the newlines with `\n` before adding the resulting single-line value to your bot's config. + +It will be loaded _lazily_. Before the first commit in a repository, Renovate will: + +- First, run `gpg import` if it hasn't been run before +- Then, run `git config user.signingkey` and `git config commit.gpgsign true` + +The `git` commands are run locally in the cloned repo instead of globally to reduce the chance of causing unintended consequences with global git configs on shared systems. + ## logContext `logContext` is included with each log entry only if `logFormat="json"` - it is not included in the pretty log output. If left as default (null), a random short ID will be selected. diff --git a/lib/config/common.ts b/lib/config/common.ts index 4be8e8a8b3e4c0655263ed6479c0a05f801bf9f6..2401e793ce87a3d40b9b7f5f8d427f567f56905e 100644 --- a/lib/config/common.ts +++ b/lib/config/common.ts @@ -92,6 +92,7 @@ export interface RenovateAdminConfig { requireConfig?: boolean; trustLevel?: 'low' | 'high'; redisUrl?: string; + gitPrivateKey?: string; } export type PostUpgradeTasks = { diff --git a/lib/constants/error-messages.ts b/lib/constants/error-messages.ts index d477d8da0d41e46374e433d02e056a6ceabb4474..b1bb53ef6de252c5f0f3f4c984fdbba2d24f9b74 100644 --- a/lib/constants/error-messages.ts +++ b/lib/constants/error-messages.ts @@ -6,6 +6,7 @@ export const SYSTEM_INSUFFICIENT_MEMORY = 'out-of-memory'; export const PLATFORM_AUTHENTICATION_ERROR = 'authentication-error'; export const PLATFORM_BAD_CREDENTIALS = 'bad-credentials'; export const PLATFORM_FAILURE = 'platform-failure'; +export const PLATFORM_GPG_FAILED = 'gpg-failed'; export const PLATFORM_INTEGRATION_UNAUTHORIZED = 'integration-unauthorized'; export const PLATFORM_NOT_FOUND = 'platform-not-found'; export const PLATFORM_RATE_LIMIT_EXCEEDED = 'rate-limit-exceeded'; diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts index d9990be694624d3c1b0f6098378718ed507e9d21..1147ab5d033dcae8555417161a8f0917b45dedca 100644 --- a/lib/platform/bitbucket-server/index.ts +++ b/lib/platform/bitbucket-server/index.ts @@ -112,7 +112,6 @@ export function cleanRepo(): Promise<void> { // Initialize GitLab by getting base branch export async function initRepo({ repository, - gitPrivateKey, localDir, optimizeForDisabled, bbUseDefaultReviewers, @@ -161,7 +160,6 @@ export async function initRepo({ config = { projectKey, repositorySlug, - gitPrivateKey, repository, prVersions: new Map<number, number>(), username: opts.username, diff --git a/lib/platform/common.ts b/lib/platform/common.ts index 77ba0ff1048fcedffb8f0e88f1aedfa9bca16ed5..bea0d2864009c8dff367ea8cd6b289563e4986de 100644 --- a/lib/platform/common.ts +++ b/lib/platform/common.ts @@ -85,7 +85,6 @@ export interface RepoConfig { export interface RepoParams { azureWorkItemId?: number; // shouldn't this be configurable within a renovate.json? bbUseDefaultReviewers?: boolean; // shouldn't this be configurable within a renovate.json? - gitPrivateKey?: string; localDir: string; optimizeForDisabled: boolean; repository: string; diff --git a/lib/platform/git/private-key.spec.ts b/lib/platform/git/private-key.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a8cdf633571336c3755904c666123639fe0ac8a --- /dev/null +++ b/lib/platform/git/private-key.spec.ts @@ -0,0 +1,35 @@ +import { getName, mocked } from '../../../test/util'; +import * as exec_ from '../../util/exec'; +import { setPrivateKey, writePrivateKey } from './private-key'; + +jest.mock('fs-extra'); +jest.mock('../../util/exec'); + +const exec = mocked(exec_); + +describe(getName(__filename), () => { + describe('writePrivateKey()', () => { + it('returns if no private key', async () => { + await expect(writePrivateKey('/tmp/some-repo')).resolves.not.toThrow(); + }); + it('throws error if failing', async () => { + setPrivateKey('some-key'); + exec.exec.mockResolvedValueOnce({ + stderr: `something wrong`, + stdout: '', + }); + await expect(writePrivateKey('/tmp/some-repo')).rejects.toThrow(); + }); + it('imports the private key', async () => { + setPrivateKey('some-key'); + exec.exec.mockResolvedValueOnce({ + stderr: `gpg: key BADC0FFEE: secret key imported\nfoo\n`, + stdout: '', + }); + await expect(writePrivateKey('/tmp/some-repo')).resolves.not.toThrow(); + }); + it('does not import the key again', async () => { + await expect(writePrivateKey('/tmp/some-repo')).resolves.not.toThrow(); + }); + }); +}); diff --git a/lib/platform/git/private-key.ts b/lib/platform/git/private-key.ts new file mode 100644 index 0000000000000000000000000000000000000000..680134f4b5632ac1072cfa2634a43a08591132e8 --- /dev/null +++ b/lib/platform/git/private-key.ts @@ -0,0 +1,45 @@ +import os from 'os'; +import path from 'path'; +import fs from 'fs-extra'; +import { PLATFORM_GPG_FAILED } from '../../constants/error-messages'; +import { logger } from '../../logger'; +import { exec } from '../../util/exec'; + +let gitPrivateKey: string; +let keyId: string; + +export function setPrivateKey(key: string): void { + gitPrivateKey = key; +} + +async function importKey(): Promise<void> { + if (keyId) { + return; + } + const keyFileName = path.join(os.tmpdir() + '/git-private.key'); + await fs.outputFile(keyFileName, gitPrivateKey); + const { stdout, stderr } = await exec(`gpg --import ${keyFileName}`); + logger.debug({ stdout, stderr }, 'Private key import result'); + keyId = (stdout + stderr) + .split('\n') + .find((line) => line.includes('secret key imported')) + .replace('gpg: key ', '') + .split(':') + .shift(); + await fs.remove(keyFileName); +} + +export async function writePrivateKey(cwd: string): Promise<void> { + if (!gitPrivateKey) { + return; + } + logger.debug('Setting git private key'); + try { + await importKey(); + await exec(`git config user.signingkey ${keyId}`, { cwd }); + await exec(`git config commit.gpgsign true`, { cwd }); + } catch (err) { + logger.warn({ err }, 'Error writing git private key'); + throw new Error(PLATFORM_GPG_FAILED); + } +} diff --git a/lib/platform/git/storage.ts b/lib/platform/git/storage.ts index b3253f1b3ef0d535e44fac3f77d102803680a71a..a94eede6486e0d4a7921288388e0376398b1e0d3 100644 --- a/lib/platform/git/storage.ts +++ b/lib/platform/git/storage.ts @@ -13,6 +13,7 @@ import { import { logger } from '../../logger'; import * as limits from '../../workers/global/limits'; import { CommitFilesConfig } from '../common'; +import { writePrivateKey } from './private-key'; declare module 'fs-extra' { export function exists(pathLike: string): Promise<boolean>; @@ -24,7 +25,6 @@ interface StorageConfig { localDir: string; baseBranch?: string; url: string; - gitPrivateKey?: string; extraCloneOpts?: Git.Options; } @@ -84,6 +84,8 @@ export class Storage { private _cwd: string | undefined; + private _privateKeySet = false; + private async _resetToBranch(branchName: string): Promise<void> { logger.debug(`resetToBranch(${branchName})`); await this._git.raw(['reset', '--hard']); @@ -196,14 +198,6 @@ export class Storage { } logger.warn({ err }, 'Cannot retrieve latest commit date'); } - // istanbul ignore if - if (config.gitPrivateKey) { - logger.debug('Git private key configured, but not being set'); - } else { - logger.debug('No git private key present - commits will be unsigned'); - await this._git.raw(['config', 'commit.gpgsign', 'false']); - } - if (global.gitAuthor) { logger.debug({ gitAuthor: global.gitAuthor }, 'Setting git author'); try { @@ -470,6 +464,10 @@ export class Storage { message, }: CommitFilesConfig): Promise<string | null> { logger.debug(`Committing files to branch ${branchName}`); + if (!this._privateKeySet) { + await writePrivateKey(this._cwd); + this._privateKeySet = true; + } try { await this._git.reset('hard'); await this._git.raw(['clean', '-fd']); diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts index f470d407c701f07c9aa154ef17fe95b0bd679cce..520e01636d104bc9431c3d46a0f7ca28acb551c1 100644 --- a/lib/platform/gitea/index.ts +++ b/lib/platform/gitea/index.ts @@ -45,7 +45,6 @@ type GiteaRenovateConfig = { interface GiteaRepoConfig { storage: GitStorage; - gitPrivateKey?: string; repository: string; localDir: string; defaultBranch: string; @@ -248,7 +247,6 @@ const platform: Platform = { async initRepo({ repository, - gitPrivateKey, localDir, optimizeForDisabled, }: RepoParams): Promise<RepoConfig> { @@ -257,7 +255,6 @@ const platform: Platform = { config = {} as any; config.repository = repository; - config.gitPrivateKey = gitPrivateKey; config.localDir = localDir; // Attempt to fetch information about repository diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts index adf62069ebdff925f6e41982a26cbc3d01cf4f55..a03c648cc5b781fc4384fe133894d63244386426 100644 --- a/lib/platform/github/index.ts +++ b/lib/platform/github/index.ts @@ -204,7 +204,6 @@ export async function initRepo({ repository, forkMode, forkToken, - gitPrivateKey, localDir, includeForks, renovateUsername, @@ -229,7 +228,6 @@ export async function initRepo({ config.localDir = localDir; config.repository = repository; [config.repositoryOwner, config.repositoryName] = repository.split('/'); - config.gitPrivateKey = gitPrivateKey; let res; try { res = await githubApi.getJson<{ fork: boolean }>(`repos/${repository}`); diff --git a/lib/platform/github/types.ts b/lib/platform/github/types.ts index a4f6ea91b6e091bc8fb89401ca0b63f2d7ff24ed..1dad39f300c75704477364114f6475c13307d444 100644 --- a/lib/platform/github/types.ts +++ b/lib/platform/github/types.ts @@ -43,7 +43,6 @@ export interface LocalRepoConfig { baseBranch: string; defaultBranch: string; enterpriseVersion: string; - gitPrivateKey?: string; repositoryOwner: string; repository: string | null; localDir: string; diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts index 486dc278d64917db3808d74eaa132d886a326138..a22c27afcd0a195bdd5667e36eb0ea9f543bbbff 100644 --- a/lib/platform/gitlab/index.ts +++ b/lib/platform/gitlab/index.ts @@ -47,7 +47,6 @@ type MergeMethod = 'merge' | 'rebase_merge' | 'ff'; const defaultConfigFile = configFileNames[0]; let config: { storage: GitStorage; - gitPrivateKey?: string; repository: string; localDir: string; defaultBranch: string; @@ -140,13 +139,11 @@ export function cleanRepo(): Promise<void> { // Initialize GitLab by getting base branch export async function initRepo({ repository, - gitPrivateKey, localDir, optimizeForDisabled, }: RepoParams): Promise<RepoConfig> { config = {} as any; config.repository = urlEscape(repository); - config.gitPrivateKey = gitPrivateKey; config.localDir = localDir; type RepoResponse = { diff --git a/lib/platform/index.ts b/lib/platform/index.ts index ff4dec6229c8de43a86dcd6d1d7e95bb388c546e..531be8c536bc1e3685c5f7abf68042ba08af3242 100644 --- a/lib/platform/index.ts +++ b/lib/platform/index.ts @@ -6,6 +6,7 @@ import { logger } from '../logger'; import * as hostRules from '../util/host-rules'; import platforms from './api.generated'; import { Platform } from './common'; +import { setPrivateKey } from './git/private-key'; export * from './common'; @@ -39,6 +40,7 @@ export function setPlatformApi(name: string): void { export async function initPlatform( config: RenovateConfig ): Promise<RenovateConfig> { + setPrivateKey(config.gitPrivateKey); setPlatformApi(config.platform); // TODO: types const platformInfo = await platform.initPlatform(config); diff --git a/lib/workers/repository/init/apis.ts b/lib/workers/repository/init/apis.ts index 526825540084a7317b864d8b55e79244c31f1df4..497474c5cfe288c4eb8ef928d44967341d8175b3 100644 --- a/lib/workers/repository/init/apis.ts +++ b/lib/workers/repository/init/apis.ts @@ -26,6 +26,5 @@ export async function initApis( config = await getPlatformConfig(config as never); npmApi.resetMemCache(); npmApi.setNpmrc(config.npmrc); - delete config.gitPrivateKey; return config; }