Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,7 @@ The following sets of tools are available:
- `body`: PR description (string, optional)
- `draft`: Create as draft PR (boolean, optional)
- `head`: Branch containing changes (string, required)
- `labels`: Labels to apply to this pull request (string[], optional)
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
Expand Down
7 changes: 7 additions & 0 deletions pkg/github/__toolsnaps__/create_pull_request.snap
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
"description": "Branch containing changes",
"type": "string"
},
"labels": {
"description": "Labels to apply to this pull request",
"items": {
"type": "string"
},
"type": "array"
},
"maintainer_can_modify": {
"description": "Allow maintainer edits",
"type": "boolean"
Expand Down
1 change: 1 addition & 0 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const (
PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues"
PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments"
PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}"
PostReposIssuesLabelsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/labels"
GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/sub_issue"
Expand Down
25 changes: 25 additions & 0 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
Type: "boolean",
Description: "Allow maintainer edits",
},
"labels": {
Type: "array",
Items: &jsonschema.Schema{Type: "string"},
Description: "Labels to apply to this pull request",
},
},
Required: []string{"owner", "repo", "title", "head", "base"},
},
Expand Down Expand Up @@ -648,6 +653,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
return utils.NewToolResultError(err.Error()), nil, nil
}

labels, err := OptionalStringArrayParam(args, "labels")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

newPR := &github.NewPullRequest{
Title: github.Ptr(title),
Head: github.Ptr(head),
Expand Down Expand Up @@ -683,6 +693,21 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create pull request", resp, bodyBytes), nil, nil
}

// Add labels if provided
if len(labels) > 0 {
_, labelsResp, labelsErr := client.Issues.AddLabelsToIssue(ctx, owner, repo, pr.GetNumber(), labels)
if labelsErr != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("pull request created (#%d) but failed to add labels", pr.GetNumber()),
labelsResp,
labelsErr,
), nil, nil
}
if labelsResp != nil {
_ = labelsResp.Body.Close()
}
}

// Return minimal response with just essential information
minimalResponse := MinimalResponse{
ID: fmt.Sprintf("%d", pr.GetID()),
Expand Down
41 changes: 41 additions & 0 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2181,6 +2181,7 @@ func Test_CreatePullRequest(t *testing.T) {
assert.Contains(t, schema.Properties, "base")
assert.Contains(t, schema.Properties, "draft")
assert.Contains(t, schema.Properties, "maintainer_can_modify")
assert.Contains(t, schema.Properties, "labels")
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "title", "head", "base"})

// Setup mock PR for success case
Expand Down Expand Up @@ -2269,6 +2270,46 @@ func Test_CreatePullRequest(t *testing.T) {
expectError: true,
expectedErrMsg: "failed to create pull request",
},
{
name: "successful PR creation with labels",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR),
PostReposIssuesLabelsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, []*github.Label{
{Name: github.Ptr("bug")},
{Name: github.Ptr("enhancement")},
}),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"title": "Test PR",
"head": "feature-branch",
"base": "main",
"labels": []any{"bug", "enhancement"},
},
expectError: false,
expectedPR: mockPR,
},
{
name: "labels addition fails after PR creation",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR),
PostReposIssuesLabelsByOwnerByRepoByIssueNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Label does not exist"}`))
}),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"title": "Test PR",
"head": "feature-branch",
"base": "main",
"labels": []any{"nonexistent-label"},
},
expectError: true,
expectedErrMsg: "failed to add labels",
},
}

for _, tc := range tests {
Expand Down
22 changes: 21 additions & 1 deletion ui/src/apps/pr-write/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function SuccessView({
function CreatePRApp() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [labels, setLabels] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successPR, setSuccessPR] = useState<PRResult | null>(null);
Expand All @@ -140,6 +141,10 @@ function CreatePRApp() {
useEffect(() => {
if (toolInput?.title) setTitle(toolInput.title as string);
if (toolInput?.body) setBody(toolInput.body as string);
if (toolInput?.labels) {
const labelsArr = toolInput.labels as string[];
setLabels(labelsArr.join(", "));
}
if (toolInput?.draft) setIsDraft(toolInput.draft as boolean);
if (toolInput?.maintainer_can_modify !== undefined) {
setMaintainerCanModify(toolInput.maintainer_can_modify as boolean);
Expand All @@ -154,6 +159,8 @@ function CreatePRApp() {
setError(null);
setSubmittedTitle(title);

const labelsArray = labels.split(",").map(l => l.trim()).filter(l => l !== "");

try {
const result = await callTool("create_pull_request", {
owner, repo,
Expand All @@ -163,6 +170,7 @@ function CreatePRApp() {
base,
draft: isDraft,
maintainer_can_modify: maintainerCanModify,
labels: labelsArray,
_ui_submitted: true
});

Expand All @@ -182,7 +190,7 @@ function CreatePRApp() {
} finally {
setIsSubmitting(false);
}
}, [title, body, owner, repo, head, base, isDraft, maintainerCanModify, callTool]);
}, [title, body, labels, owner, repo, head, base, isDraft, maintainerCanModify, callTool]);

if (successPR) {
return (
Expand Down Expand Up @@ -260,6 +268,18 @@ function CreatePRApp() {
/>
</FormControl>

{/* Labels */}
<FormControl sx={{ mb: 3 }}>
<FormControl.Label sx={{ fontWeight: "semibold" }}>Labels</FormControl.Label>
<TextInput
value={labels}
onChange={(e) => setLabels(e.target.value)}
placeholder="bug, enhancement, help wanted (comma separated)"
block
contrast
/>
</FormControl>

{/* Description */}
<Box sx={{ mb: 3 }}>
<Text as="label" sx={{ fontWeight: "semibold", fontSize: 1, display: "block", mb: 2 }}>
Expand Down