From 9e64c026ef231d9df2e324ede763ace06ed47875 Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Fri, 15 Dec 2023 16:01:55 -0500 Subject: [PATCH 1/9] Fix for Issue ##21762 Signed-off-by: Jason Froehlich --- .../src/actions/bitbucketServerPullRequest.ts | 97 +++++++++++- .../src/actions/gitHelpers.test.ts | 149 +++++++++++++++++- .../scaffolder-node/src/actions/gitHelpers.ts | 66 ++++++++ plugins/scaffolder-node/src/actions/index.ts | 6 +- 4 files changed, 314 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts index dc75145ebc..66327d4f6c 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts @@ -21,6 +21,8 @@ import { } from '@backstage/integration'; import { createTemplateAction, + getRepoSourceDirectory, + commitAndPushBranch, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import fetch, { RequestInit, Response } from 'node-fetch'; @@ -152,7 +154,51 @@ const findBranches = async (opts: { return undefined; }; +const createBranch = async (opts: { + project: string; + repo: string; + branchName: string; + authorization: string; + apiBaseUrl: string; + startPoint: string; +}) => { + const { project, repo, branchName, authorization, apiBaseUrl, startPoint } = + opts; + let response: Response; + const options: RequestInit = { + method: 'POST', + body: JSON.stringify({ + name: branchName, + startPoint, + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/projects/${encodeURIComponent( + project, + )}/repos/${encodeURIComponent(repo)}/branches`, + options, + ); + } catch (e) { + throw new Error(`Unable to create branch, ${e}`); + } + + if (response.status !== 200) { + throw new Error( + `Unable to create branch, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + return await response.json(); +}; /** * Creates a BitbucketServer Pull Request action. * @public @@ -161,7 +207,7 @@ export function createPublishBitbucketServerPullRequestAction(options: { integrations: ScmIntegrationRegistry; config: Config; }) { - const { integrations } = options; + const { integrations, config } = options; return createTemplateAction<{ repoUrl: string; @@ -268,7 +314,7 @@ export function createPublishBitbucketServerPullRequestAction(options: { apiBaseUrl, }); - const fromRef = await findBranches({ + let fromRef = await findBranches({ project, repo, branchName: sourceBranch, @@ -276,6 +322,53 @@ export function createPublishBitbucketServerPullRequestAction(options: { apiBaseUrl, }); + if (!fromRef) { + // create branch + ctx.logger.info( + `source branch not found -> creating branch named: ${sourceBranch} lastCommit: ${toRef.latestCommit}`, + ); + const latestCommit = toRef.latestCommit; + + fromRef = await createBranch({ + project, + repo, + branchName: sourceBranch, + authorization, + apiBaseUrl, + startPoint: latestCommit, + }); + + const remoteUrl = `https://${host}/scm/${project}/${repo}.git`; + + const auth = authConfig.token + ? { + token: token!, + } + : { + username: authConfig.username!, + password: authConfig.password!, + }; + + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + await commitAndPushBranch({ + tempDir: await ctx.createTemporaryDirectory(), + dir: getRepoSourceDirectory(ctx.workspacePath, undefined), + remoteUrl, + auth, + logger: ctx.logger, + commitMessage: + description ?? + config.getOptionalString('scaffolder.defaultCommitMessage') ?? + '', + gitAuthorInfo, + branch: sourceBranch, + }); + } + const pullRequestUrl = await createPullRequest({ project, repo, diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.test.ts b/plugins/scaffolder-node/src/actions/gitHelpers.test.ts index 7bc3f64f46..2d2220f33d 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.test.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.test.ts @@ -15,7 +15,11 @@ */ import { Git, getVoidLogger } from '@backstage/backend-common'; -import { commitAndPushRepo, initRepoAndPush } from './gitHelpers'; +import { + commitAndPushRepo, + initRepoAndPush, + commitAndPushBranch, +} from './gitHelpers'; jest.mock('@backstage/backend-common', () => ({ Git: { @@ -29,10 +33,14 @@ jest.mock('@backstage/backend-common', () => ({ fetch: jest.fn(), addRemote: jest.fn(), push: jest.fn(), + clone: jest.fn(), }), }, getVoidLogger: jest.requireActual('@backstage/backend-common').getVoidLogger, })); +jest.mock('fs-extra', () => ({ + cpSync: jest.fn(), +})); const mockedGit = Git.fromAuth({ logger: getVoidLogger(), @@ -303,3 +311,142 @@ describe('commitAndPushRepo', () => { }); }); }); + +describe('commitAndPushBranch', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('with minimal parameters', () => { + beforeEach(async () => { + await commitAndPushBranch({ + tempDir: '/tmp/repo/dir/', + dir: '/test/repo/dir/', + remoteUrl: 'git@github.com:test/repo.git', + auth: { + username: 'test-user', + password: 'test-password', + }, + commitMessage: 'commit message', + gitAuthorInfo: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, + logger: getVoidLogger(), + }); + }); + + it('clone the repo', () => { + expect(mockedGit.clone).toHaveBeenCalledWith({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + }); + }); + + it('fetch the branches', () => { + expect(mockedGit.fetch).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + }); + }); + + it('checkout the branch', () => { + expect(mockedGit.checkout).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + ref: 'master', + }); + }); + + it('stages all files in the repo', () => { + expect(mockedGit.add).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + filepath: '.', + }); + }); + + it('creates an commit', () => { + expect(mockedGit.commit).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + message: 'commit message', + author: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, + committer: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, + }); + }); + + it('pushes to the remote', () => { + expect(mockedGit.push).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + remote: 'origin', + remoteRef: 'refs/heads/master', + }); + }); + }); + + it('with token', async () => { + await commitAndPushBranch({ + tempDir: '/tmp/repo/dir/', + dir: '/test/repo/dir/', + remoteUrl: 'git@github.com:test/repo.git', + auth: { + token: 'test-token', + }, + commitMessage: 'commit message', + logger: getVoidLogger(), + }); + + expect(mockedGit.clone).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + url: 'git@github.com:test/repo.git', + }); + }); + + it('allows overriding the default branch', async () => { + await commitAndPushBranch({ + tempDir: '/tmp/repo/dir/', + dir: '/test/repo/dir/', + branch: 'trunk', + remoteUrl: 'git@github.com:test/repo.git', + auth: { + username: 'test-user', + password: 'test-password', + }, + commitMessage: 'commit message', + logger: getVoidLogger(), + }); + + expect(mockedGit.checkout).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + ref: 'trunk', + }); + }); + + it('allows overriding the remoteRef', async () => { + await commitAndPushBranch({ + tempDir: '/tmp/repo/dir/', + dir: '/test/repo/dir/', + gitAuthorInfo: { + name: 'Custom Scaffolder Author', + email: 'scaffolder@example.org', + }, + remoteUrl: 'git@github.com:test/repo.git', + auth: { + username: 'test-user', + password: 'test-password', + }, + remoteRef: 'ABC123', + commitMessage: 'commit message', + logger: getVoidLogger(), + }); + + expect(mockedGit.push).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + remote: 'origin', + remoteRef: 'ABC123', + }); + }); +}); diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.ts b/plugins/scaffolder-node/src/actions/gitHelpers.ts index 6d449a0a29..f2c88872ad 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.ts @@ -16,6 +16,7 @@ import { Git } from '@backstage/backend-common'; import { Logger } from 'winston'; +import fs from 'fs-extra'; /** * @public @@ -134,3 +135,68 @@ export async function commitAndPushRepo(input: { return { commitHash }; } + +export async function commitAndPushBranch({ + tempDir, + dir, + remoteUrl, + auth, + logger, + commitMessage, + gitAuthorInfo, + branch = 'master', + remoteRef, +}: { + tempDir: string; + dir: string; + remoteUrl: string; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; + logger: Logger; + commitMessage: string; + gitAuthorInfo?: { name?: string; email?: string }; + branch?: string; + remoteRef?: string; +}): Promise<{ commitHash: string }> { + const git = Git.fromAuth({ + ...auth, + logger, + }); + + await git.clone({ url: remoteUrl, dir: tempDir }); + await git.fetch({ dir: tempDir }); + await git.checkout({ dir: tempDir, ref: branch }); + + // copy files + fs.cpSync(dir, tempDir, { + recursive: true, + filter: path => { + return !(path.indexOf('.git') > -1); + }, + }); + + await git.add({ dir: tempDir, filepath: '.' }); + + // use provided info if possible, otherwise use fallbacks + const authorInfo = { + name: gitAuthorInfo?.name ?? 'Scaffolder', + email: gitAuthorInfo?.email ?? 'scaffolder@backstage.io', + }; + + const commitHash = await git.commit({ + dir: tempDir, + message: commitMessage, + author: authorInfo, + committer: authorInfo, + }); + + await git.push({ + dir: tempDir, + remote: 'origin', + remoteRef: remoteRef ?? `refs/heads/${branch}`, + }); + + return { commitHash }; +} diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts index 74a0bb61f9..885f34c293 100644 --- a/plugins/scaffolder-node/src/actions/index.ts +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -25,5 +25,9 @@ export { } from './executeShellCommand'; export { fetchContents, fetchFile } from './fetch'; export { type ActionContext, type TemplateAction } from './types'; -export { initRepoAndPush, commitAndPushRepo } from './gitHelpers'; +export { + initRepoAndPush, + commitAndPushRepo, + commitAndPushBranch, +} from './gitHelpers'; export { parseRepoUrl, getRepoSourceDirectory } from './util'; From fc98bb6f8cd85a646c254f2ad8ef8025be4ecb7e Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Fri, 15 Dec 2023 16:11:45 -0500 Subject: [PATCH 2/9] Fix for issue #21762 Signed-off-by: Jason Froehlich Signed-off-by: Jason Froehlich --- .changeset/unlucky-wasps-tan.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/unlucky-wasps-tan.md diff --git a/.changeset/unlucky-wasps-tan.md b/.changeset/unlucky-wasps-tan.md new file mode 100644 index 0000000000..93665449f2 --- /dev/null +++ b/.changeset/unlucky-wasps-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket': minor +--- + +Enhanced the pull request action to allow for adding new content to the PR as described in this issue #2172 From 3a9ba42b4d4c4139f6d90cf0f02a86eab977aa3d Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Fri, 15 Dec 2023 16:22:02 -0500 Subject: [PATCH 3/9] Fix for issue #21762 Signed-off-by: Jason Froehlich Signed-off-by: Jason Froehlich --- .changeset/nine-olives-swim.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nine-olives-swim.md diff --git a/.changeset/nine-olives-swim.md b/.changeset/nine-olives-swim.md new file mode 100644 index 0000000000..057629a8c3 --- /dev/null +++ b/.changeset/nine-olives-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': minor +--- + +Added function to commitAndPushBranch which allows for files to be added to the a PR for use in the bitbucket pull request action for issue #21762 From c700fdc7242f8b63560b048f89e493fa23429682 Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Sat, 30 Dec 2023 13:09:34 -0500 Subject: [PATCH 4/9] Updated release tag on commitAndPushBranch function and api-report Signed-off-by: Jason Froehlich --- plugins/scaffolder-node/api-report.md | 35 +++++++++++++++++++ .../scaffolder-node/src/actions/gitHelpers.ts | 3 ++ 2 files changed, 38 insertions(+) diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 6bb1534679..f3d3a7b2f2 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -45,6 +45,41 @@ export type ActionContext< each?: JsonObject; }; +// @public (undocumented) +export function commitAndPushBranch({ + tempDir, + dir, + remoteUrl, + auth, + logger, + commitMessage, + gitAuthorInfo, + branch, + remoteRef, +}: { + tempDir: string; + dir: string; + remoteUrl: string; + auth: + | { + username: string; + password: string; + } + | { + token: string; + }; + logger: Logger; + commitMessage: string; + gitAuthorInfo?: { + name?: string; + email?: string; + }; + branch?: string; + remoteRef?: string; +}): Promise<{ + commitHash: string; +}>; + // @public (undocumented) export function commitAndPushRepo(input: { dir: string; diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.ts b/plugins/scaffolder-node/src/actions/gitHelpers.ts index f2c88872ad..45a7761c0a 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.ts @@ -136,6 +136,9 @@ export async function commitAndPushRepo(input: { return { commitHash }; } +/** + * @public + */ export async function commitAndPushBranch({ tempDir, dir, From 76fffb321dc22fbd54ae15fb45c26102bd75e8b8 Mon Sep 17 00:00:00 2001 From: Jason Froehlich <38667521+jayfray12@users.noreply.github.com> Date: Thu, 11 Jan 2024 08:49:27 -0500 Subject: [PATCH 5/9] Update .changeset/unlucky-wasps-tan.md Co-authored-by: Ben Lambert Signed-off-by: Jason Froehlich <38667521+jayfray12@users.noreply.github.com> Signed-off-by: Jason Froehlich --- .changeset/unlucky-wasps-tan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/unlucky-wasps-tan.md b/.changeset/unlucky-wasps-tan.md index 93665449f2..df6eca4820 100644 --- a/.changeset/unlucky-wasps-tan.md +++ b/.changeset/unlucky-wasps-tan.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend-module-bitbucket': minor +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch --- Enhanced the pull request action to allow for adding new content to the PR as described in this issue #2172 From b8b8b884133d9881035f5f1d24506a8657186c88 Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Tue, 16 Jan 2024 12:07:23 -0500 Subject: [PATCH 6/9] made git commands more generic and resusable Signed-off-by: Jason Froehlich --- .changeset/nine-olives-swim.md | 2 +- .../src/actions/bitbucketServerPullRequest.ts | 40 ++- plugins/scaffolder-node/api-report.md | 84 ++++- .../src/actions/gitHelpers.test.ts | 310 ++++++++++++++---- .../scaffolder-node/src/actions/gitHelpers.ts | 112 +++++-- plugins/scaffolder-node/src/actions/index.ts | 3 + 6 files changed, 455 insertions(+), 96 deletions(-) diff --git a/.changeset/nine-olives-swim.md b/.changeset/nine-olives-swim.md index 057629a8c3..40852c08c2 100644 --- a/.changeset/nine-olives-swim.md +++ b/.changeset/nine-olives-swim.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-node': minor --- -Added function to commitAndPushBranch which allows for files to be added to the a PR for use in the bitbucket pull request action for issue #21762 +Added functions to clone a repo, create a branch, add files and push and commit to the branch. This allows for files to be added to the a PR for use in the bitbucket pull request action for issue #21762 diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts index 66327d4f6c..fc123f0b80 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketServerPullRequest.ts @@ -23,10 +23,14 @@ import { createTemplateAction, getRepoSourceDirectory, commitAndPushBranch, + addFiles, + createBranch as createGitBranch, + cloneRepo, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import fetch, { RequestInit, Response } from 'node-fetch'; import { Config } from '@backstage/config'; +import fs from 'fs-extra'; const createPullRequest = async (opts: { project: string; @@ -354,10 +358,40 @@ export function createPublishBitbucketServerPullRequestAction(options: { email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; + const tempDir = await ctx.createTemporaryDirectory(); + const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined); + await cloneRepo({ + url: remoteUrl, + dir: tempDir, + auth, + logger: ctx.logger, + ref: sourceBranch, + }); + + await createGitBranch({ + dir: tempDir, + auth, + logger: ctx.logger, + ref: sourceBranch, + }); + + // copy files + fs.cpSync(sourceDir, tempDir, { + recursive: true, + filter: path => { + return !(path.indexOf('.git') > -1); + }, + }); + + await addFiles({ + dir: tempDir, + auth, + logger: ctx.logger, + filepath: '.', + }); + await commitAndPushBranch({ - tempDir: await ctx.createTemporaryDirectory(), - dir: getRepoSourceDirectory(ctx.workspacePath, undefined), - remoteUrl, + dir: tempDir, auth, logger: ctx.logger, commitMessage: diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index f3d3a7b2f2..5dff4a8e5e 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -46,20 +46,14 @@ export type ActionContext< }; // @public (undocumented) -export function commitAndPushBranch({ - tempDir, +export function addFiles({ dir, - remoteUrl, + filepath, auth, logger, - commitMessage, - gitAuthorInfo, - branch, - remoteRef, }: { - tempDir: string; dir: string; - remoteUrl: string; + filepath: string; auth: | { username: string; @@ -68,7 +62,56 @@ export function commitAndPushBranch({ | { token: string; }; - logger: Logger; + logger?: Logger | undefined; +}): Promise; + +// @public (undocumented) +export function cloneRepo({ + url, + dir, + auth, + logger, + ref, + depth, + noCheckout, +}: { + url: string; + dir: string; + auth: + | { + username: string; + password: string; + } + | { + token: string; + }; + logger?: Logger | undefined; + ref?: string | undefined; + depth?: number | undefined; + noCheckout?: boolean | undefined; +}): Promise; + +// @public (undocumented) +export function commitAndPushBranch({ + dir, + auth, + logger, + commitMessage, + gitAuthorInfo, + branch, + remoteRef, + remote, +}: { + dir: string; + auth: + | { + username: string; + password: string; + } + | { + token: string; + }; + logger?: Logger | undefined; commitMessage: string; gitAuthorInfo?: { name?: string; @@ -76,6 +119,7 @@ export function commitAndPushBranch({ }; branch?: string; remoteRef?: string; + remote?: string; }): Promise<{ commitHash: string; }>; @@ -103,6 +147,26 @@ export function commitAndPushRepo(input: { commitHash: string; }>; +// @public (undocumented) +export function createBranch({ + dir, + auth, + logger, + ref, +}: { + dir: string; + ref: string; + auth: + | { + username: string; + password: string; + } + | { + token: string; + }; + logger?: Logger | undefined; +}): Promise; + // @public export const createTemplateAction: < TInputParams extends JsonObject = JsonObject, diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.test.ts b/plugins/scaffolder-node/src/actions/gitHelpers.test.ts index 2d2220f33d..b230b2cd06 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.test.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.test.ts @@ -19,6 +19,9 @@ import { commitAndPushRepo, initRepoAndPush, commitAndPushBranch, + addFiles, + createBranch, + cloneRepo, } from './gitHelpers'; jest.mock('@backstage/backend-common', () => ({ @@ -312,6 +315,198 @@ describe('commitAndPushRepo', () => { }); }); +describe('cloneRepo', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('with minimal parameters', () => { + beforeEach(async () => { + await cloneRepo({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + }); + + it('clone the repo', () => { + expect(mockedGit.clone).toHaveBeenCalledWith({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + ref: undefined, + depth: undefined, + noCheckout: undefined, + }); + }); + }); + + it('with token', async () => { + await cloneRepo({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + auth: { + token: 'test-token', + }, + }); + + expect(mockedGit.clone).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + url: 'git@github.com:test/repo.git', + ref: undefined, + depth: undefined, + noCheckout: undefined, + }); + }); + + it('allows overriding the default branch', async () => { + await cloneRepo({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + ref: 'trunk', + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + + expect(mockedGit.clone).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + url: 'git@github.com:test/repo.git', + ref: 'trunk', + depth: undefined, + noCheckout: undefined, + }); + }); + + it('allows overriding the depth', async () => { + await cloneRepo({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + ref: 'trunk', + depth: 2, + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + + expect(mockedGit.clone).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + url: 'git@github.com:test/repo.git', + ref: 'trunk', + depth: 2, + noCheckout: undefined, + }); + }); + + it('allows overriding the noCheckout', async () => { + await cloneRepo({ + url: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', + ref: 'trunk', + depth: 2, + noCheckout: true, + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + + expect(mockedGit.clone).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + url: 'git@github.com:test/repo.git', + ref: 'trunk', + depth: 2, + noCheckout: true, + }); + }); +}); + +describe('createBranch', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('with minimal parameters', () => { + beforeEach(async () => { + await createBranch({ + dir: '/tmp/repo/dir/', + ref: 'trunk', + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + }); + + it('create the branch', () => { + expect(mockedGit.checkout).toHaveBeenCalledWith({ + ref: 'trunk', + dir: '/tmp/repo/dir/', + }); + }); + }); + + it('with token', async () => { + await createBranch({ + dir: '/tmp/repo/dir/', + ref: 'trunk', + auth: { + token: 'test-token', + }, + }); + + expect(mockedGit.checkout).toHaveBeenCalledWith({ + ref: 'trunk', + dir: '/tmp/repo/dir/', + }); + }); +}); + +describe('addFiles', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('with minimal parameters', () => { + beforeEach(async () => { + await addFiles({ + dir: '/tmp/repo/dir/', + filepath: '.', + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + }); + + it('add files', () => { + expect(mockedGit.add).toHaveBeenCalledWith({ + filepath: '.', + dir: '/tmp/repo/dir/', + }); + }); + }); + + it('with token', async () => { + await addFiles({ + dir: '/tmp/repo/dir/', + filepath: '.', + auth: { + token: 'test-token', + }, + }); + + expect(mockedGit.add).toHaveBeenCalledWith({ + filepath: '.', + dir: '/tmp/repo/dir/', + }); + }); +}); + describe('commitAndPushBranch', () => { afterEach(() => { jest.clearAllMocks(); @@ -320,53 +515,19 @@ describe('commitAndPushBranch', () => { describe('with minimal parameters', () => { beforeEach(async () => { await commitAndPushBranch({ - tempDir: '/tmp/repo/dir/', - dir: '/test/repo/dir/', - remoteUrl: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', auth: { username: 'test-user', password: 'test-password', }, commitMessage: 'commit message', - gitAuthorInfo: { - name: 'Scaffolder', - email: 'scaffolder@backstage.io', - }, - logger: getVoidLogger(), }); }); - it('clone the repo', () => { - expect(mockedGit.clone).toHaveBeenCalledWith({ - url: 'git@github.com:test/repo.git', - dir: '/tmp/repo/dir/', - }); - }); - - it('fetch the branches', () => { - expect(mockedGit.fetch).toHaveBeenCalledWith({ - dir: '/tmp/repo/dir/', - }); - }); - - it('checkout the branch', () => { - expect(mockedGit.checkout).toHaveBeenCalledWith({ - dir: '/tmp/repo/dir/', - ref: 'master', - }); - }); - - it('stages all files in the repo', () => { - expect(mockedGit.add).toHaveBeenCalledWith({ - dir: '/tmp/repo/dir/', - filepath: '.', - }); - }); - - it('creates an commit', () => { + it('create commit', () => { expect(mockedGit.commit).toHaveBeenCalledWith({ - dir: '/tmp/repo/dir/', message: 'commit message', + dir: '/tmp/repo/dir/', author: { name: 'Scaffolder', email: 'scaffolder@backstage.io', @@ -389,58 +550,91 @@ describe('commitAndPushBranch', () => { it('with token', async () => { await commitAndPushBranch({ - tempDir: '/tmp/repo/dir/', - dir: '/test/repo/dir/', - remoteUrl: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', auth: { token: 'test-token', }, commitMessage: 'commit message', + gitAuthorInfo: { + name: 'gitCommitter', + email: 'gitCommitter@backstage.io', + }, logger: getVoidLogger(), }); - expect(mockedGit.clone).toHaveBeenCalledWith({ + expect(mockedGit.commit).toHaveBeenCalledWith({ + message: 'commit message', dir: '/tmp/repo/dir/', - url: 'git@github.com:test/repo.git', + author: { + name: 'gitCommitter', + email: 'gitCommitter@backstage.io', + }, + committer: { + name: 'gitCommitter', + email: 'gitCommitter@backstage.io', + }, + }); + + expect(mockedGit.push).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + remote: 'origin', + remoteRef: 'refs/heads/master', }); }); it('allows overriding the default branch', async () => { await commitAndPushBranch({ - tempDir: '/tmp/repo/dir/', - dir: '/test/repo/dir/', - branch: 'trunk', - remoteUrl: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', auth: { username: 'test-user', password: 'test-password', }, commitMessage: 'commit message', - logger: getVoidLogger(), + branch: 'trunk', }); - expect(mockedGit.checkout).toHaveBeenCalledWith({ + expect(mockedGit.commit).toHaveBeenCalledWith({ + message: 'commit message', dir: '/tmp/repo/dir/', - ref: 'trunk', + author: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, + committer: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, + }); + + expect(mockedGit.push).toHaveBeenCalledWith({ + dir: '/tmp/repo/dir/', + remote: 'origin', + remoteRef: 'refs/heads/trunk', }); }); it('allows overriding the remoteRef', async () => { await commitAndPushBranch({ - tempDir: '/tmp/repo/dir/', - dir: '/test/repo/dir/', - gitAuthorInfo: { - name: 'Custom Scaffolder Author', - email: 'scaffolder@example.org', - }, - remoteUrl: 'git@github.com:test/repo.git', + dir: '/tmp/repo/dir/', auth: { username: 'test-user', password: 'test-password', }, - remoteRef: 'ABC123', commitMessage: 'commit message', - logger: getVoidLogger(), + remoteRef: 'ABC123', + }); + + expect(mockedGit.commit).toHaveBeenCalledWith({ + message: 'commit message', + dir: '/tmp/repo/dir/', + author: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, + committer: { + name: 'Scaffolder', + email: 'scaffolder@backstage.io', + }, }); expect(mockedGit.push).toHaveBeenCalledWith({ diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.ts b/plugins/scaffolder-node/src/actions/gitHelpers.ts index 45a7761c0a..7f36ae0137 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.ts @@ -16,7 +16,6 @@ import { Git } from '@backstage/backend-common'; import { Logger } from 'winston'; -import fs from 'fs-extra'; /** * @public @@ -139,49 +138,114 @@ export async function commitAndPushRepo(input: { /** * @public */ -export async function commitAndPushBranch({ - tempDir, +export async function cloneRepo({ + url, + dir, + auth, + logger, + ref, + depth, + noCheckout, +}: { + url: string; + dir: string; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; + logger?: Logger | undefined; + ref?: string | undefined; + depth?: number | undefined; + noCheckout?: boolean | undefined; +}): Promise { + const git = Git.fromAuth({ + ...auth, + logger, + }); + + await git.clone({ url, dir, ref, depth, noCheckout }); +} + +/** + * @public + */ +export async function createBranch({ + dir, + auth, + logger, + ref, +}: { + dir: string; + ref: string; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; + logger?: Logger | undefined; +}): Promise { + const git = Git.fromAuth({ + ...auth, + logger, + }); + + await git.checkout({ dir, ref }); +} + +/** + * @public + */ +export async function addFiles({ + dir, + filepath, + auth, + logger, +}: { + dir: string; + filepath: string; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; + logger?: Logger | undefined; +}): Promise { + const git = Git.fromAuth({ + ...auth, + logger, + }); + + await git.add({ dir, filepath }); +} + +/** + * @public + */ +export async function commitAndPushBranch({ dir, - remoteUrl, auth, logger, commitMessage, gitAuthorInfo, branch = 'master', remoteRef, + remote = 'origin', }: { - tempDir: string; dir: string; - remoteUrl: string; // For use cases where token has to be used with Basic Auth // it has to be provided as password together with a username // which may be a fixed value defined by the provider. auth: { username: string; password: string } | { token: string }; - logger: Logger; + logger?: Logger | undefined; commitMessage: string; gitAuthorInfo?: { name?: string; email?: string }; branch?: string; remoteRef?: string; + remote?: string; }): Promise<{ commitHash: string }> { const git = Git.fromAuth({ ...auth, logger, }); - await git.clone({ url: remoteUrl, dir: tempDir }); - await git.fetch({ dir: tempDir }); - await git.checkout({ dir: tempDir, ref: branch }); - - // copy files - fs.cpSync(dir, tempDir, { - recursive: true, - filter: path => { - return !(path.indexOf('.git') > -1); - }, - }); - - await git.add({ dir: tempDir, filepath: '.' }); - // use provided info if possible, otherwise use fallbacks const authorInfo = { name: gitAuthorInfo?.name ?? 'Scaffolder', @@ -189,15 +253,15 @@ export async function commitAndPushBranch({ }; const commitHash = await git.commit({ - dir: tempDir, + dir, message: commitMessage, author: authorInfo, committer: authorInfo, }); await git.push({ - dir: tempDir, - remote: 'origin', + dir, + remote, remoteRef: remoteRef ?? `refs/heads/${branch}`, }); diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts index 885f34c293..c1c8551c53 100644 --- a/plugins/scaffolder-node/src/actions/index.ts +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -29,5 +29,8 @@ export { initRepoAndPush, commitAndPushRepo, commitAndPushBranch, + addFiles, + createBranch, + cloneRepo, } from './gitHelpers'; export { parseRepoUrl, getRepoSourceDirectory } from './util'; From f7cc86f9dd4878d751f580877a8a9e4ef4f4f696 Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Tue, 16 Jan 2024 15:00:05 -0500 Subject: [PATCH 7/9] clean-up code, fixed issue number and added import Signed-off-by: Jason Froehlich --- .changeset/unlucky-wasps-tan.md | 2 +- .../package.json | 1 + .../scaffolder-node/src/actions/gitHelpers.ts | 49 +++++++------------ 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/.changeset/unlucky-wasps-tan.md b/.changeset/unlucky-wasps-tan.md index df6eca4820..97813cb416 100644 --- a/.changeset/unlucky-wasps-tan.md +++ b/.changeset/unlucky-wasps-tan.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-bitbucket': patch --- -Enhanced the pull request action to allow for adding new content to the PR as described in this issue #2172 +Enhanced the pull request action to allow for adding new content to the PR as described in this issue #21762 diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index 03ebab43fc..e045bb9dc8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -28,6 +28,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", + "fs-extra": "10.1.0", "node-fetch": "^2.6.7", "yaml": "^2.0.0" }, diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.ts b/plugins/scaffolder-node/src/actions/gitHelpers.ts index 7f36ae0137..2026e75247 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.ts @@ -138,15 +138,7 @@ export async function commitAndPushRepo(input: { /** * @public */ -export async function cloneRepo({ - url, - dir, - auth, - logger, - ref, - depth, - noCheckout, -}: { +export async function cloneRepo(options: { url: string; dir: string; // For use cases where token has to be used with Basic Auth @@ -158,6 +150,8 @@ export async function cloneRepo({ depth?: number | undefined; noCheckout?: boolean | undefined; }): Promise { + const { url, dir, auth, logger, ref, depth, noCheckout } = options; + const git = Git.fromAuth({ ...auth, logger, @@ -169,12 +163,7 @@ export async function cloneRepo({ /** * @public */ -export async function createBranch({ - dir, - auth, - logger, - ref, -}: { +export async function createBranch(options: { dir: string; ref: string; // For use cases where token has to be used with Basic Auth @@ -183,6 +172,7 @@ export async function createBranch({ auth: { username: string; password: string } | { token: string }; logger?: Logger | undefined; }): Promise { + const { dir, ref, auth, logger } = options; const git = Git.fromAuth({ ...auth, logger, @@ -194,12 +184,7 @@ export async function createBranch({ /** * @public */ -export async function addFiles({ - dir, - filepath, - auth, - logger, -}: { +export async function addFiles(options: { dir: string; filepath: string; // For use cases where token has to be used with Basic Auth @@ -208,6 +193,7 @@ export async function addFiles({ auth: { username: string; password: string } | { token: string }; logger?: Logger | undefined; }): Promise { + const { dir, filepath, auth, logger } = options; const git = Git.fromAuth({ ...auth, logger, @@ -219,16 +205,7 @@ export async function addFiles({ /** * @public */ -export async function commitAndPushBranch({ - dir, - auth, - logger, - commitMessage, - gitAuthorInfo, - branch = 'master', - remoteRef, - remote = 'origin', -}: { +export async function commitAndPushBranch(options: { dir: string; // For use cases where token has to be used with Basic Auth // it has to be provided as password together with a username @@ -241,6 +218,16 @@ export async function commitAndPushBranch({ remoteRef?: string; remote?: string; }): Promise<{ commitHash: string }> { + const { + dir, + auth, + logger, + commitMessage, + gitAuthorInfo, + branch = 'master', + remoteRef, + remote = 'origin', + } = options; const git = Git.fromAuth({ ...auth, logger, From c1cf957843ea231027896203d089acd6f75c367c Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Tue, 16 Jan 2024 15:22:13 -0500 Subject: [PATCH 8/9] Add fs-extra Signed-off-by: Jason Froehlich --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 33ac9d7aab..a7e2c1ac51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8045,6 +8045,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + fs-extra: 10.1.0 msw: ^1.0.0 node-fetch: ^2.6.7 yaml: ^2.0.0 From 20a0f4b29a85419aa786f2db539a493fdff2da6d Mon Sep 17 00:00:00 2001 From: Jason Froehlich Date: Tue, 16 Jan 2024 15:55:13 -0500 Subject: [PATCH 9/9] updated api reports Signed-off-by: Jason Froehlich --- plugins/scaffolder-node/api-report.md | 35 +++------------------------ 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 5dff4a8e5e..1d9adb20d9 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -46,12 +46,7 @@ export type ActionContext< }; // @public (undocumented) -export function addFiles({ - dir, - filepath, - auth, - logger, -}: { +export function addFiles(options: { dir: string; filepath: string; auth: @@ -66,15 +61,7 @@ export function addFiles({ }): Promise; // @public (undocumented) -export function cloneRepo({ - url, - dir, - auth, - logger, - ref, - depth, - noCheckout, -}: { +export function cloneRepo(options: { url: string; dir: string; auth: @@ -92,16 +79,7 @@ export function cloneRepo({ }): Promise; // @public (undocumented) -export function commitAndPushBranch({ - dir, - auth, - logger, - commitMessage, - gitAuthorInfo, - branch, - remoteRef, - remote, -}: { +export function commitAndPushBranch(options: { dir: string; auth: | { @@ -148,12 +126,7 @@ export function commitAndPushRepo(input: { }>; // @public (undocumented) -export function createBranch({ - dir, - auth, - logger, - ref, -}: { +export function createBranch(options: { dir: string; ref: string; auth: