diff --git a/.changeset/few-hairs-compare.md b/.changeset/few-hairs-compare.md new file mode 100644 index 0000000000..48cd7b7f25 --- /dev/null +++ b/.changeset/few-hairs-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +GitLab MR: introduce 'skip' commit action. diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index eba3426349..69d92ac5d9 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -55,6 +55,7 @@ "@gitbeaker/node": "^35.8.0", "@gitbeaker/rest": "^39.25.0", "luxon": "^3.0.0", + "winston": "^3.2.1", "yaml": "^2.0.0", "zod": "^3.22.4" }, diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index f00055a876..4ed062d21a 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -193,7 +193,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index 46e60584c9..cffa5fd666 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -30,6 +30,22 @@ const mockGitlabClient = { }, Branches: { create: jest.fn(), + show: jest.fn(async (_repoID: string | number, name: string) => { + if (['main', 'existing-branch'].includes(name)) { + return { + name, + merged: name === 'main', + protected: name === 'main', + default: name === 'main', + developers_can_push: name !== 'main', + developers_can_merge: name !== 'main', + can_push: name !== 'main', + web_url: `https://foo.bar.baz/owner/repo/-/tree/${name}`, + commit: { message: 'last change' }, + }; + } + throw new Error(`Unknown branch ${name}`); + }), }, Commits: { create: jest.fn(), @@ -83,6 +99,28 @@ const mockGitlabClient = { }, ), }, + RepositoryFiles: { + show: jest.fn( + async (repoID: string | number, filePath: string, ref: string) => { + if (repoID !== 'owner/repo') throw new Error('repo does not exist'); + if (filePath !== 'source/auto.txt') + throw new Error('filePath does not exist'); + return { + file_name: 'auto.txt', + file_path: 'source/auto.txt', + size: 11, + encoding: 'base64', + content: 'Zm9vLWJhci1iYXo=', + content_sha256: + '269dce1a5bb90188b2d9cf542a7c30e410c7d8251e34a97bfea56062df51ae23', + ref, + blob_id: 'a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba', + commit_id: 'd5a3ff139356ce33e37e73add446f16869741b50', + last_commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + }; + }, + ), + }, }; jest.mock('@gitbeaker/node', () => ({ @@ -149,6 +187,7 @@ describe('createGitLabMergeRequest', () => { 'new-mr', 'test', ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -184,6 +223,7 @@ describe('createGitLabMergeRequest', () => { 'new-mr', 'main', ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -191,7 +231,6 @@ describe('createGitLabMergeRequest', () => { 'Create my new MR', { description: 'This MR is really good', removeSourceBranch: false }, ); - expect(ctx.output).toHaveBeenCalledWith('targetBranchName', 'main'); }); }); @@ -216,6 +255,12 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -244,6 +289,12 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -278,6 +329,12 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -311,6 +368,12 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -344,6 +407,12 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -376,6 +445,12 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -404,6 +479,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -425,6 +505,16 @@ describe('createGitLabMergeRequest', () => { }, ]), ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This MR is really good', + removeSourceBranch: false, + }, + ); }); }); @@ -446,6 +536,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -467,6 +562,16 @@ describe('createGitLabMergeRequest', () => { }, ]), ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This MR is really good', + removeSourceBranch: false, + }, + ); }); }); @@ -490,6 +595,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -504,6 +614,16 @@ describe('createGitLabMergeRequest', () => { }, ], ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); }); it('commitAction is update when update is passed in options', async () => { @@ -525,6 +645,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -539,6 +664,16 @@ describe('createGitLabMergeRequest', () => { }, ], ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); }); it('commitAction is auto when auto is passed in options', async () => { @@ -558,6 +693,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -579,6 +719,16 @@ describe('createGitLabMergeRequest', () => { }, ]), ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); }); it('commitAction is auto when auto is passed in options with targetPath', async () => { @@ -600,6 +750,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -621,6 +776,16 @@ describe('createGitLabMergeRequest', () => { }, ]), ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); }); it('commitAction is delete when delete is passed in options', async () => { @@ -642,6 +807,11 @@ describe('createGitLabMergeRequest', () => { const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -656,6 +826,119 @@ describe('createGitLabMergeRequest', () => { }, ], ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'other MR description', + removeSourceBranch: false, + }, + ); + }); + it('commitAction skip skips commit', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'MR description', + commitAction: 'skip', + }; + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); + }); + it('commitAction skip reuses existing branch', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'existing-branch', + description: 'MR description', + commitAction: 'skip', + }; + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.show).toHaveBeenCalledWith( + 'owner/repo', + 'existing-branch', + ); + expect(mockGitlabClient.Branches.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'existing-branch', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); + }); + it('commitAction auto skips unmodified files', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'MR description', + commitAction: 'auto', + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!', 'auto.txt': 'foo-bar-baz' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'Create my new MR', + expect.arrayContaining([ + { + action: 'create', + filePath: 'source/foo.txt', + content: 'SGVsbG8gdGhlcmUh', + encoding: 'base64', + execute_filemode: false, + }, + ]), + ); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'MR description', + removeSourceBranch: false, + }, + ); }); }); @@ -681,6 +964,11 @@ describe('createGitLabMergeRequest', () => { await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', @@ -719,6 +1007,11 @@ describe('createGitLabMergeRequest', () => { await instance.handler(ctx); + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( 'owner/repo', 'new-mr', diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts index 733365c6df..ec9ae7317d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts @@ -20,25 +20,58 @@ import { SerializedFile, serializeDirectoryContents, } from '@backstage/plugin-scaffolder-node'; -import { Types } from '@gitbeaker/core'; +import { Gitlab, Types } from '@gitbeaker/core'; import path from 'path'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { InputError } from '@backstage/errors'; -import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; +import { + LoggerService, + resolveSafeChildPath, +} from '@backstage/backend-plugin-api'; import { createGitlabApi } from './helpers'; import { examples } from './gitlabMergeRequest.examples'; +import { createHash } from 'crypto'; -function getFileAction( - fileInfo: { file: SerializedFile; targetPath: string | undefined }, +function computeSha256(file: SerializedFile): string { + const hash = createHash('sha256'); + hash.update(file.content); + return hash.digest('hex'); +} + +async function getFileAction( + fileInfo: { file: SerializedFile; targetPath?: string }, + target: { repoID: string; branch: string }, + api: Gitlab, + logger: LoggerService, remoteFiles: Types.RepositoryTreeSchema[], - defaultCommitAction: 'create' | 'delete' | 'update' | 'auto' | undefined, -): 'create' | 'delete' | 'update' { - if (!defaultCommitAction || defaultCommitAction === 'auto') { + defaultCommitAction: + | 'create' + | 'delete' + | 'update' + | 'skip' + | 'auto' = 'auto', +): Promise<'create' | 'delete' | 'update' | 'skip'> { + if (defaultCommitAction === 'auto') { const filePath = path.join(fileInfo.targetPath ?? '', fileInfo.file.path); - return remoteFiles && - remoteFiles.some(remoteFile => remoteFile.path === filePath) - ? 'update' - : 'create'; + + if (remoteFiles?.some(remoteFile => remoteFile.path === filePath)) { + try { + const targetFile = await api.RepositoryFiles.show( + target.repoID, + filePath, + target.branch, + ); + if (computeSha256(fileInfo.file) === targetFile.content_sha256) { + return 'skip'; + } + } catch (error) { + logger.warn( + `Unable to retrieve detailed information for remote file ${filePath}`, + ); + } + return 'update'; + } + return 'create'; } return defaultCommitAction; } @@ -62,7 +95,7 @@ export const createPublishGitlabMergeRequestAction = (options: { sourcePath?: string; targetPath?: string; token?: string; - commitAction?: 'create' | 'delete' | 'update' | 'auto'; + commitAction?: 'create' | 'delete' | 'update' | 'skip' | 'auto'; /** @deprecated projectID passed as query parameters in the repoUrl */ projectid?: string; removeSourceBranch?: boolean; @@ -78,7 +111,9 @@ export const createPublishGitlabMergeRequestAction = (options: { repoUrl: { type: 'string', title: 'Repository Location', - description: `Accepts the format 'gitlab.com?repo=project_name&owner=group_name' where 'project_name' is the repository name and 'group_name' is a group or username`, + description: `\ +Accepts the format 'gitlab.com?repo=project_name&owner=group_name' where \ +'project_name' is the repository name and 'group_name' is a group or username`, }, /** @deprecated projectID is passed as query parameters in the repoUrl */ projectid: { @@ -109,8 +144,11 @@ export const createPublishGitlabMergeRequestAction = (options: { sourcePath: { type: 'string', title: 'Working Subdirectory', - description: - 'Subdirectory of working directory to copy changes from', + description: `\ +Subdirectory of working directory to copy changes from. \ +For reasons of backward compatibility, any specified 'targetPath' input will \ +be applied in place of an absent/falsy value for this input. \ +Circumvent this behavior using '.'`, }, targetPath: { type: 'string', @@ -126,8 +164,9 @@ export const createPublishGitlabMergeRequestAction = (options: { title: 'Commit action', type: 'string', enum: ['create', 'update', 'delete', 'auto'], - description: - 'The action to be used for git commit. Defaults to auto. "auto" is custom action provide by backstage, (automatic assign create or update action) /!\\ Use more api calls /!\\ *', + description: `\ +The action to be used for git commit. Defaults to the custom 'auto' action provided by backstage, +which uses additional API calls in order to detect whether to 'create', 'update' or 'skip' each source file.`, }, removeSourceBranch: { title: 'Delete source branch', @@ -224,7 +263,7 @@ export const createPublishGitlabMergeRequestAction = (options: { } let remoteFiles: Types.RepositoryTreeSchema[] = []; - if (!ctx.input.commitAction || ctx.input.commitAction === 'auto') { + if ((ctx.input.commitAction ?? 'auto') === 'auto') { try { remoteFiles = await api.Repositories.tree(repoID, { ref: targetBranch, @@ -237,37 +276,70 @@ export const createPublishGitlabMergeRequestAction = (options: { ); } } + const actions: Types.CommitAction[] = + ctx.input.commitAction === 'skip' + ? [] + : ( + ( + await Promise.all( + fileContents.map(async file => { + const action = await getFileAction( + { file, targetPath }, + { repoID, branch: targetBranch! }, + api, + ctx.logger, + remoteFiles, + ctx.input.commitAction, + ); + return { file, action }; + }), + ) + ).filter(o => o.action !== 'skip') as { + file: SerializedFile; + action: Types.CommitAction['action']; + }[] + ).map(({ file, action }) => ({ + action, + filePath: targetPath + ? path.posix.join(targetPath, file.path) + : file.path, + encoding: 'base64', + content: file.content.toString('base64'), + execute_filemode: file.executable, + })); - const actions: Types.CommitAction[] = fileContents.map(file => ({ - action: getFileAction( - { file, targetPath }, - remoteFiles, - ctx.input.commitAction, - ), - filePath: targetPath - ? path.posix.join(targetPath, file.path) - : file.path, - encoding: 'base64', - content: file.content.toString('base64'), - execute_filemode: file.executable, - })); - - try { - await api.Branches.create(repoID, branchName, String(targetBranch)); - } catch (e) { - throw new InputError( - `The branch creation failed. Please check that your repo does not already contain a branch named '${branchName}'. ${e}`, - ); + let createBranch: boolean; + if (actions.length) { + createBranch = true; + } else { + try { + await api.Branches.show(repoID, branchName); + createBranch = false; + ctx.logger.info( + `Using existing branch ${branchName} without modification.`, + ); + } catch (e) { + createBranch = true; + } } - - try { - await api.Commits.create(repoID, branchName, ctx.input.title, actions); - } catch (e) { - throw new InputError( - `Committing the changes to ${branchName} failed. Please check that none of the files created by the template already exists. ${e}`, - ); + if (createBranch) { + try { + await api.Branches.create(repoID, branchName, String(targetBranch)); + } catch (e) { + throw new InputError( + `The branch creation failed. Please check that your repo does not already contain a branch named '${branchName}'. ${e}`, + ); + } + } + if (actions.length) { + try { + await api.Commits.create(repoID, branchName, title, actions); + } catch (e) { + throw new InputError( + `Committing the changes to ${branchName} failed. Please check that none of the files created by the template already exists. ${e}`, + ); + } } - try { const mergeRequestUrl = await api.MergeRequests.create( repoID, diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index d3dddc21e9..5c513d75cd 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -334,7 +334,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/yarn.lock b/yarn.lock index f3f2c7beaa..8bbf86bd74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7456,6 +7456,7 @@ __metadata: "@gitbeaker/node": ^35.8.0 "@gitbeaker/rest": ^39.25.0 luxon: ^3.0.0 + winston: ^3.2.1 yaml: ^2.0.0 zod: ^3.22.4 languageName: unknown