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:
- What a the deployer is and how it works
- Deployment targets and processors
- How to write and configure your own deployment processor
- Best practices for accessing content inside the deployment processor
Important Documentation Links:
- Deployer Documentation: https://docs.craftercms.org/en/4.1/reference/modules/deployer/index.html
- Out of the box processors: https://docs.craftercms.org/en/4.1/reference/modules/deployer/index.html#deployer-processors
- Script Processor: https://docs.craftercms.org/en/4.1/reference/modules/deployer/index.html#groovy-script-processor
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:
- How to access the input stream for deployed content items properly
- How to get configuration values from the deployment target
- 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