Skip to content

Commit a148245

Browse files
committed
chore: create pipeline command
1 parent a9bd61e commit a148245

7 files changed

Lines changed: 845 additions & 0 deletions

File tree

command-snapshot.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@
77
"flags": ["api-version", "flags-dir", "json", "pipeline-id", "project-id", "target-org"],
88
"plugin": "@salesforce/plugin-devops-center"
99
},
10+
{
11+
"alias": [],
12+
"command": "devops:pipeline:create",
13+
"flagAliases": [],
14+
"flagChars": ["d", "n", "o", "r"],
15+
"flags": [
16+
"api-version",
17+
"bitbucket-project",
18+
"create-repo",
19+
"description",
20+
"flags-dir",
21+
"json",
22+
"name",
23+
"repo",
24+
"repo-owner",
25+
"repo-type",
26+
"target-org"
27+
],
28+
"plugin": "@salesforce/plugin-devops-center"
29+
},
1030
{
1131
"alias": [],
1232
"command": "devops:project:create",

messages/devops.pipeline.create.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# summary
2+
3+
Create a DevOps Center pipeline.
4+
5+
# description
6+
7+
Provide the URL of an existing repository, or use `--create-repo` with a repository name to create one. After you create the pipeline, add stages, and activate the pipeline.
8+
9+
# flags.target-org.summary
10+
11+
Username or alias of the DevOps Center org.
12+
13+
# flags.name.summary
14+
15+
Name of the pipeline.
16+
17+
# flags.repo.summary
18+
19+
URL of an existing repository or the name of a repository to create.
20+
21+
# flags.repo-type.summary
22+
23+
Type of the source code repository. Required when creating a repository using '--create-repo'.
24+
25+
# flags.create-repo.summary
26+
27+
Create a repository if it doesn't exist.
28+
29+
# flags.repo-owner.summary
30+
31+
Owner (organization or user) of the repository. Required when creating a repository using '--create-repo'.
32+
33+
# flags.bitbucket-project.summary
34+
35+
Bitbucket project key for the repository. Used when creating a Bitbucket repository with '--create-repo'.
36+
37+
# flags.description.summary
38+
39+
Description of the pipeline.
40+
41+
# examples
42+
43+
- Create a pipeline and associate it with an existing GitHub repository.
44+
45+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo https://github.com/myorg/myrepo
46+
47+
- Create a pipeline and associate it with a new GitHub repository.
48+
49+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo my-new-repo --repo-type github --repo-owner myorg --create-repo
50+
51+
- Create a pipeline with a description and associate it with a Bitbucket repository.
52+
53+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo https://bitbucket.org/myorg/myrepo --description "Main CI/CD pipeline for production releases"
54+
55+
# error.RepoTypeRequired
56+
57+
The --repo-type flag is required when using --create-repo. Specify --repo-type github or --repo-type bitbucket.
58+
59+
# error.RepoCreationFailed
60+
61+
Failed to create repository "%s". Verify the --repo-owner has permission to create repositories and the repository name is available. Details: %s
62+
63+
# error.RepoNotFound
64+
65+
Repository "%s" was not found or you don't have access. Verify the repository URL is correct and your DevOps Center org has the required VCS credentials.
66+
67+
# error.RepoValidationFailed
68+
69+
Failed to validate repository "%s". When using an existing repository, provide the full URL (e.g. https://github.com/owner/repo). To create a new repository, use --create-repo with --repo-type and --repo-owner.
70+
71+
# error.RepoOwnerRequired
72+
73+
The --repo-owner flag is required when using --create-repo. Specify the GitHub or Bitbucket organization or user that will own the repository.
74+
75+
# error.VcsCredentialsMissing
76+
77+
No VCS credentials found for repository "%s". Connect to Bitbucket in your DevOps Center org before creating a pipeline.
78+
79+
# error.RepoTypeDetectionFailed
80+
81+
Unable to detect the repository type from the URL "%s". Use --repo-type to specify the repository type.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/CreatePipelineResult",
4+
"definitions": {
5+
"CreatePipelineResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"pipelineId": {
12+
"type": "string"
13+
},
14+
"name": {
15+
"type": "string"
16+
},
17+
"description": {
18+
"type": "string"
19+
},
20+
"status": {
21+
"type": "string"
22+
},
23+
"repository": {
24+
"$ref": "#/definitions/RepoInfo"
25+
},
26+
"error": {
27+
"type": "string"
28+
}
29+
},
30+
"required": ["success"],
31+
"additionalProperties": false
32+
},
33+
"RepoInfo": {
34+
"type": "object",
35+
"properties": {
36+
"repoUrl": {
37+
"type": "string"
38+
},
39+
"repoType": {
40+
"type": "string"
41+
},
42+
"created": {
43+
"type": "boolean"
44+
}
45+
},
46+
"required": ["repoUrl", "repoType", "created"],
47+
"additionalProperties": false
48+
}
49+
}
50+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 { createPipeline, CreatePipelineResult, detectRepoType } from '../../../utils/createPipeline.js';
20+
21+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
22+
const messages = Messages.loadMessages('@salesforce/plugin-devops-center', 'devops.pipeline.create');
23+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
24+
25+
export default class DevopsPipelineCreate extends SfCommand<CreatePipelineResult> {
26+
public static readonly summary = messages.getMessage('summary');
27+
public static readonly description = messages.getMessage('description');
28+
public static readonly examples = messages.getMessages('examples');
29+
30+
public static readonly flags = {
31+
'target-org': Flags.requiredOrg({
32+
char: 'o',
33+
summary: messages.getMessage('flags.target-org.summary'),
34+
required: true,
35+
}),
36+
'api-version': Flags.orgApiVersion(),
37+
name: Flags.string({
38+
summary: messages.getMessage('flags.name.summary'),
39+
char: 'n',
40+
required: true,
41+
}),
42+
repo: Flags.string({
43+
summary: messages.getMessage('flags.repo.summary'),
44+
char: 'r',
45+
required: true,
46+
}),
47+
'repo-type': Flags.string({
48+
summary: messages.getMessage('flags.repo-type.summary'),
49+
options: ['github', 'bitbucket'],
50+
}),
51+
'create-repo': Flags.boolean({
52+
summary: messages.getMessage('flags.create-repo.summary'),
53+
default: false,
54+
}),
55+
'repo-owner': Flags.string({
56+
summary: messages.getMessage('flags.repo-owner.summary'),
57+
}),
58+
'bitbucket-project': Flags.string({
59+
summary: messages.getMessage('flags.bitbucket-project.summary'),
60+
}),
61+
description: Flags.string({
62+
summary: messages.getMessage('flags.description.summary'),
63+
char: 'd',
64+
}),
65+
};
66+
67+
public async run(): Promise<CreatePipelineResult> {
68+
const { flags } = await this.parse(DevopsPipelineCreate);
69+
const org: Org = flags['target-org'];
70+
const connection = org.getConnection(flags['api-version']);
71+
const repoType = this.resolveRepoType(flags['repo-type'], flags['repo'], flags['create-repo']);
72+
73+
if (flags['create-repo'] && !flags['repo-owner']) {
74+
this.error(messages.getMessage('error.RepoOwnerRequired'));
75+
}
76+
77+
let result: CreatePipelineResult;
78+
try {
79+
result = await createPipeline({
80+
connection,
81+
name: flags['name'],
82+
description: flags['description'],
83+
repo: flags['repo'],
84+
repoType,
85+
createRepo: flags['create-repo'],
86+
repoOwner: flags['repo-owner'],
87+
bitbucketProject: flags['bitbucket-project'],
88+
});
89+
} catch (error: unknown) {
90+
const errMsg = error instanceof Error ? error.message : String(error);
91+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
92+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
93+
}
94+
this.handleApiError(errMsg, flags['repo']);
95+
throw error;
96+
}
97+
98+
if (result.success) {
99+
this.printSuccessOutput(result, flags['repo'], org.getUsername());
100+
} else {
101+
this.error(`Failed to create pipeline: ${result.error ?? ''}`);
102+
}
103+
104+
return result;
105+
}
106+
107+
private resolveRepoType(flagRepoType: string | undefined, repo: string, isCreateRepo: boolean): string {
108+
if (isCreateRepo && !flagRepoType) {
109+
this.error(messages.getMessage('error.RepoTypeRequired'));
110+
}
111+
if (flagRepoType) return flagRepoType;
112+
113+
const detected = detectRepoType(repo);
114+
if (!detected) {
115+
this.error(messages.getMessage('error.RepoTypeDetectionFailed', [repo]));
116+
}
117+
return detected;
118+
}
119+
120+
private handleApiError(errMsg: string, repo: string): void {
121+
if (errMsg.includes('REPOSITORY_CREATION_FAILED')) {
122+
this.error(messages.getMessage('error.RepoCreationFailed', [repo, errMsg]));
123+
}
124+
if (errMsg.includes('REPO_NOT_FOUND_OR_UNAUTHORIZED')) {
125+
this.error(messages.getMessage('error.RepoNotFound', [repo]));
126+
}
127+
if (errMsg.includes('SOURCE_CODE_SERVICE_ERROR')) {
128+
this.error(messages.getMessage('error.RepoValidationFailed', [repo]));
129+
}
130+
if (errMsg.includes('BITBUCKET_API_ERROR') && errMsg.includes('ProviderInfo is missing')) {
131+
this.error(messages.getMessage('error.VcsCredentialsMissing', [repo]));
132+
}
133+
}
134+
135+
private printSuccessOutput(result: CreatePipelineResult, repoFlag: string, username: string | undefined): void {
136+
if (result.repository?.created) {
137+
this.log(`Created repository: ${repoFlag} (${result.repository.repoType})`);
138+
}
139+
this.log(`Successfully created pipeline: ${result.name ?? ''}`);
140+
this.log(` Pipeline ID: ${result.pipelineId ?? ''}`);
141+
this.log(` Repository: ${result.repository?.repoUrl ?? ''} (${result.repository?.repoType ?? ''})`);
142+
this.log(` Status: ${result.status ?? 'Inactive'}`);
143+
this.log(' Next steps:');
144+
const orgLabel = username ?? '<org>';
145+
const pipelineIdLabel = result.pipelineId ?? '<ID>';
146+
this.log(
147+
` Add pipeline stages: sf devops pipeline stage add --target-org ${orgLabel} --pipeline-id ${pipelineIdLabel}`
148+
);
149+
this.log(
150+
` Attach a project: sf devops pipeline attach-project --target-org ${orgLabel} --pipeline-id ${pipelineIdLabel} --project-id <ID>`
151+
);
152+
}
153+
}

0 commit comments

Comments
 (0)