Skip to content

Commit 2e556e0

Browse files
committed
chore: activate pipeline
1 parent 0fd5982 commit 2e556e0

7 files changed

Lines changed: 434 additions & 0 deletions

File tree

command-snapshot.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
[
2+
{
3+
"alias": [],
4+
"command": "devops:pipeline:activate",
5+
"flagAliases": [],
6+
"flagChars": ["o"],
7+
"flags": ["api-version", "flags-dir", "json", "pipeline-id", "target-org"],
8+
"plugin": "@salesforce/plugin-devops-center"
9+
},
210
{
311
"alias": [],
412
"command": "devops:pipeline:attach-project",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# summary
2+
3+
Activate a DevOps Center pipeline for deployments.
4+
5+
# description
6+
7+
A pipeline must have at least one stage before you activate it. You can't modify the pipeline stages after you activate and promote changes through it.
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.
16+
17+
# examples
18+
19+
- Activate a pipeline using the pipeline ID.
20+
21+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001
22+
23+
# error.NoStages
24+
25+
Can't activate pipeline %s. Add at least one stage, then try again.
26+
27+
# error.AlreadyActive
28+
29+
Pipeline %s is already active.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/ActivatePipelineResult",
4+
"definitions": {
5+
"ActivatePipelineResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"pipelineId": {
12+
"type": "string"
13+
},
14+
"status": {
15+
"type": "string"
16+
},
17+
"stageCount": {
18+
"type": "number"
19+
},
20+
"error": {
21+
"type": "string"
22+
}
23+
},
24+
"required": ["success", "pipelineId"],
25+
"additionalProperties": false
26+
}
27+
}
28+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 { activatePipeline, ActivatePipelineResult } from '../../../utils/activatePipeline.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.activate');
25+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
26+
27+
export default class DevopsPipelineActivate extends SfCommand<ActivatePipelineResult> {
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+
};
45+
46+
public async run(): Promise<ActivatePipelineResult> {
47+
const { flags } = await this.parse(DevopsPipelineActivate);
48+
const org: Org = flags['target-org'];
49+
const connection = org.getConnection(flags['api-version']);
50+
const pipelineId = flags['pipeline-id'];
51+
52+
let stages: PipelineStageRecord[];
53+
try {
54+
stages = await fetchPipelineStages(connection, pipelineId);
55+
} catch (error: unknown) {
56+
const errMsg = error instanceof Error ? error.message : String(error);
57+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
58+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
59+
}
60+
throw error;
61+
}
62+
63+
if (stages.length === 0) {
64+
this.error(messages.getMessage('error.NoStages', [pipelineId]));
65+
}
66+
67+
let result: ActivatePipelineResult;
68+
try {
69+
result = await activatePipeline({ connection, pipelineId });
70+
result.stageCount = stages.length;
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+
if (errMsg.includes('already active') || errMsg.includes('ALREADY_ACTIVE')) {
77+
this.error(messages.getMessage('error.AlreadyActive', [pipelineId]));
78+
}
79+
throw error;
80+
}
81+
82+
if (result.success) {
83+
this.log('Successfully activated the pipeline.');
84+
this.log(` Pipeline ID: ${pipelineId}`);
85+
this.log(` Status: ${result.status ?? 'Active'}`);
86+
this.log(` Stages: ${result.stageCount ?? stages.length}`);
87+
} else {
88+
this.error(`Failed to activate pipeline: ${result.error ?? ''}`);
89+
}
90+
91+
return result;
92+
}
93+
}

src/utils/activatePipeline.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 ActivatePipelineParams = {
20+
connection: Connection;
21+
pipelineId: string;
22+
};
23+
24+
export type ActivatePipelineResult = {
25+
success: boolean;
26+
pipelineId: string;
27+
status?: string;
28+
stageCount?: number;
29+
error?: string;
30+
};
31+
32+
/**
33+
* Activates a DevOps Center pipeline via the Connect API.
34+
* POST /services/data/v{version}/connect/devops/pipelines/{pipelineId}/activate
35+
*/
36+
export async function activatePipeline(params: ActivatePipelineParams): Promise<ActivatePipelineResult> {
37+
const { connection, pipelineId } = params;
38+
39+
const path = `/services/data/v${connection.getApiVersion()}/connect/devops/pipelines/${pipelineId}/activate`;
40+
41+
await connection.request({
42+
method: 'POST',
43+
url: path,
44+
body: '{}',
45+
headers: { 'Content-Type': 'application/json' },
46+
});
47+
48+
return {
49+
success: true,
50+
pipelineId,
51+
status: 'Active',
52+
};
53+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 esmock from 'esmock';
18+
import { expect, test } from '@oclif/test';
19+
import sinon from 'sinon';
20+
import { Org } from '@salesforce/core';
21+
22+
describe('devops pipeline activate', () => {
23+
let sandbox: sinon.SinonSandbox;
24+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
let ActivateCommand: any;
26+
const mockConnection = { getApiVersion: () => '65.0' };
27+
const mockOrg = { id: '1', getOrgId: () => '1', getConnection: () => mockConnection, getUsername: () => 'testOrg' };
28+
const activatePipelineStub = sinon.stub();
29+
const fetchPipelineStagesStub = sinon.stub();
30+
31+
before(async () => {
32+
const mod = await esmock('../../../../src/commands/devops/pipeline/activate.js', {
33+
'../../../../src/utils/activatePipeline.js': {
34+
activatePipeline: activatePipelineStub,
35+
},
36+
'../../../../src/utils/pipelineUtils.js': {
37+
fetchPipelineStages: fetchPipelineStagesStub,
38+
},
39+
});
40+
ActivateCommand = mod.default;
41+
});
42+
43+
beforeEach(() => {
44+
sandbox = sinon.createSandbox();
45+
activatePipelineStub.reset();
46+
fetchPipelineStagesStub.reset();
47+
});
48+
49+
afterEach(() => {
50+
sandbox.restore();
51+
});
52+
53+
describe('successful activation', () => {
54+
test
55+
.stdout()
56+
.stderr()
57+
.it('activates pipeline and logs success', async (ctx) => {
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
60+
fetchPipelineStagesStub.resolves([
61+
{ Id: '1', Name: 'Integration' },
62+
{ Id: '2', Name: 'UAT' },
63+
{ Id: '3', Name: 'Production' },
64+
]);
65+
activatePipelineStub.resolves({
66+
success: true,
67+
pipelineId: '0XB000000000001',
68+
status: 'Active',
69+
});
70+
71+
await ActivateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001']);
72+
73+
expect(ctx.stdout).to.contain('Successfully activated the pipeline.');
74+
expect(ctx.stdout).to.contain('0XB000000000001');
75+
expect(ctx.stdout).to.contain('Active');
76+
expect(ctx.stdout).to.contain('3');
77+
});
78+
});
79+
80+
describe('no stages error', () => {
81+
test
82+
.stdout()
83+
.stderr()
84+
.it('errors when pipeline has no stages', async (ctx) => {
85+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
86+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
87+
fetchPipelineStagesStub.resolves([]);
88+
89+
try {
90+
await ActivateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001']);
91+
expect.fail('should have thrown');
92+
} catch (e) {
93+
// expected
94+
}
95+
96+
expect(ctx.stderr).to.contain('Add at least one stage');
97+
});
98+
});
99+
100+
describe('already active error', () => {
101+
test
102+
.stdout()
103+
.stderr()
104+
.it('errors when pipeline is already active', async (ctx) => {
105+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
106+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
107+
fetchPipelineStagesStub.resolves([{ Id: '1', Name: 'Integration' }]);
108+
activatePipelineStub.rejects(new Error('Pipeline is already active'));
109+
110+
try {
111+
await ActivateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001']);
112+
expect.fail('should have thrown');
113+
} catch (e) {
114+
// expected
115+
}
116+
117+
expect(ctx.stderr).to.contain('already active');
118+
});
119+
});
120+
121+
describe('DevOps Center not enabled', () => {
122+
test
123+
.stdout()
124+
.stderr()
125+
.it('shows DevOps Center not enabled error', async (ctx) => {
126+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
127+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
128+
fetchPipelineStagesStub.rejects(new Error("sObject type 'DevopsPipelineStage' is not supported"));
129+
130+
try {
131+
await ActivateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001']);
132+
} catch (e) {
133+
// expected
134+
}
135+
136+
expect(ctx.stderr).to.contain("DevOps Center isn't enabled");
137+
});
138+
});
139+
140+
describe('rethrows other errors', () => {
141+
test
142+
.stdout()
143+
.stderr()
144+
.it('rethrows non-DevOps errors', async () => {
145+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
146+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
147+
fetchPipelineStagesStub.resolves([{ Id: '1', Name: 'Integration' }]);
148+
activatePipelineStub.rejects(new Error('Network error'));
149+
150+
try {
151+
await ActivateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001']);
152+
expect.fail('should have thrown');
153+
} catch (e: unknown) {
154+
expect((e as Error).message).to.contain('Network error');
155+
}
156+
});
157+
});
158+
});

0 commit comments

Comments
 (0)