diff --git a/command-snapshot.json b/command-snapshot.json index bbbf967..40f575f 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -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" @@ -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" }, { diff --git a/messages/devops.pipeline.create.md b/messages/devops.pipeline.create.md index 679c666..835bf56 100644 --- a/messages/devops.pipeline.create.md +++ b/messages/devops.pipeline.create.md @@ -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 diff --git a/messages/devops.pipeline.update.md b/messages/devops.pipeline.update.md index 54b2e2b..f82cd66 100644 --- a/messages/devops.pipeline.update.md +++ b/messages/devops.pipeline.update.md @@ -4,7 +4,7 @@ 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. @@ -12,9 +12,13 @@ A pipeline must have at least one stage before you can activate it. You can't mo 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 @@ -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 diff --git a/schemas/devops-pipeline-create.json b/schemas/devops-pipeline-create.json index ac72ed6..042b87e 100644 --- a/schemas/devops-pipeline-create.json +++ b/schemas/devops-pipeline-create.json @@ -14,9 +14,6 @@ "name": { "type": "string" }, - "description": { - "type": "string" - }, "status": { "type": "string" }, diff --git a/src/commands/devops/pipeline/create.ts b/src/commands/devops/pipeline/create.ts index c27ac16..efcebd9 100644 --- a/src/commands/devops/pipeline/create.ts +++ b/src/commands/devops/pipeline/create.ts @@ -64,9 +64,10 @@ export default class DevopsPipelineCreate extends SfCommand { - 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 = { name, vcsType: repoType, - stages: DEFAULT_STAGES, + stages: stageNames.map((stageName) => ({ name: stageName })), }; if (createRepo) { @@ -157,10 +149,6 @@ export async function createPipeline(params: CreatePipelineParams): Promise({ method: 'POST', url: path, @@ -172,7 +160,6 @@ export async function createPipeline(params: CreatePipelineParams): Promise { ]); 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'); @@ -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'); @@ -140,7 +140,7 @@ describe('devops pipeline update', () => { 'testOrg', '--pipeline-id', '0XB000000000001', - '--no-active', + '--deactivate', '--name', 'Archived Pipeline', ]); @@ -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' }]); @@ -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() @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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'); diff --git a/test/utils/createPipeline.test.ts b/test/utils/createPipeline.test.ts index b880409..9200755 100644 --- a/test/utils/createPipeline.test.ts +++ b/test/utils/createPipeline.test.ts @@ -231,28 +231,51 @@ describe('createPipeline utilities', () => { expect(providerInfo.bitbucketProjectKey).to.equal('PROJ'); }); - it('includes description when provided', async () => { + it('uses custom stage names when stages are provided', async () => { (connectionStub.request as sinon.SinonStub).resolves({ - id: '0XB000000000003', + id: '0XB000000000005', message: 'Created', status: 'Inactive', }); (connectionStub.getApiVersion as sinon.SinonStub).returns('65.0'); - const result = await createPipeline({ + await createPipeline({ connection: connectionStub as unknown as Connection, - name: 'Described Pipeline', - description: 'My description', + name: 'Custom Stages Pipeline', repo: 'https://github.com/myorg/myrepo', repoType: 'github', + stages: ['Dev', 'QA', 'Prod'], }); - expect(result.success).to.be.true; - expect(result.description).to.equal('My description'); + const callArgs = (connectionStub.request as sinon.SinonStub).firstCall.args[0]; + const body = JSON.parse(callArgs.body as string) as Record; + expect(body.stages).to.deep.equal([{ name: 'Dev' }, { name: 'QA' }, { name: 'Prod' }]); + }); + + it('falls back to default stages when stages is an empty array', async () => { + (connectionStub.request as sinon.SinonStub).resolves({ + id: '0XB000000000006', + message: 'Created', + status: 'Inactive', + }); + (connectionStub.getApiVersion as sinon.SinonStub).returns('65.0'); + + await createPipeline({ + connection: connectionStub as unknown as Connection, + name: 'Default Stages Pipeline', + repo: 'https://github.com/myorg/myrepo', + repoType: 'github', + stages: [], + }); const callArgs = (connectionStub.request as sinon.SinonStub).firstCall.args[0]; const body = JSON.parse(callArgs.body as string) as Record; - expect(body.description).to.equal('My description'); + expect(body.stages).to.deep.equal([ + { name: 'Integration' }, + { name: 'UAT' }, + { name: 'Staging' }, + { name: 'Production' }, + ]); }); it('propagates API errors', async () => {