Skip to content

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):

  1. install — setup Node + pnpm; install dependencies
  2. validate — typecheck (tsc --noEmit)
  3. test:unit — Vitest unit tests with 80% line coverage gate
  4. test:integration — integration tests (gated by RUN_INTEGRATION=1 CI/CD variable scoped to the dev environment)
  5. deploy — OIDC credentials via $CICD_ROLE_ARN; SST deploy
  6. statusupdate-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:

  1. install / setup
  2. On merge request: sst diff drift detection
  3. On push to main: OIDC credentials; sst deploy
  4. On push to main: SSM output verification (every /bank/ path in the module's docs/design/MOD-NNN.md outputs table must resolve)
  5. 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):

  1. Allowed to merge: platform-leads only
  2. Allowed to push: No one (force-push disabled; all changes via MR)
  3. Required approvals on MR: 1
  4. 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-cicd IAM 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-policy sub constraint 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).