kubernetes/kubernetes · error

unexpected merge commit format: %q

Error message

unexpected merge commit format: %q

What it means

Validation error from getMergeCommitInfo when the first line of the commit body does not match the expected GitHub merge-commit format 'Merge pull request #<n> from <branch>' (main.go:440-442). After trimming, lines[0] is split into fields; fewer than 4 fields triggers this. The %q is the offending first line.

Source

Thrown at hack/apidiff-changelog/main.go:442

func outputName(path string) string {
	re := regexp.MustCompile(`[^a-zA-Z0-9_-]`)
	return re.ReplaceAllString(path, "_") + ".out"
}

// getMergeCommitInfo extracts the PR title and description from a GitHub merge commit.
// The merge commit body is expected in the format produced by GitHub:
// first line "Merge pull request #<number> from <branch>", last line is the PR title.
func getMergeCommitInfo(commit string) (title, description string, err error) {
	cmd := exec.Command("git", "show", "--no-patch", "--format=%B", commit)
	out, err := cmd.Output()
	if err != nil {
		return "", "", fmt.Errorf("git show %s: %w", commit, err)
	}
	lines := trimTrailingEmpty(strings.Split(strings.TrimRight(string(out), "\n"), "\n"))
	// First line: "Merge pull request #<number> from <branch>".
	words := strings.Fields(lines[0])
	if len(words) < 4 {
		return "", "", fmt.Errorf("unexpected merge commit format: %q", lines[0])
	}
	prNum := strings.TrimPrefix(words[3], "#")
	title = lines[len(lines)-1]
	description = fmt.Sprintf("See [PR #%s](https://github.com/kubernetes/kubernetes/pull/%s).", prNum, prNum)
	return title, description, nil
}

// trimTrailingEmpty removes trailing empty strings from a slice.
func trimTrailingEmpty(lines []string) []string {
	for len(lines) > 0 && lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	}
	return lines
}

// compareApidiff runs apidiff on two module state files, prints the formatted
// comparison report to stdout, and returns the incompatible (non-tolerated)
// changes for changelog verification. Returns an empty string if there are none.

View on GitHub (pinned to b882c60b40)

Solutions

  1. Pass the actual merge commit SHA (the one whose message starts with 'Merge pull request #...').
  2. If the repo only squash-merges, do not use -merge-commit — populate the changelog title/description manually via -update-changelog then edit.
  3. Inspect the commit: `git show --no-patch --format=%B <commit>` and confirm the first line shape.

Example fix

# before (squash-merge commit)
$ git show --no-patch --format=%B abc123
Add DeviceTaintRules API
# -> first run: unexpected merge commit format: "Add DeviceTaintRules API"

# after: use -update-changelog and edit the placeholder instead
$ ./apidiff-changelog -base main -update-changelog pkg/client
Defensive patterns

Strategy: validation

Validate before calling

// Confirm the commit body matches GitHub's merge format before passing it in.
func looksLikeGHMerge(body string) bool {
    words := strings.Fields(strings.TrimSpace(body))
    return len(words) >= 4 && words[0] == "Merge" && words[1] == "pull" && words[2] == "request"
}

Type guard

null

Try / catch

words := strings.Fields(lines[0])
if len(words) < 4 {
    return "","", fmt.Errorf("unexpected merge commit format: %q", lines[0])
}

Prevention

When it happens

Trigger: -merge-commit points at a commit whose body's first line is not a GitHub-style merge line. Examples: a squash-merge commit (first line is the PR title), a direct (non-PR) commit, a commit created with a custom merge message, or an empty body.

Common situations: Repo uses squash-merges instead of merge-commits (very common in many Kubernetes sub-projects); passing the PR head commit instead of the merge commit; passing a regular commit SHA; GitHub 'Merge branch' style merges.

Related errors


AI-assisted analysis of kubernetes/kubernetes@b882c60b40 (2026-08-07). Data as JSON: /api/errors/3d856f254d862bcc. Report an issue: GitHub.