Skip to content

Commit aab7971

Browse files
committed
chore: attach-project command
1 parent bdce833 commit aab7971

8 files changed

Lines changed: 515 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:attach-project",
5+
"flagAliases": [],
6+
"flagChars": ["o"],
7+
"flags": ["api-version", "flags-dir", "json", "pipeline-id", "project-id", "target-org"],
8+
"plugin": "@salesforce/plugin-devops-center"
9+
},
210
{
311
"alias": [],
412
"command": "devops:project:create",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# summary
2+
3+
Attach a DevOps Center project to a pipeline.
4+
5+
# description
6+
7+
You can attach a project to only one pipeline.
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+
# flags.project-id.summary
18+
19+
ID of the DevOps Center project.
20+
21+
# examples
22+
23+
- Attach a project to a pipeline using the project ID and pipeline ID.
24+
25+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --project-id 0Hn000000000001
26+
27+
# error.AlreadyAttached
28+
29+
Project %s is already attached to pipeline %s. Remove the project, and then try again.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
},
9393
"pull-request": {
9494
"description": "Commands for managing DevOps Center pull requests."
95+
},
96+
"pipeline": {
97+
"description": "Commands for managing DevOps Center pipelines."
9598
}
9699
}
97100
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/AttachProjectResult",
4+
"definitions": {
5+
"AttachProjectResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"projectId": {
12+
"type": "string"
13+
},
14+
"pipelineId": {
15+
"type": "string"
16+
},
17+
"error": {
18+
"type": "string"
19+
}
20+
},
21+
"required": ["success", "projectId", "pipelineId"],
22+
"additionalProperties": false
23+
}
24+
}
25+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 { attachProject, AttachProjectResult, findExistingAttachment } from '../../../utils/attachProject.js';
20+
21+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
22+
const messages = Messages.loadMessages('@salesforce/plugin-devops-center', 'devops.pipeline.attach-project');
23+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
24+
25+
export default class DevopsPipelineAttachProject extends SfCommand<AttachProjectResult> {
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+
'pipeline-id': Flags.string({
38+
summary: messages.getMessage('flags.pipeline-id.summary'),
39+
required: true,
40+
}),
41+
'project-id': Flags.string({
42+
summary: messages.getMessage('flags.project-id.summary'),
43+
required: true,
44+
}),
45+
};
46+
47+
public async run(): Promise<AttachProjectResult> {
48+
const { flags } = await this.parse(DevopsPipelineAttachProject);
49+
const org: Org = flags['target-org'];
50+
const connection = org.getConnection(flags['api-version']);
51+
const projectId = flags['project-id'];
52+
const pipelineId = flags['pipeline-id'];
53+
54+
let existingPipelineId: string | undefined;
55+
try {
56+
existingPipelineId = await findExistingAttachment(connection, projectId);
57+
} catch (error: unknown) {
58+
const errMsg = error instanceof Error ? error.message : String(error);
59+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
60+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
61+
}
62+
throw error;
63+
}
64+
65+
if (existingPipelineId) {
66+
this.error(messages.getMessage('error.AlreadyAttached', [projectId, existingPipelineId]));
67+
}
68+
69+
let result: AttachProjectResult;
70+
try {
71+
result = await attachProject({ connection, projectId, pipelineId });
72+
} catch (error: unknown) {
73+
const errMsg = error instanceof Error ? error.message : String(error);
74+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
75+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
76+
}
77+
throw error;
78+
}
79+
80+
if (result.success) {
81+
this.log('Successfully attached project to pipeline.');
82+
this.log(` Project ID: ${projectId}`);
83+
this.log(` Pipeline ID: ${pipelineId}`);
84+
} else {
85+
this.error(`Failed to attach project to pipeline: ${result.error ?? ''}`);
86+
}
87+
88+
return result;
89+
}
90+
}

src/utils/attachProject.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 AttachProjectParams = {
20+
connection: Connection;
21+
projectId: string;
22+
pipelineId: string;
23+
};
24+
25+
export type AttachProjectResult = {
26+
success: boolean;
27+
projectId: string;
28+
pipelineId: string;
29+
error?: string;
30+
};
31+
32+
/**
33+
* Checks if a project is already attached to a pipeline.
34+
* Returns the existing pipeline ID if found, undefined otherwise.
35+
*/
36+
export async function findExistingAttachment(connection: Connection, projectId: string): Promise<string | undefined> {
37+
const result = await connection.query<{ DevopsPipelineId: string }>(
38+
`SELECT DevopsPipelineId FROM DevopsProjectPipeline WHERE DevopsProjectId = '${projectId}' LIMIT 1`
39+
);
40+
return (result.records ?? [])[0]?.DevopsPipelineId;
41+
}
42+
43+
/**
44+
* Attaches a DevOps Center project to a pipeline by creating a DevopsProjectPipeline junction record.
45+
*/
46+
export async function attachProject(params: AttachProjectParams): Promise<AttachProjectResult> {
47+
const { connection, projectId, pipelineId } = params;
48+
49+
const result = await connection.sobject('DevopsProjectPipeline').create({
50+
DevopsProjectId: projectId,
51+
DevopsPipelineId: pipelineId,
52+
});
53+
54+
if (result.success) {
55+
return { success: true, projectId, pipelineId };
56+
}
57+
58+
const errorMessages = result.errors?.map((e) => (typeof e === 'string' ? e : JSON.stringify(e))).join('; ');
59+
return {
60+
success: false,
61+
projectId,
62+
pipelineId,
63+
error: errorMessages ?? 'Unknown error',
64+
};
65+
}

0 commit comments

Comments
 (0)