feat: allow signing git commits from scaffolder

adds support to sign git commits with configured PGP key in scaffolder
actions. configuration can be done either by integration or by using the
default signing key in the scaffolder config. note that this
pgp-plugin is used for signing and that it is limited to using RSA keys
and signatures made with SHA1 hashing algorithm.

this change does not support github commit signing as it cannot be done
the same way due to GitHub app and REST API being used.

closes #25934

refers to #26333

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-08-29 08:40:14 +03:00
committed by Hellgren Heikki
parent 37416e06d6
commit 4f8b5b66a1
34 changed files with 473 additions and 26 deletions
@@ -16,12 +16,12 @@
import { Git } from '../scm';
import {
commitAndPushRepo,
initRepoAndPush,
commitAndPushBranch,
addFiles,
createBranch,
cloneRepo,
commitAndPushBranch,
commitAndPushRepo,
createBranch,
initRepoAndPush,
} from './gitHelpers';
import { mockServices } from '@backstage/backend-test-utils';
import { loggerToWinstonLogger } from './loggerToWinstonLogger';
@@ -224,6 +224,32 @@ describe('commitAndPushRepo', () => {
});
});
it('creates commit with signing key', async () => {
await commitAndPushRepo({
dir: '/test/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
logger: loggerToWinstonLogger(mockServices.logger.mock()),
commitMessage: 'commit message',
signingKey: 'test-signing-key',
});
expect(mockedGit.commit).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
message: 'commit message',
author: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
committer: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
signingKey: 'test-signing-key',
});
});
it('pushes to the remote', () => {
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
@@ -31,6 +31,7 @@ export async function initRepoAndPush(input: {
defaultBranch?: string;
commitMessage?: string;
gitAuthorInfo?: { name?: string; email?: string };
signingKey?: string;
}): Promise<{ commitHash: string }> {
const {
dir,
@@ -40,6 +41,7 @@ export async function initRepoAndPush(input: {
defaultBranch = 'master',
commitMessage = 'Initial commit',
gitAuthorInfo,
signingKey,
} = input;
const git = Git.fromAuth({
...auth,
@@ -64,6 +66,7 @@ export async function initRepoAndPush(input: {
message: commitMessage,
author: authorInfo,
committer: authorInfo,
signingKey,
});
await git.push({
@@ -89,6 +92,7 @@ export async function commitAndPushRepo(input: {
gitAuthorInfo?: { name?: string; email?: string };
branch?: string;
remoteRef?: string;
signingKey?: string;
}): Promise<{ commitHash: string }> {
const {
dir,
@@ -98,6 +102,7 @@ export async function commitAndPushRepo(input: {
gitAuthorInfo,
branch = 'master',
remoteRef,
signingKey,
} = input;
const git = Git.fromAuth({
@@ -120,6 +125,7 @@ export async function commitAndPushRepo(input: {
message: commitMessage,
author: authorInfo,
committer: authorInfo,
signingKey,
});
await git.push({
@@ -213,6 +219,7 @@ export async function commitAndPushBranch(options: {
branch?: string;
remoteRef?: string;
remote?: string;
signingKey?: string;
}): Promise<{ commitHash: string }> {
const {
dir,
@@ -223,6 +230,7 @@ export async function commitAndPushBranch(options: {
branch = 'master',
remoteRef,
remote = 'origin',
signingKey,
} = options;
const git = Git.fromAuth({
...auth,
@@ -240,6 +248,7 @@ export async function commitAndPushBranch(options: {
message: commitMessage,
author: authorInfo,
committer: authorInfo,
signingKey,
});
await git.push({
+10 -1
View File
@@ -16,6 +16,12 @@
jest.mock('isomorphic-git');
jest.mock('isomorphic-git/http/node');
jest.mock('fs-extra');
jest.mock('@isomorphic-git/pgp-plugin', () => ({
...jest.requireActual('@isomorphic-git/pgp-plugin'),
pgp: {
sign: jest.fn().mockResolvedValue({ signature: 'sign' }),
},
}));
import * as isomorphic from 'isomorphic-git';
import { Git } from './git';
@@ -140,8 +146,9 @@ describe('Git', () => {
name: 'comitter',
email: 'test@backstage.io',
};
const signingKey = 'test-signing-key';
await git.commit({ dir, message, author, committer });
await git.commit({ dir, message, author, committer, signingKey });
expect(isomorphic.commit).toHaveBeenCalledWith({
fs,
@@ -149,6 +156,8 @@ describe('Git', () => {
message,
author,
committer,
signingKey,
onSign: expect.any(Function),
});
});
});
+20 -6
View File
@@ -15,14 +15,16 @@
*/
import git, {
ProgressCallback,
MergeResult,
ReadCommitResult,
AuthCallback,
MergeResult,
ProgressCallback,
ReadCommitResult,
} from 'isomorphic-git';
import http from 'isomorphic-git/http/node';
import fs from 'fs-extra';
import { LoggerService } from '@backstage/backend-plugin-api';
// @ts-ignore
import { pgp } from '@isomorphic-git/pgp-plugin';
function isAuthCallbackOptions(
options: StaticAuthOptions | AuthCallbackOptions,
@@ -137,12 +139,21 @@ export class Git {
message: string;
author: { name: string; email: string };
committer: { name: string; email: string };
signingKey?: string;
}): Promise<string> {
const { dir, message, author, committer } = options;
const { dir, message, author, committer, signingKey } = options;
this.config.logger?.info(
`Committing file to repo {dir=${dir},message=${message}}`,
);
return git.commit({ fs, dir, message, author, committer });
return git.commit({
fs,
dir,
message,
author,
committer,
signingKey,
onSign: signingKey ? pgp.sign : undefined,
});
}
/** https://isomorphic-git.org/docs/en/clone */
@@ -241,8 +252,9 @@ export class Git {
ours?: string;
author: { name: string; email: string };
committer: { name: string; email: string };
signingKey?: string;
}): Promise<MergeResult> {
const { dir, theirs, ours, author, committer } = options;
const { dir, theirs, ours, author, committer, signingKey } = options;
this.config.logger?.info(
`Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`,
);
@@ -255,6 +267,8 @@ export class Git {
theirs,
author,
committer,
signingKey,
onSign: signingKey ? pgp.sign : undefined,
});
}