One of the challenges when implementing the shared responsibility (or eventual consistency) model is the potential for complex conflicts to be introduced to the downstream repositories. Without any controls on what changes can be made to a downstream project, it may become impractical to continue to push changes downstream.
One way to constrain the changes introduced to downstream CaC Git repositories is to automatically validate changes during a pull request (PR). This allows the platform team to introduce minimum requirements that all downstream CaC projects must adhere to while also allowing internal customers to customize their projects.
Parsing OCL
CaC projects persist their configuration in the Octopus Configuration Language (OCL). This format is parsed by the @octopusdeploy/ocl
JavaScript library.
The @octopusdeploy/ocl
library offers a low level parser that exposes individual OCL tokens. In addition, the library exposes a wrapper that allows the OCL data structure to be accessed via a read-only JavaScript object. This wrapped object can then be passed to any JavaScript library used to compare values or validate objects.
Validating PRs with GitHub Actions
The workflow shown below is an example that combines the @octopusdeploy/ocl
and expect
libraries to verify that the merge result of a CaC Git repository meets certain minimum requirements:
on: pull_request_target
jobs:
validate-ocl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20.x'
- run: npm install @octopusdeploy/ocl
- run: npm install expect
- uses: actions/github-script@v7
with:
script: |
const {parseOclWrapper} = require("@octopusdeploy/ocl")
const fs = require("fs")
const path =require("path")
const {expect} = require("expect");
/**
* This function performs the validation of the Octopus CaC OCL file
* @param ocl The OCL file to parse
*/
function checkPr(ocl) {
// Read the file
const fileContents = fs.readFileSync(ocl, 'utf-8')
// Parse the file
const deploymentProcess = parseOclWrapper(fileContents)
// Verify the contents
expect(deploymentProcess.step).not.toHaveLength(0)
expect(deploymentProcess.step[0].name).toBe("Manual Intervention")
expect(deploymentProcess.step[0].action[0].action_type).toBe("Octopus.Manual")
}
try {
checkPr('./deployment_process.ocl')
} catch (error) {
console.log(error.matcherResult.message)
process.exit(1)
}
Let’s break this workflow down.
The workflow is triggered on the pull_request_target
event. This event runs workflows from the target branch, typically the main
branch, meaning the pull request is validated according to the rules in the mainline branch. This prevents pull requests from bypassing checks by modifying the workflow file:
on: pull_request_target
We start by checking out the Git repository contents:
- uses: actions/checkout@v3
The workflow requires Node.js to be installed:
- uses: actions/setup-node@v3
with:
node-version: '20.x'
The required libraries are installed via npm
:
- run: npm install @octopusdeploy/ocl
- run: npm install expect
The verification script is executed with the actions/github-script
action:
- uses: actions/github-script@v7
with:
script: |
The libraries are exposed to the script with require
statements:
const {parseOclWrapper} = require("@octopusdeploy/ocl")
const fs = require("fs")
const path =require("path")
const {expect} = require("expect");
The verification logic is defined in the function called checkPr
whose parameter is the name of the OCL file to parse:
/**
* This function performs the validation of the Octopus CaC OCL file
* @param ocl The OCL file to parse
*/
function checkPr(ocl) {
The file contents are read to a string and passed to the parseOclWrapper
function:
// Read the file
const fileContents = fs.readFileSync(ocl, 'utf-8')
// Parse the file
const deploymentProcess = parseOclWrapper(fileContents)
The deploymentProcess
variable references a read-only object that allows the data stored in the OCL file to be accessed with standard dot notation. Here we use the expect
library, often used with unit tests, to verify the properties of the OCL file:
// Verify the contents
expect(deploymentProcess.step).not.toHaveLength(0)
expect(deploymentProcess.step[0].name).toBe("Manual Intervention")
expect(deploymentProcess.step[0].action[0].action_type).toBe("Octopus.Manual")
}
The final step is to call the checkPr
function, catch any exceptions, and print them to the console:
try {
checkPr('./deployment_process.ocl')
} catch (error) {
console.log(error.matcherResult.message)
process.exit(1)
}
Diagnosing validation errors
The output of your validation script depends on the libraries used. The expect
library is nice because it provides detailed differences between the expected and actual values. The end result of a failed validation looks something like this, where the JSON representation of the OCL data is presented as a diff showing which properties differed between the expected and input objects:
Tips and tricks
Because the validation process is plain JavaScript code you are free to implement any libraries and logic you need.
The example below embeds a step OCL snippet as a string, parses the string, and uses the toEqual
function to perform a deep comparison of the input OCL to the expected step:
const {parseOclWrapper} = require("@octopusdeploy/ocl")
const fs = require("fs")
const path =require("path")
const {expect} = require("expect");
const LastStep = `
step "display-rest-api-id" {
name = "Display REST API ID"
action {
action_type = "Octopus.Script"
notes = "Displays the API Gateway ID created by the CloudFormation template."
properties = {
Octopus.Action.Script.ScriptBody = "echo \\"REST API ID: #{Octopus.Action[Create API Gateway].Output.AwsOutputs[RestApi]}\\""
Octopus.Action.Script.ScriptSource = "Inline"
Octopus.Action.Script.Syntax = "Bash"
}
worker_pool = "hosted-ubuntu"
}
}`
/**
* This function performs the validation of the Octopus CaC OCL file
* @param ocl The OCL file to parse
*/
function checkPr(ocl) {
// Read the file
const fileContents = fs.readFileSync(ocl, 'utf-8')
// Parse the file
const deploymentProcess = parseOclWrapper(fileContents)
// Parse the fixed step defined above
const requiredStep = parseOclWrapper(LastStep)
// Verify the contents
expect(deploymentProcess.step[deploymentProcess.step.length - 1]).toEqual(requiredStep.step[0])
}
try {
checkPr('./deployment_process.ocl')
} catch (error) {
console.log(error.matcherResult.message)
process.exit(1)
}
This example uses the lodash
library to clone the wrapper (because the wrapper is a read-only object) and remove the name
property from both the template and actual OCL wrappers. This has the effect of comparing two OCL steps, but disregarding any changes to the step name:
const _ = require("lodash");
const {parseOclWrapper} = require("@octopusdeploy/ocl")
const fs = require("fs")
const path =require("path")
const {expect} = require("expect");
const LastStep = `
step "display-rest-api-id" {
name = "Display REST API ID"
action {
action_type = "Octopus.Script"
notes = "Displays the API Gateway ID created by the CloudFormation template."
properties = {
Octopus.Action.Script.ScriptBody = "echo \\"REST API ID: #{Octopus.Action[Create API Gateway].Output.AwsOutputs[RestApi]}\\""
Octopus.Action.Script.ScriptSource = "Inline"
Octopus.Action.Script.Syntax = "Bash"
}
worker_pool = "hosted-ubuntu"
}
}`
/**
* This function performs the validation of the Octopus CaC OCL file
* @param ocl The OCL file to parse
*/
function checkPr(ocl) {
// Read the file
const fileContents = fs.readFileSync(ocl, 'utf-8')
// Parse the file
const deploymentProcess = parseOclWrapper(fileContents)
// Parse the fixed step defined above
const requiredStep = parseOclWrapper(LastStep)
// Verify the contents
const expectedWithoutName = _.cloneDeep(_.omit(requiredStep.step[0], ['name']))
const sourceWithoutName = _.cloneDeep(_.omit(deploymentProcess.step[deploymentProcess.step.length - 1], ['name']))
expect(sourceWithoutName).toEqual(expectedWithoutName)
}
try {
checkPr('./deployment_process.ocl')
} catch (error) {
console.log(error.matcherResult.message)
process.exit(1)
}
Help us continuously improve
Please let us know if you have any feedback about this page.
Page updated on Thursday, November 9, 2023