Skip to content

πŸ› fix: normalize lane prefixes in PR titles - #7239

Merged
clubanderson merged 1 commit into
v4from
fix/pr-title-normalization-clubanderson
Sep 17, 2026
Merged

clubanderson merged 1 commit into
v4from
fix/pr-title-normalization-clubanderson

Conversation

@clubanderson

Copy link
Copy Markdown
Member

Summary

This fixes agent-authored PR titles that are dead on arrival in repositories enforcing Conventional Commits from the first character.

ProjectBluefin has two representative gates:

  • projectbluefin/common runs ^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?(!)?: .+ against the whole PR title. A leading [ from [scanner] fails before the valid fix(ci): ... header can be considered.
  • projectbluefin/review uses amannn/action-semantic-pull-request@v6.1.1, whose parser starts at ^(\w*)(?:\((.*)\))?!?: (.*)$; [ is not a word character, so the header fails to parse.

The server now normalizes only PR titles shaped like [lane] <valid Conventional Commits header> into <valid Conventional Commits header> [lane]. Moving the lane to the end keeps it in the PR title for attribution and preserves it through squash-merge history, while making the type token start at position 0 for both gate styles.

Why code, not another prompt tweak

#7159 / commit c361986 already added prose saying the lane prefix is not used for PRs, but prose did not hold: custom dashboard prompt overrides can shadow the default policy, and projectbluefin/review#614 still opened with a prefix after that change was live. The normalization is therefore enforced in pkg/github at the hive-open-pr request watcher, the server-side choke point agents cannot bypass.

Scope

Validation

$ cd /tmp/hv-fix-clubanderson/src
$ gofmt -l pkg/github/pr_title.go pkg/github/pr_title_test.go pkg/github/pr_request_watcher.go pkg/policies/source_sync_test.go
$ go vet ./pkg/github/ ./pkg/policies/
$ go test ./pkg/github/ ./pkg/policies/
ok  	github.com/hivecommons/hive/pkg/github	55.205s
ok  	github.com/hivecommons/hive/pkg/policies	9.086s

Move leading [lane] prefixes to the end of PR titles when the remainder is already a Conventional Commits header, preserving the agent record without breaking target repository title gates. Backfill the PR-title prompt guidance into remaining PR-capable defaults and pin it with tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Sep 16, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign danathar for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Sep 16, 2026
@kubestellar-hive

Copy link
Copy Markdown
Contributor

Quality review: coverage gap at the choke point (test attached)

Coverage check on this branch: NormalizePRTitle is 100% covered and the unit tests are thorough (both ProjectBluefin gate regexes re-asserted, conservative cases, 256-rune boundary). Nice.

However, the single wiring line this PR argues for β€” title = NormalizePRTitle(title) in handleOnePRRequest (pr_request_watcher.go:324) β€” is executed but never asserted. Mutation check: deleting that line still passes the entire ./pkg/github/ suite, because newPRMockServer only echoes the POSTed title back and no test inspects it. The PR's core claim is that this is "the server-side choke point agents cannot bypass", so a silent regression here would reintroduce the exact projectbluefin/review#614 failure mode with green CI.

Suggested test (verified on this branch)

Append to src/pkg/github/pr_request_watcher_test.go β€” passes as-is, and fails with title POSTed to GitHub = "[scanner] fix(ci): ..." if the wiring line is removed:

// Wiring: handleOnePRRequest must pass the request title through
// NormalizePRTitle before POSTing, so a lane-prefixed Conventional Commits
// title reaches GitHub with the lane moved to the end. This pins the
// server-side choke point itself, not just the pure function.
func TestPRRequestWatcher_NormalizesLanePrefixedTitle(t *testing.T) {
	var postedTitle string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == "GET" && strings.HasSuffix(r.URL.Path, "/repos/o/r"):
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, `{"name":"r","default_branch":"main"}`)
		case r.Method == "GET" && strings.Contains(r.URL.Path, "/compare/"):
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, `{"files":[]}`)
		case r.Method == "GET" && strings.HasSuffix(r.URL.Path, "/pulls"):
			_, _ = io.WriteString(w, `[]`)
		case r.Method == "GET" && strings.Contains(r.URL.Path, "/issues/"):
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, `{"number":1,"title":"ordinary issue","body":"implement the requested change","state":"open"}`)
		case r.Method == "POST" && strings.HasSuffix(r.URL.Path, "/pulls"):
			body, _ := io.ReadAll(r.Body)
			var np map[string]any
			_ = json.Unmarshal(body, &np)
			postedTitle = asString(np["title"])
			w.Header().Set("Content-Type", "application/json")
			_, _ = io.WriteString(w, `{"number":42,"html_url":"https://github.com/o/r/pull/42"}`)
		default:
			w.WriteHeader(200)
			_, _ = io.WriteString(w, `{}`)
		}
	}))
	defer srv.Close()
	c := testClient(t, srv.URL)

	dir := t.TempDir()
	old := prRequestDirForTest
	prRequestDirForTest = dir
	defer func() { prRequestDirForTest = old }()

	_, err := WritePRRequest(dir, PRRequest{Repo: "o/r", Head: "scanner/fix-title", Title: "[scanner] fix(ci): retry transient GHCR errors", Body: "Fixes #1", Agent: "scanner"})
	if err != nil {
		t.Fatal(err)
	}

	c.ProcessPRRequestsOnce(context.Background())

	want := "fix(ci): retry transient GHCR errors [scanner]"
	if postedTitle != want {
		t.Fatalf("title POSTed to GitHub = %q, want %q", postedTitle, want)
	}
}

Validation on fix/pr-title-normalization-clubanderson:

  • gofmt -l clean; go test -run TestPRRequestWatcher_NormalizesLanePrefixedTitle ./pkg/github/ β†’ ok
  • With title = NormalizePRTitle(title) deleted: the new test FAILs (mutation killed); without it, full ./pkg/github/ suite stays green (gap confirmed)

Not opening a separate PR since these files belong to this open PR's ground β€” applying the snippet here is mechanical.


Quality agent (hold-gated mode).

β€” hive: agent=quality backend=copilot model=claude-fable-5 copilot=1.0.78

@clubanderson
clubanderson merged commit 3822a3a into v4 Sep 17, 2026
67 of 72 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the fix/pr-title-normalization-clubanderson branch September 17, 2026 00:06
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

We'd love to hear how your experience was: share feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant