Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gold-rice-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"changesets-gitlab": minor
---

feat: add optional INPUT_MERGE_WHEN_PIPELINE_SUCCEEDS, INPUT_SQUASH, and INPUT_SQUASH_COMMIT_MESSAGE inputs to support auto-merge and squash settings for created merge requests
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ GitLab CI cli for [changesets](https://github.com/atlassian/changesets) like its
- `INPUT_PUBLISHED` - Command executed after published
- `INPUT_ONLY_CHANGESETS` - Command executed on only changesets detected
- `INPUT_REMOVE_SOURCE_BRANCH` - Enables the merge request "Delete source branch" checkbox. Default false.
- `INPUT_MERGE_WHEN_PIPELINE_SUCCEEDS` - Sets auto-merge on the created merge request (merge when all checks pass). If not set, no changes are made.
- `INPUT_SQUASH` - Enables or disables squash commits for the merge request. If not set, no changes are made.
- `INPUT_SQUASH_COMMIT_MESSAGE` - The optional squash commit message used when squash is enabled.
- `INPUT_TARGET_BRANCH` -> The merge request target branch. Defaults to current branch
- `INPUT_CREATE_GITLAB_RELEASES` - A boolean value to indicate whether to create Gitlab releases after publish or not. Default true.
- `INPUT_LABELS` - A comma separated string of labels to be added to the version package Gitlab Merge request
Expand Down
8 changes: 7 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { MainCommandOptions } from './types.js'
import {
execSync,
FALSY_VALUES,
getOptionalBooleanInput,
getOptionalInput,
getUsername,
TRUTHY_VALUES,
Expand Down Expand Up @@ -108,7 +109,12 @@ export const main = async ({
mrTitle: getOptionalInput('title'),
mrTargetBranch: getOptionalInput('target_branch'),
commitMessage: getOptionalInput('commit'),
removeSourceBranch: getInput('remove_source_branch') === 'true',
removeSourceBranch: getOptionalBooleanInput('remove_source_branch'),
mergeWhenPipelineSucceeds: getOptionalBooleanInput(
'merge_when_pipeline_succeeds',
),
squash: getOptionalBooleanInput('squash'),
squashCommitMessage: getOptionalInput('squash_commit_message'),
hasPublishScript,
})
if (onlyChangesets) {
Expand Down
68 changes: 55 additions & 13 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import fs from 'node:fs/promises'
import path from 'node:path'

import { exec } from '@actions/exec'
import type { Gitlab } from '@gitbeaker/core'
import type {
Gitlab,
MergeRequestSchema,
EditMergeRequestOptions,
AcceptMergeRequestOptions,
} from '@gitbeaker/core'
import type { Package } from '@manypkg/get-packages'
import { getPackages } from '@manypkg/get-packages'
import pLimit from 'p-limit'
Expand Down Expand Up @@ -207,6 +212,9 @@ export interface VersionOptions {
cwd?: string
mrTitle?: string
removeSourceBranch?: boolean
mergeWhenPipelineSucceeds?: boolean
squash?: boolean
squashCommitMessage?: string
mrTargetBranch?: string
commitMessage?: string
hasPublishScript?: boolean
Expand All @@ -220,6 +228,9 @@ export async function runVersion({
mrTargetBranch = context.ref,
commitMessage = 'Version Packages',
removeSourceBranch = false,
mergeWhenPipelineSucceeds,
squash,
squashCommitMessage,
hasPublishScript = false,
}: VersionOptions) {
const currentBranch = context.ref
Expand Down Expand Up @@ -322,28 +333,59 @@ ${
perPage: 1,
})
console.log(JSON.stringify(searchResult, null, 2))

let mergeRequest: MergeRequestSchema
const mergeRequestOptions: EditMergeRequestOptions = {
description: await mrBodyPromise,
removeSourceBranch,
labels,
squash,
}
if (searchResult.length === 0) {
console.log(
`creating merge request from ${versionBranch} to ${mrTargetBranch}.`,
)
await api.MergeRequests.create(
mergeRequest = await api.MergeRequests.create(
context.projectId,
versionBranch,
mrTargetBranch,
finalMrTitle,
{
description: await mrBodyPromise,
removeSourceBranch,
labels,
},
mergeRequestOptions,
)
} else {
console.log(`updating found merge request !${searchResult[0].iid}`)
await api.MergeRequests.edit(context.projectId, searchResult[0].iid, {
title: finalMrTitle,
description: await mrBodyPromise,
removeSourceBranch,
labels,
})
mergeRequestOptions.title = finalMrTitle
mergeRequest = await api.MergeRequests.edit(
context.projectId,
searchResult[0].iid,
mergeRequestOptions,
)
}

const acceptRequest: AcceptMergeRequestOptions = {}
if (
mergeWhenPipelineSucceeds !== undefined &&
mergeRequest.merge_when_pipeline_succeeds !== mergeWhenPipelineSucceeds
) {
acceptRequest.mergeWhenPipelineSucceeds = mergeWhenPipelineSucceeds
}
Comment thread
kriakiku marked this conversation as resolved.
if (
squash === true &&
squashCommitMessage !== undefined &&
mergeRequest.squash_commit_message !== squashCommitMessage
Comment thread
kriakiku marked this conversation as resolved.
) {
acceptRequest.squashCommitMessage = squashCommitMessage
}

if (Object.keys(acceptRequest).length > 0) {
console.log(
`updating merge request !${mergeRequest.iid} with options:`,
acceptRequest,
)
await api.MergeRequests.accept(
context.projectId,
mergeRequest.iid,
acceptRequest,
)
}
Comment on lines +365 to 390

@coderabbitai coderabbitai Bot Apr 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Calling MergeRequests.accept can merge the MR immediately — guard against unintended merges.

GitLab's accept endpoint (PUT /merge_requests/:iid/merge) performs a merge, not a settings update. It only defers to auto-merge when merge_when_pipeline_succeeds=true is sent and there is a pipeline in progress. Otherwise, if the MR is mergeable, it will be merged immediately. This produces at least two problematic scenarios with the current logic:

  1. User sets only INPUT_SQUASH_COMMIT_MESSAGE (no INPUT_MERGE_WHEN_PIPELINE_SUCCEEDS). acceptRequest ends up as { squashCommitMessage }, and accept is invoked → the MR can be merged right now, even though the user only wanted to set a squash commit message.
  2. User explicitly sets INPUT_MERGE_WHEN_PIPELINE_SUCCEEDS=false while the MR currently has it true. The diff check passes, accept is called with mergeWhenPipelineSucceeds=false → immediate merge.

Suggested guards:

  • Only call accept when mergeWhenPipelineSucceeds === true (the only case this API was meant to cover here).
  • When mergeWhenPipelineSucceeds !== true but squash/squashCommitMessage differ, persist those via MergeRequests.edit instead (the squash_commit_message field is editable on the MR resource), not via accept.
🔒 Proposed guard
-  const acceptRequest: AcceptMergeRequestOptions = {}
-  if (
-    mergeWhenPipelineSucceeds !== undefined &&
-    mergeRequest.merge_when_pipeline_succeeds !== mergeWhenPipelineSucceeds
-  ) {
-    acceptRequest.mergeWhenPipelineSucceeds = mergeWhenPipelineSucceeds
-  }
-  if (
-    squash === true &&
-    squashCommitMessage !== undefined &&
-    mergeRequest.squash_commit_message !== squashCommitMessage
-  ) {
-    acceptRequest.squashCommitMessage = squashCommitMessage
-  }
-
-  if (Object.keys(acceptRequest).length > 0) {
+  // Only invoke accept when explicitly opting into auto-merge; accept will
+  // otherwise merge the MR immediately.
+  if (
+    mergeWhenPipelineSucceeds === true &&
+    mergeRequest.merge_when_pipeline_succeeds !== true
+  ) {
+    const acceptRequest: AcceptMergeRequestOptions = {
+      mergeWhenPipelineSucceeds: true,
+      ...(squash === true && squashCommitMessage !== undefined
+        ? { squashCommitMessage }
+        : {}),
+    }
     console.log(
       `updating merge request !${mergeRequest.iid} with options:`,
-      acceptRequest,
+      acceptRequest,
     )
     await api.MergeRequests.accept(
       context.projectId,
       mergeRequest.iid,
       acceptRequest,
     )
   }
GitLab API PUT /projects/:id/merge_requests/:merge_request_iid/merge behavior with merge_when_pipeline_succeeds false vs true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/run.ts` around lines 365 - 390, The current logic builds acceptRequest
and calls api.MergeRequests.accept which will immediately merge the MR unless
merge_when_pipeline_succeeds=true; change this to only call
api.MergeRequests.accept when mergeWhenPipelineSucceeds === true (and include
that field in acceptRequest), and when mergeWhenPipelineSucceeds is undefined or
false but only squash/squashCommitMessage need updating, call
api.MergeRequests.edit (the MR edit endpoint) to persist
squash_commit_message/squash settings instead of accept; locate the accept flow
(acceptRequest variable, MergeRequests.accept call, and the checks around
mergeWhenPipelineSucceeds, squash, and squashCommitMessage) and split the logic
so accept is used exclusively for the auto-merge-when-pipeline-succeeds case
while edits use MergeRequests.edit for settings-only updates.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kriakiku Is this true?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the only way to specify squashCommitMessage and mergeWhenPipelineSucceeds is by calling the Accept endpoint. This may trigger an immediate merge of the request if no additional rules are set up in the repository (such as requiring an approval from someone other than the author). The GitLab API doesn’t allow doing this any other way. Should I update the README to clarify that setting these parameters via the Accept endpoint might result in an immediate merge?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I update the README to clarify that setting these parameters via the Accept endpoint might result in an immediate merge?

Yes, this is a bit confusing to me.

}
18 changes: 14 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,21 @@ export const execSync = (command: string) =>
// eslint-disable-next-line sonarjs/os-command
execSync_(command, { stdio: 'inherit' })

export const FALSY_VALUES = new Set(['false', '0'])

export const TRUTHY_VALUES = new Set(['true', '1'])

export const getOptionalInput = (name: string) => getInput(name) || undefined
export const getOptionalBooleanInput = (name: string): boolean | undefined => {
const input = getInput(name)
if (TRUTHY_VALUES.has(input)) {
return true
}
if (FALSY_VALUES.has(input)) {
return false
}
return undefined
}
Comment thread
kriakiku marked this conversation as resolved.

// eslint-disable-next-line sonarjs/function-return-type
export const getUsername = (api: Gitlab) => {
Expand All @@ -172,10 +186,6 @@ export const getUsername = (api: Gitlab) => {
export const cjsRequire =
typeof require === 'undefined' ? createRequire(import.meta.url) : require

export const FALSY_VALUES = new Set(['false', '0'])

export const TRUTHY_VALUES = new Set(['true', '1'])

export const GITLAB_MAX_TAGS = 4

export const HTTP_STATUS_NOT_FOUND = 404
Loading