├── .gitattributes ├── 9781484254721.jpg ├── Contributing.md ├── LICENSE.txt ├── Listing 10-1 CallingCode.cls ├── Listing 10-2 ConfigService.cls ├── Listing 10-3 CallingCode.cls ├── Listing 11-1 Scheduler.cls ├── Listing 5-1 OrderingService.cls ├── Listing 5-2 OrderingService.cls ├── Listing 5-3 OrderingService.cls ├── Listing 5-4 selector.cls ├── Listing 5-5 sfdx-project.json ├── Listing 6-1 scratch-def.json ├── Listing 6-2 sfdx-project.json ├── Listing 6-3 .forceignore ├── Listing 6-4 userdefinition.json ├── Listing 7-1 Dockerfile ├── Listing 7-2_ Sample .gitlab-ci.yml ├── Listing 7-3_ Sample .gitlab-ci.yml ├── Listing 7-4_ Sample .gitlab-ci.yml ├── Listing 7-5_ Sample .gitlab-ci.yml ├── Listing 8-1 Test.cls ├── Listing 9-1 ruby.sh ├── Listing 9-10 node.js ├── Listing 9-13 lookup custom metadata.cls ├── Listing 9-14 approvalProcess.xml ├── Listing 9-15 replace.xslt ├── Listing 9-2 bash.sh ├── Listing 9-3 build.xml ├── Listing 9-4.gradle ├── Listing 9-5 package.json ├── Listing 9-6 jq.json ├── Listing 9-7 jq.json ├── Listing 9-8 jq.txt ├── Listing 9-9 query.sh ├── README.md └── errata.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /9781484254721.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/mastering-salesforce-devops-master/2229e91529235ce8390a99e11c4cdb2275996593/9781484254721.jpg -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Freeware License, some rights reserved 2 | 3 | Copyright (c) 2019 Andrew Davis 4 | 5 | Permission is hereby granted, free of charge, to anyone obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to work with the Software within the limits of freeware distribution and fair use. 8 | This includes the rights to use, copy, and modify the Software for personal use. 9 | Users are also allowed and encouraged to submit corrections and modifications 10 | to the Software for the benefit of other users. 11 | 12 | It is not allowed to reuse, modify, or redistribute the Software for 13 | commercial use in any way, or for a user’s educational materials such as books 14 | or blog articles without prior permission from the copyright holder. 15 | 16 | The above copyright notice and this permission notice need to be included 17 | in all copies or substantial portions of the software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /Listing 10-1 CallingCode.cls: -------------------------------------------------------------------------------- 1 | // *Listing 10-1: The original state of CallingCode.cls, which directly performs SOQL queries of a configuration object* 2 | 3 | 4 | public with sharing class CallingCode { 5 | public CallingCode() { 6 | String product = 'myProduct'; 7 | Boolean enabled = [SELECT feature_enabled__c 8 | FROM Configuration_Object__c 9 | WHERE product__c = :product][0] 10 | .feature_enabled__c; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Listing 10-2 ConfigService.cls: -------------------------------------------------------------------------------- 1 | // *Listing 10-2: ConfigService.cls, an abstraction layer that contains both the old and the new ways of accessing configuration data * 2 | 3 | 4 | public with sharing abstract class ConfigService { 5 | public static Boolean isFeatureEnabled(String product) { 6 | // return theOldWay(product); 7 | return theNewWay(product); 8 | } 9 | 10 | private static Boolean theOldWay(String product) { 11 | return [SELECT feature_enabled__c 12 | FROM Configuration_Object__c 13 | WHERE product__c = :product][0] 14 | .feature_enabled__c; 15 | } 16 | 17 | private static Boolean theNewWay(String product) { 18 | return [SELECT feature_enabled__c 19 | FROM Configuration_Metadata__mdt 20 | WHERE product__c = :product][0] 21 | .feature_enabled__c; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Listing 10-3 CallingCode.cls: -------------------------------------------------------------------------------- 1 | // Listing 10-3: CallingCode.cls after adding the abstraction layer instead of directly accessing configuration data 2 | 3 | public with sharing class CallingCode { 4 | public CallingCode() { 5 | String product = 'myProduct'; 6 | Boolean enabled = ConfigService.isFeatureEnabled(product); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Listing 11-1 Scheduler.cls: -------------------------------------------------------------------------------- 1 | // Listing 11-1: Starter code for scheduling and aborting jobs 2 | 3 | public with sharing class JobScheduler { 4 | public static void scheduleAll(){ 5 | System.schedule('scheduledJob1','0 0 2 ? * SAT', new ScheduledJob1()); 6 | System.schedule('ScheduledJob2','0 1 * * * ?', new ScheduledJob1()); 7 | } 8 | 9 | public static void abortAll(){ 10 | for(CronTrigger ct : getScheduledJobs()){ 11 | system.abortJob(ct.Id); 12 | } 13 | } 14 | 15 | private static CronTrigger[] getScheduledJobs() { 16 | final string SCHEDULED_JOB = '7'; 17 | return [ 18 | SELECT Id, CronJobDetail.Name, CronExpression, State 19 | FROM CronTrigger 20 | WHERE CronJobDetail.JobType = :SCHEDULED_JOB]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Listing 5-1 OrderingService.cls: -------------------------------------------------------------------------------- 1 | // *Listing 5-1: OrderingService has an explicit dependency on FedExService (from Philippe Ozil's blog post)* 2 | 3 | 4 | public class OrderingService { 5 | 6 | private FedExService shippingService = new FedExService(); 7 | 8 | public void ship(Order order) { 9 | // Do something... 10 | 11 | // Use the shipping service to generate a tracking number 12 | String trackingNumber = shippingService.generateTrackingNumber(); 13 | 14 | // Do some other things... 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Listing 5-2 OrderingService.cls: -------------------------------------------------------------------------------- 1 | // *Listing 5-2: OrderingService refactored to use inversion of control to determine the ShippingService indirectly (from Philippe Ozil's blog post)* 2 | 3 | 4 | public class DHLImpl implements ShippingService { 5 | public String generateTrackingNumber() { 6 | return 'DHL-XXXX'; 7 | } 8 | } 9 | 10 | public class FedExImpl implements ShippingService { 11 | public String generateTrackingNumber() { 12 | return 'FEX-XXXX'; 13 | } 14 | } 15 | 16 | public class ShippingStrategy { 17 | public static ShippingService getShippingService(Order order) { 18 | // Use FedEx in the US or DHL otherwise 19 | if (order.ShippingCountry == 'United States') { 20 | return new FedExImpl(); 21 | } 22 | else { 23 | return new DHLImpl(); 24 | } 25 | } 26 | } 27 | 28 | public class OrderingService { 29 | public void ship(Order order) { 30 | // Do something... 31 | 32 | // Get the appropriate shipping service 33 | // We only see the interface here, not the implementation class 34 | ShippingService shipping = ShippingStrategy.getShippingService(order); 35 | 36 | // Use the shipping service to generate a tracking number 37 | String trackingNumber = shipping.generateTrackingNumber(); 38 | 39 | // Do some other things... 40 | } 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /Listing 5-3 OrderingService.cls: -------------------------------------------------------------------------------- 1 | // *Listing 5-3: OrderingService refactored to use dependency injection to determine the behavior at runtime. (from Philippe Ozil's blog post)* 2 | 3 | 4 | public class Injector { 5 | public static Object instantiate(String className) { 6 | // Load the Type corresponding to the class name 7 | Type t = Type.forName(className); 8 | // Create a new instance of the class 9 | // and return it as an Object 10 | return t.newInstance(); 11 | } 12 | } 13 | 14 | // Get the service implementation from a custom metadata type 15 | // ServiceImplementation.load() runs a SOQL query to retrieve the metadata 16 | Service_Implementation__mdt services = ServiceImplementation.load(); 17 | 18 | // Inject the shipping service implementation 19 | // (services.shipping is either FedExImpl, DHLImpl or any other implementation) 20 | ShippingService shipping = (ShippingService)Injector.instantiate(services.shipping); 21 | 22 | // Use the shipping service to generate a tracking number 23 | String trackingNumber = shipping.generateTrackingNumber(); 24 | 25 | 26 | -------------------------------------------------------------------------------- /Listing 5-4 selector.cls: -------------------------------------------------------------------------------- 1 | // *Listing 5-4: An example of Selector syntax showing multiple methods chained together (from Trailhead)* 2 | 3 | 4 | public List selectOpportunityInfo(Set idSet) { 5 | List opportunityInfos = new List(); 6 | for(Opportunity opportunity : Database.query( 7 | newQueryFactory(false). 8 | selectField(Opportunity.Id). 9 | selectField(Opportunity.Amount). 10 | selectField(Opportunity.StageName). 11 | selectField('Account.Name'). 12 | selectField('Account.AccountNumber'). 13 | selectField('Account.Owner.Name'). 14 | setCondition('id in :idSet'). 15 | toSOQL())) 16 | opportunityInfos.add(new OpportunityInfo(opportunity)); 17 | return opportunityInfos; 18 | } 19 | -------------------------------------------------------------------------------- /Listing 5-5 sfdx-project.json: -------------------------------------------------------------------------------- 1 | // *Listing 5-5: An `sfdx-project.json` file showing package dependencies.* 2 | 3 | { 4 | "packageDirectories": [ 5 | { 6 | "path": "force-app/healthcase", 7 | "package": "healthcare", 8 | "versionName": "ver 0.1", 9 | "versionNumber": "0.1.0.NEXT", 10 | "dependencies": [ 11 | { 12 | "package": "industry@0.1.0.12546" 13 | }, 14 | { 15 | "package": "schema", 16 | "versionNumber": "0.1.0.LATEST" 17 | } 18 | ] 19 | }, 20 | { 21 | "path": "force-app/schema", 22 | "package": "schema", 23 | "versionName": "ver 0.1", 24 | "versionNumber": "0.1.0.NEXT", 25 | "default": true 26 | } 27 | ], 28 | "packageAliases": { 29 | "industry@0.1.0.12546": "04t1T000000703YQAQ", 30 | "schema": "0Ho6F000000XZIpSAO" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Listing 6-1 scratch-def.json: -------------------------------------------------------------------------------- 1 | # *Listing 6-1: Sample Scratch Org Definition File* 2 | 3 | 4 | { 5 | "orgName": "Acme", 6 | "edition": "Enterprise", 7 | "features": ["Communities", "ServiceCloud", "Chatbot"], 8 | "settings": { 9 | "orgPreferenceSettings": { 10 | "networksEnabled": true, 11 | "s1DesktopEnabled": true, 12 | "s1EncryptedStoragePref2": false 13 | }, 14 | "omniChannelSettings": { 15 | "enableOmniChannel": true 16 | }, 17 | "caseSettings": { 18 | "systemUserEmail": "support@acme.com" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Listing 6-2 sfdx-project.json: -------------------------------------------------------------------------------- 1 | // *Listing 6-2: Sample `sfdx-project.json` configuration file* 2 | 3 | { 4 | "namespace": "", 5 | "sfdcLoginUrl": "https://login.salesforce.com", 6 | "sourceApiVersion": "43.0", 7 | "packageDirectories": [ 8 | { 9 | "path": "util", 10 | "default": true, 11 | "package": "Expense Manager - Util", 12 | "versionName": "Spring ‘18", 13 | "versionDescription": "Welcome to Spring 2018 Release of Expense Manager Util Package", 14 | "versionNumber": "4.7.0.NEXT", 15 | "definitionFile": "config/scratch-org-def.json" 16 | }, 17 | { 18 | "path": "exp-core", 19 | "default": false, 20 | "package": "Expense Manager", 21 | "versionName": "v 3.2", 22 | "versionDescription": "Spring 2018 Release", 23 | "versionNumber": "3.2.0.NEXT", 24 | "definitionFile": "config/scratch-org-def.json", 25 | "dependencies": [ 26 | { 27 | "package": "Expense Manager - Util", 28 | "versionNumber": "4.7.0.LATEST" 29 | }, 30 | { 31 | "package": "External Apex Library - 1.0.0.4" 32 | } 33 | ] 34 | } 35 | ], 36 | "packageAliases": { 37 | "Expense Manager - Util": "0HoB00000004CFpKAM", 38 | "External Apex Library - 1.0.0.4": "04tB0000000IB1EIAW", 39 | "Expense Manager": "0HoB00000004CFuKAM" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Listing 6-3 .forceignore: -------------------------------------------------------------------------------- 1 | # *Listing 6-3:Sample `.forceignore` File* 2 | 3 | 4 | # Specify a relative path to a directory from the project root 5 | helloWorld/main/default/classes 6 | 7 | # Specify a wildcard directory - any directory named “classes” is excluded 8 | classes 9 | 10 | # Specify file extensions 11 | .cls 12 | .pdf 13 | 14 | # Specify a specific file 15 | helloWorld/main/default/HelloWorld.cls 16 | -------------------------------------------------------------------------------- /Listing 6-4 userdefinition.json: -------------------------------------------------------------------------------- 1 | // *Listing 6-4: Sample User Definition file* 2 | 3 | { 4 | "Username": "tester1@sfdx.org", 5 | "LastName": "Hobbs", 6 | "Email": "tester1@sfdx.org", 7 | "Alias": "tester1", 8 | "TimeZoneSidKey": "America/Denver", 9 | "LocaleSidKey": "en_US", 10 | "EmailEncodingKey": "UTF-8", 11 | "LanguageLocaleKey": "en_US", 12 | "profileName": "Standard Platform User", 13 | "permsets": ["Dreamhouse", "Cloudhouse"], 14 | "generatePassword": true 15 | } 16 | -------------------------------------------------------------------------------- /Listing 7-1 Dockerfile: -------------------------------------------------------------------------------- 1 | # *Listing 7-1: myCompany/salesforceDXimage - a sample Dockerfile for Salesforce DX* 2 | 3 | 4 | FROM node:8 5 | 6 | #Installing Salesforce DX CLI 7 | RUN yarn global add sfdx-cli 8 | RUN sfdx --version 9 | 10 | #SFDX environment 11 | ENV SFDX_AUTOUPDATE_DISABLE true 12 | ENV SFDX_USE_GENERIC_UNIX_KEYCHAIN true 13 | ENV SFDX_DOMAIN_RETRY 300 14 | -------------------------------------------------------------------------------- /Listing 7-2_ Sample .gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # *Listing 7-2: Sample .gitlab-ci.yml file for running tests* 2 | 3 | image: "myCompany/salesforceDXimage:latest" 4 | 5 | stages: 6 | - build 7 | - test 8 | 9 | create_test_org: 10 | stage: build 11 | script: 12 | - sfdx force:org:create -a testOrg --setdefaultusername --wait 10 13 | - sfdx force:source:push 14 | only: 15 | - master 16 | 17 | run_tests: 18 | stage: test 19 | image: ruby:latest 20 | script: 21 | - ruby -I test test/path/to/the_test.rb 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /Listing 7-3_ Sample .gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # *Listing 7-3: Sample .gitlab-ci.yml file for managing org-level metadata publishing* 2 | 3 | image: myCompany/salesforceDXimage:latest 4 | 5 | stages: 6 | - deploy 7 | 8 | deploy_to_SIT: 9 | stage: deploy 10 | script: 11 | - echo Hello World 12 | only: 13 | - /^SIT/ 14 | 15 | deploy_to_staging: 16 | stage: deploy 17 | script: 18 | - echo Hello World 19 | only: 20 | - master 21 | 22 | deploy_to_production: 23 | stage: deploy 24 | script: 25 | - echo Hello World 26 | only: 27 | - tags 28 | - /^v[0-9.]+$/ 29 | when: manual 30 | -------------------------------------------------------------------------------- /Listing 7-4_ Sample .gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # *Listing 7-4: Sample .gitlab-ci.yml YAML file with repetitive blocks* 2 | 3 | image: myCompany/salesforceDXimage:latest 4 | 5 | stages: 6 | - install 7 | - deploy 8 | - test 9 | 10 | update_packages_in_SIT: 11 | stage: install 12 | script: 13 | - ./scripts/getInstalledPackages.sh $TARGET_ORG 14 | - ./scripts/updatePackages.sh $TARGET_ORG 15 | variables: 16 | - TARGET_ORG: SIT 17 | only: 18 | - /^SIT/ 19 | 20 | update_packages_in_staging: 21 | stage: install 22 | script: 23 | - ./scripts/getInstalledPackages.sh $TARGET_ORG 24 | - ./scripts/updatePackages.sh $TARGET_ORG 25 | variables: 26 | - TARGET_ORG: staging 27 | only: 28 | - master 29 | 30 | update_packages_in_production: 31 | stage: install 32 | script: 33 | - ./scripts/getInstalledPackages.sh $TARGET_ORG 34 | - ./scripts/updatePackages.sh $TARGET_ORG 35 | variables: 36 | - TARGET_ORG: production 37 | only: 38 | - tags 39 | - /^v[0-9.]+$/ 40 | when: manual 41 | -------------------------------------------------------------------------------- /Listing 7-5_ Sample .gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # *Listing 7-5: Sample .gitlab-ci.yml file using YAML anchors* 2 | 3 | 4 | image: myCompany/salesforceDXimage:latest 5 | 6 | stages: 7 | - install 8 | - deploy 9 | - Test 10 | 11 | .fake_job: 12 | <<: &update_packages 13 | stage: install 14 | script: 15 | - ./scripts/getInstalledPackages.sh $TARGET_ORG 16 | - ./scripts/updatePackages.sh $TARGET_ORG 17 | 18 | update_packages_in_SIT: 19 | <<: *update_packages 20 | variables: 21 | - TARGET_ORG: SIT 22 | only: 23 | - /^SIT/ 24 | 25 | update_packages_in_staging: 26 | <<: *update_packages 27 | variables: 28 | - TARGET_ORG: staging 29 | only: 30 | - master 31 | 32 | update_packages_in_production: 33 | <<: *update_packages 34 | variables: 35 | - TARGET_ORG: production 36 | only: 37 | - tags 38 | - /^v[0-9.]+$/ 39 | when: manual 40 | -------------------------------------------------------------------------------- /Listing 8-1 Test.cls: -------------------------------------------------------------------------------- 1 | // *Listing 8-1: A sample BDD-style test in Apex* 2 | 3 | 4 | @isTest 5 | static void itShouldUpdateReservedSpotsOnInsert() { 6 | System.runAs(TestUtil.careerPlanner()) { 7 | // Given 8 | Workshop__c thisEvent = TestFactory.aWorkshopWithFreeSpaces(); 9 | Integer initialAttendance = TestUtil.currentAttendance(thisEvent); 10 | final Integer PRIMARY_ATTENDEES = 3; 11 | final Integer NUMBER_EACH = 4; 12 | 13 | // When 14 | Test.startTest(); 15 | TestFactory.insertAdditionalRegistrations(thisEvent, PRIMARY_ATTENDEES, NUMBER_EACH); 16 | Test.stopTest(); 17 | 18 | // Then 19 | Integer expectedAttendance = initialAttendance + PRIMARY_ATTENDEES * NUMBER_EACH; 20 | system.assertEquals(expectedAttendance, TestUtil.currentAttendance(thisEvent), 21 | 'The attendance was not updated correctly after an insert'); 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /Listing 9-1 ruby.sh: -------------------------------------------------------------------------------- 1 | # *Listing 9-1: A simple Ruby script* 2 | 3 | #!/usr/bin/env ruby 4 | input = $stdin.read 5 | puts input.gsub(/Alpha/, 'Beta') 6 | 7 | -------------------------------------------------------------------------------- /Listing 9-10 node.js: -------------------------------------------------------------------------------- 1 | // *Listing 9-10: This Node.js code snippet makes use of both the official salesforce/core module and the unofficial sfdx-node module to authorize an org.* 2 | 3 | const { SfdxProjectJson, Org } = require("@salesforce/core"); 4 | const sfdx = require("sfdx-node"); 5 | 6 | const authWeb = async (destination, isDevHub) => { 7 | if (!isDevHub) { 8 | try { 9 | const orgObj = await Org.create(destination); 10 | return orgObj; 11 | } catch (e) { 12 | // Do nothing 13 | } 14 | } 15 | return sfdx.auth.webLogin({ 16 | setdefaultdevhubusername: isDevHub, 17 | setalias: destination 18 | }); 19 | }; 20 | 21 | module.exports = { 22 | authWeb 23 | }; 24 | -------------------------------------------------------------------------------- /Listing 9-13 lookup custom metadata.cls: -------------------------------------------------------------------------------- 1 | // *Listing 9-13: An example of looking up Custom Metadata records based on an Org ID* 2 | 3 | 4 | public static String getEndpoint(String serviceName) { 5 | String orgId = UserInfo.getOrganizationId(); 6 | 7 | API_Endpoint__mdt endpoint = [ 8 | SELECT URL__c 9 | FROM API_Endpoint__mdt 10 | WHERE OrgId__c = :orgId 11 | AND ServiceName__c = :serviceName 12 | AND isActive__c=true 13 | LIMIT 1]; 14 | 15 | return endpoint; 16 | } 17 | -------------------------------------------------------------------------------- /Listing 9-14 approvalProcess.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | false 9 | 10 | 11 | wrongUser@yourOrg.com.sandbox 12 | user 13 | 14 | FirstResponse 15 | 16 | 17 | Step_1 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Listing 9-15 replace.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Listing 9-2 bash.sh: -------------------------------------------------------------------------------- 1 | # *Listing 9-2: A simple Bash script* 2 | 3 | #!/bin/bash 4 | cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | -------------------------------------------------------------------------------- /Listing 9-3 build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Retrieving from Dev... 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Listing 9-4.gradle: -------------------------------------------------------------------------------- 1 | // *Listing 9-4: A simple Gradle script that imports existing Ant targets* 2 | 3 | 4 | logging.level = LogLevel.INFO 5 | ant.importBuild 'ant/build.xml' 6 | 7 | task deploy2QA (dependsOn: ['deployAndDestroyQA', 'deployProjectToQA']) 8 | 9 | task deploy2Full (dependsOn: ['deployAndDestroyFull', 'deployProjectToFull']) 10 | 11 | task deploy2Training (dependsOn: ['deployAndDestroyTraining', 'deployProjectToTraining']) 12 | 13 | task deploy2Prod (dependsOn: ['deployAndDestroyProd', 'deployProjectToProd']) 14 | -------------------------------------------------------------------------------- /Listing 9-5 package.json: -------------------------------------------------------------------------------- 1 | // *Listing 9-5: The scripts section from a package.json file, showing some common script commands* 2 | 3 | 4 | 5 | "scripts": { 6 | "lint": "npm run lint:lwc && npm run lint:aura", 7 | "lint:lwc": "eslint **/lwc/**", 8 | "lint:aura": "sfdx force:lightning:lint force-app/main/default/aura --exit", 9 | "test": "npm run lint && npm run test:unit", 10 | "test:unit": "lwc-jest", 11 | ... 12 | }, 13 | -------------------------------------------------------------------------------- /Listing 9-6 jq.json: -------------------------------------------------------------------------------- 1 | // *Listing 9-6: Simply piping sfdx JSON output into jq provides a nicely formatted output* 2 | 3 | // $ sfdx force:config:list --json | jq 4 | { 5 | "status": 0, 6 | "result": [ 7 | { 8 | "key": "defaultdevhubusername", 9 | "location": "Local", 10 | "value": "MyDevHub" 11 | }, 12 | { 13 | "key": "defaultusername", 14 | "location": "Local", 15 | "value": "test-v1asf98g72x5@example.com" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Listing 9-7 jq.json: -------------------------------------------------------------------------------- 1 | // *Listing 9-7: JQ allows you to go further by querying the results* 2 | 3 | // $ sfdx force:config:list --json | jq '.result[0]' 4 | { 5 | "key": "defaultdevhubusername", 6 | "location": "Local", 7 | "value": "MyDevHub" 8 | } 9 | -------------------------------------------------------------------------------- /Listing 9-8 jq.txt: -------------------------------------------------------------------------------- 1 | *Listing 9-8: JQ provides many sophisticated filtering options* 2 | 3 | 4 | $ sfdx force:config:list --json | 5 | jq '.result[] | select(.key == "defaultdevhubusername").value' 6 | 7 | "MyDevHub" 8 | -------------------------------------------------------------------------------- /Listing 9-9 query.sh: -------------------------------------------------------------------------------- 1 | # *Listing 9-9: This runs a simple query to show which users have created the most scratch orgs on our default dev hub. Bash allows commands to be strung together easily. Note the use of `\` to allow commands to span multiple lines.* 2 | 3 | 4 | #!/bin/bash 5 | DEFAULT_DEVHUB=$(sfdx force:config:list --json | \ 6 | jq --raw-output '.result[] | select(.key == "defaultdevhubusername").value') 7 | 8 | sfdx force:data:soql:query --query \ 9 | 'SELECT CreatedBy.Name, Count(Id) FROM ScratchOrgInfo 10 | GROUP BY CreatedBy.Name 11 | ORDER BY Count(Id) DESC 12 | LIMIT 10' \ 13 | --targetusername $DEFAULT_DEVHUB 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | 4 | 5 | This repository accompanies [*Mastering Salesforce DevOps*](https://www.apress.com/9781484254721) by Andrew Davis (Apress, 2019). 6 | 7 | 8 | 9 | [comment]: #cover 10 | 11 | ![Cover image](9781484254721.jpg) 12 | 13 | 14 | 15 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 16 | 17 | 18 | 19 | ## Releases 20 | 21 | 22 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 23 | 24 | 25 | 26 | ## Contributions 27 | 28 | 29 | 30 | See the file Contributing.md for more information on how you can contribute to this repository. -------------------------------------------------------------------------------- /errata.md: -------------------------------------------------------------------------------- 1 | # Errata for _Mastering Salesforce DevOps_ 2 | 3 | On **page 155** The "Free Limited Access Licenses" for Salesforce may be going away. 4 | 5 | --- 6 | 7 | On **page 165** Salesforce support will _not_ clone you're orgs using the Default Organization Template (DOT) process. You're on your own! 8 | 9 | --- 10 | 11 | On **page 284** Change 'ApexPMD' to 'PMD': 12 | 13 | "I am aware of two linting solutions for Apex code: **PMD** and SonarLint." 14 | 15 | --- 16 | 17 | On **page 286** Change 'ApexPMD' to 'PMD': 18 | 19 | "Figure 8-7. Instant feedback on Apex from **PMD**" 20 | 21 | --- 22 | 23 | On **page 295** Point to the original Domain Builder URL: 24 | 25 | Footnote 20, pointing to the Apex Domain Builder, should point to the original Domain Builder repository: **https://github.com/rsoesemann/apex-domainbuilder** 26 | 27 | --- 28 | 29 | On **page 301** Failed to credit Robert Sösemann as the author of Domain Builder: 30 | 31 | This section should read: 32 | "A new open source framework called the Apex Domain Builder (**https://github.com/rsoesemann/apex-domainbuilder**) offers a very performant and readable way of creating test data. **This module was originally created by Robert Sösemann, also the author of most of the Apex rules for PMD, and a leader in promoting clean coding practices for Salesforce.**" 33 | 34 | --- 35 | 36 | On **page 314** Change 'ApexPMD' to 'PMD': 37 | 38 | "**PMD** 39 | "...Robert Sösemann did most of the foundational work for **PMD** and remains its biggest and most popular champion...." 40 | 41 | --- 42 | 43 | On **page 315** Change 'ApexPMD' to 'Apex PMD': 44 | Although the underlying engine is just called 'PMD', the VS Code extension by Chuck Jonas is called "Apex PMD". Therefore on this page, the text should read: 45 | 46 | "PMD can be run from the command line, or from within the **Apex PMD** extension for VS Code, and its results output in multiple formats such as CSV and HTML." 47 | 48 | --- 49 | 50 | Not an erratum, but I just learned about Browserforce by Matias Rolke. 51 | The tool automates many org setup tasks that can only be done through the UI. 52 | It maintains state to ensure that you don't perform an action twice. 53 | Lots of other cool capabilities as well: https://github.com/amtrack/sfdx-browserforce-plugin 54 | 55 | --- 56 | --------------------------------------------------------------------------------