diff --git a/.changeset/deep-kings-strive.md b/.changeset/deep-kings-strive.md new file mode 100644 index 0000000000..afe7415ec5 --- /dev/null +++ b/.changeset/deep-kings-strive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added `allowEmpty` input option to the `gitlab:repo:push` action, allowing empty commits. Required from GitLab 18.8 or later. diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index a5a4de0f8b..e05d0af2f7 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -155,6 +155,7 @@ export const createGitlabRepoPushAction: (options: { targetPath?: string | undefined; token?: string | undefined; commitAction?: 'auto' | 'update' | 'create' | 'delete' | undefined; + allowEmpty?: boolean | undefined; }, { projectid: string; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.test.ts index 25b6c89ae4..2a50ee9411 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.test.ts @@ -181,4 +181,25 @@ describe('gitlab:repo:push', () => { ); }); }); + + describe('Push an empty commit to gitlab repository', () => { + it(`Should ${examples[3].description}`, async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + mockDir.setContent({ [workspacePath]: {} }); + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'feature-branch', + 'Initial Commit', + [], + { allowEmpty: true }, + ); + expect(ctx.output).toHaveBeenCalledWith( + 'commitHash', + 'f8a2c9bd4e2915b0792b43235c779e82ddad54af', + ); + }); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.ts index 56a59f25bc..e43dbddb42 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.examples.ts @@ -73,4 +73,23 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Push an empty commit to gitlab repository (from GitLab 18.8+ on)', + example: yaml.stringify({ + steps: [ + { + id: 'pushChanges', + action: 'gitlab:repo:push', + name: 'Push empty commit to gitlab repository', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + commitMessage: 'Initial Commit', + branchName: 'feature-branch', + allowEmpty: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts index 8005c0791b..c0fef779cd 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts @@ -57,6 +57,11 @@ describe('createGitLabCommit', () => { beforeEach(() => { mockDir.clear(); + jest.clearAllMocks(); + + mockGitlabClient.Commits.create.mockResolvedValue({ + id: 'bb6bce457ed069a38ef8d16ef38602972c7735c5', + }); const config = new ConfigReader({ integrations: { @@ -447,4 +452,95 @@ describe('createGitLabCommit', () => { ); }); }); + + describe('allowEmpty parameter', () => { + it('passes allowEmpty: true to Commits.create when specified', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + commitMessage: 'Empty commit', + branchName: 'some-branch', + allowEmpty: true, + }; + mockDir.setContent({ + [workspacePath]: {}, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'some-branch', + 'Empty commit', + [], + { allowEmpty: true }, + ); + }); + + it('does not pass 5th argument when allowEmpty is not specified', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + commitMessage: 'Normal commit', + branchName: 'some-branch', + }; + mockDir.setContent({ + [workspacePath]: { + 'foo.txt': 'content', + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'some-branch', + 'Normal commit', + [ + { + action: 'create', + filePath: 'foo.txt', + content: 'Y29udGVudA==', + encoding: 'base64', + execute_filemode: false, + }, + ], + ); + // Verify only 4 arguments were passed (no 5th) + expect(mockGitlabClient.Commits.create.mock.calls[0]).toHaveLength(4); + }); + + it('passes allowEmpty: false to Commits.create when explicitly set', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + commitMessage: 'Commit with files', + branchName: 'some-branch', + allowEmpty: false, + }; + mockDir.setContent({ + [workspacePath]: { + 'foo.txt': 'content', + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'some-branch', + 'Commit with files', + [ + { + action: 'create', + filePath: 'foo.txt', + content: 'Y29udGVudA==', + encoding: 'base64', + execute_filemode: false, + }, + ], + { allowEmpty: false }, + ); + }); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts index 24f2907041..51ca19d1d0 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts @@ -83,6 +83,12 @@ export const createGitlabRepoPushAction = (options: { 'The action to be used for git commit. Defaults to create, but can be set to update or delete', }) .optional(), + allowEmpty: z => + z + .boolean({ + description: 'Allow an empty commit to be created.', + }) + .optional(), }, output: { projectid: z => @@ -107,6 +113,7 @@ export const createGitlabRepoPushAction = (options: { sourcePath, token, commitAction, + allowEmpty, } = ctx.input; const { owner, repo, project } = parseRepoUrl(repoUrl, integrations); @@ -212,12 +219,21 @@ export const createGitlabRepoPushAction = (options: { const commitId = await ctx.checkpoint({ key: `commit.create.${repoID}.${branchName}`, fn: async () => { - const commit = await api.Commits.create( - repoID, - branchName, - ctx.input.commitMessage, - actions, - ); + const commit = + allowEmpty !== undefined + ? await api.Commits.create( + repoID, + branchName, + ctx.input.commitMessage, + actions, + { allowEmpty } as any, + ) + : await api.Commits.create( + repoID, + branchName, + ctx.input.commitMessage, + actions, + ); return commit.id; }, });