Deep dive
Deep Dive: Our CI/CD
The delivery pipeline written as a generic model - real structure, pseudo-code implementations. Any provider can run it; that is the point.
This is the CI/CD model we deploy engagements on, written as code. Not a product and not a specific tool's syntax - TypeScript pseudo-code where the structure is real and the implementations are stubs. We keep it in this form deliberately: the model is the asset, and Jenkins, GitHub Actions, GitLab, or a folder of plain shell scripts are interchangeable implementations of it. The same model runs today behind both a pipeline server and a no-CI-server script variant.
Read it the way you would read an architecture: the function boundaries and the order of operations are the design.
Four pipelines, separated on purpose
interface PipelineContext {
workspace: Workspace;
secrets: Secrets;
artifacts: ArtifactRegistry;
analyser: ArtifactAnalyser;
cloud: CloudProvider;
}
function pipeline(ctx: PipelineContext) {
// build all images, this is just code into artifacts
ciPipeline(ctx);
// create cloud infra
// this does global and env specific setups
infraPipeline(ctx);
// post infra work, db create, users, rotations
infraOpsPipeline(ctx);
// deploy things to specific env
cdPipeline(ctx);
}Build, provision, operate, deploy - four pipelines, four concerns. CI turns code into artifacts and knows nothing about environments. Infra provisions cloud resources and knows nothing about applications. Infra-ops covers the awkward middle nobody assigns an owner to - database creation, users, rotations - the steps that need both what was built and what will run. CD takes versions to an environment. When these are one pipeline, every concern leaks into every other; separated, each can change, fail, and rerun alone.
CI: code into artifacts, idempotently
function ciPipeline(ctx: PipelineContext) {
/**
* Step 1 - Java builds
* configure entire workspace
* common lib, app, and sql bundle
* lib scanned for sonar and dependencies
*/
ctx.workspace.agent('jdk17');
var opsRepo = cloneRepo('https://ops-repo', 'main');
const settingsXmlTemplate = opsRepo.file('templates/settings.xml');
ctx.workspace.configure(mavenConfigurer(settingsXmlTemplate));
var appRepo = cloneRepo('https://app-repo', 'main');
if (!ctx.artifacts.exists(appRepo.file('lib-common/pom.xml'))) {
const bundle = ctx.workspace.build('lib-common/pom.xml', mavenBuilder);
ctx.artifacts.publish('lib-common/pom.xml', bundle);
}
if (!ctx.analyser.exists(appRepo.file('lib-common/pom.xml'), sonarScanner)) {
ctx.analyser.publish('lib-common/pom.xml', sonarScanner);
}
if (!ctx.analyser.exists(appRepo.file('lib-common/pom.xml'), dependencyTrackScanner)) {
ctx.analyser.publish('lib-common/pom.xml', dependencyTrackScanner);
}
if (!ctx.artifacts.exists(appRepo.file('app-xxx/pom.xml'))) {
const jarBundle = ctx.workspace.build('app-xxx/pom.xml', mavenBuilder);
ctx.artifacts.publish('app-xxx/pom.xml', jarBundle);
}
if (!ctx.artifacts.exists(appRepo.file('app-xxx/sql.yaml'))) {
const sqlBundle = ctx.workspace.build('app-xxx/sql.yaml', sqlBuilder);
ctx.artifacts.publish('sql/app-xxx/sql.yaml', sqlBundle);
}
/**
* Step 2 - node builds
* get template file and render per project
* node is per project vs workspace
*/
ctx.workspace.agent('node20');
var opsRepo = cloneRepo('https://ops-repo', 'main');
const npmrcTemplate = opsRepo.file('templates/.npmrc');
var appRepo = cloneRepo('https://app-repo', 'main');
if (!ctx.artifacts.exists(appRepo.file('app-ui/package.json'))) {
ctx.workspace.configure(npmConfigurer(appRepo.file(npmrcTemplate)));
const bundle = ctx.workspace.build('app-ui/package.json', npmBuilder);
ctx.artifacts.publish('app-ui/package.json', bundle);
}
/**
* Step 3 - now do docker builds
* Note: our docker build source is in the ops repo
* configure workspace and then build each
* docker needs a login first so any builds are done.
* Dockerfile code downloads from artifact store (build above)
* docker builder needs to pull dependencies (in dockerfile from)
*/
ctx.workspace.agent('docker');
var opsRepo = cloneRepo('https://ops-repo', 'main');
const dockerConfigTemplate = opsRepo.file('templates/docker-config.json');
ctx.workspace.configure(dockerConfigurer(dockerConfigTemplate));
ctx.workspace.configure(dockerSecretsConfigurer());
dockerRepoConnector();
// build base images first
if (!ctx.artifacts.exists(appRepo.file('base/ubuntu/docker-compose.yaml'))) {
const bundle = ctx.workspace.build('base/ubuntu/docker-compose.yaml', dockerBuilder);
ctx.artifacts.publish('base/ubuntu/docker-compose.yaml', bundle);
}
if (!ctx.artifacts.exists(appRepo.file('base/jdk17/docker-compose.yaml'))) {
const bundle = ctx.workspace.build('base/jdk17/docker-compose.yaml', dockerBuilder);
ctx.artifacts.publish('base/jdk17/docker-compose.yaml', bundle);
}
if (!ctx.artifacts.exists(appRepo.file('base/node20/docker-compose.yaml'))) {
const bundle = ctx.workspace.build('base/node20/docker-compose.yaml', dockerBuilder);
ctx.artifacts.publish('base/node20/docker-compose.yaml', bundle);
}
// now app builds
if (!ctx.artifacts.exists(appRepo.file('docker-app-xxx/docker-compose.yaml'))) {
const bundle = ctx.workspace.build('docker-app-xxx/docker-compose.yaml', dockerBuilder);
ctx.artifacts.publish('docker-app-xxx/docker-compose.yaml', bundle);
}
if (!ctx.artifacts.exists(appRepo.file('docker-app-ui/docker-compose.yaml'))) {
const bundle = ctx.workspace.build('docker-app-ui/docker-compose.yaml', dockerBuilder);
ctx.artifacts.publish('docker-app-ui/docker-compose.yaml', bundle);
}
}Two things carry this function. First, every build is gated on artifacts.exists(...) - create-if-not-exists is the default pattern, so the pipeline can rerun from the top at any time and only do missing work. Second, the order: base images are built before app images, from controlled sources, so an application build never reaches the public internet for a base it cannot reproduce. Scanning publishes through the same exists-gate - SBOMs and static analysis are artifacts too, not side effects.
Infra and infra-ops: provision, then operate
function infraPipeline(ctx: PipelineContext) {
ctx.workspace.agent('infra');
var opsRepo = cloneRepo('https://ops-repo', 'main');
// aws config
const awsCredentialsTemplate = opsRepo.file('templates/aws/credentials');
const awsConfigTemplate = opsRepo.file('templates/aws/.config');
ctx.workspace.configure(awsConfigurer(awsCredentialsTemplate, awsConfigTemplate));
// helm repo loader
ctx.workspace.configure(helmRepoLoader());
// create remote state for TF
ctx.cloud.cli(createRemoteState());
// TF plan and apply
ctx.cloud.cli(terraformPlanner('global'));
ctx.cloud.cli(terraformApply('global'));
}
function infraOpsPipeline(ctx: PipelineContext) {
ctx.workspace.agent('infra');
var opsRepo = cloneRepo('https://ops-repo', 'main');
const awsCredentialsTemplate = opsRepo.file('templates/aws/credentials');
const awsConfigTemplate = opsRepo.file('templates/aws/.config');
ctx.workspace.configure(awsConfigurer(awsCredentialsTemplate, awsConfigTemplate));
const rdsHost = ctx.cloud.getResource('infra-dev-1-rds-pg-instance');
const cognitoUserPoolId = ctx.cloud.getResource('infra-dev-1-user-pool');
ctx.workspace.agent('sql');
var opsRepo = cloneRepo('https://ops-repo', 'main');
// sets up pg env vars for connectivity
ctx.workspace.configure(pgSqlHostConfigurer(rdsHost));
// run through our scripts
var sqlCommands = opsRepo.file('sql/new-db-setup-template.sql');
sqlCommands = sqlCommands.replace('$DB_NAME', 'db-sample-1');
sqlCommands = sqlCommands.replace('$DB_USER', 'db-sample-user');
// run by connecting to infra host configured
sqlClientRunner(sqlCommands);
ctx.cloud.cli(cognitoInitialUserCreate(cognitoUserPoolId));
}Terraform owns provisioning with remote state; that part is short because it should be. The second function is the one most setups are missing as a named thing: post-provision operations. Creating the database and its roles, seeding the first identity-pool user - work that is not infrastructure and not application, and that goes untracked when it has no pipeline of its own. Note also what is absent: no hardcoded endpoints anywhere. cloud.getResource('...') resolves everything at runtime by name.
CD: desired state, promoted
function cdPipeline(ctx: PipelineContext) {
/**
* Step 0 - get deploy versions
* remember CI is based on SemVer in code
* while deploy is based on SemVer that is configured or "desired"
*/
var opsRepo = cloneRepo('https://ops-repo', 'main');
var deployConfig = opsRepo.file('environments/dev/deploy.yaml');
const appXXXVersion = deployConfig['app-xxx']; // "1.0.0"
/**
* Step 1 - get resources for env
*/
ctx.workspace.agent('infra');
const rdsHost = ctx.cloud.getResource('infra-dev-1-rds-pg-instance');
const ecrRepo = ctx.cloud.getResource('infra-dev-1-apps-1-ecr');
const clusterName = 'infra-dev-1-eks';
/**
* Step 2 - apply sql as deploy job based on version
*/
ctx.workspace.agent('sql');
ctx.workspace.configure(pgSqlHostConfigurer(rdsHost));
// get our sql bundle and run the updates.sql from within bundle
const sqlBundle = ctx.artifacts.get('group:name:' + appXXXVersion);
var updatesFile = sqlBundle.extract().get('updates.sql');
sqlClientRunner(updatesFile);
/**
* Step 3 - promote containers
* we need to ensure we are also connected to
* cloud docker repo
*/
ctx.workspace.agent('infra');
dockerRepoConnector();
dockerRepoCloudConnector(ecrRepo);
// get from artifact store and publish to cloud
const dockerImage = ctx.artifacts.get('acme/app-xxx/' + appXXXVersion);
ctx.cloud.promoteImage(dockerImage, ecrRepo);
/**
* Step 4 - deploy artifacts
*/
clusterConnector(clusterName);
var appsTemplate = opsRepo.file('apps/k8/app-xxx.yaml');
var appsDeploy = appsTemplate.replace('$XXX', 'xxxx');
ctx.cloud.cli(kubeCtlApply(appsDeploy));
}Step 0 is the model's center of gravity: CI versions come from the code (SemVer in the build files), CD versions come from a committed deploy config - the desired state of the environment. CI is dynamic, CD is configured. From there it is a promotion, never a rebuild: schema updates applied from the versioned SQL bundle, the already-built image promoted from the artifact store to the cloud registry, templated manifests applied to the cluster. Deploying is moving a number in a YAML file; everything else follows from it.
The interfaces are the portability claim
interface Workspace {
agent: (runtime: string) => void;
configure: (configurer: any) => void;
build: (artifact: string, builder: any) => any;
}
interface ArtifactRegistry {
/**
* Uses the core artifact file and extracts out group, artifact and version.
* @param artifact artifact file such as pom, toml, package.json, docker-compose
* @returns does this group/artifact exist in registry
*/
exists: (artifact: string) => Boolean;
publish: (artifact: string, bundle: any) => void;
get: (artifact: string) => any;
}
interface ArtifactAnalyser {
exists: (artifact: string, analyser: any) => Boolean;
publish: (artifact: string, analyser: any) => void;
}
interface CloudProvider {
getResource: (name: string) => string;
cli: (runner: any) => void;
promoteImage: (image: string, repo: string) => void;
}Four small interfaces carry the entire model: somewhere to run builds, somewhere to keep artifacts, somewhere to record analysis, and a cloud you can ask questions. Any CI provider you already pay for implements these. So does a folder of shell scripts and the AWS CLI - which is exactly how the no-server variant works. Adopting the model is not a migration; it is mapping these four interfaces onto whatever you run today.
Don't take our word for it - ask ChatGPT what it thinks of this piece.


