Skip to content

Add paconn package #3953

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

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/tools/paconn-cli/paconn/__pycache__
/tools/paconn-cli/paconn/apimanager/__pycache__
/tools/paconn-cli/paconn/authentication/__pycache__
/tools/paconn-cli/paconn/commands/__pycache__
/tools/paconn-cli/paconn/common/__pycache__
/tools/paconn-cli/paconn/operations/__pycache__
/tools/paconn-cli/paconn/settings/__pycache__
tools/paconn-cli/PowerPlatformSolutionPackager.psm1
67 changes: 67 additions & 0 deletions tools/paconn-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,73 @@ Arguments
line parameters are ignored.
```

### Package Power Platform Solution Components

The package operation creates a structured Power Platform solution package from component ZIP files. This is useful for creating distributable packages that contain multiple Power Platform components (connectors, flows, AI plugins) in a standardized format.

Package solution components by running:

`paconn package`

or

`paconn package --source [Path to directory containing ZIP files]`

or

`paconn package --source [Path to directory] --custom-mappings '{"*MyConnector*": "Connector.zip", "*MyFlow*": "Flow.zip"}'`

The packaging process follows these steps:

1. **Rename ZIP files** according to Power Platform conventions:
- Files containing "Connector" → "Connector.zip" (required)
- Files containing "Flow" → "Flow.zip" (required)
- Files containing "AIPlugin" → "AIPlugin.zip" (optional)

2. **Move all ZIP files** to a "PkgAssets" folder

3. **Create intro.md** from "readme.md" (or first available .md file)

4. **Compress PkgAssets** folder into "package.zip"

5. **Create final ConnectorPackage.zip** containing intro.md and package.zip

6. **Clean up intermediate files** and folders

The final ConnectorPackage.zip is ready for distribution and deployment to Power Platform environments.

```
Arguments
--source -src : Source directory containing the Power Platform solution ZIP
files to package. Defaults to current directory.
--dest -d : Destination path for the final ConnectorPackage.zip file.
Defaults to current directory.
--format -f : Package format. Currently only "standard" format is supported
(ConnectorPackage.zip with intro.md and package.zip).
--custom-mappings -cm : JSON string containing custom file renaming mappings.
Example: '{"*MyConnector*": "Connector.zip", "*MyFlow*": "Flow.zip"}'.
--overwrite -w : Overwrite existing package files if they exist.
--settings -s : A settings file containing required parameters.
When a settings file is specified some command
line parameters are ignored.
```

**Package Examples:**

```bash
# Package solution components in current directory
paconn package

# Package components from specific source directory
paconn package --source ./my-solution

# Package with custom file mappings
paconn package --custom-mappings '{"*MyConnector*": "Connector.zip", "*MyFlow*": "Flow.zip"}'

# Package and overwrite existing files
paconn package --overwrite
```


### Best Practice

Expand Down
3 changes: 3 additions & 0 deletions tools/paconn-cli/paconn.pyproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
<Compile Include="paconn\authentication\__init__.py" />
<Compile Include="paconn\commands\commands.py" />
<Compile Include="paconn\commands\validate.py" />
<Compile Include="paconn\commands\package.py" />
<Compile Include="paconn\commands\__init__.py" />
<Compile Include="paconn\settings\settingsbuilder.py" />
<Compile Include="paconn\operations\validate.py" />
<Compile Include="paconn\operations\package.py" />
<Compile Include="paconn\operations\json_keys.py" />
<Compile Include="paconn\settings\settingsserializer.py" />
<Compile Include="paconn\common\util.py" />
Expand All @@ -45,6 +47,7 @@
<Compile Include="paconn\apimanager\apimanager.py" />
<Compile Include="paconn\commands\download.py" />
<Compile Include="paconn\commands\login.py" />
<Compile Include="paconn\commands\logout.py" />
<Compile Include="paconn\operations\upsert.py" />
<Compile Include="paconn\settings\settings.py" />
<Compile Include="paconn\apimanager\flowrp.py" />
Expand Down
1 change: 1 addition & 0 deletions tools/paconn-cli/paconn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
_CREATE = 'create'
_UPDATE = 'update'
_VALIDATE = 'validate'
_PACKAGE = 'package'
5 changes: 4 additions & 1 deletion tools/paconn-cli/paconn/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from knack.commands import CommandGroup

from paconn import __CLI_NAME__
from paconn import _COMMAND_GROUP, _LOGIN, _LOGOUT, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE
from paconn import _COMMAND_GROUP, _LOGIN, _LOGOUT, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _PACKAGE


# pylint: disable=unused-argument
Expand Down Expand Up @@ -39,3 +39,6 @@ def operation_group(name):

with CommandGroup(self, _COMMAND_GROUP, operation_group(_VALIDATE)) as command_group:
command_group.command(_VALIDATE, _VALIDATE)

with CommandGroup(self, _COMMAND_GROUP, operation_group(_PACKAGE)) as command_group:
command_group.command(_PACKAGE, _PACKAGE)
33 changes: 32 additions & 1 deletion tools/paconn-cli/paconn/commands/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

from knack.help_files import helps # pylint: disable=unused-import
from paconn import _COMMAND_GROUP, _LOGIN, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE
from paconn import _COMMAND_GROUP, _LOGIN, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _PACKAGE

helps[_COMMAND_GROUP] = """
short-summary: Microsoft Power Platform Connectors CLI
Expand Down Expand Up @@ -55,3 +55,34 @@
- name: Validate swagger
text: paconn validate
"""

helps[_PACKAGE] = """
type: command
short-summary: Package Power Platform solution components into a distributable format.
long-summary: |
Creates a structured Power Platform solution package from component ZIP files.

The packaging process:
1. Renames ZIP files according to Power Platform conventions:
- Files containing "Connector" → "Connector.zip" (required)
- Files containing "Flow" → "Flow.zip" (required)
- Files containing "AIPlugin" → "AIPlugin.zip" (optional)
2. Moves all ZIP files to a "PkgAssets" folder
3. Creates "intro.md" from "readme.md" (or first available .md file)
4. Compresses PkgAssets folder into "package.zip"
5. Creates final "ConnectorPackage.zip" containing intro.md and package.zip
6. Cleans up intermediate files

The final ConnectorPackage.zip is ready for distribution and deployment.
examples:
- name: Package solution components in current directory
text: paconn package
- name: Package components from specific source directory
text: paconn package --source ./my-solution
- name: Package with custom file mappings
text: >
paconn package --custom-mappings
'{"*MyConnector*": "Connector.zip", "*MyFlow*": "Flow.zip"}'
- name: Package and overwrite existing files
text: paconn package --overwrite
"""
72 changes: 72 additions & 0 deletions tools/paconn-cli/paconn/commands/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -----------------------------------------------------------------------------
# Copyright (c) 2025 Troy Taylor ([email protected]). All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#
# Permission is hereby granted to Microsoft Corporation and any other party
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of this software under the terms of the MIT License.
# -----------------------------------------------------------------------------
"""
Package command - Creates a Power Platform solution package from component files.
"""

from paconn import _PACKAGE

from paconn.common.util import display
from paconn.settings.settingsbuilder import SettingsBuilder

import paconn.operations.package


# pylint: disable=too-many-arguments
def package(
source,
destination,
package_format,
settings_file,
overwrite,
custom_mappings=None):
"""
Package command - Creates a Power Platform solution package.

Processes Power Platform solution zip files in the specified directory:
- Files containing "Connector" are renamed to "Connector.zip" (required)
- Files containing "Flow" are renamed to "Flow.zip" (required)
- Files containing "AIPlugin" are renamed to "AIPlugin.zip" (optional)
After renaming, all zip files are moved to a new "PkgAssets" folder.
The readme.md file (or first available .md file) is copied to intro.md.
The PkgAssets folder is compressed into a "package.zip" file.
Finally, a "ConnectorPackage.zip" is created containing intro.md and package.zip.
"""
# Parse custom mappings if provided
parsed_custom_mappings = None
if custom_mappings:
try:
import json
parsed_custom_mappings = json.loads(custom_mappings)
except json.JSONDecodeError:
from knack.util import CLIError
raise CLIError('Invalid JSON format for --custom-mappings parameter.')

# Get settings (minimal settings needed for this operation)
settings = SettingsBuilder.get_settings(
environment=None,
settings_file=settings_file,
api_properties=None,
api_definition=None,
icon=None,
script=None,
connector_id=None,
powerapps_url=None,
powerapps_version=None)

package_path = paconn.operations.package.package(
source=source,
destination=destination,
package_format=package_format,
settings=settings,
overwrite=overwrite,
custom_mappings=parsed_custom_mappings)

display('Power Platform solution package created: {}'.format(package_path))
45 changes: 34 additions & 11 deletions tools/paconn-cli/paconn/commands/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

from knack.arguments import ArgumentsContext
from paconn import _LOGIN, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE
from paconn import _LOGIN, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _PACKAGE

CLIENT_SECRET = 'client_secret'
CLIENT_SECRET_OPTIONS = ['--secret', '-r']
Expand Down Expand Up @@ -272,28 +272,51 @@ def load_arguments(self, command):
required=False,
help=SETTINGS_HELP)

with ArgumentsContext(self, _VALIDATE) as arg_context:
arg_context.argument(
API_DEFINITION,
options_list=API_DEFINITION_OPTIONS,
SETTINGS,
options_list=SETTINGS_OPTIONS,
type=str,
required=False,
help=API_DEFINITION_HELP)
help=SETTINGS_HELP)

with ArgumentsContext(self, _PACKAGE) as arg_context:
arg_context.argument(
POWERAPPS_URL,
options_list=POWERAPPS_URL_OPTIONS,
'source',
options_list=['--source', '-src'],
type=str,
required=False,
help=POWERAPPS_URL_HELP)
help='Source directory containing the Power Platform solution ZIP files to package. Defaults to current directory.')
arg_context.argument(
POWERAPPS_VERSION,
options_list=POWERAPPS_VERSION_OPTIONS,
'destination',
options_list=['--dest', '-d'],
type=str,
required=False,
help=POWERAPPS_VERSION_HELP)
help='Destination path for the final ConnectorPackage.zip file. Defaults to current directory.')
arg_context.argument(
'package_format',
options_list=['--format', '-f'],
type=str,
required=False,
choices=['standard'],
help='Package format. Currently only "standard" format is supported (ConnectorPackage.zip with intro.md and package.zip).')
arg_context.argument(
SETTINGS,
options_list=SETTINGS_OPTIONS,
type=str,
required=False,
help=SETTINGS_HELP)
arg_context.argument(
'overwrite',
options_list=['--overwrite', '-w'],
type=bool,
required=False,
nargs='?',
default=False,
const=True,
help='Overwrite existing package files if they exist.')
arg_context.argument(
'custom_mappings',
options_list=['--custom-mappings', '-cm'],
type=str,
required=False,
help='JSON string containing custom file renaming mappings. Example: \'{"*MyConnector*": "Connector.zip", "*MyFlow*": "Flow.zip"}\'.')
Loading