Skip to content
← Public packages

@kody/github

Call GitHub REST, GraphQL, and pull requests with a GitHub OAuth App or a personal access token.

src/pr/merge.ts

85 lines · 2.7 KB · TypeScript
import { isConfirmed, isDryRun, pickAuthInput, request } from '../core.ts'
import { normalizePrLocator } from './pr-input.ts'

type MergeMethod = 'merge' | 'squash' | 'rebase'

const MERGE_METHODS: Array<MergeMethod> = ['merge', 'squash', 'rebase']

/**
 * Merge a pull request.
 *
 * Pass `dryRun: true` to preview the merge request without calling GitHub.
 * Live merges require `confirm: true`.
 *
 * @param params - PR locator plus `mergeMethod`, optional `commitTitle`,
 *   `commitMessage`, `sha`, `integrationName` or `secretName`, `dryRun`, and `confirm`.
 * @returns `{ merged, sha, message }` from GitHub, or a dry-run preview.
 * @example
 * import mergePr from 'kody:@kody/github/pr/merge'
 * const preview = await mergePr({ owner: 'example-org', repo: 'example', prNumber: 1, mergeMethod: 'squash', dryRun: true })
 * // => { dryRun: true, wouldMerge: true, repository: 'example-org/example', prNumber: 1, mergeMethod: 'squash' }
 */
export default async function mergePr(params: Record<string, unknown> = {}) {
	const auth = pickAuthInput(params)
	const { owner, repo, prNumber } = normalizePrLocator(params)
	const mergeMethod = readMergeMethod(params)
	const body: Record<string, unknown> = { merge_method: mergeMethod }
	if (typeof params.commitTitle === 'string' && params.commitTitle.trim()) {
		body.commit_title = params.commitTitle.trim()
	}
	if (typeof params.commitMessage === 'string' && params.commitMessage.trim()) {
		body.commit_message = params.commitMessage.trim()
	}
	if (typeof params.sha === 'string' && params.sha.trim()) {
		body.sha = params.sha.trim()
	}

	if (isDryRun(params)) {
		return {
			dryRun: true,
			wouldMerge: true,
			repository: `${owner}/${repo}`,
			prNumber,
			mergeMethod,
			body,
			auth,
		}
	}

	if (!isConfirmed(params)) {
		throw new Error(
			`Merging ${owner}/${repo}#${prNumber} requires dryRun: true (preview) or confirm: true (live merge).`,
		)
	}

	const response = await request<Record<string, unknown>>({
		...auth,
		method: 'PUT',
		path: `/repos/${owner}/${repo}/pulls/${prNumber}/merge`,
		body,
		throwOnError: true,
		confirm: true,
	})
	const result = response.data!

	return {
		merged: result.merged as boolean,
		sha: result.sha as string,
		message: result.message as string,
		repository: `${owner}/${repo}`,
		prNumber,
		mergeMethod,
		auth: response.auth,
	}
}

function readMergeMethod(params: Record<string, unknown>): MergeMethod {
	const raw = params.mergeMethod ?? params.merge_method
	if (raw === undefined) return 'squash'
	if (typeof raw === 'string' && MERGE_METHODS.includes(raw as MergeMethod)) {
		return raw as MergeMethod
	}
	throw new Error(
		`mergeMethod must be one of ${MERGE_METHODS.map((method) => `"${method}"`).join(', ')}`,
	)
}