Skip to content

Commit bdce833

Browse files
committed
chore: create new project command
1 parent ec0bb9b commit bdce833

7 files changed

Lines changed: 428 additions & 4 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:project:create",
5+
"flagAliases": [],
6+
"flagChars": ["d", "n", "o"],
7+
"flags": ["api-version", "description", "flags-dir", "json", "name", "target-org"],
8+
"plugin": "@salesforce/plugin-devops-center"
9+
},
210
{
311
"alias": [],
412
"command": "devops:project:list",

messages/devops.project.create.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# summary
22

3-
Create a DevOps Center project in a Salesforce org.
3+
Create a DevOps Center project in a DevOps Center org.
44

55
# description
66

7-
Creates a new DevOps Center project with the specified name and optional description. Requires a DevOps Center org; use `--target-org` to specify the target org.
7+
Creates a new DevOps Center project with the specified name and optional description.
88

99
# flags.target-org.summary
1010

@@ -20,10 +20,10 @@ Description of the new project; if not specified, the description is blank.
2020

2121
# examples
2222

23-
- Create a new DevOps Center project in the specified org:
23+
- Create a new DevOps Center project in the specified org.
2424

2525
<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "MyApp Release"
2626

27-
- Create a project with a name and description:
27+
- Create a project with a name and description.
2828

2929
<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Platform Update" --description "Platform services update"

schemas/devops-project-create.json

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/CreateProjectResult",
4+
"definitions": {
5+
"CreateProjectResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"projectId": {
12+
"type": "string"
13+
},
14+
"name": {
15+
"type": "string"
16+
},
17+
"description": {
18+
"type": "string"
19+
},
20+
"error": {
21+
"type": "string"
22+
}
23+
},
24+
"required": ["success"],
25+
"additionalProperties": false
26+
}
27+
}
28+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 { createProject, CreateProjectResult } from '../../../utils/createProject.js';
20+
21+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
22+
const messages = Messages.loadMessages('@salesforce/plugin-devops-center', 'devops.project.create');
23+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
24+
25+
export default class DevopsProjectCreate extends SfCommand<CreateProjectResult> {
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+
description: Flags.string({
43+
summary: messages.getMessage('flags.description.summary'),
44+
char: 'd',
45+
}),
46+
};
47+
48+
public async run(): Promise<CreateProjectResult> {
49+
const { flags } = await this.parse(DevopsProjectCreate);
50+
const org: Org = flags['target-org'];
51+
const connection = org.getConnection(flags['api-version']);
52+
53+
let result: CreateProjectResult;
54+
try {
55+
result = await createProject({
56+
connection,
57+
name: flags['name'],
58+
description: flags['description'] ?? '',
59+
});
60+
} catch (error: unknown) {
61+
const errMsg = error instanceof Error ? error.message : String(error);
62+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
63+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
64+
}
65+
throw error;
66+
}
67+
68+
if (result.success) {
69+
this.log(`Successfully created project: ${result.name ?? ''}`);
70+
this.log(` ID: ${result.projectId ?? ''}`);
71+
this.log(` Name: ${result.name ?? ''}`);
72+
if (result.description) {
73+
this.log(` Description: ${result.description}`);
74+
}
75+
} else {
76+
this.error(`Failed to create project: ${result.error ?? ''}`);
77+
}
78+
79+
return result;
80+
}
81+
}

src/utils/createProject.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 CreateProjectParams = {
20+
connection: Connection;
21+
name: string;
22+
description: string;
23+
};
24+
25+
export type CreateProjectResult = {
26+
success: boolean;
27+
projectId?: string;
28+
name?: string;
29+
description?: string;
30+
error?: string;
31+
};
32+
33+
/**
34+
* Creates a new DevOps Center project via sObject create on DevopsProject.
35+
*/
36+
export async function createProject(params: CreateProjectParams): Promise<CreateProjectResult> {
37+
const { connection, name, description } = params;
38+
39+
const result = await connection.sobject('DevopsProject').create({
40+
Name: name,
41+
Description: description || null,
42+
});
43+
44+
if (result.success) {
45+
return {
46+
success: true,
47+
projectId: result.id,
48+
name,
49+
description: description || undefined,
50+
};
51+
}
52+
53+
const errorMessages = result.errors?.map((e) => (typeof e === 'string' ? e : JSON.stringify(e))).join('; ');
54+
return {
55+
success: false,
56+
error: errorMessages ?? 'Unknown error',
57+
};
58+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 project create', () => {
23+
let sandbox: sinon.SinonSandbox;
24+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
let CreateCommand: any;
26+
const mockConnection = { getApiVersion: () => '65.0' };
27+
const mockOrg = { id: '1', getOrgId: () => '1', getConnection: () => mockConnection };
28+
const createProjectStub = sinon.stub();
29+
30+
before(async () => {
31+
const mod = await esmock('../../../../src/commands/devops/project/create.js', {
32+
'../../../../src/utils/createProject.js': {
33+
createProject: createProjectStub,
34+
},
35+
});
36+
CreateCommand = mod.default;
37+
});
38+
39+
beforeEach(() => {
40+
sandbox = sinon.createSandbox();
41+
createProjectStub.reset();
42+
});
43+
44+
afterEach(() => {
45+
sandbox.restore();
46+
});
47+
48+
describe('successful creation', () => {
49+
test
50+
.stdout()
51+
.stderr()
52+
.it('logs success with name and id', async (ctx) => {
53+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
55+
createProjectStub.resolves({
56+
success: true,
57+
projectId: '1Qg000000000001',
58+
name: 'MyApp Release',
59+
});
60+
61+
await CreateCommand.run(['--target-org', 'testOrg', '--name', 'MyApp Release']);
62+
63+
expect(ctx.stdout).to.contain('Successfully created project: MyApp Release');
64+
expect(ctx.stdout).to.contain('1Qg000000000001');
65+
});
66+
});
67+
68+
describe('successful creation with description', () => {
69+
test
70+
.stdout()
71+
.stderr()
72+
.it('logs description when provided', async (ctx) => {
73+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
75+
createProjectStub.resolves({
76+
success: true,
77+
projectId: '1Qg000000000002',
78+
name: 'Platform Update',
79+
description: 'Platform services update',
80+
});
81+
82+
await CreateCommand.run([
83+
'--target-org',
84+
'testOrg',
85+
'--name',
86+
'Platform Update',
87+
'--description',
88+
'Platform services update',
89+
]);
90+
91+
expect(ctx.stdout).to.contain('Platform Update');
92+
expect(ctx.stdout).to.contain('Platform services update');
93+
});
94+
});
95+
96+
describe('creation failure', () => {
97+
test
98+
.stdout()
99+
.stderr()
100+
.it('shows failure error', async (ctx) => {
101+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
103+
createProjectStub.resolves({
104+
success: false,
105+
error: 'DUPLICATE_VALUE: Name already exists',
106+
});
107+
108+
try {
109+
await CreateCommand.run(['--target-org', 'testOrg', '--name', 'Duplicate']);
110+
} catch (e) {
111+
// expected
112+
}
113+
114+
expect(ctx.stderr).to.contain('Failed to create project');
115+
});
116+
});
117+
118+
describe('DevOps Center not enabled', () => {
119+
test
120+
.stdout()
121+
.stderr()
122+
.it('shows DevOps Center not enabled error', async (ctx) => {
123+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
124+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
125+
createProjectStub.rejects(new Error("sObject type 'DevopsProject' is not supported"));
126+
127+
try {
128+
await CreateCommand.run(['--target-org', 'testOrg', '--name', 'Test']);
129+
} catch (e) {
130+
// expected
131+
}
132+
133+
expect(ctx.stderr).to.contain("DevOps Center isn't enabled");
134+
});
135+
});
136+
137+
describe('rethrows other errors', () => {
138+
test
139+
.stdout()
140+
.stderr()
141+
.it('rethrows non-DevOps errors', async () => {
142+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143+
sandbox.stub(Org, 'create' as any).returns(mockOrg);
144+
createProjectStub.rejects(new Error('Network error'));
145+
146+
try {
147+
await CreateCommand.run(['--target-org', 'testOrg', '--name', 'Test']);
148+
expect.fail('should have thrown');
149+
} catch (e: unknown) {
150+
expect((e as Error).message).to.contain('Network error');
151+
}
152+
});
153+
});
154+
});

0 commit comments

Comments
 (0)