diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f94712a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Christopher Carswell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e67d1de --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +## MCDevTools Powershell Automated Deployment Manager +### Description +The MCDevTools Powershell Automated Deployment Manager make use of [Accenture MCDevTools](https://github.com/Accenture/sfmc-devtools) and it's ability to bulk create and update solutions across multiple Business Units and instances with a single powerful command. + +### Features +- Unified creation and updating of solutions across many Business Units and instances with a single script +- Essential for enterprise deployments solutions of Salesforce Marketing Cloud to ensure correct versioning across all Business Unit +- Version control can be applied as per usual as the script will automatically execute itself in a way that doesn't require manually copying to the ./template directory for the execution +- Incredible possibilities to expand automations for any solutions when the MCDevTools Powershell Automated Deployment Manager is combined with the merge fields functionality of [Accenture MCDevTools](https://github.com/Accenture/sfmc-devtools). + +### Incredible Possibilities for Automated Deployment +Using this PowerShell Automated Deployment Manager along with [Accenture MCDevTools](https://github.com/Accenture/sfmc-devtools) merge field functionality, there are endless possibilities to expand and replace any metadata available such as: +- Query activity source and target Data Extensions +- Automation timezone start times (such as starting at 7am in the local timezone) +- Dynamic folder naming +- Dynamic Email content such as displaying images/colour/translations specific to the Business Unit + +The options are practically limitless, as long as it's editable (and replaceable) within the metadata. [Instructions below](#advanced-scenarios). + +### Pre-requisites +- Working knowledge of [Accenture MCDevTools](https://github.com/Accenture/sfmc-devtools), specifically the `mcdev retrieve` and `mcdev deploy` functionality. + +### Preparation +1. Install [Accenture MCDevTools](https://github.com/Accenture/sfmc-devtools) and setup authorisation correctly as per usual. +2. Copy the `src` directory of this solution into your local project directory which has [Accenture MCDevTools](https://github.com/Accenture/sfmc-devtools) installed like this: + + ```` + [local project]\src\packagedSolutions\solutionA + ```` + +### Configuration +4. Configure the `markets` and `marketlists` as required in `.mcdevrc.json` - see below for [examples](#example-markets-configuration) + + ```` + [local project]\.mcdevrc.json + ```` +5. The script will allow targeting of the solution per market (Business Unit), or per marketlist (list of Business Units, even across instances) + 1. Configuring the market list will allow for bulk deployment to as many Business Units as listed + + +### Usage +6. Use `mcdev retrieve` to retrieve the items you would like to package for deployment. + 1. The items will now be in the `[local project]\retrieve` directory +7. Copy the items you need for your solution to the `solutionA` subdirectories (`asset`, `automation`, `emailSend` etc.) + 1. See [example deployment](#example-deployment) below for more info + 2. Blank sample files of these are already in the SolutionA directory for example, but should be deleted + +8. In the script file, make the following configurations: + 1. #CONFIG-A: list out the items that should be deployed at the top of the file (must match the name of the file itself) + 2. #CONFIG-B: indicate the types of items that should be deployed (`asset`, `automation`, `emailSend` etc.) + 3. #CONFIG-C: adjust depending on how many levels from the script file to the project directory `[local project]` (default: 3) + + +### Execution +Open the terminal from your project directory `[local project]` and navigate to: + +```` +cd src\packagedSolutions\solutionA +```` +### Option A +Builds definition for all markets in given market list. The items will appear in the `[local project]/deploy` directory +```` +[SCRIPT] [MARKET_LIST] +```` + +#### Example: + +```` +./solutionA_deployment.ps1 SolutionA +```` + +### Option B +Builds definition for given market in given list. The items will appear in the `[local project]/deploy` directory +```` +[SCRIPT] [MARKET_LIST] [BUSINESS_UNIT] +```` + +#### Examples: +```` +./solutionA_deployment.ps1 SolutionA prod/BusinessUnitA +./solutionA_deployment.ps1 SolutionA prod/BusinessUnitB +```` + + +### Option C +Add the 'deploy' argument to deploy to Salesforce Marketing Cloud. The items will appear in the `[local project]/deploy` directory and also be deployed directly to Salesforce Marketing Cloud +```` +[SCRIPT] [MARKET_LIST] [BUSINESS_UNIT] deploy +```` + +#### Examples: +```` +./solutionA_deployment.ps1 SolutionA deploy +./solutionA_deployment.ps1 SolutionA prod/BusinessUnitA deploy +```` + + +### Notes +- The solution will automatically move the deployment package to the `.[local project]\template` directory for `mcdev bdb` deployment +- This allows tracking of the script and related packaged items in your `[local project]\src\packagedSolutions\solutionA` directory for source control, while keeping the `[local project]\template` directory free for deployment purposes. +- Multiple projects can work concurrently in this way + +### Tips +- Feel free to place this in a subdirectory, but adjust #CONFIG-C accordingly +- Feel free to rename `src`, `packagedSolutions` and `solutionA` to anything you like +- Review the items in the `[local project]\deploy` directory first to ensure the items are correct before deploying to Salesforce Marketing Cloud +- Because the solution automatically removes and copies the package to `[local project]\template` directory each time, it's not necessary to apply version control to this directory, so ensure to add it to the `[local project]\.gitignore` file +- For only deploying a subset of items, and not all at once, simply comment out the items not required and run the script. This allows for quicker iterative deployment + +#### Example: +```` +$dataExtension_List = @( + 'DataExtensionA' + #'DataExtensionB', + #'DataExtensionB' +) +```` + +## Configuration Examples +### Example Markets Configuration +List all your Business Units here across all your instances +```` +"markets": { + "BusinessUnitA ": { + "buName": "BusinessUnitA" + }, + "BusinessUnitB ": { + "buName": "BusinessUnitB" + } +} +```` +Create a configuration for a grouping (marketlist) of Business Units (markets) + +#### MarketList Example A +- Grouping solution specific Business Units together (even across instances) +- Useful when only some Business Units require (or are ready) for a specific solution + +```` +"marketList": { + "SolutionA": { + "description": "Business Units that require SolutionA", + "prod/BusinessUnitA": ["BusinessUnitA"] + "prod/BusinessUnitB": ["BusinessUnitB"] + } +} +```` +#### MarketList Example B +- Grouping all Production Business Units together (even across instances) +- Useful for when updating to all Business Units at once + +```` +"marketList": { + "PRD": { + "description": "Production Business Units", + "prod/BusinessUnitA": ["BusinessUnitA"] + "prod/BusinessUnitB": ["BusinessUnitB"] + } +} +```` +## Deployment Examples +### Automation Deployment +To create a packaged solution of a Query Activity, Automation and Data Extension, follow these steps +1. Go to the `[local project]` directory and open up the Terminal +2. Run `mcdev retrieve` +3. Follow the prompts to retrieve from the Business Unit as necessary +4. Copy the directory and metadata of the items you need for your solution (query, automation, dataExtension) +5. Paste these into the `[local project]\src\packagedSolutions\solutionA` directory like this: + ```` + 1.[local project]\src\packagedSolutions\solutionA\query + [metadata of file(s)].sql + [metadata of file(s)].json + 2.[local project]\src\packagedSolutions\solutionA\dataExtension + [metadata of file(s)].json + 3.[local project]\src\packagedSolutions\solutionA\automation + [metadata of file(s)].json + ```` +6. Modify your solution in the metadata as necessary (updating names, metadata etc.) +7. Now your solution is ready for deployment! +8. Deploy by navigating to the script in the terminal with `cd src\packagedSolutions\solutionA` +9. Execute the script with `./solutionA_deployment.ps1 SolutionA`. This will deploy your packaged solution to all the Business Unit (markets) part of `SolutionA` market list +10. The script will build all configuration as necessary and place them in the `[local project]\deploy` directory. +11. Review as necessary. +12. When ready to deploy to Salesforce Marketing Cloud, use `./solutionA_deployment.ps1 SolutionA deploy` +13. This will rebuild the packaged solution again, and place once again in the `[local project]\deploy` directory, but now will `deploy` directly to Salesforce Marketing Cloud + + +## Advanced Scenarios + +### Merge Fields for Naming +A great use case is the way that each Business Unit can have it's own customised deployment, which using the packaged solution as a 'template'. To perform this, you would use 'merge field's as part of the market configuration in `.mcdevrc.json` + +#### Merge Field Example +```` +"markets": { + "BusinessUnitA ": { + "buName": "BusinessUnitA", + "prefix": "BU_A" + }, + "BusinessUnitB ": { + "buName": "BusinessUnitB", + "prefix": "BU_B" + } +} +```` +Notice how we added the `prefix` property to the market configuration. This can now be used dynamically within the packaged solution metadata like this: + +#### Merge Field Data Extension Example + +```` +{ + "CustomerKey": "{{{prefix}}}_DataExtensionA", + "Name": "{{{prefix}}}_DataExtensionA", + "Description": "", + "IsSendable": "false", + "IsTestable": "false", + "Fields": [ + { + "Name": "SubscriberKey", + "DefaultValue": "", + "MaxLength": "255", + "IsRequired": "true", + "IsPrimaryKey": "true", + "FieldType": "Text" + } + ], + "r__folder_ContentType": "dataextension", + "r__folder_Path": "Data Extensions" +} +```` +Notice how the `{{{prefix}}}` was added to the CustomerKey and Name fields. +Now when we run the script, the merge fields will replaced by the values from the Business Unit we are deploying against. If we applied the script to two markets as per our configuration above, the following would be created: + +#### Merge Field Deploy Example + +File: `[local project]\deploy\prod\BusinessUnitA\dataExtension\DataExtensionA.dataExtension-meta.json` + +```` +{ + "CustomerKey": "BU_A_DataExtensionA", + "Name": "BU_A_DataExtensionA", + "Description": "", + "IsSendable": "false", + "IsTestable": "false", + "Fields": [ + { + "Name": "SubscriberKey", + "DefaultValue": "", + "MaxLength": "255", + "IsRequired": "true", + "IsPrimaryKey": "true", + "FieldType": "Text" + } + ], + "r__folder_ContentType": "dataextension", + "r__folder_Path": "Data Extensions" +} +```` + +File: `[local project]\deploy\prod\BusinessUnitB\dataExtension\DataExtensionA.dataExtension-meta.json` + +```` +{ + "CustomerKey": "BU_B_DataExtensionA", + "Name": "BU_B_DataExtensionA", + "Description": "", + "IsSendable": "false", + "IsTestable": "false", + "Fields": [ + { + "Name": "SubscriberKey", + "DefaultValue": "", + "MaxLength": "255", + "IsRequired": "true", + "IsPrimaryKey": "true", + "FieldType": "Text" + } + ], + "r__folder_ContentType": "dataextension", + "r__folder_Path": "Data Extensions" +} +```` + +Note how the package has been created for two Business Units, and each Data Extension has their own `CustomerKey` and `Name`. + diff --git a/src/packagedSolutions/solutionA/asset/message/MessageA/MessageA.asset-message-meta.json b/src/packagedSolutions/solutionA/asset/message/MessageA/MessageA.asset-message-meta.json new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/asset/message/MessageA/views.html.content.asset-message-meta.html b/src/packagedSolutions/solutionA/asset/message/MessageA/views.html.content.asset-message-meta.html new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/automation/AutomationA.automation-meta.json b/src/packagedSolutions/solutionA/automation/AutomationA.automation-meta.json new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/dataExtension/DataExtensionA.dataExtension-meta.json b/src/packagedSolutions/solutionA/dataExtension/DataExtensionA.dataExtension-meta.json new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/emailSend/EmailSendA-emailSend-meta.json b/src/packagedSolutions/solutionA/emailSend/EmailSendA-emailSend-meta.json new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/query/QueryA.query-meta.sql b/src/packagedSolutions/solutionA/query/QueryA.query-meta.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/query/QueryA.query.meta.json b/src/packagedSolutions/solutionA/query/QueryA.query.meta.json new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/script/ScriptA.script-meta.ssjs b/src/packagedSolutions/solutionA/script/ScriptA.script-meta.ssjs new file mode 100644 index 0000000..e69de29 diff --git a/src/packagedSolutions/solutionA/solutionA_deployment.ps1 b/src/packagedSolutions/solutionA/solutionA_deployment.ps1 new file mode 100644 index 0000000..f871d1b --- /dev/null +++ b/src/packagedSolutions/solutionA/solutionA_deployment.ps1 @@ -0,0 +1,191 @@ +#Script builds definition for Solution A + +# Must be executed from the folder in which the script resides: .src/packagedSolutions/solutionA/solutionA_deployment.ps1 +# No need to copy the solution to '/template' folder first, the script takes care of that + +#CONFIG-A: Configure the items that should be deployed below +$asset_List = @( + 'MessageA' +) + +$dataExtension_List = @( + 'DataExtensionA' +) + +$query_List = @( + 'QueryA' +) + +$emailSend_List = @( + 'EmailSendA' +) + +$script_List = @( + 'ScriptA' +) + +$automation_List = @( + 'AutomationA' +) + +function RunCommand($Command) { + Write-Output $Command + Invoke-Expression $Command +} + +function RunCommandWithItemList($prefix, $commandType, $items, $suffix = $null) { + $itemString = $items -join ', ' + $command = if ($null -ne $suffix) { + "$prefix $commandType `"$itemString`" $suffix" + } + else { + "$prefix $commandType `"$itemString`"" + } + RunCommand $command +} + +#CONFIG-B: Configure the items that should be deployed below +function BuildItemsForList($marketList) { + $prefix = "mcdev bdb $marketList" + RunCommandWithItemList $prefix 'asset' $asset_List + RunCommandWithItemList $prefix 'dataExtension' $dataExtension_List + RunCommandWithItemList $prefix 'query' $query_List + RunCommandWithItemList $prefix 'emailSend' $emailSend_List + RunCommandWithItemList $prefix 'script' $script_List + RunCommandWithItemList $prefix 'automation' $automation_List +} + +#CONFIG-B: Configure the items that should be deployed below +function BuildItems($market, $bu) { + $prefix = "mcdev bd $bu" + $suffix = $market + RunCommandWithItemList $prefix 'asset' $asset_List $suffix + RunCommandWithItemList $prefix 'dataExtension' $dataExtension_List $suffix + RunCommandWithItemList $prefix 'query' $query_List $suffix + RunCommandWithItemList $prefix 'emailSend' $emailSend_List $suffix + RunCommandWithItemList $prefix 'script' $script_List $suffix + RunCommandWithItemList $prefix 'automation' $automation_List $suffix +} + + +function ShowDeploymentMessage($message) { + Write-Host "" + Write-Host "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-" + Write-Host "$message" + Write-Host "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-" + Write-Host "" +} + +function CopyToTemplateDirectory($currentDirectory) { + + # Go two levels up in the directory hierarchy to get to the 'root' folder of the project + #CONFIG-C: Configure here depending on hierarchy + $parentDirectory = (Split-Path -Path $currentDirectory -Parent) + $parentDirectory = (Split-Path -Path $parentDirectory -Parent) + $parentDirectory = (Split-Path -Path $parentDirectory -Parent) + + # Construct the destination path by appending "template" to the parent directory + $destinationPath = Join-Path -Path $parentDirectory -ChildPath "template" + + # Check if the destination directory exists + if (Test-Path -Path $destinationPath) { + # If it exists, remove all folders and files + Remove-Item -Path $destinationPath -Recurse -Force + Write-Host "Deleting folder: $destinationPath" + } + else { + # If it doesn't exist, show a message indicating that it doesn't need to be deleted + Write-Host "The destination directory $destinationPath does not exist." + } + + # Create the destination directory if it doesn't exist + New-Item -ItemType Directory -Force -Path $destinationPath | Out-Null + Write-Host "Creating folder: $destinationPath" + + # Get all items in the source directory except the "template" folder + $itemsToCopy = Get-ChildItem -Path $currentDirectory + Write-Host "Copying these items: $itemsToCopy" to $destinationPath + + # Copy each item to the destination directory + foreach ($item in $itemsToCopy) { + Copy-Item -Path $item.FullName -Destination $destinationPath -Recurse -Force + } + # Change the current working directory to the root folder before executing mcdevtools commands + Set-Location -Path $parentDirectory +} + +ShowDeploymentMessage "Step 0: Prepping the template folder" + +# Get the current directory +$currentDirectory = (Get-Location).Path + +# Copying everything over to the /template folder before executing mcdevtools. This way we avoid a few manual steps which normally has to be taken and allows for only working from the src folder +CopyToTemplateDirectory($currentDirectory) + +# Removing the deploy folder before executing mcdevtool commands +if (Test-Path -Path deploy) { + Write-Host "Deleting everything from mcdev 'deploy' directory before executing mcdevtools commands" + rmdir deploy -r +} + +# Executing command +#./solutionA_deployment PRD prod/BusinessUnitA deploy +if ($args.Length -eq 3 -and $args[2] -eq 'deploy') { + $json = Get-Content ".\.mcdevrc.json " | ConvertFrom-Json + $marketList = $args[0] + $bu = $args[1] + $market = $json.marketList.$marketList.$bu + + # Check if the market selected exists before continuing + if ($market -eq $null) { + ShowDeploymentMessage "Error: Market or MarketList not found." + } + else { + ShowDeploymentMessage "Step 1: Building definition for Market: $market" + BuildItems $market $bu + ShowDeploymentMessage "Step 2: Deploying Market: $market to Marketing Cloud" + mcdev d * #Performs deployment to Business Unit + ShowDeploymentMessage "Success: Deployed Market: $market to Marketing Cloud" + + } +} +#./solutionA_deployment PRD prod/BusinessUnitA +elseif ($args.Length -eq 2 -and $args[1] -ne 'deploy') { + $json = Get-Content ".\.mcdevrc.json " | ConvertFrom-Json + $marketList = $args[0] + $bu = $args[1] + $market = $json.marketList.$marketList.$bu + + # Check if the market selected exists before continuing + if ($market -eq $null) { + ShowDeploymentMessage "Error: Market or MarketList not found." + } + else { + ShowDeploymentMessage "Step 1: Building definition for Market: $market" + BuildItems $market $bu + ShowDeploymentMessage "Success: Deployment definition build completed for Market: $market. Not deployed to Marketing Cloud yet" + } +} +#./solutionA_deployment PRD deploy +elseif ($args.Length -eq 2 -and $args[1] -eq 'deploy') { + $marketList = $args[0] + ShowDeploymentMessage "Step 1: Building definition for Market List: $marketList" + BuildItemsForList $marketList + ShowDeploymentMessage "Step 2: Deploying Market List: $marketList to Marketing Cloud" + mcdev d * #Performs deployment to Business Unit + ShowDeploymentMessage "Success: Deployed Market List: $marketList to Marketing Cloud" +} +#./solutionA_deployment PRD +elseif ($args.Length -eq 1) { + $marketList = $args[0] + ShowDeploymentMessage "Step 1: Building definition for Market List: $marketList" + BuildItemsForList $marketList + ShowDeploymentMessage "Success: Deployment definition build completed for Market List: $marketList. Not deployed to Marketing Cloud yet" +} +else { + ShowDeploymentMessage "Error: Missing arguments - refer to script for more information" +} +# Final Step: Change the current working directory back to previous folder for simpler re-execution +Set-Location -Path $currentDirectory + +