Understanding and Leveraging Deployment Processors

Deployment Processors are an excellent way to extend your CrafterCMS project with new channel support, workflow events and better monitoring. In the blog / vlog we'll cover everything you need to know to get started building customer deployment extentions.

In this video blog we cover:

  1. What a the deployer is and how it works
  2. Deployment targets and processors
  3. How to write and configure your own deployment processor
  4. Best practices for accessing content inside the deployment processor

Important Documentation Links:

Code examples discussed in the video:

Basic Deployment Processor:

logger.info("Invoking example deployer processor")

for(path : originalChangeSet.getCreatedFiles()) {
    logger.info("Do create things!")
}

for(path : originalChangeSet.getUpdatedFiles()) {
    logger.info("Do update things!")
}

for(path : originalChangeSet.getDeletedFiles()) {
    logger.info("Do delete things!")
}

logger.info("I'm done!")

Elaborated Example:

The following example demonstrates:

  1. How to access the input stream for deployed content items properly
  2. How to get configuration values from the deployment target
  3. Basic code factoring

Please see the video for the full explaination.

def contextFactory = applicationContext.getBean("contextFactory")
def contentStoreService = applicationContext.getBean("crafter.contentStoreService")
def context = contextFactory.getObject()

def contentAccessHelper = new ContentAccessHelper(contentStoreService, context)

def someTargValue = applicationContext.getEnvironment().getProperty('target.myCustomParams.myParam')

logger.info("invoking example deployer processor")
logger.info("Target value: {}", someTargValue)

for(path : originalChangeSet.getCreatedFiles()) {
    if(contentAccessHelper.isAsset(path)) {
        def is

        try {
            is = contentAccessHelper.retrieveStaticAsset(path)
            logger.info("CREATE w bytes: {} {}", path, is.available())
        }
        finally {
            if(is) is.close()
        }
    }
}

for(path : originalChangeSet.getUpdatedFiles()) {
    // Note: Update is only called if the file is different. 
    // The deployer ignores repeated deployments of the same file

    if(contentAccessHelper.isAsset(path)) {
        def is

        try {
            is = contentAccessHelper.retrieveStaticAsset(path)
            logger.info("UPDATE w bytes: {} {}", path, is.available())
        }
        finally {
            if(is) is.close()
        }
    }
}

// don't do anything with deleted files
// for(path : originalChangeSet.getDeletedFiles()) {
// }

logger.info("invoking example deployer processor complete")


/**
 * Helper class for dealing with assets
 */
protected class ContentAccessHelper {
    def contentStoreService
    def context
    def remoteAssetPattern = ""

    def ContentAccessHelper(contentStoreService, context) {
        this.contentStoreService = contentStoreService
        this.context = context
    }

    /**
     * get the input stream of an asset
     *  This service should be used to get asset input streams for the following 3 reasosns:
     * 1. This code works with blob store and without
     * 2. This code does not assume direct access to system resources (which is
     * disabled for scripting)
      * 3. This code future proofs your code for other repository updates
     */
    def retrieveStaticAsset(binaryPath) 
    throws Exception {

        if(!isRemoteAsset(binaryPath)) {
            // item is in our repository
            def binaryContent = contentStoreService.findContent(this.context, binaryPath)

            if(binaryContent != null) {
                return binaryContent.getInputStream()
            }
            else {
                throw new Exception("Content at path returned null via findContent {}", binaryPath)
            }
        }
        else {
            // item is remote
            throw new Exception("Remote assets not supported by processor at this time {}", binaryPath)
        }
    }

    /**
     * @return true if asset is a remote asset
     */
    def isRemoteAsset(path) {
        return false
    }

    /**
     * @return true if path is an asset
     */
    def isAsset(contentPath) {
        return (contentPath) ? contentPath.startsWith("/static-assets") : false
    }   
}

Configuring the Deployment Processor in a Target

version: 4.1.3.0
target:
  env: preview
  siteName: t1
  localRepoPath: /home/russdanner/crafter-installs/4.1.3-support/craftercms/crafter-authoring/data/repos/sites/t1/sandbox

  myCustomParams:
    myParam: "a value"

  search:
    indexIdFormat: '%s-preview'
  deployment:
    scheduling:
      enabled: false
    pipeline:
    - processorName: gitDiffProcessor


    - processorName: scriptProcessor
      scriptPath: '/home/russdanner/crafter-installs/next/craftercms/crafter-authoring/bin/crafter-deployer/ContentAccessExample.groovy'
      excludeFiles:
      - ^/.*\.keep$
      includeFiles: ["^/site/website/.*$", "^/static-assets/.*$"]
      failDeploymentOnFailure: true
      
    - processorName: searchIndexingProcessor
      excludeFiles:
      - ^/sources/.*$
    - processorName: httpMethodCallProcessor
      method: GET
      url: ${target.engineUrl}/api/1/site/cache/clear.json?crafterSite=${target.siteName}&token=${target.engineManagementToken}
    - processorName: httpMethodCallProcessor
      includeFiles:
      - ^/?config/studio/content-types.*$
      method: GET
      url: ${target.engineUrl}/api/1/site/context/graphql/rebuild.json?crafterSite=${target.siteName}&token=${target.engineManagementToken}
    - processorName: fileOutputProcessor
      processorLabel: fileOutputProcessor