MOD-156 — CI/CD pipeline platform¶
Purpose¶
MOD-156 delivers the CI/CD control plane that governs how every module in every bank code repository is built, tested, and deployed. It provides two reusable GitLab CI template files (one for Lambda modules, one for pure IaC), plus the GitLab-side configuration — Environments, protected branches, OIDC federation — that enforces the bank's change and deployment standards uniformly across all code repositories.
FRs delivered here: FR-733, FR-734, FR-737, FR-738, FR-739.
FRs delivered by bank-wiki (out of scope for this module): FR-735
(generate-workflows.py generator) and FR-736 (repository_dispatch
extension to update-wiki.py).
Platform note: CI/CD runs from GitLab only. GitHub is retired across
the totara-bank workspace. Do not reference GitHub Actions workflows,
gh commands, or .github/workflows/ paths anywhere in the build
system. See bank-wiki/source/pages/architecture/ci-integration.md for
the platform-wide Git remote policy.
Architecture¶
bank-wiki (source of truth)
└── scripts/generate-workflows.py ← FR-735 (wiki-side)
│
▼
per-repo .gitlab-ci.yml (generated or hand-authored per SD)
│
├── include: bank-platform/.gitlab/ci/templates/lambda.gitlab-ci.yml
└── include: bank-platform/.gitlab/ci/templates/iac.gitlab-ci.yml
│
▼
┌────────────────────────────────────────┐
│ this module │
│ │
│ .gitlab/ci/templates/ │
│ lambda.gitlab-ci.yml (FR-733) │
│ iac.gitlab-ci.yml (FR-734) │
│ │
│ src/ │
│ config/repos.ts (8 repos) │
│ config/environments.ts (FR-737) │
│ config/branch-protection.ts (FR-738)│
│ provision-*.ts (GitLab API) │
│ provision.ts (entrypoint) │
└────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ GitLab (totara-bank group) │
│ │
│ $CICD_ROLE_ARN (FR-739) │
│ Environments: │
│ dev (0 approvers) │
│ uat (1 × platform-leads) │
│ prod (2 × platform-leads) │
│ main branch protection (FR-738) │
└────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ AWS IAM │
│ cicd role (provisioned by MOD-104) │
│ trust policy sub claim: │
│ project_path:totara-bank/{repo}: │
│ ref_type:branch:ref:main │
│ — trust policy covers all repos │
│ across dev / uat / prod │
└────────────────────────────────────────┘
Why scripts, not Pulumi¶
ADR-025 mandates SST/Pulumi for AWS IaC. MOD-156 provisions GitLab
resources, not AWS resources. Following the precedent set by MOD-103
(Neon REST-API-based provisioning), MOD-156 uses a TypeScript script
(src/provision.ts) that calls the GitLab API. The script is
idempotent — re-running it reconciles to the source config in
src/config/*.ts. No Pulumi state is kept for GitLab resources.
The one AWS call MOD-156 makes is reading the cicd role ARN from SSM
(/bank/{env}/iam/cicd/arn, owned by MOD-104) so it can project that
ARN into every repo as a GitLab CI/CD variable ($CICD_ROLE_ARN).
The variable holds a role ARN — it is not a secret. The role's trust
policy is what enforces who may assume it.
Reusable CI template files¶
Template files live at bank-platform/.gitlab/ci/templates/ and are
included by per-repo pipeline files using GitLab's include keyword:
# In per-repo .gitlab-ci.yml
include:
- project: 'totara-bank/bank-platform'
ref: main
file: '.gitlab/ci/templates/lambda.gitlab-ci.yml'
lambda.gitlab-ci.yml (FR-733)¶
Parameters: MODULE_ID, MODULE_DIR, NODE_VERSION, STAGE.
Stage sequence (no skip flags, no bypass conditions):
install— setup Node + pnpm; install dependenciesvalidate— typecheck (tsc --noEmit)test:unit— Vitest unit tests with 80% line coverage gatetest:integration— integration tests (gated byRUN_INTEGRATION=1CI/CD variable scoped to thedevenvironment)deploy— OIDC credentials via$CICD_ROLE_ARN; SST deploystatus—update-wiki.py --status Built
The environment: $STAGE binding at the job level is what wires FR-737
approval gates into the pipeline — every deploy to uat or prod must
first pass the Environment's reviewer gate.
iac.gitlab-ci.yml (FR-734)¶
Parameters: MODULE_ID, MODULE_DIR, STAGE.
Stage sequence:
install/ setup- On merge request:
sst diffdrift detection - On push to main: OIDC credentials;
sst deploy - On push to main: SSM output verification (every
/bank/path in the module'sdocs/design/MOD-NNN.mdoutputs table must resolve) - On push to main: wiki status push
OIDC bootstrapping (FR-739)¶
GitLab CI cannot resolve SSM before it has AWS credentials, and it
needs the role ARN to get credentials. MOD-156 breaks the chicken
and egg by reading the ARN from SSM once, at provision time, using
the orchestrator's AWS credentials, and writing it into each repo
as a GitLab CI/CD variable ($CICD_ROLE_ARN). The template files
reference $CICD_ROLE_ARN directly in the OIDC credential step:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials
id_tokens:
AWS_TOKEN:
aud: sts.amazonaws.com
with:
role-to-assume: $CICD_ROLE_ARN
role-session-name: gitlab-ci-${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}
aws-region: ap-southeast-2
The AWS role trust policy accepts GitLab OIDC JWT sub claims of the
form project_path:totara-bank/{repo}:ref_type:branch:ref:main. This
policy is owned by MOD-104; MOD-156 requires MOD-104 to cover all eight
repos × three environments.
Environments (FR-737)¶
| Environment | Approvers | Group |
|---|---|---|
dev |
0 | — |
uat |
1 | platform-leads |
prod |
2 | platform-leads |
Reviewers are enforced at the GitLab Environment level (Settings →
CI/CD → Environments → edit → required approvals). A hand-written
bypass pipeline targeting environment: prod still hits the
two-approver gate because environment-level rules apply regardless of
pipeline YAML.
Branch protection (FR-738)¶
Applied to main on every code repo (Settings → Repository →
Protected branches):
- Allowed to merge:
platform-leadsonly - Allowed to push: No one (force-push disabled; all changes via MR)
- Required approvals on MR: 1
- Code owner approval: enabled
SSM outputs¶
MOD-156 does not publish SSM outputs of its own. It consumes MOD-104's output and projects it into GitLab state. The published surface is GitLab-side, not SSM-side.
| Output | Location | Consumed by |
|---|---|---|
$CICD_ROLE_ARN |
Every code repo's GitLab CI/CD variables | Every template's OIDC step |
lambda.gitlab-ci.yml |
bank-platform/.gitlab/ci/templates/ |
Every Lambda module's pipeline |
iac.gitlab-ci.yml |
bank-platform/.gitlab/ci/templates/ |
Every IaC module's pipeline |
MOD-156 consumes /bank/{env}/iam/cicd/arn from MOD-104. No other
/bank/ SSM paths are read.
Policy satisfaction¶
DT-007 (GATE): The CI pipeline is the single mandatory path to any
deployed environment. Proved by: (a) the reusable templates have no
skip flags on the deploy step, (b) the AWS-side cicd role trust policy
accepts only the registered project_path × environment combinations —
any job missing the environment binding cannot assume the role and
therefore cannot deploy, (c) per-environment scoping means feature
branches and forks cannot deploy regardless of what pipeline YAML they
carry.
DT-010 (AUTO): All environment and branch-protection state
originates in src/config/*.ts under version control. The drift test
(dt-010-auto.test.ts) reads live GitLab state and asserts it equals
the config. Any manual configuration creates drift and fails the test.
OPS-006 (LOG): Every pipeline execution is logged as an immutable
GitLab CI/CD pipeline record (runs cannot be edited; reruns create new
records). Every status transition is additionally logged as an
append-only commit in bank-wiki by update-wiki.py. bank-wiki's
main branch is protected against force push. No S3 Object Lock archive
is needed.
SD09 exception¶
SD09 (bank-brand) uses a self-contained marketing.gitlab-ci.yml
that deploys to Cloudflare Pages. It does not consume the Lambda or IaC
reusable templates and has no AWS OIDC dependency. The MOD-156 dependency
declared in MOD-169's YAML is organisational (shared CI/CD governance),
not a concrete artefact dependency.
Dependencies¶
- MOD-104: provides the OIDC provider and the
bank-platform-cicdIAM role whose ARN MOD-156 reads from/bank/{env}/iam/cicd/arn. MOD-104 must be redeployed after applying this module's handoff to widen the trust-policysubconstraint to all eight repos × three environments. - MOD-076: provides the ADOT Lambda layer ARN at
/bank/{env}/observability/adot-layer-arn, read by generated per-module pipelines (not by MOD-156 directly).