diff --git a/command-snapshot.json b/command-snapshot.json index 9a5b0b02..a3d0563a 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -63,6 +63,40 @@ "flags": ["api-version", "description", "flags-dir", "json", "loglevel", "name", "target-dev-hub"], "plugin": "@salesforce/plugin-packaging" }, + { + "alias": [], + "command": "package:bundle:install", + "flagAliases": ["apiversion", "target-hub-org", "targetdevhubusername", "targetusername", "u"], + "flagChars": ["b", "o", "v", "w"], + "flags": [ + "api-version", + "bundle", + "flags-dir", + "json", + "loglevel", + "target-dev-hub", + "target-org", + "verbose", + "wait" + ], + "plugin": "@salesforce/plugin-packaging" + }, + { + "alias": [], + "command": "package:bundle:install:list", + "flagAliases": ["apiversion", "createdlastdays", "target-hub-org", "targetdevhubusername"], + "flagChars": ["c", "s", "v"], + "flags": ["api-version", "created-last-days", "flags-dir", "json", "loglevel", "status", "target-org", "verbose"], + "plugin": "@salesforce/plugin-packaging" + }, + { + "alias": [], + "command": "package:bundle:install:report", + "flagAliases": ["apiversion", "packageinstallrequestid", "target-hub-org", "targetdevhubusername"], + "flagChars": ["i", "v"], + "flags": ["api-version", "flags-dir", "json", "loglevel", "package-install-request-id", "target-org", "verbose"], + "plugin": "@salesforce/plugin-packaging" + }, { "alias": [], "command": "package:bundle:delete", @@ -136,7 +170,7 @@ "alias": [], "command": "package:bundle:version:report", "flagAliases": ["apiversion", "target-hub-org", "targetdevhubusername"], - "flagChars": ["p", "v"], + "flagChars": ["b", "v"], "flags": ["api-version", "bundle-version", "flags-dir", "json", "loglevel", "target-dev-hub", "verbose"], "plugin": "@salesforce/plugin-packaging" }, diff --git a/messages/bundle_install.md b/messages/bundle_install.md new file mode 100644 index 00000000..48ecb5ac --- /dev/null +++ b/messages/bundle_install.md @@ -0,0 +1,54 @@ +# summary + +Install a package bundle version in the target org. + +# description + +Install a specific version of a package bundle in the target org. During developer preview, bundles can be installed only in scratch orgs. + +# examples + +Install a package bundle version in a scratch org: + +sf package bundle install --bundle MyPkgBundle1@0.1 --target-org my-scratch-org --wait 10 + +# flags.bundle.summary + +Package bundle version to install (format: BundleName@Version). + +# flags.target-org.summary + +Target org for the bundle installation. + +# flags.wait.summary + +Number of minutes to wait for the installation to complete. + +# flags.verbose.summary + +Display extended installation detail. + +# requestInProgress + +Installing bundle. + +# bundleInstallWaitingStatus + +%d minutes remaining until timeout. Install status: %s + +# bundleInstallFinalStatus + +Install status: %s + +# bundleInstallSuccess + +Successfully installed bundle [%s] + +# bundleInstallError + +Encountered errors installing the bundle! %s + +# bundleInstallInProgress + +Bundle installation is currently %s. You can continue to query the status using +sf package bundle install:report -i %s -o %s diff --git a/messages/bundle_install_list.md b/messages/bundle_install_list.md new file mode 100644 index 00000000..87893af8 --- /dev/null +++ b/messages/bundle_install_list.md @@ -0,0 +1,69 @@ +# summary + +List package bundle installation requests. + +# description + +Shows the details of each request to install a package bundle version in the target org. + +All filter parameters are applied using the AND logical operator (not OR). + +To get information about a specific request, run "<%= config.bin %> package bundle install report" and supply the request ID. + +# flags.status.summary + +Status of the installation request, used to filter the list. + +# flags.verbose.summary + +Displays additional information at a slight performance cost, such as validation text for each package bundle install request. + +# flags.created-last-days.summary + +Number of days since the request was created, starting at 00:00:00 of first day to now. Use 0 for today. + +# examples + +- List all package bundle installation requests in your default Dev Hub org: + + <%= config.bin %> <%= command.id %> + +- List package bundle installation requests from the last 3 days in the Dev Hub org with username devhub@example.com: + + <%= config.bin %> <%= command.id %> --created-last-days 3 --target-dev-hub + +- List package bundle installation requests with status Error: + + <%= config.bin %> <%= command.id %> --status Error + +- List package bundle installation requests with status Queued: + + <%= config.bin %> <%= command.id %> --status Queued + +- List package bundle installation requests with status Success that were created today: + + <%= config.bin %> <%= command.id %> --created-last-days 0 --status Success + +# id + +ID + +# status + +Status + +# package-bundle-version-id + +Package Bundle Version ID + +# development-organization + +Development Organization + +# created-by + +Created By + +# validation-error + +Validation Error diff --git a/messages/bundle_install_report.md b/messages/bundle_install_report.md new file mode 100644 index 00000000..dd687597 --- /dev/null +++ b/messages/bundle_install_report.md @@ -0,0 +1,53 @@ +# summary + +Report on the status of a package bundle installation request. + +# description + +Use this command to check the status of a package bundle installation request. The command returns information about the request, including its current status and details about the package bundle version being installed. + +# examples + +- Report on a package bundle installation request: + + <%= config.bin %> <%= command.id %> --package-install-request-id 0Ho0x0000000000000 + +- Report on a package bundle installation request using an alias: + + <%= config.bin %> force:package:bundle:install:report -i 0Ho0x0000000000000 + +# flags.package-install-request-id.summary + +The ID of the package bundle installation request to report on. + +# id + +ID + +# status + +Status + +# package-bundle-version-id + +Package Bundle Version ID + +# development-organization + +Development Organization + +# validation-error + +Validation Error + +# created-date + +Created Date + +# created-by + +Created By + +# flags.verbose.summary + +Show verbose output. diff --git a/schemas/package-bundle-install-list.json b/schemas/package-bundle-install-list.json new file mode 100644 index 00000000..a3f37cc9 --- /dev/null +++ b/schemas/package-bundle-install-list.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PackageBundleInstallRequestResults", + "definitions": { + "PackageBundleInstallRequestResults": { + "type": "array", + "items": { + "$ref": "#/definitions/BundleSObjects.PkgBundleVersionInstallReqResult" + } + }, + "BundleSObjects.PkgBundleVersionInstallReqResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "Id": { + "type": "string" + }, + "InstallStatus": { + "$ref": "#/definitions/BundleSObjects.PkgBundleVersionInstallReqStatus" + }, + "ValidationError": { + "type": "string" + }, + "CreatedDate": { + "type": "string" + }, + "CreatedById": { + "type": "string" + }, + "Error": { + "type": "array", + "items": { + "type": "string" + } + }, + "PackageBundleVersionID": { + "type": "string" + }, + "DevelopmentOrganization": { + "type": "string" + } + }, + "required": [ + "CreatedById", + "CreatedDate", + "DevelopmentOrganization", + "Id", + "InstallStatus", + "PackageBundleVersionID", + "ValidationError" + ] + }, + "BundleSObjects.PkgBundleVersionInstallReqStatus": { + "type": "string", + "enum": ["Queued", "Success", "Error"] + } + } +} diff --git a/schemas/package-bundle-install-report.json b/schemas/package-bundle-install-report.json new file mode 100644 index 00000000..c1a2e381 --- /dev/null +++ b/schemas/package-bundle-install-report.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/ReportCommandResult", + "definitions": { + "ReportCommandResult": { + "type": "array", + "items": { + "$ref": "#/definitions/BundleSObjects.PkgBundleVersionInstallReqResult" + } + }, + "BundleSObjects.PkgBundleVersionInstallReqResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "Id": { + "type": "string" + }, + "InstallStatus": { + "$ref": "#/definitions/BundleSObjects.PkgBundleVersionInstallReqStatus" + }, + "ValidationError": { + "type": "string" + }, + "CreatedDate": { + "type": "string" + }, + "CreatedById": { + "type": "string" + }, + "Error": { + "type": "array", + "items": { + "type": "string" + } + }, + "PackageBundleVersionID": { + "type": "string" + }, + "DevelopmentOrganization": { + "type": "string" + } + }, + "required": [ + "CreatedById", + "CreatedDate", + "DevelopmentOrganization", + "Id", + "InstallStatus", + "PackageBundleVersionID", + "ValidationError" + ] + }, + "BundleSObjects.PkgBundleVersionInstallReqStatus": { + "type": "string", + "enum": ["Queued", "Success", "Error"] + } + } +} diff --git a/schemas/package-bundle-install.json b/schemas/package-bundle-install.json new file mode 100644 index 00000000..7d4be399 --- /dev/null +++ b/schemas/package-bundle-install.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/BundleSObjects.PkgBundleVersionInstallReqResult", + "definitions": { + "BundleSObjects.PkgBundleVersionInstallReqResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "Id": { + "type": "string" + }, + "InstallStatus": { + "$ref": "#/definitions/BundleSObjects.PkgBundleVersionInstallReqStatus" + }, + "ValidationError": { + "type": "string" + }, + "CreatedDate": { + "type": "string" + }, + "CreatedById": { + "type": "string" + }, + "Error": { + "type": "array", + "items": { + "type": "string" + } + }, + "PackageBundleVersionID": { + "type": "string" + }, + "DevelopmentOrganization": { + "type": "string" + } + }, + "required": [ + "CreatedById", + "CreatedDate", + "DevelopmentOrganization", + "Id", + "InstallStatus", + "PackageBundleVersionID", + "ValidationError" + ] + }, + "BundleSObjects.PkgBundleVersionInstallReqStatus": { + "type": "string", + "enum": ["Queued", "Success", "Error"] + } + } +} diff --git a/schemas/package-bundle-version-report.json b/schemas/package-bundle-version-report.json index 772bf5e3..0004bafc 100644 --- a/schemas/package-bundle-version-report.json +++ b/schemas/package-bundle-version-report.json @@ -1,10 +1,17 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/BundleSObjects.BundleVersion", + "$ref": "#/definitions/BundleVersionReportResult", "definitions": { - "BundleSObjects.BundleVersion": { + "BundleVersionReportResult": { "type": "object", + "additionalProperties": false, "properties": { + "componentPackages": { + "type": "array", + "items": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageVersion" + } + }, "Id": { "type": "string" }, @@ -47,19 +54,293 @@ } }, "required": [ + "CreatedById", + "CreatedDate", "Id", + "IsReleased", + "LastModifiedById", + "LastModifiedDate", + "MajorVersion", + "MinorVersion", "PackageBundle", "VersionName", + "componentPackages" + ] + }, + "PackagingSObjects.SubscriberPackageVersion": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "SubscriberPackageId": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "PublisherName": { + "type": "string" + }, + "MajorVersion": { + "type": "number" + }, + "MinorVersion": { + "type": "number" + }, + "PatchVersion": { + "type": "number" + }, + "BuildNumber": { + "type": "number" + }, + "ReleaseState": { + "type": "string" + }, + "IsManaged": { + "type": "boolean" + }, + "IsDeprecated": { + "type": "boolean" + }, + "IsPasswordProtected": { + "type": "boolean" + }, + "IsBeta": { + "type": "boolean" + }, + "Package2ContainerOptions": { + "$ref": "#/definitions/PackageType" + }, + "IsSecurityReviewed": { + "type": "boolean" + }, + "IsOrgDependent": { + "type": "boolean" + }, + "AppExchangePackageName": { + "type": "string" + }, + "AppExchangeDescription": { + "type": "string" + }, + "AppExchangePublisherName": { + "type": "string" + }, + "AppExchangeLogoUrl": { + "type": "string" + }, + "ReleaseNotesUrl": { + "type": "string" + }, + "PostInstallUrl": { + "type": "string" + }, + "RemoteSiteSettings": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageRemoteSiteSettings" + }, + "CspTrustedSites": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageCspTrustedSites" + }, + "Profiles": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageProfiles" + }, + "Dependencies": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageDependencies" + }, + "InstallValidationStatus": { + "$ref": "#/definitions/PackagingSObjects.InstallValidationStatus" + } + }, + "required": [ + "Id", + "SubscriberPackageId", + "Name", + "Description", + "PublisherName", "MajorVersion", "MinorVersion", - "IsReleased", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById" + "PatchVersion", + "BuildNumber", + "ReleaseState", + "IsManaged", + "IsDeprecated", + "IsPasswordProtected", + "IsBeta", + "Package2ContainerOptions", + "IsSecurityReviewed", + "IsOrgDependent", + "AppExchangePackageName", + "AppExchangeDescription", + "AppExchangePublisherName", + "AppExchangeLogoUrl", + "ReleaseNotesUrl", + "PostInstallUrl", + "RemoteSiteSettings", + "CspTrustedSites", + "Profiles", + "Dependencies", + "InstallValidationStatus" ], "additionalProperties": false }, + "PackageType": { + "type": "string", + "enum": ["Managed", "Unlocked"] + }, + "PackagingSObjects.SubscriberPackageRemoteSiteSettings": { + "type": "object", + "properties": { + "settings": { + "type": "array", + "items": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageRemoteSiteSetting" + } + } + }, + "required": ["settings"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageRemoteSiteSetting": { + "type": "object", + "properties": { + "secure": { + "type": "boolean" + }, + "url": { + "type": "string" + } + }, + "required": ["secure", "url"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageCspTrustedSites": { + "type": "object", + "properties": { + "settings": { + "type": "array", + "items": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageCspTrustedSite" + } + } + }, + "required": ["settings"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageCspTrustedSite": { + "type": "object", + "properties": { + "endpointUrl": { + "type": "string" + } + }, + "required": ["endpointUrl"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageProfiles": { + "type": "object", + "properties": { + "destinationProfiles": { + "type": "array", + "items": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageDestinationProfile" + } + }, + "sourceProfiles": { + "type": "array", + "items": { + "$ref": "#/definitions/PackagingSObjects.SubscriberPackageSourceProfile" + } + } + }, + "required": ["destinationProfiles", "sourceProfiles"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageDestinationProfile": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "noAccess": { + "type": "boolean" + }, + "profileId": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": ["description", "displayName", "name", "noAccess", "profileId", "type"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageSourceProfile": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["label", "value"], + "additionalProperties": false + }, + "PackagingSObjects.SubscriberPackageDependencies": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subscriberPackageVersionId": { + "type": "string" + } + }, + "required": ["subscriberPackageVersionId"], + "additionalProperties": false + } + } + }, + "required": ["ids"], + "additionalProperties": false + }, + "PackagingSObjects.InstallValidationStatus": { + "type": "string", + "enum": [ + "NO_ERRORS_DETECTED", + "BETA_INSTALL_INTO_PRODUCTION_ORG", + "CANNOT_INSTALL_EARLIER_VERSION", + "CANNOT_UPGRADE_BETA", + "CANNOT_UPGRADE_UNMANAGED", + "DEPRECATED_INSTALL_PACKAGE", + "EXTENSIONS_ON_LOCAL_PACKAGES", + "PACKAGE_NOT_INSTALLED", + "PACKAGE_HAS_IN_DEV_EXTENSIONS", + "INSTALL_INTO_DEV_ORG", + "NO_ACCESS", + "PACKAGING_DISABLED", + "PACKAGING_NO_ACCESS", + "PACKAGE_UNAVAILABLE", + "PACKAGE_UNAVAILABLE_CRC", + "PACKAGE_UNAVAILABLE_ZIP", + "UNINSTALL_IN_PROGRESS", + "UNKNOWN_ERROR", + "NAMESPACE_COLLISION" + ] + }, "BundleSObjects.Bundle": { "type": "object", "properties": { @@ -102,6 +383,64 @@ "SystemModstamp" ], "additionalProperties": false + }, + "BundleSObjects.BundleVersion": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "PackageBundle": { + "$ref": "#/definitions/BundleSObjects.Bundle" + }, + "VersionName": { + "type": "string" + }, + "MajorVersion": { + "type": "string" + }, + "MinorVersion": { + "type": "string" + }, + "Ancestor": { + "anyOf": [ + { + "$ref": "#/definitions/BundleSObjects.BundleVersion" + }, + { + "type": "null" + } + ] + }, + "IsReleased": { + "type": "boolean" + }, + "CreatedDate": { + "type": "string" + }, + "CreatedById": { + "type": "string" + }, + "LastModifiedDate": { + "type": "string" + }, + "LastModifiedById": { + "type": "string" + } + }, + "required": [ + "Id", + "PackageBundle", + "VersionName", + "MajorVersion", + "MinorVersion", + "IsReleased", + "CreatedDate", + "CreatedById", + "LastModifiedDate", + "LastModifiedById" + ], + "additionalProperties": false } } } diff --git a/src/commands/package/bundle/install.ts b/src/commands/package/bundle/install.ts new file mode 100644 index 00000000..bd8f2cab --- /dev/null +++ b/src/commands/package/bundle/install.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { + Flags, + loglevel, + orgApiVersionFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; +import { BundleSObjects, BundleInstallOptions, PackageBundleInstall } from '@salesforce/packaging'; +import { Messages, Lifecycle } from '@salesforce/core'; +import { camelCaseToTitleCase, Duration } from '@salesforce/kit'; +import { requiredHubFlag } from '../../../utils/hubFlag.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'bundle_install'); +export type BundleInstall = BundleSObjects.PkgBundleVersionInstallReqResult; + +export class PackageBundlesInstall extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly requiresProject = true; + public static readonly flags = { + loglevel, + bundle: Flags.string({ + char: 'b', + summary: messages.getMessage('flags.bundle.summary'), + required: true, + }), + 'target-org': requiredOrgFlagWithDeprecations, + 'api-version': orgApiVersionFlagWithDeprecations, + 'target-dev-hub': requiredHubFlag, + wait: Flags.integer({ + char: 'w', + summary: messages.getMessage('flags.wait.summary'), + default: 0, + }), + verbose: Flags.boolean({ + summary: messages.getMessage('flags.verbose.summary'), + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(PackageBundlesInstall); + + // Get the target org connection + const targetOrg = flags['target-org']; + const targetDevHub = flags['target-dev-hub']; + const connection = targetOrg.getConnection(flags['api-version']); + + const options: BundleInstallOptions = { + connection, + project: this.project!, + PackageBundleVersion: flags.bundle, + DevelopmentOrganization: targetDevHub.getOrgId() ?? '', + }; + + // Set up lifecycle events for progress tracking + Lifecycle.getInstance().on( + 'bundle-install-progress', + // no async methods + // eslint-disable-next-line @typescript-eslint/require-await + async (data: BundleSObjects.PkgBundleVersionInstallReqResult & { remainingWaitTime: Duration }) => { + if ( + data.InstallStatus !== BundleSObjects.PkgBundleVersionInstallReqStatus.success && + data.InstallStatus !== BundleSObjects.PkgBundleVersionInstallReqStatus.error + ) { + const status = messages.getMessage('bundleInstallWaitingStatus', [ + data.remainingWaitTime.minutes, + data.InstallStatus, + ]); + if (flags.verbose) { + this.log(status); + } else { + this.spinner.status = status; + } + } + } + ); + + const result = await PackageBundleInstall.installBundle(connection, this.project!, { + ...options, + polling: { + timeout: Duration.minutes(flags.wait), + frequency: Duration.seconds(5), + }, + }); + + const finalStatusMsg = messages.getMessage('bundleInstallFinalStatus', [result.InstallStatus]); + if (flags.verbose) { + this.log(finalStatusMsg); + } else { + this.spinner.stop(finalStatusMsg); + } + + switch (result.InstallStatus) { + case BundleSObjects.PkgBundleVersionInstallReqStatus.error: + throw messages.createError('bundleInstallError', [result.ValidationError || 'Unknown error']); + case BundleSObjects.PkgBundleVersionInstallReqStatus.success: + this.log(messages.getMessage('bundleInstallSuccess', [result.Id])); + break; + default: + this.log( + messages.getMessage('bundleInstallInProgress', [ + camelCaseToTitleCase(result.InstallStatus as string), + result.Id, + targetOrg.getUsername() ?? '', + ]) + ); + } + + return result; + } +} diff --git a/src/commands/package/bundle/install/list.ts b/src/commands/package/bundle/install/list.ts new file mode 100644 index 00000000..0847e943 --- /dev/null +++ b/src/commands/package/bundle/install/list.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand } from '@salesforce/sf-plugins-core'; +import { Connection, Messages } from '@salesforce/core'; +import { BundleSObjects, PackageBundleInstall } from '@salesforce/packaging'; +import chalk from 'chalk'; +import { requiredHubFlag } from '../../../../utils/hubFlag.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'bundle_install_list'); + +type Status = BundleSObjects.PkgBundleVersionInstallReqStatus; +export type PackageBundleInstallRequestResults = BundleSObjects.PkgBundleVersionInstallReqResult[]; + +export class PackageBundleInstallListCommand extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + loglevel, + 'target-org': requiredHubFlag, + 'api-version': orgApiVersionFlagWithDeprecations, + 'created-last-days': Flags.integer({ + char: 'c', + deprecateAliases: true, + aliases: ['createdlastdays'], + summary: messages.getMessage('flags.created-last-days.summary'), + }), + status: Flags.custom({ + options: [ + BundleSObjects.PkgBundleVersionInstallReqStatus.queued, + BundleSObjects.PkgBundleVersionInstallReqStatus.success, + BundleSObjects.PkgBundleVersionInstallReqStatus.error, + ], + })({ + char: 's', + summary: messages.getMessage('flags.status.summary'), + }), + verbose: Flags.boolean({ + summary: messages.getMessage('flags.verbose.summary'), + }), + }; + + private connection!: Connection; + + public async run(): Promise { + const { flags } = await this.parse(PackageBundleInstallListCommand); + this.connection = flags['target-org'].getConnection(flags['api-version']); + const results = await PackageBundleInstall.getInstallStatuses( + this.connection, + flags.status, + flags['created-last-days'] + ); + + if (results.length === 0) { + this.warn('No results found'); + } else { + const data = results.map((r) => ({ + Id: r.Id ?? 'N/A', + Status: r.InstallStatus ?? 'Unknown', + 'Package Bundle Version Id': r.PackageBundleVersionID ?? 'N/A', + 'Development Organization': r.DevelopmentOrganization ?? 'N/A', + 'Created Date': r.CreatedDate ?? 'N/A', + 'Created By': r.CreatedById ?? 'N/A', + ...(flags.verbose + ? { + 'Validation Error': r.ValidationError ?? 'N/A', + } + : {}), + })); + + this.table({ + data, + overflow: 'wrap', + title: chalk.blue(`Package Bundle Install Requests [${results.length}]`), + }); + } + + return results; + } +} diff --git a/src/commands/package/bundle/install/report.ts b/src/commands/package/bundle/install/report.ts new file mode 100644 index 00000000..0a8762dc --- /dev/null +++ b/src/commands/package/bundle/install/report.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand } from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; +import { BundleSObjects, PackageBundleInstall } from '@salesforce/packaging'; +import chalk from 'chalk'; +import { camelCaseToTitleCase } from '@salesforce/kit'; +import { requiredHubFlag } from '../../../../utils/hubFlag.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'bundle_install_report'); + +export type ReportCommandResult = BundleSObjects.PkgBundleVersionInstallReqResult[]; + +export class PackageBundleInstallReportCommand extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + loglevel, + 'target-org': requiredHubFlag, + 'api-version': orgApiVersionFlagWithDeprecations, + // eslint-disable-next-line sf-plugin/id-flag-suggestions + 'package-install-request-id': Flags.salesforceId({ + length: 'both', + deprecateAliases: true, + aliases: ['packageinstallrequestid'], + char: 'i', + summary: messages.getMessage('flags.package-install-request-id.summary'), + required: true, + }), + verbose: Flags.boolean({ + summary: messages.getMessage('flags.verbose.summary'), + required: false, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(PackageBundleInstallReportCommand); + const result = await PackageBundleInstall.getInstallStatus( + flags['package-install-request-id'], + flags['target-org'].getConnection(flags['api-version']) + ); + this.display(result, flags.verbose); + return [result]; + } + + private display(record: BundleSObjects.PkgBundleVersionInstallReqResult, verbose: boolean): void { + const data = [ + { + name: messages.getMessage('id'), + value: record.Id, + }, + { + name: messages.getMessage('status'), + value: camelCaseToTitleCase(record.InstallStatus), + }, + { + name: messages.getMessage('package-bundle-version-id'), + value: record.PackageBundleVersionID ?? 'N/A', + }, + { + name: messages.getMessage('development-organization'), + value: record.DevelopmentOrganization, + }, + { + name: messages.getMessage('validation-error'), + value: record.ValidationError ?? 'N/A', + }, + { + name: messages.getMessage('created-date'), + value: record.CreatedDate, + }, + { + name: messages.getMessage('created-by'), + value: record.CreatedById, + }, + ...(verbose + ? [ + { + name: 'ValidationError', + value: record.ValidationError ?? 'N/A', + }, + ] + : []), + ]; + + this.table({ data, title: chalk.blue('Package Bundle Install Request') }); + } +} diff --git a/src/commands/package/bundle/version/report.ts b/src/commands/package/bundle/version/report.ts index d6c8ed61..1bb22265 100644 --- a/src/commands/package/bundle/version/report.ts +++ b/src/commands/package/bundle/version/report.ts @@ -13,8 +13,11 @@ import { requiredHubFlag } from '../../../../utils/hubFlag.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'bundle_version_report'); +export type BundleVersionReportResult = BundleSObjects.BundleVersion & { + componentPackages: PackagingSObjects.SubscriberPackageVersion[]; +}; -export class PackageBundleVersionReportCommand extends SfCommand { +export class PackageBundleVersionReportCommand extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly examples = messages.getMessages('examples'); public static readonly flags = { @@ -22,7 +25,7 @@ export class PackageBundleVersionReportCommand extends SfCommand { + public async run(): Promise { const { flags } = await this.parse(PackageBundleVersionReportCommand); const connection = flags['target-dev-hub'].getConnection(flags['api-version']); const results = await PackageBundleVersion.report(connection, flags['bundle-version']); @@ -43,8 +46,13 @@ export class PackageBundleVersionReportCommand extends SfCommand { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + const testHubOrg = new MockTestOrgData(); + let installStub = $$.SANDBOX.stub(PackageBundleInstall, 'installBundle'); + const config = new Config({ root: import.meta.url }); + + // stubs + let logStub: sinon.SinonStub; + let warnStub: sinon.SinonStub; + + const stubSpinner = (cmd: PackageBundlesInstall) => { + $$.SANDBOX.stub(cmd.spinner, 'start'); + $$.SANDBOX.stub(cmd.spinner, 'stop'); + $$.SANDBOX.stub(cmd.spinner, 'status').value(''); + }; + + before(async () => { + await $$.stubAuths(testOrg, testHubOrg); + await config.load(); + }); + + beforeEach(async () => { + logStub = $$.SANDBOX.stub(SfCommand.prototype, 'log'); + warnStub = $$.SANDBOX.stub(SfCommand.prototype, 'warn'); + }); + + afterEach(() => { + $$.restore(); + }); + + describe('package:bundle:install', () => { + it('should install a package bundle version successfully', async () => { + installStub.resolves(pkgBundleInstallSuccessResult); + + const cmd = new PackageBundlesInstall( + ['-b', 'TestBundle@1.0', '--target-org', 'test@org.org', '--target-dev-hub', 'test@hub.org'], + config + ); + stubSpinner(cmd); + const res = await cmd.run(); + expect(res).to.deep.equal({ + Id: '08c3i000000fylgAAA', + InstallStatus: 'Success', + PackageBundleVersionID: '1Q83i000000fxw1AAA', + DevelopmentOrganization: '00D3i000000TNHYCA4', + ValidationError: '', + CreatedDate: '2022-11-03 09:46', + CreatedById: '0053i000001ZIyGAAW', + Error: [], + }); + expect(warnStub.callCount).to.equal(0); + expect(logStub.callCount).to.equal(1); + expect(logStub.args[0]).to.deep.equal(['Successfully installed bundle [08c3i000000fylgAAA]']); + }); + + it('should install a package bundle version with wait option', async () => { + installStub = $$.SANDBOX.stub(PackageBundleInstall, 'installBundle'); + installStub.resolves(pkgBundleInstallSuccessResult); + + const cmd = new PackageBundlesInstall( + ['-b', 'TestBundle@1.0', '--target-org', 'test@org.org', '--target-dev-hub', 'test@hub.org', '-w', '10'], + config + ); + stubSpinner(cmd); + const res = await cmd.run(); + expect(res).to.deep.equal({ + Id: '08c3i000000fylgAAA', + InstallStatus: 'Success', + PackageBundleVersionID: '1Q83i000000fxw1AAA', + DevelopmentOrganization: '00D3i000000TNHYCA4', + ValidationError: '', + CreatedDate: '2022-11-03 09:46', + CreatedById: '0053i000001ZIyGAAW', + Error: [], + }); + expect(warnStub.callCount).to.equal(0); + expect(logStub.callCount).to.equal(1); + expect(logStub.args[0]).to.deep.equal(['Successfully installed bundle [08c3i000000fylgAAA]']); + }); + + // This test does very little to test the verbose command except make sure that it is there. + it('should install a package bundle version with verbose option', async () => { + installStub = $$.SANDBOX.stub(PackageBundleInstall, 'installBundle'); + installStub.resolves(pkgBundleInstallSuccessResult); + + const cmd = new PackageBundlesInstall( + ['-b', 'TestBundle@1.0', '--target-org', 'test@org.org', '--target-dev-hub', 'test@hub.org', '--verbose'], + config + ); + stubSpinner(cmd); + const res = await cmd.run(); + expect(res).to.deep.equal({ + Id: '08c3i000000fylgAAA', + InstallStatus: 'Success', + PackageBundleVersionID: '1Q83i000000fxw1AAA', + DevelopmentOrganization: '00D3i000000TNHYCA4', + ValidationError: '', + CreatedDate: '2022-11-03 09:46', + CreatedById: '0053i000001ZIyGAAW', + Error: [], + }); + expect(warnStub.callCount).to.equal(0); + expect(logStub.callCount).to.equal(2); + expect(logStub.args[0]).to.deep.equal(['Install status: Success']); + expect(logStub.args[1]).to.deep.equal(['Successfully installed bundle [08c3i000000fylgAAA]']); + }); + + it('should handle queued status', async () => { + installStub = $$.SANDBOX.stub(PackageBundleInstall, 'installBundle'); + installStub.resolves(pkgBundleInstallQueuedResult); + + const cmd = new PackageBundlesInstall( + ['-b', 'TestBundle@1.0', '--target-org', 'test@org.org', '--target-dev-hub', 'test@hub.org'], + config + ); + stubSpinner(cmd); + const res = await cmd.run(); + expect(res).to.deep.equal({ + Id: '08c3i000000fylgBBB', + InstallStatus: 'Queued', + PackageBundleVersionID: '1Q83i000000fxw1AAA', + DevelopmentOrganization: '00D3i000000TNHYCA4', + ValidationError: '', + CreatedDate: '2022-11-03 10:00', + CreatedById: '0053i000001ZIyGAAW', + Error: [], + }); + expect(warnStub.callCount).to.equal(0); + expect(logStub.callCount).to.equal(1); + expect(logStub.args[0]).to.deep.equal([ + 'Bundle installation is currently Queued. You can continue to query the status using\nsf package bundle install:report -i 08c3i000000fylgBBB -o test@org.org', + ]); + }); + + it('should handle error status', async () => { + installStub = $$.SANDBOX.stub(PackageBundleInstall, 'installBundle'); + installStub.resolves(pkgBundleInstallErrorResult); + + try { + const cmd = new PackageBundlesInstall( + ['-b', 'TestBundle@1.0', '--target-org', 'test@org.org', '--target-dev-hub', 'test@hub.org'], + config + ); + stubSpinner(cmd); + await cmd.run(); + assert.fail('the above should throw an error'); + } catch (e) { + expect((e as Error).message).to.equal( + 'Encountered errors installing the bundle! Installation failed due to validation errors' + ); + } + }); + + it('should handle error status with unknown error', async () => { + const errorResult = { ...pkgBundleInstallErrorResult, ValidationError: '' }; + installStub = $$.SANDBOX.stub(PackageBundleInstall, 'installBundle'); + installStub.resolves(errorResult); + + try { + const cmd = new PackageBundlesInstall( + ['-b', 'TestBundle@1.0', '--target-org', 'test@org.org', '--target-dev-hub', 'test@hub.org'], + config + ); + stubSpinner(cmd); + await cmd.run(); + assert.fail('the above should throw an error'); + } catch (e) { + expect((e as Error).message).to.equal('Encountered errors installing the bundle! Unknown error'); + } + }); + }); +}); diff --git a/test/commands/bundle/bundleInstallList.test.ts b/test/commands/bundle/bundleInstallList.test.ts new file mode 100644 index 00000000..ddeb9580 --- /dev/null +++ b/test/commands/bundle/bundleInstallList.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { Config } from '@oclif/core'; +import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; +import { expect } from 'chai'; +import { PackageBundleInstall, BundleSObjects } from '@salesforce/packaging'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import sinon from 'sinon'; +import { PackageBundleInstallListCommand } from '../../../src/commands/package/bundle/install/list.js'; + +describe('package:bundle:install:list - tests', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + let sfCommandStubs: ReturnType; + let getInstallStatusesStub: sinon.SinonStub; + const config = new Config({ root: import.meta.url }); + + beforeEach(async () => { + await $$.stubAuths(testOrg); + await config.load(); + sfCommandStubs = stubSfCommandUx($$.SANDBOX); + + getInstallStatusesStub = $$.SANDBOX.stub(PackageBundleInstall, 'getInstallStatuses'); + }); + + afterEach(() => { + $$.restore(); + }); + + it('should list bundle install requests', async () => { + const cmd = new PackageBundleInstallListCommand(['--target-org', testOrg.username], config); + + const mockResults: BundleSObjects.PkgBundleVersionInstallReqResult[] = [ + { + Id: 'test-id-1', + InstallStatus: BundleSObjects.PkgBundleVersionInstallReqStatus.success, + PackageBundleVersionID: 'bundle-version-id-1', + DevelopmentOrganization: 'dev-org-1', + CreatedDate: '2023-01-01T00:00:00Z', + CreatedById: 'user-id-1', + ValidationError: '', + Error: [], + }, + ]; + + getInstallStatusesStub.resolves(mockResults); + + await cmd.run(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(sfCommandStubs.table.calledOnce).to.be.true; + expect(getInstallStatusesStub.calledOnce).to.be.true; + }); + + it('should show warning when no results found', async () => { + const cmd = new PackageBundleInstallListCommand(['--target-org', testOrg.username], config); + + getInstallStatusesStub.resolves([]); + + await cmd.run(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(sfCommandStubs.warn.calledOnce).to.be.true; + expect(sfCommandStubs.warn.firstCall.args[0]).to.equal('No results found'); + }); + + // This test does very little to test the verbose command except make sure that it is there. + it('should handle verbose flag', async () => { + const cmd = new PackageBundleInstallListCommand(['--target-org', testOrg.username, '--verbose'], config); + + const mockResults: BundleSObjects.PkgBundleVersionInstallReqResult[] = [ + { + Id: 'test-id-1', + InstallStatus: BundleSObjects.PkgBundleVersionInstallReqStatus.error, + PackageBundleVersionID: 'bundle-version-id-1', + DevelopmentOrganization: 'dev-org-1', + CreatedDate: '2023-01-01T00:00:00Z', + CreatedById: 'user-id-1', + ValidationError: 'Installation failed', + Error: ['Test error'], + }, + ]; + + getInstallStatusesStub.resolves(mockResults); + + await cmd.run(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(sfCommandStubs.table.calledOnce).to.be.true; + expect(getInstallStatusesStub.calledOnce).to.be.true; + }); +}); diff --git a/test/commands/bundle/bundleInstallReport.test.ts b/test/commands/bundle/bundleInstallReport.test.ts new file mode 100644 index 00000000..ce5a985b --- /dev/null +++ b/test/commands/bundle/bundleInstallReport.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { Config } from '@oclif/core'; +import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; +import { expect } from 'chai'; +import { PackageBundleInstall, BundleSObjects } from '@salesforce/packaging'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import sinon from 'sinon'; +import { PackageBundleInstallReportCommand } from '../../../src/commands/package/bundle/install/report.js'; + +describe('package:bundle:install:report - tests', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + let sfCommandStubs: ReturnType; + let getInstallStatusStub: sinon.SinonStub; + const config = new Config({ root: import.meta.url }); + + beforeEach(async () => { + await $$.stubAuths(testOrg); + await config.load(); + sfCommandStubs = stubSfCommandUx($$.SANDBOX); + + getInstallStatusStub = $$.SANDBOX.stub(PackageBundleInstall, 'getInstallStatus'); + }); + + afterEach(() => { + $$.restore(); + }); + + it('should report on a package bundle installation request', async () => { + const requestId = '0Ho0x0000000000000'; + const mockResult: BundleSObjects.PkgBundleVersionInstallReqResult = { + Id: requestId, + InstallStatus: BundleSObjects.PkgBundleVersionInstallReqStatus.queued, + PackageBundleVersionID: '0Ho0x0000000000001', + DevelopmentOrganization: 'test-org@example.com', + ValidationError: '', + CreatedDate: '2025-01-01T00:00:00.000+0000', + CreatedById: '0050x0000000000001', + Error: [], + }; + + getInstallStatusStub.resolves(mockResult); + + const cmd = new PackageBundleInstallReportCommand( + ['--package-install-request-id', requestId, '--target-org', testOrg.username], + config + ); + + await cmd.run(); + + expect(getInstallStatusStub.calledOnce).to.be.true; + expect(getInstallStatusStub.firstCall.args[0]).to.equal(requestId); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(sfCommandStubs.table.calledOnce).to.be.true; + }); + + it('should report on a package bundle installation request using alias flag', async () => { + const requestId = '0Ho0x0000000000000'; + const mockResult: BundleSObjects.PkgBundleVersionInstallReqResult = { + Id: requestId, + InstallStatus: BundleSObjects.PkgBundleVersionInstallReqStatus.success, + PackageBundleVersionID: '0Ho0x0000000000001', + DevelopmentOrganization: 'test-org@example.com', + ValidationError: '', + CreatedDate: '2025-01-01T00:00:00.000+0000', + CreatedById: '0050x0000000000001', + Error: [], + }; + + getInstallStatusStub.resolves(mockResult); + + const cmd = new PackageBundleInstallReportCommand(['-i', requestId, '--target-org', testOrg.username], config); + + await cmd.run(); + + expect(getInstallStatusStub.calledOnce).to.be.true; + expect(getInstallStatusStub.firstCall.args[0]).to.equal(requestId); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(sfCommandStubs.table.calledOnce).to.be.true; + }); + + it('should throw error when package-install-request-id flag is missing', async () => { + const cmd = new PackageBundleInstallReportCommand(['--target-org', testOrg.username], config); + + try { + await cmd.run(); + expect.fail('Expected error was not thrown'); + } catch (error) { + expect((error as Error).message).to.include('Missing required flag'); + } + }); + + // This test: should be rewritten once we have a unfied devhub and bundle, and should be rewritten to be "No default org found" + it('should throw error when target-dev-hub flag is missing', async () => { + const requestId = '0Ho0x0000000000000'; + const cmd = new PackageBundleInstallReportCommand(['--package-install-request-id', requestId], config); + + try { + await cmd.run(); + expect.fail('Expected error was not thrown'); + } catch (error) { + expect((error as Error).message).to.include('No default dev hub found'); + } + }); + + it('should handle API errors gracefully', async () => { + const requestId = '0Ho0x0000000000000'; + const errorMessage = 'Package bundle installation request not found'; + + getInstallStatusStub.rejects(new Error(errorMessage)); + + const cmd = new PackageBundleInstallReportCommand( + ['--package-install-request-id', requestId, '--target-org', testOrg.username], + config + ); + + try { + await cmd.run(); + expect.fail('Expected error was not thrown'); + } catch (error) { + expect((error as Error).message).to.include(errorMessage); + } + }); +});