Skip to content

Commit 0fd5982

Browse files
committed
chore: add stage command
1 parent a148245 commit 0fd5982

8 files changed

Lines changed: 516 additions & 1 deletion

File tree

command-snapshot.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
],
2828
"plugin": "@salesforce/plugin-devops-center"
2929
},
30+
{
31+
"alias": [],
32+
"command": "devops:pipeline:stage:add",
33+
"flagAliases": [],
34+
"flagChars": ["n", "o"],
35+
"flags": ["api-version", "flags-dir", "json", "name", "next-stage-id", "pipeline-id", "target-org"],
36+
"plugin": "@salesforce/plugin-devops-center"
37+
},
3038
{
3139
"alias": [],
3240
"command": "devops:project:create",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# summary
2+
3+
Add a stage to a DevOps Center pipeline.
4+
5+
# description
6+
7+
Inserts an empty stage before the stage specified by `--next-stage-id`. The new stage doesn't include a branch or environment. Configure them separately after you create the stage.
8+
9+
# flags.target-org.summary
10+
11+
Username or alias of the DevOps Center org.
12+
13+
# flags.pipeline-id.summary
14+
15+
ID of the pipeline where the stage is added.
16+
17+
# flags.name.summary
18+
19+
Name of the pipeline stage, such as Integration, UAT, or Staging.
20+
21+
# flags.next-stage-id.summary
22+
23+
ID of the stage that follows the new stage in the pipeline.
24+
25+
# examples
26+
27+
- Add a Development stage before Integration in a specific pipeline.
28+
29+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --name "Development" --next-stage-id 0Xc000000000002
30+
31+
- Add a QA stage before UAT in a specific pipeline.
32+
33+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --name "QA" --next-stage-id 0Xc000000000003
34+
35+
# error.StageNotFound
36+
37+
Stage %s not found in pipeline %s. Check the stage ID and try again.

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@
9494
"description": "Commands for managing DevOps Center pull requests."
9595
},
9696
"pipeline": {
97-
"description": "Commands for managing DevOps Center pipelines."
97+
"description": "Commands for managing DevOps Center pipelines.",
98+
"subtopics": {
99+
"stage": {
100+
"description": "Commands for managing pipeline stages."
101+
}
102+
}
98103
}
99104
}
100105
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/AddPipelineStageResult",
4+
"definitions": {
5+
"AddPipelineStageResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"stageId": {
12+
"type": "string"
13+
},
14+
"name": {
15+
"type": "string"
16+
},
17+
"nextStageId": {
18+
"type": "string"
19+
},
20+
"pipelineId": {
21+
"type": "string"
22+
},
23+
"error": {
24+
"type": "string"
25+
}
26+
},
27+
"required": ["success"],
28+
"additionalProperties": false
29+
}
30+
}
31+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 { addPipelineStage, AddPipelineStageResult } from '../../../../utils/addPipelineStage.js';
20+
import { fetchPipelineStages } from '../../../../utils/pipelineUtils.js';
21+
import { PipelineStageRecord } from '../../../../utils/types.js';
22+
23+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
24+
const messages = Messages.loadMessages('@salesforce/plugin-devops-center', 'devops.pipeline.stage.add');
25+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
26+
27+
export default class DevopsPipelineStageAdd extends SfCommand<AddPipelineStageResult> {
28+
public static readonly summary = messages.getMessage('summary');
29+
public static readonly description = messages.getMessage('description');
30+
public static readonly examples = messages.getMessages('examples');
31+
32+
public static readonly flags = {
33+
'target-org': Flags.requiredOrg({
34+
char: 'o',
35+
summary: messages.getMessage('flags.target-org.summary'),
36+
required: true,
37+
}),
38+
'api-version': Flags.orgApiVersion(),
39+
'pipeline-id': Flags.salesforceId({
40+
summary: messages.getMessage('flags.pipeline-id.summary'),
41+
required: true,
42+
char: undefined,
43+
}),
44+
name: Flags.string({
45+
summary: messages.getMessage('flags.name.summary'),
46+
char: 'n',
47+
required: true,
48+
}),
49+
'next-stage-id': Flags.salesforceId({
50+
summary: messages.getMessage('flags.next-stage-id.summary'),
51+
required: true,
52+
char: undefined,
53+
}),
54+
};
55+
56+
public async run(): Promise<AddPipelineStageResult> {
57+
const { flags } = await this.parse(DevopsPipelineStageAdd);
58+
const org: Org = flags['target-org'];
59+
const connection = org.getConnection(flags['api-version']);
60+
const pipelineId = flags['pipeline-id'];
61+
const nextStageId = flags['next-stage-id'];
62+
63+
let stages: PipelineStageRecord[];
64+
try {
65+
stages = await fetchPipelineStages(connection, pipelineId);
66+
} catch (error: unknown) {
67+
const errMsg = error instanceof Error ? error.message : String(error);
68+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
69+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
70+
}
71+
throw error;
72+
}
73+
74+
if (!stages.some((s) => s.Id === nextStageId)) {
75+
this.error(messages.getMessage('error.StageNotFound', [nextStageId, pipelineId]));
76+
}
77+
78+
let result: AddPipelineStageResult;
79+
try {
80+
result = await addPipelineStage({
81+
connection,
82+
pipelineId,
83+
name: flags['name'],
84+
nextStageId,
85+
});
86+
} catch (error: unknown) {
87+
const errMsg = error instanceof Error ? error.message : String(error);
88+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
89+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
90+
}
91+
throw error;
92+
}
93+
94+
if (result.success) {
95+
this.log(`Successfully added stage "${result.name ?? ''}" to the pipeline.`);
96+
this.log(` Stage ID: ${result.stageId ?? ''}`);
97+
this.log(` Position: before "${nextStageId}"`);
98+
this.log(` Pipeline ID: ${pipelineId}`);
99+
} else {
100+
this.error(`Failed to add stage: ${result.error ?? ''}`);
101+
}
102+
103+
return result;
104+
}
105+
}

src/utils/addPipelineStage.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 { Connection } from '@salesforce/core';
18+
19+
export type AddPipelineStageParams = {
20+
connection: Connection;
21+
pipelineId: string;
22+
name: string;
23+
nextStageId: string;
24+
};
25+
26+
export type AddPipelineStageResult = {
27+
success: boolean;
28+
stageId?: string;
29+
name?: string;
30+
nextStageId?: string;
31+
pipelineId?: string;
32+
error?: string;
33+
};
34+
35+
type SObjectCreateResult = {
36+
id: string;
37+
success: boolean;
38+
errors?: Array<{ message: string }>;
39+
};
40+
41+
/**
42+
* Adds a stage to a DevOps Center pipeline via sObject create.
43+
* POST /services/data/v{version}/sobjects/DevopsPipelineStage
44+
*/
45+
export async function addPipelineStage(params: AddPipelineStageParams): Promise<AddPipelineStageResult> {
46+
const { connection, pipelineId, name, nextStageId } = params;
47+
48+
const result = await connection.sobject('DevopsPipelineStage').create({
49+
Name: name,
50+
DevopsPipelineId: pipelineId,
51+
NextStageId: nextStageId,
52+
});
53+
54+
const createResult = result as unknown as SObjectCreateResult;
55+
56+
if (!createResult.success) {
57+
const errorMsg = createResult.errors?.map((e) => e.message).join('; ') ?? 'Unknown error';
58+
return {
59+
success: false,
60+
error: errorMsg,
61+
};
62+
}
63+
64+
return {
65+
success: true,
66+
stageId: createResult.id,
67+
name,
68+
nextStageId,
69+
pipelineId,
70+
};
71+
}

0 commit comments

Comments
 (0)