Creating an artefact for compiled languages is well understood, and is an integral part of software delivery for languages such as .NET, Java and Typescript, however, for interpretive languages (Python, Ruby, PHP, Javascript), because the code in source control can be run without a “build”, it is tempting to deploy from source control. This has the following challenges:
Fulfilling dependencies in production environment, can have network issues and, even with lock files, can result is different runtime outcomes.
Manual steps are required as branches are used to separate environments, e.g. test, staging, production. Which requires deploy-time developer effort and can lead to errors, i.e. untested code being merged into production.
Package & Publish
Resolving dependencies at build time, adding any other runtime components and creating an immutable package for deployment can be achieved using the CDAF technology-agnostic package mechanism. The “build” artefact completes the development team’s Continuous Integration (CI) stage.
The Continuous Delivery (CD) would be limited to automated testing of the package, and then publication. Publication can be to a Container Registry, Package Registry (Nexus, Artifactory, Azure DevOps, GitLab, GitHub, etc.) or a proprietary asset registry such as Octopus Deploy or Mulesoft AnyPoint Exchange. The following example uses a Container Registry.
The following overview has two examples, one using the CDAF release package with automated testing, and one performing direct image build and push.
pip resolves Python dependencies, and gathers these, along with helper scripts, to produce a release package. The release package is then used to construct a runtime image, which in turn is smoke tested using docker-compose. The tested image is then pushed to the registry.
NPM resolves NodeJS dependencies, builds an image and pushes it to the registry.
graph LR
subgraph python["Python"]
python-git[(Git)]
python-build-artefact[(Build)]
python-release.ps1
subgraph docker-compose
image-container
test-container
end
push
end
subgraph node["NodeJS"]
node-git[(Git)]
node-build
node-push["push"]
end
registry[(Docker Registry)]
python-git -- "CI (pip)" -->
python-build-artefact -- "CD" -->
python-release.ps1 -->
image-container -->
push --> registry
test-container -. "smoke test" .-> image-container
node-git -- "CI (NPM)" -->
node-build -->
node-push --> registry
classDef dashed stroke-dasharray: 5, 5
class python,node dashed
classDef dotted stroke-dasharray: 2, 2
class docker-compose dotted
classDef blue fill:#007FFF
class registry blue
Note: the Python release.ps1 is an intermediary artefact, and not used to deploy to the runtime environments.
A declarative deployment ensures all components are Released in a predictable way, with the assurance the same combination of component versions that were tested align with what is released.
Release Manifest
The release contains a manifest of components and their version. This is the release declaration. The deployment is responsible for ensuring these components are applied at as declared at each promotion stage, e.g. test, staging, production. In the flow below, the release is continuously deployed through to staging, but continuously deployed, i.e. gated, to production.
For each deployment, the same image is used to create the running container.
flowchart LR
registry[(Docker Registry)]
subgraph test
p1["Python v0.2.135"] ~~~
n1["NodeJS v1.0.3"]
end
subgraph staging
p2["Python v0.2.135"] ~~~
n2["NodeJS v1.0.3"]
end
subgraph production
p3["Python v0.2.135"] ~~~
n3["NodeJS v1.0.3"]
end
test -- "auto promote" --> staging
staging -- "gated promote" --> production
registry --> test
registry --> staging
registry --> production
classDef blue fill:#007FFF
class registry blue
For context see Behaviour Tests as a Product. The development of test automation is autonomous and is not tightly coupled with the solution development. When the solution is ready to execute a set of tests, the versioned package is obtained and executed.
Environment Definitions
Configuration Management within the source allows for test execution by simply invoking the test package for the desired environment, e.g. ./package.ps1 PROD, or targeted testing ./package.ps1 PROD_WEB
context target testSuite featureSet resources secrets
local PROD_CLASS SpecFlow.Specs data supabase creds_prd
local PROD_WEB_1 Selenium.Specs login https://example.com/Calculator.html vault_prd
local PROD_WEB_2 Selenium.Specs dashboard https://example.com/Calculator.html
local TEST_CLASS SpecFlow.Specs data supabase creds_tst
local PROD_WEB_1 Selenium.Specs login https://test.example.com/Calculator.html vault_tst
local PROD_WEB_2 Selenium.Specs dashboard https://test.example.com/Calculator.html
The delivery phase of the pipeline pushes the package to a registry, with a semantic version. It is these versions that the solution delivery process can consume to provide predictable test results.
graph LR
subgraph Component A
Rbuild["Build"] -->
Rtest["Test"] -->
Rpublish["Publish</br>1.0.1"]
end
subgraph Component B
Pbuild["Build"] -->
Ptest["Test"] -->
Ppublish["Publish</br>0.5.9"]
end
subgraph Test Product
Sbuild["Build"] -->
Stest["Test"] -->
Spublish["Publish</br>1.2.0"]
end
subgraph Release
TEST:::release
PROD:::release
end
store[(Registry)]
Rpublish --> store
Ppublish --> store
Spublish --> store
store --> TEST
TEST --> PROD
classDef release fill:lightgreen
In this example, current production tests are at version 1.1.0. While the next release of 1.2.0 is in progressing through the development lifecycle, the scheduled tests can still consume version 1.1.0 with confidence. The scheduled tests may have a reduced scope, i.e. web only, this is where the ability to select a subset is valuable, e.g. ./package.ps1 PROD_WEB.