Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
"alias": [],
"command": "devops:pipeline:create",
"flagAliases": [],
"flagChars": ["d", "n", "o", "r"],
"flagChars": ["n", "o", "r", "s"],
"flags": [
"api-version",
"bitbucket-project-key",
"bitbucket-workspace",
"create-repo",
"description",
"flags-dir",
"json",
"name",
"repo",
"repo-owner",
"repo-type",
"stage",
"target-org"
],
"plugin": "@salesforce/plugin-devops-center"
Expand Down Expand Up @@ -81,7 +81,7 @@
"command": "devops:pipeline:update",
"flagAliases": [],
"flagChars": ["n", "o"],
"flags": ["active", "api-version", "flags-dir", "json", "name", "pipeline-id", "target-org"],
"flags": ["activate", "api-version", "deactivate", "flags-dir", "json", "name", "pipeline-id", "target-org"],
"plugin": "@salesforce/plugin-devops-center"
},
{
Expand Down
18 changes: 11 additions & 7 deletions messages/devops.pipeline.create.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,31 @@ Bitbucket workspace that will own the repository. Required when creating a Bitbu

Bitbucket project key to associate with the repository. Optional when creating a Bitbucket repository using '--create-repo'.

# flags.description.summary
# flags.stage.summary

Description of the pipeline.
Name of a pipeline stage, in promotion order. Repeat the flag for each stage. Defaults to Integration, UAT, Staging, and Production.

# examples

- Create a pipeline and associate it with an existing GitHub repository.
- Create a pipeline and associate it with an existing GitHub repository:

<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo https://github.com/myorg/myrepo

- Create a pipeline and associate it with a new GitHub repository.
- Create a pipeline and associate it with a new GitHub repository:

<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo my-new-repo --repo-type github --repo-owner myorg --create-repo

- Create a pipeline and create a new Bitbucket repository.
- Create a pipeline and create a new Bitbucket repository:

<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo my-new-repo --repo-type bitbucket --bitbucket-workspace myworkspace --bitbucket-project-key PROJ --create-repo

- Create a pipeline with a description and associate it with an existing Bitbucket repository.
- Create a pipeline and associate it with an existing Bitbucket repository:

<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo https://bitbucket.org/myworkspace/myrepo --description "Main CI/CD pipeline for production releases"
<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo https://bitbucket.org/myworkspace/myrepo

- Create a pipeline with custom stage names instead of the default stages:

<%= config.bin %> <%= command.id %> --target-org my-devops-org --name "Release Pipeline" --repo https://github.com/myorg/myrepo --stage Dev --stage QA --stage Prod

# error.RepoTypeRequired

Expand Down
16 changes: 10 additions & 6 deletions messages/devops.pipeline.update.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ Update a DevOps Center pipeline.

# description

Activate, deactivate, or rename a DevOps Center pipeline. Use --active to activate, --no-active to deactivate, and --name to rename. You can combine --no-active and --name in one command.
Activate, deactivate, or rename a DevOps Center pipeline. Use --activate to activate, --deactivate to deactivate, and --name to rename. You can combine --deactivate and --name in one command.

A pipeline must have at least one stage before you can activate it. You can't modify the pipeline stages after you activate and promote changes through it.

# flags.pipeline-id.summary

ID of the pipeline.

# flags.active.summary
# flags.activate.summary

Activate the pipeline. Use --no-active to deactivate.
Activate the pipeline. Can't be used with --deactivate.

# flags.deactivate.summary

Deactivate the pipeline. Can't be used with --activate.

# flags.name.summary

Expand All @@ -24,15 +28,15 @@ New name for the pipeline.

- Activate a pipeline:

<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --active
<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --activate

- Deactivate and rename in one step.

<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --no-active --name "My Pipeline"
<%= config.bin %> <%= command.id %> --target-org my-devops-org --pipeline-id 0XB000000000001 --deactivate --name "My Pipeline"

# error.NoFlags

Provide at least one of --active/--no-active or --name.
Provide at least one of --activate, --deactivate, or --name.

# error.NoStages

Expand Down
3 changes: 0 additions & 3 deletions schemas/devops-pipeline-create.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"status": {
"type": "string"
},
Expand Down
9 changes: 5 additions & 4 deletions src/commands/devops/pipeline/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ export default class DevopsPipelineCreate extends SfCommand<CreatePipelineResult
'bitbucket-project-key': Flags.string({
summary: messages.getMessage('flags.bitbucket-project-key.summary'),
}),
description: Flags.string({
summary: messages.getMessage('flags.description.summary'),
char: 'd',
stage: Flags.string({
summary: messages.getMessage('flags.stage.summary'),
char: 's',
multiple: true,
}),
};

Expand Down Expand Up @@ -102,13 +103,13 @@ export default class DevopsPipelineCreate extends SfCommand<CreatePipelineResult
result = await createPipeline({
connection,
name: flags['name'],
description: flags['description'],
repo: flags['repo'],
repoType,
createRepo: flags['create-repo'],
repoOwner: flags['repo-owner'],
bitbucketWorkspace: flags['bitbucket-workspace'],
bitbucketProjectKey: flags['bitbucket-project-key'],
stages: flags['stage'],
});
} catch (error: unknown) {
const errMsg = error instanceof Error ? error.message : String(error);
Expand Down
14 changes: 10 additions & 4 deletions src/commands/devops/pipeline/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ export default class DevopsPipelineUpdate extends SfCommand<PipelineUpdateResult
required: true,
char: undefined,
}),
active: Flags.boolean({
summary: messages.getMessage('flags.active.summary'),
activate: Flags.boolean({
summary: messages.getMessage('flags.activate.summary'),
required: false,
allowNo: true,
exclusive: ['deactivate'],
}),
deactivate: Flags.boolean({
summary: messages.getMessage('flags.deactivate.summary'),
required: false,
exclusive: ['activate'],
}),
name: Flags.string({
summary: messages.getMessage('flags.name.summary'),
Expand Down Expand Up @@ -70,7 +75,8 @@ export default class DevopsPipelineUpdate extends SfCommand<PipelineUpdateResult
const org: Org = flags['target-org'];
const connection = org.getConnection(flags['api-version']);
const pipelineId = flags['pipeline-id'];
const activateFlag = flags.active;
// Derive tri-state: true = activate, false = deactivate, undefined = neither requested.
const activateFlag = flags.activate ? true : flags.deactivate ? false : undefined;
const newName = flags.name;

if (activateFlag === undefined && newName === undefined) {
Expand Down
27 changes: 7 additions & 20 deletions src/utils/createPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,19 @@ export type RepoInfo = {
export type CreatePipelineParams = {
connection: Connection;
name: string;
description?: string;
repo: string;
repoType: string;
createRepo?: boolean;
repoOwner?: string;
bitbucketWorkspace?: string;
bitbucketProjectKey?: string;
stages?: string[];
};

export type CreatePipelineResult = {
success: boolean;
pipelineId?: string;
name?: string;
description?: string;
status?: string;
repository?: RepoInfo;
error?: string;
Expand All @@ -51,7 +50,7 @@ type ConnectPipelineResponse = {
status: string;
};

const DEFAULT_STAGES = [{ name: 'Integration' }, { name: 'UAT' }, { name: 'Staging' }, { name: 'Production' }];
const DEFAULT_STAGE_NAMES = ['Integration', 'UAT', 'Staging', 'Production'];

/**
* Detects repo type from a URL. Returns 'github' or 'bitbucket', or undefined.
Expand Down Expand Up @@ -120,24 +119,17 @@ export class GitHubOwnerNotFoundError extends Error {
* POST /services/data/v{version}/connect/devops/pipelines
*/
export async function createPipeline(params: CreatePipelineParams): Promise<CreatePipelineResult> {
const {
connection,
name,
description,
repo,
repoType,
createRepo,
repoOwner,
bitbucketWorkspace,
bitbucketProjectKey,
} = params;
const { connection, name, repo, repoType, createRepo, repoOwner, bitbucketWorkspace, bitbucketProjectKey, stages } =
params;

const path = `/services/data/v${connection.getApiVersion()}/connect/devops/pipelines`;

const stageNames = stages && stages.length > 0 ? stages : DEFAULT_STAGE_NAMES;

const payload: Record<string, unknown> = {
name,
vcsType: repoType,
stages: DEFAULT_STAGES,
stages: stageNames.map((stageName) => ({ name: stageName })),
};

if (createRepo) {
Expand All @@ -157,10 +149,6 @@ export async function createPipeline(params: CreatePipelineParams): Promise<Crea
payload.vcsRepoUrl = repo;
}

if (description) {
payload.description = description;
}

const data = await connection.request<ConnectPipelineResponse>({
method: 'POST',
url: path,
Expand All @@ -172,7 +160,6 @@ export async function createPipeline(params: CreatePipelineParams): Promise<Crea
success: true,
pipelineId: data.id,
name,
description,
status: data.status ?? 'Inactive',
repository: {
repoUrl: repo,
Expand Down
45 changes: 36 additions & 9 deletions test/commands/devops/pipeline/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('devops pipeline update', () => {
]);
activatePipelineStub.resolves({ success: true, pipelineId: '0XB000000000001', status: 'Active' });

await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--activate']);

expect(ctx.stdout).to.contain('Successfully activated the pipeline.');
expect(ctx.stdout).to.contain('0XB000000000001');
Expand All @@ -97,7 +97,7 @@ describe('devops pipeline update', () => {
fetchPipelineStagesStub.resolves([{ Id: '1', Name: 'Integration' }]);
queryStub.resolves({ records: [{ IsActive: true, Name: 'My Pipeline' }] });

await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--no-active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--deactivate']);

expect(ctx.stdout).to.contain('Successfully deactivated the pipeline.');
expect(ctx.stdout).to.contain('0XB000000000001');
Expand Down Expand Up @@ -140,7 +140,7 @@ describe('devops pipeline update', () => {
'testOrg',
'--pipeline-id',
'0XB000000000001',
'--no-active',
'--deactivate',
'--name',
'Archived Pipeline',
]);
Expand All @@ -159,7 +159,7 @@ describe('devops pipeline update', () => {
test
.stdout()
.stderr()
.it('errors when neither --active nor --name is provided', async (ctx) => {
.it('errors when neither --activate, --deactivate, nor --name is provided', async (ctx) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sandbox.stub(Org, 'create' as any).returns(mockOrg);
fetchPipelineStagesStub.resolves([{ Id: '1', Name: 'Integration' }]);
Expand All @@ -175,6 +175,33 @@ describe('devops pipeline update', () => {
});
});

describe('mutually exclusive activate/deactivate', () => {
test
.stdout()
.stderr()
.it('errors when both --activate and --deactivate are provided', async (ctx) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sandbox.stub(Org, 'create' as any).returns(mockOrg);
fetchPipelineStagesStub.resolves([{ Id: '1', Name: 'Integration' }]);

try {
await UpdateCommand.run([
'--target-org',
'testOrg',
'--pipeline-id',
'0XB000000000001',
'--activate',
'--deactivate',
]);
expect.fail('should have thrown');
} catch (e) {
// expected
}

expect(ctx.stderr).to.contain('cannot also be provided');
});
});

describe('no stages error on activate', () => {
test
.stdout()
Expand All @@ -185,7 +212,7 @@ describe('devops pipeline update', () => {
fetchPipelineStagesStub.resolves([]);

try {
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--activate']);
expect.fail('should have thrown');
} catch (e) {
// expected
Expand All @@ -206,7 +233,7 @@ describe('devops pipeline update', () => {
queryStub.resolves({ records: [{ IsActive: true, Name: 'My Pipeline' }] });

try {
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--activate']);
expect.fail('should have thrown');
} catch (e) {
// expected
Expand All @@ -227,7 +254,7 @@ describe('devops pipeline update', () => {
queryStub.resolves({ records: [{ IsActive: false, Name: 'My Pipeline' }] });

try {
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--no-active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--deactivate']);
expect.fail('should have thrown');
} catch (e) {
// expected
Expand All @@ -247,7 +274,7 @@ describe('devops pipeline update', () => {
fetchPipelineStagesStub.rejects(new Error("sObject type 'DevopsPipelineStage' is not supported"));

try {
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--activate']);
} catch (e) {
// expected
}
Expand All @@ -267,7 +294,7 @@ describe('devops pipeline update', () => {
activatePipelineStub.rejects(new Error('Network error'));

try {
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--active']);
await UpdateCommand.run(['--target-org', 'testOrg', '--pipeline-id', '0XB000000000001', '--activate']);
expect.fail('should have thrown');
} catch (e: unknown) {
expect((e as Error).message).to.contain('Network error');
Expand Down
Loading
Loading