Skip to content

Commit

Permalink
feat: write git private key (#6321)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
rarkins and viceice committed May 29, 2020
1 parent 064812b commit efb851a
Show file tree
Hide file tree
Showing 14 changed files with 100 additions and 22 deletions.
9 changes: 9 additions & 0 deletions docs/usage/self-hosted-configuration.md
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lib/config/common.ts
Expand Up @@ -92,6 +92,7 @@ export interface RenovateAdminConfig {
requireConfig?: boolean;
trustLevel?: 'low' | 'high';
redisUrl?: string;
gitPrivateKey?: string;
}

export type PostUpgradeTasks = {
Expand Down
1 change: 1 addition & 0 deletions lib/constants/error-messages.ts
Expand Up @@ -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';
Expand Down
2 changes: 0 additions & 2 deletions lib/platform/bitbucket-server/index.ts
Expand Up @@ -112,7 +112,6 @@ export function cleanRepo(): Promise<void> {
// Initialize GitLab by getting base branch
export async function initRepo({
repository,
gitPrivateKey,
localDir,
optimizeForDisabled,
bbUseDefaultReviewers,
Expand Down Expand Up @@ -161,7 +160,6 @@ export async function initRepo({
config = {
projectKey,
repositorySlug,
gitPrivateKey,
repository,
prVersions: new Map<number, number>(),
username: opts.username,
Expand Down
1 change: 0 additions & 1 deletion lib/platform/common.ts
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions 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();
});
});
});
45 changes: 45 additions & 0 deletions 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);
}
}
16 changes: 7 additions & 9 deletions lib/platform/git/storage.ts
Expand Up @@ -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>;
Expand All @@ -24,7 +25,6 @@ interface StorageConfig {
localDir: string;
baseBranch?: string;
url: string;
gitPrivateKey?: string;
extraCloneOpts?: Git.Options;
}

Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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']);
Expand Down
3 changes: 0 additions & 3 deletions lib/platform/gitea/index.ts
Expand Up @@ -45,7 +45,6 @@ type GiteaRenovateConfig = {

interface GiteaRepoConfig {
storage: GitStorage;
gitPrivateKey?: string;
repository: string;
localDir: string;
defaultBranch: string;
Expand Down Expand Up @@ -248,7 +247,6 @@ const platform: Platform = {

async initRepo({
repository,
gitPrivateKey,
localDir,
optimizeForDisabled,
}: RepoParams): Promise<RepoConfig> {
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions lib/platform/github/index.ts
Expand Up @@ -204,7 +204,6 @@ export async function initRepo({
repository,
forkMode,
forkToken,
gitPrivateKey,
localDir,
includeForks,
renovateUsername,
Expand All @@ -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}`);
Expand Down
1 change: 0 additions & 1 deletion lib/platform/github/types.ts
Expand Up @@ -43,7 +43,6 @@ export interface LocalRepoConfig {
baseBranch: string;
defaultBranch: string;
enterpriseVersion: string;
gitPrivateKey?: string;
repositoryOwner: string;
repository: string | null;
localDir: string;
Expand Down
3 changes: 0 additions & 3 deletions lib/platform/gitlab/index.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions lib/platform/index.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion lib/workers/repository/init/apis.ts
Expand Up @@ -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;
}

0 comments on commit efb851a

Please sign in to comment.