Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ add new command 'm365 pp pipeline list' to list Powe… #6434

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
105 changes: 105 additions & 0 deletions docs/docs/cmd/pp/pipeline/pipeline-list.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# pp pipeline list

Lists the Power Platform pipelines for the specified environment.

## Usage

```sh
m365 pp pipeline list [options]
```

## Options

`-e, --environmentName <environment>`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're missing the line ```md definition-list above this line. Do compare with other mdx files to see. And if you want you can build the docs and serve them locally to see if the page looks correctly...

: The name or id of the environment for which to list the pipelines.

`--asAdmin`
: Run the command as admin and retrieve pipelines from environments you do not have explicitly assigned permissions to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A closing ``` is necessary just below the last option and before the <Global/> component

<Global />

## Remarks

:::warning

This command is based on an API that is currently in preview and is subject to change once the API reaches general availability.

Register the CLI for Microsoft 365 or Microsoft Entra application as a management application for the Power Platform using

m365 pp managementapp add [options]

:::

## Examples

Retrieve the list of deployment pipelines in the specified Power Platform environment.

```sh
m365 pp pipeline list --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5
```

Retrieve the list of deployment pipelines as admin in the specified Power Platform environment.

```sh
m365 pp pipeline list --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 --asAdmin
```

## Response

<Tabs>
<TabItem value="JSON">

```json
[
{
"name": "Sales Pipeline",
"deploymentpipelineid": "3ef59632-f087-ef11-8a69-7c1e521ba3ec",
"ownerid": "27f5bb85-6b7a-ef11-ac22-000d3a4e3cc8",
"statuscode": 1
}
]
```
</TabItem>

<TabItem value="Text">

```text
deploymentpipelineid: 3ef59632-f087-ef11-8a69-7c1e521ba3ec
name : Sales Pipeline
ownerid : 27f5bb85-6b7a-ef11-ac22-000d3a4e3cc8
statuscode : 1
```
</TabItem>

<TabItem value="CSV">

```csv
name,deploymentpipelineid,ownerid,statuscode
Sales Pipeline,3ef59632-f087-ef11-8a69-7c1e521ba3ec,27f5bb85-6b7a-ef11-ac22-000d3a4e3cc8,1
```
</TabItem>

<TabItem value="Markdown">

```md
# m365 pp pipeline list --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5

Date: 17/10/2024

## environmentName (default) (/providers/Microsoft.BusinessAppPlatform/environments/Default-e1dd4023-a656-480a-8a0e-c1b1eec51e1d)

Property | Value
---------|-------
name | Sales Pipeline
deploymentpipelineid | 3ef59632-f087-ef11-8a69-7c1e521ba3ec
ownerid | 27f5bb85-6b7a-ef11-ac22-000d3a4e3cc8
statuscode | 1
```

</TabItem>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove superfluous line breaks like this one.

</Tabs>
9 changes: 9 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,15 @@ const sidebars: SidebarsConfig = {
}
]
},
{
pipeline: [
{
type: 'doc',
label: 'pipeline list',
id: 'cmd/pp/pipeline/pipeline-list'
}
]
},
{
solution: [
{
Expand Down
1 change: 1 addition & 0 deletions src/m365/pp/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default {
GATEWAY_LIST: `${prefix} gateway list`,
MANAGEMENTAPP_ADD: `${prefix} managementapp add`,
MANAGEMENTAPP_LIST: `${prefix} managementapp list`,
PIPELINE_LIST: `${prefix} pipeline list`,
SOLUTION_GET: `${prefix} solution get`,
SOLUTION_LIST: `${prefix} solution list`,
SOLUTION_PUBLISH: `${prefix} solution publish`,
Expand Down
138 changes: 138 additions & 0 deletions src/m365/pp/commands/pipeline/pipeline-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import assert from "assert";
import commands from "../../commands.js";
import command from './pipeline-list.js';
import sinon from 'sinon';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import auth from '../../../../Auth.js';
import { Logger } from '../../../../cli/Logger.js';
import { pid } from '../../../../utils/pid.js';
import { session } from "../../../../utils/session.js";
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import { accessToken } from '../../../../utils/accessToken.js';
import { CommandError } from "../../../../Command.js";
import { powerPlatform } from "../../../../utils/powerPlatform.js";

describe(commands.PIPELINE_LIST, () => {
const environmentName = 'environmentName';
const mockPipelineListResponse: any = [
{
'_ownerid_value': 'owner1',
deploymentpipelineid: 'deploymentpipelineid1',
name: 'pipeline1',
statuscode: 'statuscode1'
}
];
const mockEnvironmentResponse = {
"id": `/providers/Microsoft.BusinessAppPlatform/environments/Default-Environment`,
"type": "Microsoft.BusinessAppPlatform/environments",
"location": "unitedstates",
"name": "Default-Environment",
"properties": {
"displayName": "contoso (default)",
"isDefault": true,
linkedEnvironmentMetadata: {
instanceApiUrl: 'https://contoso.crm.dynamics.com'
}
}
};

let log: string[];
let logger: Logger;

let loggerLogSpy: sinon.SinonSpy;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
sinon.stub(accessToken, 'assertDelegatedAccessToken').returns();
auth.connection.active = true;
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
loggerLogSpy = sinon.spy(logger, 'log');
});

afterEach(() => {
sinonUtil.restore([
request.get
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(commands.PIPELINE_LIST, command.name);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('defines correct properties for the default output', () => {
assert.deepStrictEqual(command.defaultProperties(), ['name', 'deploymentpipelineid', '_ownerid_value', 'statuscode']);
});

it('retrieves pipelines in the specified Power Platform environment', async () => {
const getEnvironmentStub = await sinon.stub(powerPlatform as any, 'getDynamicsInstanceApiUrl').callsFake(() => Promise.resolve(mockEnvironmentResponse));
const getPipelineStub = sinon.stub(request, 'get').callsFake(async (opts) => {
if ((opts.url as string).indexOf('/api/data/v9.0/deploymentpipelines') > -1) {
return {
value: [
{
'_ownerid_value': 'owner1',
deploymentpipelineid: 'deploymentpipelineid1',
name: 'pipeline1',
statuscode: 'statuscode1'
}
]
};
}
throw new Error('Invalid request');
});

await command.action(logger, {
options: {
environmentName: environmentName,
asAdmin: false
}
});

assert(getEnvironmentStub.called);
assert(getPipelineStub.called);

assert(loggerLogSpy.calledWith(sinon.match(mockPipelineListResponse)));
});

it('correctly handles error when retrieving environment details or pipelines', async () => {
const errorMessage = 'An error has occurred';
sinon.stub(request, 'get').callsFake(async () => {
throw errorMessage;
});

await assert.rejects(command.action(logger, {
options: {
environmentName: environmentName,
asAdmin: false
}
}), new CommandError(errorMessage));
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this linebreak as well.. and i see another couple of line breaks that can be removed here.

});
89 changes: 89 additions & 0 deletions src/m365/pp/commands/pipeline/pipeline-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@

import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { powerPlatform } from '../../../../utils/powerPlatform.js';
import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js';
import commands from '../../commands.js';

interface Options extends GlobalOptions {
environmentName: string;
asAdmin?: boolean;
}

interface CommandArgs {
DevPio marked this conversation as resolved.
Show resolved Hide resolved
options: Options;
}

class PpPipelineListCommand extends PowerPlatformCommand {

constructor() {
super();
this.#initTelemetry();
this.#initOptions();
}

public get name(): string {
DevPio marked this conversation as resolved.
Show resolved Hide resolved
return commands.PIPELINE_LIST;
}

public get description(): string {
return 'Lists Microsoft Power Platform pipelines in the specified Power Platform environment.';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the exact same description as is in the docs file.

}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
asAdmin: !!args.options.asAdmin
});
});
}

#initOptions(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I overlooked this the previous review, but you're actually using the older setup of options and validation. We've since moved to zod, which is a tool to help parse options and validate the schema.

Would you mind reworking this command so that it aligns with the new way of working on this count? You can find a good example in recyclebinitem-restore.ts.

this.options.unshift(
{
option: '-e, --environmentName <environmentName>'
},
{
option: '--asAdmin'
}
);
}

public defaultProperties(): string[] | undefined {
return ['name', 'deploymentpipelineid', '_ownerid_value', 'statuscode'];
}

public async commandAction(logger: Logger, args: any): Promise<void> {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this superfluous line break

try {
const dynamicsApiUrl = await powerPlatform.getDynamicsInstanceApiUrl(args.options.environmentName, args.options.asAdmin);

const pipelines = await this.listPipelines(dynamicsApiUrl);

await logger.log(pipelines);
}
catch (ex: any) {
this.handleRejectedODataJsonPromise(ex);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this superfluous line break

}

private async listPipelines(instanceUrl: string): Promise<any> {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this superfluous line break

const pipelineListRequestOptions: CliRequestOptions = {
url: `${instanceUrl}/api/data/v9.0/deploymentpipelines`,
headers: {
accept: 'application/json'
},
responseType: 'json'
};

DevPio marked this conversation as resolved.
Show resolved Hide resolved
const pipelines = await request.get<any>(pipelineListRequestOptions);

return pipelines.value;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this sup... you get the gist of it :-)

}

export default new PpPipelineListCommand();
Loading