Skip to content

Commit 412394b

Browse files
committed
chore: create pull request command
1 parent 8dc021e commit 412394b

7 files changed

Lines changed: 1251 additions & 2121 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# summary
2+
3+
Create a pull request for a work item branch.
4+
5+
# description
6+
7+
The pull request title defaults to the work item subject. The work item must have an associated branch and repository. Requires VCS authentication: set GITHUB_TOKEN (or use `gh auth login`) for GitHub, or set BITBUCKET_TOKEN for Bitbucket.
8+
9+
# flags.work-item-name.summary
10+
11+
Name of the work item, such as WI-000001.
12+
13+
# flags.work-item-id.summary
14+
15+
ID of the work item.
16+
17+
# flags.title.summary
18+
19+
Title of the pull request.
20+
21+
# flags.body.summary
22+
23+
Description of the pull request.
24+
25+
# examples
26+
27+
- Create a pull request for a work item using the default title.
28+
29+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --work-item-name WI-000001
30+
31+
- Create a pull request with a custom title and description.
32+
33+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --work-item-id 0Wx000000000001 --title "Fix: Login timeout" --body "Resolves the 30s timeout on the login page"
34+
35+
# error.NoBranch
36+
37+
Work item %s doesn't have an associated branch. Mark the work item as In Progress to create a branch, then try again.
38+
39+
# error.NoRepo
40+
41+
Work item %s doesn't have an associated repository. Verify the project is connected to a source control repository and try again.
42+
43+
# error.NoTargetBranch
44+
45+
Unable to determine the target branch for work item %s. Verify the pipeline stages are configured correctly.
46+
47+
# error.NoProvider
48+
49+
Unable to determine the VCS provider for work item %s. Verify the project is connected to a supported source control provider (GitHub or Bitbucket).
50+
51+
# flags.target-org.summary
52+
53+
Username or alias of the DevOps Center org.
54+
55+
# error.NoToken
56+
57+
No authentication token found for %s. For GitHub, set the GITHUB_TOKEN environment variable or run "gh auth login". For Bitbucket, set the BITBUCKET_TOKEN environment variable.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
"@salesforce/dev-scripts": "^11",
2222
"@salesforce/plugin-command-reference": "^3.1.38",
2323
"@salesforce/ts-sinon": "^1.4.30",
24-
"@types/chai": "^5.2.3",
25-
"chai": "^6.2.2",
2624
"eslint-plugin-sf-plugin": "^1.20.13",
2725
"esmock": "^2.7.6",
2826
"oclif": "^4",
@@ -91,6 +89,9 @@
9189
"description": "Commands for managing DevOps Center work item statuses."
9290
}
9391
}
92+
},
93+
"pull-request": {
94+
"description": "Commands for managing DevOps Center pull requests."
9495
}
9596
}
9697
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Messages, Org } from '@salesforce/core';
18+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
19+
import {
20+
createPullRequest,
21+
CreatePullRequestResult,
22+
fetchWorkItemDetail,
23+
resolveGitHubToken,
24+
WorkItemDetail,
25+
} from '../../../utils/createPullRequest.js';
26+
27+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
28+
const messages = Messages.loadMessages('@salesforce/plugin-devops-center', 'devops.pull-request.create');
29+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
30+
31+
export default class DevopsPullRequestCreate extends SfCommand<CreatePullRequestResult> {
32+
public static readonly summary = messages.getMessage('summary');
33+
public static readonly description = messages.getMessage('description');
34+
public static readonly examples = messages.getMessages('examples');
35+
36+
public static readonly flags = {
37+
'target-org': Flags.requiredOrg({
38+
char: 'o',
39+
summary: messages.getMessage('flags.target-org.summary'),
40+
required: true,
41+
}),
42+
'api-version': Flags.orgApiVersion(),
43+
'work-item-name': Flags.string({
44+
summary: messages.getMessage('flags.work-item-name.summary'),
45+
char: 'n',
46+
exactlyOne: ['work-item-name', 'work-item-id'],
47+
}),
48+
'work-item-id': Flags.salesforceId({
49+
summary: messages.getMessage('flags.work-item-id.summary'),
50+
char: 'w',
51+
exactlyOne: ['work-item-name', 'work-item-id'],
52+
}),
53+
title: Flags.string({
54+
summary: messages.getMessage('flags.title.summary'),
55+
}),
56+
body: Flags.string({
57+
summary: messages.getMessage('flags.body.summary'),
58+
}),
59+
};
60+
61+
public async run(): Promise<CreatePullRequestResult> {
62+
const { flags } = await this.parse(DevopsPullRequestCreate);
63+
const org: Org = flags['target-org'];
64+
const connection = org.getConnection(flags['api-version']);
65+
66+
const filter = flags['work-item-name'] ? { name: flags['work-item-name'] } : { id: flags['work-item-id']! };
67+
68+
let detail: WorkItemDetail;
69+
try {
70+
detail = await fetchWorkItemDetail(connection, filter);
71+
} catch (error: unknown) {
72+
const errMsg = error instanceof Error ? error.message : String(error);
73+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
74+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
75+
}
76+
throw error;
77+
}
78+
79+
if (!detail.branchName) {
80+
this.error(messages.getMessage('error.NoBranch', [detail.workItemName]));
81+
}
82+
if (!detail.repoOwner || !detail.repoName) {
83+
this.error(messages.getMessage('error.NoRepo', [detail.workItemName]));
84+
}
85+
if (!detail.targetBranch) {
86+
this.error(messages.getMessage('error.NoTargetBranch', [detail.workItemName]));
87+
}
88+
if (!detail.provider) {
89+
this.error(messages.getMessage('error.NoProvider', [detail.workItemName]));
90+
}
91+
92+
let token: string | undefined;
93+
if (detail.provider === 'github') {
94+
token = await resolveGitHubToken();
95+
} else {
96+
token = process.env.BITBUCKET_TOKEN;
97+
}
98+
if (!token) {
99+
this.error(messages.getMessage('error.NoToken', [detail.provider]));
100+
}
101+
102+
const prTitle = flags['title'] ?? detail.subject;
103+
104+
const result = await createPullRequest({
105+
owner: detail.repoOwner,
106+
repo: detail.repoName,
107+
head: detail.branchName,
108+
base: detail.targetBranch,
109+
title: prTitle,
110+
body: flags['body'],
111+
provider: detail.provider,
112+
token,
113+
});
114+
115+
if (result.success) {
116+
this.log(`Successfully created pull request for ${detail.workItemName}.`);
117+
this.log(` Title: ${result.title ?? prTitle}`);
118+
this.log(` URL: ${result.url ?? ''}`);
119+
this.log(` Source: ${detail.workItemName}${result.targetBranch ?? detail.targetBranch}`);
120+
}
121+
122+
return result;
123+
}
124+
}

0 commit comments

Comments
 (0)