Merge pull request #28816 from drodil/git_commit_signing

feat: allow signing git commits from scaffolder
This commit is contained in:
Ben Lambert
2025-03-11 11:15:41 +01:00
committed by GitHub
34 changed files with 477 additions and 26 deletions
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch
'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch
'@backstage/plugin-scaffolder-backend-module-bitbucket': patch
'@backstage/plugin-scaffolder-backend-module-gerrit': patch
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
'@backstage/plugin-scaffolder-backend-module-azure': patch
'@backstage/plugin-scaffolder-backend-module-gitea': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-node': patch
'@backstage/integration': patch
---
Allow signing git commits using configured private PGP key in scaffolder
+35
View File
@@ -62,6 +62,11 @@ export interface Config {
tenantId?: string;
personalAccessToken?: string;
}[];
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/**
@@ -94,6 +99,11 @@ export interface Config {
* @visibility secret
*/
appPassword?: string;
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/** Integration configuration for Bitbucket Cloud */
@@ -108,6 +118,11 @@ export interface Config {
* @visibility secret
*/
appPassword: string;
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/** Integration configuration for Bitbucket Server */
@@ -137,6 +152,11 @@ export interface Config {
* @visibility frontend
*/
apiBaseUrl?: string;
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/** Integration configuration for Gerrit */
@@ -172,6 +192,11 @@ export interface Config {
* @visibility secret
*/
password?: string;
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/** Integration configuration for GitHub */
@@ -269,6 +294,11 @@ export interface Config {
* @visibility frontend
*/
baseUrl?: string;
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/** Integration configuration for Google Cloud Storage */
@@ -349,6 +379,11 @@ export interface Config {
* @visibility secret
*/
password?: string;
/**
* PGP signing key for signing commits.
* @visibility secret
*/
commitSigningKey?: string;
}>;
/** Integration configuration for Harness Code */
harness?: Array<{
+7
View File
@@ -201,6 +201,7 @@ export type AzureIntegrationConfig = {
token?: string;
credential?: AzureDevOpsCredential;
credentials?: AzureDevOpsCredential[];
commitSigningKey?: string;
};
// @public
@@ -237,6 +238,7 @@ export type BitbucketCloudIntegrationConfig = {
username?: string;
appPassword?: string;
token?: string;
commitSigningKey?: string;
};
// @public @deprecated
@@ -267,6 +269,7 @@ export type BitbucketIntegrationConfig = {
token?: string;
username?: string;
appPassword?: string;
commitSigningKey?: string;
};
// @public
@@ -297,6 +300,7 @@ export type BitbucketServerIntegrationConfig = {
token?: string;
username?: string;
password?: string;
commitSigningKey?: string;
};
// @public @deprecated
@@ -395,6 +399,7 @@ export type GerritIntegrationConfig = {
gitilesBaseUrl: string;
username?: string;
password?: string;
commitSigningKey?: string;
};
// @public
@@ -630,6 +635,7 @@ export type GiteaIntegrationConfig = {
baseUrl?: string;
username?: string;
password?: string;
commitSigningKey?: string;
};
// @public
@@ -744,6 +750,7 @@ export type GitLabIntegrationConfig = {
apiBaseUrl: string;
token?: string;
baseUrl: string;
commitSigningKey?: string;
};
// @public
+6
View File
@@ -57,6 +57,11 @@ export type AzureIntegrationConfig = {
* If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used.
*/
credentials?: AzureDevOpsCredential[];
/**
* Signing key for commits
*/
commitSigningKey?: string;
};
/**
@@ -349,6 +354,7 @@ export function readAzureIntegrationConfig(
return {
host,
credentials,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
@@ -62,6 +62,11 @@ export type BitbucketIntegrationConfig = {
* See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/
*/
appPassword?: string;
/**
* Signing key for commits
*/
commitSigningKey?: string;
};
/**
@@ -100,6 +105,7 @@ export function readBitbucketIntegrationConfig(
token,
username,
appPassword,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
@@ -51,6 +51,9 @@ export type BitbucketCloudIntegrationConfig = {
* The access token to use for requests to Bitbucket Cloud (bitbucket.org).
*/
token?: string;
/** PGP private key for signing commits. */
commitSigningKey?: string;
};
/**
@@ -74,6 +77,7 @@ export function readBitbucketCloudIntegrationConfig(
apiBaseUrl,
username,
appPassword,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
@@ -64,6 +64,11 @@ export type BitbucketServerIntegrationConfig = {
* See https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication
*/
password?: string;
/**
* Signing key for commits
*/
commitSigningKey?: string;
};
/**
@@ -99,6 +104,7 @@ export function readBitbucketServerIntegrationConfig(
token,
username,
password,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
@@ -60,6 +60,11 @@ export type GerritIntegrationConfig = {
* The password or http token to use for authentication.
*/
password?: string;
/**
* The signing key to use for signing commits.
*/
commitSigningKey?: string;
};
/**
@@ -119,6 +124,7 @@ export function readGerritIntegrationConfig(
gitilesBaseUrl,
username,
password,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
+6
View File
@@ -45,6 +45,11 @@ export type GiteaIntegrationConfig = {
* The password or http token to use for authentication.
*/
password?: string;
/**
* Signing key to to sign commits
*/
commitSigningKey?: string;
};
/**
@@ -79,5 +84,6 @@ export function readGiteaConfig(config: Config): GiteaIntegrationConfig {
baseUrl,
username,
password,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
+12 -1
View File
@@ -54,6 +54,11 @@ export type GitLabIntegrationConfig = {
* If no baseUrl is provided, it will default to `https://${host}`
*/
baseUrl: string;
/**
* Signing key to sign commits
*/
commitSigningKey?: string;
};
/**
@@ -95,7 +100,13 @@ export function readGitLabIntegrationConfig(
);
}
return { host, token, apiBaseUrl, baseUrl };
return {
host,
token,
apiBaseUrl,
baseUrl,
commitSigningKey: config.getOptionalString('commitSigningKey'),
};
}
/**
@@ -27,6 +27,7 @@ export function createPublishAzureAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -20,10 +20,10 @@ import {
ScmIntegrationRegistry,
} from '@backstage/integration';
import {
initRepoAndPush,
getRepoSourceDirectory,
parseRepoUrl,
createTemplateAction,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
@@ -54,6 +54,7 @@ export function createPublishAzureAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
}>({
id: 'publish:azure',
examples,
@@ -103,6 +104,11 @@ export function createPublishAzureAction(options: {
type: 'string',
description: 'The token to use for authorization to Azure',
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -134,6 +140,7 @@ export function createPublishAzureAction(options: {
gitCommitMessage = 'initial commit',
gitAuthorName,
gitAuthorEmail,
signCommit,
} = ctx.input;
const { project, repo, host, organization } = parseRepoUrl(
@@ -151,6 +158,7 @@ export function createPublishAzureAction(options: {
const credentialProvider =
DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations);
const credentials = await credentialProvider.getCredentials({ url: url });
const integrationConfig = integrations.azure.byHost(host);
if (credentials === undefined && ctx.input.token === undefined) {
throw new InputError(
@@ -212,6 +220,15 @@ export function createPublishAzureAction(options: {
password: ctx.input.token ?? credentials!.token,
};
const signingKey =
integrationConfig?.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
remoteUrl,
@@ -222,6 +239,7 @@ export function createPublishAzureAction(options: {
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
ctx.output('commitHash', commitResult?.commitHash);
@@ -39,6 +39,7 @@ export function createPublishBitbucketCloudAction(options: {
gitCommitMessage?: string;
sourcePath?: string;
token?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -18,8 +18,8 @@ import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
createTemplateAction,
initRepoAndPush,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
@@ -113,6 +113,7 @@ export function createPublishBitbucketCloudAction(options: {
gitCommitMessage?: string;
sourcePath?: string;
token?: string;
signCommit?: boolean;
}>({
id: 'publish:bitbucketCloud',
examples,
@@ -158,6 +159,11 @@ export function createPublishBitbucketCloudAction(options: {
description:
'The token to use for authorization to BitBucket Cloud',
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -185,6 +191,7 @@ export function createPublishBitbucketCloudAction(options: {
defaultBranch = 'master',
gitCommitMessage,
repoVisibility = 'private',
signCommit,
} = ctx.input;
const { workspace, project, repo, host } = parseRepoUrl(
@@ -256,6 +263,15 @@ export function createPublishBitbucketCloudAction(options: {
};
}
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
remoteUrl,
@@ -266,6 +282,7 @@ export function createPublishBitbucketCloudAction(options: {
gitCommitMessage ||
config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
ctx.output('commitHash', commitResult?.commitHash);
@@ -29,6 +29,7 @@ export function createPublishBitbucketServerAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -21,8 +21,8 @@ import {
} from '@backstage/integration';
import {
createTemplateAction,
initRepoAndPush,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
@@ -137,6 +137,7 @@ export function createPublishBitbucketServerAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
}>({
id: 'publish:bitbucketServer',
description:
@@ -197,6 +198,11 @@ export function createPublishBitbucketServerAction(options: {
type: 'string',
description: `Sets the author email for the commit.`,
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -227,6 +233,7 @@ export function createPublishBitbucketServerAction(options: {
gitCommitMessage = 'initial commit',
gitAuthorName,
gitAuthorEmail,
signCommit,
} = ctx.input;
const { project, repo, host } = parseRepoUrl(repoUrl, integrations);
@@ -279,6 +286,15 @@ export function createPublishBitbucketServerAction(options: {
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
const auth = authConfig.token
? {
token: token!,
@@ -298,6 +314,7 @@ export function createPublishBitbucketServerAction(options: {
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
if (enableLFS) {
@@ -44,6 +44,7 @@ export function createPublishBitbucketAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -21,8 +21,8 @@ import {
} from '@backstage/integration';
import {
createTemplateAction,
initRepoAndPush,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { Config } from '@backstage/config';
@@ -220,6 +220,7 @@ export function createPublishBitbucketAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
}>({
id: 'publish:bitbucket',
description:
@@ -280,6 +281,11 @@ export function createPublishBitbucketAction(options: {
type: 'string',
description: `Sets the default author email for the commit.`,
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -313,6 +319,7 @@ export function createPublishBitbucketAction(options: {
gitCommitMessage = 'initial commit',
gitAuthorName,
gitAuthorEmail,
signCommit,
} = ctx.input;
const { workspace, project, repo, host } = parseRepoUrl(
@@ -380,6 +387,14 @@ export function createPublishBitbucketAction(options: {
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
let auth;
@@ -409,6 +424,7 @@ export function createPublishBitbucketAction(options: {
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
if (enableLFS && host !== 'bitbucket.org') {
@@ -22,6 +22,7 @@ export function createPublishGerritAction(options: {
gitAuthorName?: string;
gitAuthorEmail?: string;
sourcePath?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -38,6 +39,7 @@ export function createPublishGerritReviewAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -24,8 +24,8 @@ import {
} from '@backstage/integration';
import {
createTemplateAction,
initRepoAndPush,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { examples } from './gerrit.examples';
@@ -99,6 +99,7 @@ export function createPublishGerritAction(options: {
gitAuthorName?: string;
gitAuthorEmail?: string;
sourcePath?: string;
signCommit?: boolean;
}>({
id: 'publish:gerrit',
supportsDryRun: true,
@@ -143,6 +144,11 @@ export function createPublishGerritAction(options: {
type: 'string',
description: `Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.`,
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -172,6 +178,7 @@ export function createPublishGerritAction(options: {
gitAuthorEmail,
gitCommitMessage = 'initial commit',
sourcePath,
signCommit,
} = ctx.input;
const { repo, host, owner, workspace } = parseRepoUrl(
repoUrl,
@@ -233,6 +240,15 @@ export function createPublishGerritAction(options: {
email: gitEmail,
};
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath),
remoteUrl,
@@ -241,6 +257,7 @@ export function createPublishGerritAction(options: {
logger: ctx.logger,
commitMessage: generateCommitMessage(config, gitCommitMessage),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
ctx.output('remoteUrl', remoteUrl);
@@ -19,8 +19,8 @@ import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
createTemplateAction,
commitAndPushRepo,
createTemplateAction,
getRepoSourceDirectory,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
@@ -48,6 +48,7 @@ export function createPublishGerritReviewAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
}>({
id: 'publish:gerrit:review',
description: 'Creates a new Gerrit review.',
@@ -88,6 +89,11 @@ export function createPublishGerritReviewAction(options: {
type: 'string',
description: `Sets the default author email for the commit.`,
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -112,6 +118,7 @@ export function createPublishGerritReviewAction(options: {
gitAuthorName,
gitAuthorEmail,
gitCommitMessage,
signCommit,
} = ctx.input;
const { host, repo } = parseRepoUrl(repoUrl, integrations);
@@ -139,6 +146,14 @@ export function createPublishGerritReviewAction(options: {
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
const changeId = generateGerritChangeId();
const commitMessage = `${gitCommitMessage}\n\nChange-Id: ${changeId}`;
@@ -150,6 +165,7 @@ export function createPublishGerritReviewAction(options: {
gitAuthorInfo,
branch,
remoteRef: `refs/for/${branch}`,
signingKey: signCommit ? signingKey : undefined,
});
const repoContentsUrl = `${integrationConfig.config.gitilesBaseUrl}/${repo}/+/refs/heads/${branch}`;
@@ -23,6 +23,7 @@ export function createPublishGiteaAction(options: {
gitAuthorName?: string;
gitAuthorEmail?: string;
sourcePath?: string;
signCommit?: boolean;
},
JsonObject
>;
@@ -219,6 +219,7 @@ export function createPublishGiteaAction(options: {
gitAuthorName?: string;
gitAuthorEmail?: string;
sourcePath?: string;
signCommit?: boolean;
}>({
id: 'publish:gitea',
description:
@@ -268,6 +269,11 @@ export function createPublishGiteaAction(options: {
type: 'string',
description: `Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.`,
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
},
},
output: {
@@ -298,6 +304,7 @@ export function createPublishGiteaAction(options: {
gitAuthorEmail,
gitCommitMessage = 'initial commit',
sourcePath,
signCommit,
} = ctx.input;
const { repo, host, owner } = parseRepoUrl(repoUrl, integrations);
@@ -338,6 +345,16 @@ export function createPublishGiteaAction(options: {
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
// The owner to be used should be either the org name or user authenticated with the gitea server
const remoteUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}.git`;
const commitResult = await initRepoAndPush({
@@ -144,6 +144,7 @@ export function createPublishGitlabAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
setUserAsOwner?: boolean;
topics?: string[];
settings?: {
@@ -75,6 +75,7 @@ describe('publish:gitlab', () => {
host: 'gitlab.com',
token: 'tokenlols',
apiBaseUrl: 'https://api.gitlab.com',
commitSigningKey: 'test-signing-key',
},
{
host: 'hosted.gitlab.com',
@@ -470,6 +471,31 @@ describe('publish:gitlab', () => {
});
});
it('should call initRepoAndPush with the signing key', async () => {
mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 });
mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 });
mockGitlabClient.Groups.allProjects.mockResolvedValue([]);
mockGitlabClient.Projects.create.mockResolvedValue({
http_url_to_repo: 'http://mockurl.git',
});
await action.handler({
...mockContext,
input: { ...mockContext.input, signCommit: true },
});
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
defaultBranch: 'master',
remoteUrl: 'http://mockurl.git',
auth: { username: 'oauth2', password: 'tokenlols' },
logger: mockContext.logger,
commitMessage: 'initial commit',
gitAuthorInfo: {},
signingKey: 'test-signing-key',
});
});
it('should not call initRepoAndPush when sourcePath is false', async () => {
mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 });
mockGitlabClient.Namespaces.show.mockResolvedValue({
@@ -16,13 +16,13 @@
import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { Gitlab, VariableType } from '@gitbeaker/rest';
import {
initRepoAndPush,
createTemplateAction,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { Gitlab, VariableType } from '@gitbeaker/rest';
import { Config } from '@backstage/config';
import { examples } from './gitlab.examples';
@@ -49,6 +49,7 @@ export function createPublishGitlabAction(options: {
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
signCommit?: boolean;
setUserAsOwner?: boolean;
/** @deprecated in favour of settings.topics field */
topics?: string[];
@@ -122,6 +123,11 @@ export function createPublishGitlabAction(options: {
type: 'string',
description: `Sets the default author email for the commit.`,
},
signCommit: {
title: 'Sign commit',
type: 'boolean',
description: 'Sign commit with configured PGP private key',
},
sourcePath: {
title: 'Source Path',
description:
@@ -349,6 +355,7 @@ export function createPublishGitlabAction(options: {
branches = [],
projectVariables = [],
skipExisting = false,
signCommit,
} = ctx.input;
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
@@ -452,6 +459,15 @@ export function createPublishGitlabAction(options: {
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const signingKey =
integrationConfig.config.commitSigningKey ??
config.getOptionalString('scaffolder.defaultCommitSigningKey');
if (signCommit && !signingKey) {
throw new Error(
'Signing commits is enabled but no signing key is provided in the configuration',
);
}
const shouldSkipPublish =
typeof ctx.input.sourcePath === 'boolean' && !ctx.input.sourcePath;
if (!shouldSkipPublish) {
@@ -474,6 +490,7 @@ export function createPublishGitlabAction(options: {
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
if (branches) {
+5
View File
@@ -27,6 +27,11 @@ export interface Config {
email?: string;
};
/**
* Default PGP signing key for signing commits.
* @visibility secret
*/
defaultCommitSigningKey?: string;
/**
* The commit message used when new components are created.
*/
+1
View File
@@ -60,6 +60,7 @@
"@backstage/integration": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/types": "workspace:^",
"@isomorphic-git/pgp-plugin": "^0.0.7",
"concat-stream": "^2.0.0",
"fs-extra": "^11.2.0",
"globby": "^11.0.0",
+3
View File
@@ -106,6 +106,7 @@ export function commitAndPushBranch(options: {
branch?: string;
remoteRef?: string;
remote?: string;
signingKey?: string;
}): Promise<{
commitHash: string;
}>;
@@ -129,6 +130,7 @@ export function commitAndPushRepo(input: {
};
branch?: string;
remoteRef?: string;
signingKey?: string;
}): Promise<{
commitHash: string;
}>;
@@ -242,6 +244,7 @@ export function initRepoAndPush(input: {
name?: string;
email?: string;
};
signingKey?: string;
}): Promise<{
commitHash: string;
}>;
@@ -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,
});
}
+117 -3
View File
@@ -7794,6 +7794,7 @@ __metadata:
"@backstage/integration": "workspace:^"
"@backstage/plugin-scaffolder-common": "workspace:^"
"@backstage/types": "workspace:^"
"@isomorphic-git/pgp-plugin": ^0.0.7
concat-stream: ^2.0.0
fs-extra: ^11.2.0
globby: ^11.0.0
@@ -10933,6 +10934,59 @@ __metadata:
languageName: node
linkType: hard
"@isomorphic-git/pgp-plugin@npm:^0.0.7":
version: 0.0.7
resolution: "@isomorphic-git/pgp-plugin@npm:0.0.7"
dependencies:
"@isomorphic-pgp/sign-and-verify": ^0.0.10
"@isomorphic-pgp/util": ^0.0.6
checksum: 5361b4e58933f87747607b4bcfdf7ce2e4d75f591d5839ecd313fba55196020ee0e867c307fbfa2cd0aa85442942a5efca40653dfac0acabe8043e80f1dbebc1
languageName: node
linkType: hard
"@isomorphic-pgp/parser@npm:^0.0.3":
version: 0.0.3
resolution: "@isomorphic-pgp/parser@npm:0.0.3"
dependencies:
array-buffer-to-hex: ^1.0.0
base64-js: ^1.3.0
bn.js: ^4.11.8
clz-buffer: ^1.0.0
concat-buffers: ^1.0.0
crc: ^3.8.0
isomorphic-textencoder: ^1.0.1
select-case: ^1.0.0
checksum: 3ddfb1d39e7a9c0da98940c95bf2a9b7a59819a1ee0a4839fcbf3152c386aa974a16854043e6cd5f987db2f9475b480c10f3ef5686f0da1495675cb399cdd22f
languageName: node
linkType: hard
"@isomorphic-pgp/sign-and-verify@npm:^0.0.10":
version: 0.0.10
resolution: "@isomorphic-pgp/sign-and-verify@npm:0.0.10"
dependencies:
"@isomorphic-pgp/parser": ^0.0.3
"@isomorphic-pgp/util": ^0.0.6
"@wmhilton/crypto-hash": ^1.0.2
array-buffer-to-hex: ^1.0.0
isomorphic-textencoder: ^1.0.1
jsbn: ^1.1.0
sha.js: ^2.4.11
checksum: 3d43408abdb0d53c4514797ef47ec1d8f634e4a6b4a80f0df9ac826b9ba51df1a9cedcb26f7ddbee4594827de9676d7cda57549d321ea87bc95facfaa7127146
languageName: node
linkType: hard
"@isomorphic-pgp/util@npm:^0.0.6":
version: 0.0.6
resolution: "@isomorphic-pgp/util@npm:0.0.6"
dependencies:
"@isomorphic-pgp/parser": ^0.0.3
array-buffer-to-hex: ^1.0.0
concat-buffers: ^1.0.0
sha.js: ^2.4.11
checksum: 721a15ddb4597f90b7b897b2d18a5819e86988ac44510f9aee9dc6a7823a62c3f1ba5b12eb35792c803b3b1c6d4af924ae0bc31123f21fbc6f5a8e8e7962a412
languageName: node
linkType: hard
"@istanbuljs/load-nyc-config@npm:^1.0.0":
version: 1.0.0
resolution: "@istanbuljs/load-nyc-config@npm:1.0.0"
@@ -21814,6 +21868,13 @@ __metadata:
languageName: node
linkType: hard
"@wmhilton/crypto-hash@npm:^1.0.2":
version: 1.0.2
resolution: "@wmhilton/crypto-hash@npm:1.0.2"
checksum: 99c7e46856d75dbd828922ed8088dcd85ac405d58976f785e3d3062e3e4044b11073ef43a6c74976d42abd2c320820993afcd6a1d41db22f19bf7a9fe5557140
languageName: node
linkType: hard
"@xmldom/is-dom-node@npm:^1.0.1":
version: 1.0.1
resolution: "@xmldom/is-dom-node@npm:1.0.1"
@@ -23063,6 +23124,13 @@ __metadata:
languageName: node
linkType: hard
"array-buffer-to-hex@npm:^1.0.0":
version: 1.0.0
resolution: "array-buffer-to-hex@npm:1.0.0"
checksum: 8de16fbb42fc79c839e5579e3e7dbf9a36dfa52cfcb4c0576a0e9cc6eeb5bf3f066b671858a292c97fcf9bd117c6eebc121085325e836dfb62b2737babd29264
languageName: node
linkType: hard
"array-differ@npm:^3.0.0":
version: 3.0.0
resolution: "array-differ@npm:3.0.0"
@@ -23989,7 +24057,7 @@ __metadata:
languageName: node
linkType: hard
"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9":
"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9":
version: 4.12.0
resolution: "bn.js@npm:4.12.0"
checksum: 39afb4f15f4ea537b55eaf1446c896af28ac948fdcf47171961475724d1bb65118cca49fa6e3d67706e4790955ec0e74de584e45c8f1ef89f46c812bee5b5a12
@@ -24314,7 +24382,7 @@ __metadata:
languageName: node
linkType: hard
"buffer@npm:^5.5.0, buffer@npm:^5.7.1":
"buffer@npm:^5.1.0, buffer@npm:^5.5.0, buffer@npm:^5.7.1":
version: 5.7.1
resolution: "buffer@npm:5.7.1"
dependencies:
@@ -25153,6 +25221,13 @@ __metadata:
languageName: node
linkType: hard
"clz-buffer@npm:^1.0.0":
version: 1.0.0
resolution: "clz-buffer@npm:1.0.0"
checksum: 00a177ec2e7732e2c6393e3825de5f9d4a530d77fefb1797cdf1782a3d72407c43941e0c30d4511dd6c0536ed0652cf17d356ce8408e4c3234e80e1d5c88e980
languageName: node
linkType: hard
"cmd-extension@npm:^1.0.2":
version: 1.0.2
resolution: "cmd-extension@npm:1.0.2"
@@ -25541,6 +25616,13 @@ __metadata:
languageName: node
linkType: hard
"concat-buffers@npm:^1.0.0":
version: 1.0.0
resolution: "concat-buffers@npm:1.0.0"
checksum: 5b1d0215687a1ab27d9181d9a5036664e0f86f7da5bda5a9e446962572736e5c345eb3ca40b7d276198efad703754f8870fd216e70a36990d231b0b27c5fe9d7
languageName: node
linkType: hard
"concat-map@npm:0.0.1":
version: 0.0.1
resolution: "concat-map@npm:0.0.1"
@@ -25942,6 +26024,15 @@ __metadata:
languageName: node
linkType: hard
"crc@npm:^3.8.0":
version: 3.8.0
resolution: "crc@npm:3.8.0"
dependencies:
buffer: ^5.1.0
checksum: dabbc4eba223b206068b92ca82bb471d583eb6be2384a87f5c3712730cfd6ba4b13a45e8ba3ef62174d5a781a2c5ac5c20bf36cf37bba73926899bd0aa19186f
languageName: node
linkType: hard
"create-ecdh@npm:^4.0.4":
version: 4.0.4
resolution: "create-ecdh@npm:4.0.4"
@@ -29389,6 +29480,13 @@ __metadata:
languageName: node
linkType: hard
"fast-text-encoding@npm:^1.0.0":
version: 1.0.6
resolution: "fast-text-encoding@npm:1.0.6"
checksum: 9d58f694314b3283e785bf61954902536da228607ad246905e30256f9ab8331f780ac987e7222c9f5eafd04168d07e12b8054c85cedb76a2c05af0e82387a903
languageName: node
linkType: hard
"fast-uri@npm:^3.0.1":
version: 3.0.1
resolution: "fast-uri@npm:3.0.1"
@@ -33010,6 +33108,15 @@ __metadata:
languageName: node
linkType: hard
"isomorphic-textencoder@npm:^1.0.1":
version: 1.0.1
resolution: "isomorphic-textencoder@npm:1.0.1"
dependencies:
fast-text-encoding: ^1.0.0
checksum: 9bd0c04bff7b1dc9e410d2f9a2705ca4cc288872929d88fbb7758f06b5147de8dbaea5f95ce6cf4d314f064643f3d42d69a07bbb68df0261e81153d2d56f77f3
languageName: node
linkType: hard
"isomorphic-timers-promises@npm:^1.0.1":
version: 1.0.1
resolution: "isomorphic-timers-promises@npm:1.0.1"
@@ -33807,7 +33914,7 @@ __metadata:
languageName: node
linkType: hard
"jsbn@npm:1.1.0":
"jsbn@npm:1.1.0, jsbn@npm:^1.1.0":
version: 1.1.0
resolution: "jsbn@npm:1.1.0"
checksum: 944f924f2bd67ad533b3850eee47603eed0f6ae425fd1ee8c760f477e8c34a05f144c1bd4f5a5dd1963141dc79a2c55f89ccc5ab77d039e7077f3ad196b64965
@@ -43006,6 +43113,13 @@ __metadata:
languageName: node
linkType: hard
"select-case@npm:^1.0.0":
version: 1.0.0
resolution: "select-case@npm:1.0.0"
checksum: 19cf1c8fd16ae4b811dea9fc4a11860dc764b9f7d2e6f7c8910814d91617e0d0d508ce2186aa9f99a3708834caf261e963769fed3a313552e1dae3d28d64042a
languageName: node
linkType: hard
"select-hose@npm:^2.0.0":
version: 2.0.0
resolution: "select-hose@npm:2.0.0"