Skip to content

feat: add deployment pipeline, Helm charts, and Terraform infrastructure#414

Open
linusnorton wants to merge 68 commits intomasterfrom
feat/deployment-pipeline
Open

feat: add deployment pipeline, Helm charts, and Terraform infrastructure#414
linusnorton wants to merge 68 commits intomasterfrom
feat/deployment-pipeline

Conversation

@linusnorton
Copy link
Copy Markdown
Collaborator

@linusnorton linusnorton commented Feb 24, 2026

Summary

Adds full CI/CD deployment pipeline for CATH, adapted from the expressjs-monorepo-template. Targets SDS clusters (dev/stg/prod) with a dedicated cath namespace, using the shared pip-ss-kv Key Vault.

Docker

  • Fix broken Dockerfiles for web, api, and crons (multi-stage builds with yarn workspaces focus --production)
  • Add postgres Dockerfile with migration runner (prisma migrate deploy) and Prisma Studio

Helm Charts

  • Add postgres Helm chart with pip-ss-kv secrets
  • Add umbrella chart (helm/cath-service/) aggregating all subcharts + Redis + PostgreSQL
  • Add values templates for preview (ephemeral DB/Redis), staging, and production
  • Fix crons Helm values (correct image, identity, Key Vault references)
  • Add team: cath annotations to all Chart.yaml files

GitHub Actions Pipeline (3-tier)

  • Orchestrators: workflow.main.yml (push to master), workflow.preview.yml (PR)
  • Stages: build, deploy, smoke-test, e2e, promote-images, cleanup, infrastructure
  • Jobs: Docker build matrix, Helm deploy/publish/cleanup, Terraform plan/apply, change detection, image promotion, PR comment with deployment URLs
  • Remove old CI-only workflows (test.yml, e2e.yml, osv-scanner.yml) — functionality absorbed into new pipeline

Terraform Infrastructure

  • PostgreSQL Flexible Server module (business_area = "sds")
  • Redis module with private endpoint
  • Application Insights
  • Key Vault integration (writes secrets to pip-ss-kv-{env})

Key Configuration

Setting Value
Preview cluster sds-dev-00
Staging cluster sds-stg-00
Key Vault pip-ss-kv (shared with PIP)
Azure creds AZURE_CREDENTIALS_SDS_DEV / AZURE_CREDENTIALS_SDS_STG
Registry hmctspublic.azurecr.io/cath/cath-*

Companion PR

A separate PR will be created in sds-flux-config to set up the cath namespace with HelmRelease, ImageRepository/Policy, identity, and service account configs.

Test plan

  • Verify Docker builds: docker build -f apps/web/Dockerfile . (and api, crons, postgres)
  • Verify Helm chart resolves: helm dependency update helm/cath-service/
  • Verify Terraform validates: cd infrastructure && terraform init && terraform validate
  • Verify existing tests still pass: yarn test
  • Push to branch and check GitHub Actions syntax is valid
  • Ensure required GitHub secrets are configured (AZURE_CREDENTIALS_SDS_DEV, etc.)

Summary by CodeRabbit

  • New Features

    • Managed PostgreSQL and Redis releases added with preview/staging Helm charts and deployment templates.
    • PR preview and staged pipelines with reusable stages (build, deploy, smoke, e2e, promote) plus image build/promotion and Helm publish.
  • Chores

    • Modular CI/CD introduced with many reusable workflows for lint, test, build, deploy, cleanup and promotion.
    • Service images and runtime builds adjusted for production; health probe and startup scripts added for the Postgres service.

Adds full CI/CD pipeline adapted from the expressjs-monorepo-template,
targeting SDS clusters (dev/stg/prod) with the cath namespace.

- Fix Dockerfiles for web, api, and crons apps (multi-stage builds)
- Add postgres Dockerfile with migration runner and Prisma Studio
- Add postgres Helm chart with pip-ss-kv Key Vault secrets
- Add umbrella Helm chart (cath-service) aggregating all subcharts
- Add Helm values templates for preview, staging, and production
- Add 3-tier GitHub Actions pipeline (orchestrator/stage/job workflows)
- Add Terraform infrastructure (PostgreSQL, Redis, App Insights, Key Vault)
- Fix crons Helm values (correct image, identity, Key Vault references)
- Add team annotations to all Chart.yaml files
- Remove old CI-only workflows (test.yml, e2e.yml, osv-scanner.yml)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removed legacy E2E/OSV workflows and replaced them with a modular, reusable GitHub Actions CI/CD pipeline: change detection, staged build/deploy/promote workflows, Helm deployments, Terraform infra, helper scripts, umbrella Helm chart, Dockerfile/runtime adjustments and a new Postgres service with health proxy. (34 words)

Changes

Cohort / File(s) Summary
Removed legacy workflows
​.github/workflows/e2e.yml, ​.github/workflows/osv-scanner.yml
Deleted legacy E2E and OSV scanner workflows.
Top-level pipelines
​.github/workflows/workflow.main.yml, ​.github/workflows/workflow.preview.yml
Added orchestrating pipelines that chain reusable stage workflows with conditional execution and change detection.
Stage workflows
​.github/workflows/stage.build.yml, ​.github/workflows/stage.deploy.yml, ​.github/workflows/stage.e2e.yml, ​.github/workflows/stage.infrastructure.yml, ​.github/workflows/stage.promote-images.yml, ​.github/workflows/stage.smoke-test.yml, ​.github/workflows/stage.cleanup.yml
New stage-level workflows composing reusable job workflows for build, deploy, test, infra, promote, smoke-test and cleanup.
Reusable job workflows
​.github/workflows/job.detect-changes.yml, ​.github/workflows/job.build-and-publish-images.yml, ​.github/workflows/job.e2e-test.yml, ​.github/workflows/job.helm-deploy.yml, ​.github/workflows/job.helm-publish.yml, ​.github/workflows/job.helm-cleanup.yml, ​.github/workflows/job.lint.yml, ​.github/workflows/job.osv-scanner.yml, ​.github/workflows/job.promote-images.yml, ​.github/workflows/job.save-successful-sha.yml, ​.github/workflows/job.smoke-test.yml, ​.github/workflows/job.terraform-fmt.yml, ​.github/workflows/job.terraform.yml, ​.github/workflows/job.pr-comment.yml, ​.github/workflows/job.test.yml
Introduced reusable workflows for change detection, image build/publish, Helm deploy/publish/cleanup, lint/test/OSV/terraform, PR comments and SHA caching; job.test.yml converted to workflow_call and had lint/Sonar steps removed.
Build helper scripts
​.github/workflows/jobs/build-and-publish-images/detect-affected-apps.sh, ​.github/workflows/jobs/build-and-publish-images/generate-build-metadata.sh, ​.github/workflows/jobs/build-and-publish-images/set-image-variables.sh
Added scripts to detect affected apps (Turborepo), generate build metadata from Chart.yaml, and compute/export per-app image tags and release name.
PR helper script
​.github/workflows/jobs/pr-comment/get-deployment-urls.sh
New script to query Kubernetes ingresses and emit JSON mapping of app -> https://host for PR comments and downstream stages.
App Dockerfiles & runtime
apps/api/Dockerfile, apps/crons/Dockerfile, apps/web/Dockerfile, apps/postgres/Dockerfile, apps/postgres/health-server.mjs, apps/postgres/start.sh, apps/postgres/package.json
Dockerfiles updated for Corepack, explicit production builds and workspace pruning; new Postgres app, health proxy, start script and http-proxy dependency. Check runtime ports, NODE_ENV and CMD changes.
App Helm charts / values
apps/*/helm/Chart.yaml (api, crons, web), apps/crons/helm/values.yaml, apps/postgres/helm/Chart.yaml, apps/postgres/helm/values.yaml, apps/api/helm/values.yaml, apps/web/helm/values.yaml
Added chart annotations (team/home), new Postgres chart and values, and updated values for crons/web/api to reference pip-ss-kv secret mappings and releaseName overrides.
Umbrella Helm chart & values
helm/cath-service/Chart.yaml, helm/cath-service/values.template.yaml, helm/cath-service/values.preview.yaml, helm/cath-service/values.preview.template.yaml
New umbrella chart declaring subcharts (postgres, web, api, crons, redis, postgresql) and environment-specific values/templates for preview/stg.
Terraform infra
infrastructure/.terraform-version, infrastructure/state.tf, infrastructure/variables.tf, infrastructure/main.tf, infrastructure/keyvault.tf, infrastructure/appinsights.tf, infrastructure/postgres.tf, infrastructure/redis.tf, infrastructure/output.tf
Added Terraform Azure configuration: backend, providers (incl. aliased provider), resource groups, Key Vault data source, App Insights module, Postgres and Redis modules, Key Vault secrets and variables. Review provider subscriptions and module inputs.

Sequence Diagram

sequenceDiagram
    actor Dev
    participant GitHub
    participant Detect as Detect Changes
    participant Build as Build Stage
    participant Infra as Infra Stage
    participant Deploy as Helm Deploy
    participant Smoke as Smoke Test
    participant E2E as E2E Test
    participant Promote as Promote Images

    Dev->>GitHub: push / open PR
    GitHub->>Detect: run change detection
    Detect-->>Build: code change? (affected-apps, helm-apps)
    Detect-->>Infra: infra change?

    alt Code changes present
        Build->>Build: lint, test, build & publish images
        Build-->>Deploy: metadata (short-sha, timestamp, affected-apps, helm-apps)
    end

    alt Infra changes present
        Infra->>Infra: terraform fmt / plan/apply
        Infra-->>Deploy: infra ready
    end

    Deploy->>Deploy: helm deploy, register DNS, produce URLs
    Deploy-->>Smoke: deployment URLs
    Smoke->>Smoke: health checks (web, api)
    Smoke-->>E2E: validated URLs
    E2E->>E2E: run E2E tests
    E2E-->>Promote: on success, trigger image promotion & helm publish
    Promote->>GitHub: cache successful SHA
Loading
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes—adding deployment pipeline, Helm charts, and Terraform infrastructure—and aligns with the PR's primary objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deployment-pipeline

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-advanced-security
Copy link
Copy Markdown

This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
.github/workflows/job.test.yml (3)

56-56: ⚠️ Potential issue | 🔴 Critical

actions/upload-artifact@v6 does not exist — workflow will fail.

The latest major release is upload-artifact@v4. Version 6 does not exist; using it will cause the step to fail immediately. Downgrade to @v4.

🐛 Proposed fix
-        uses: actions/upload-artifact@v6
+        uses: actions/upload-artifact@v4

65-65: ⚠️ Potential issue | 🔴 Critical

actions/download-artifact@v7 does not exist — workflow will fail.

The latest major release is download-artifact@v4, and upload and download versions must match. Update to @v4 to pair with the upload step.

🐛 Proposed fix
-        uses: actions/download-artifact@v7
+        uses: actions/download-artifact@v4

78-80: ⚠️ Potential issue | 🟠 Major

SonarSource/sonarqube-scan-action@master is unpinned.

Referencing a mutable branch tag is a supply-chain risk — a compromised push to master runs in your pipeline. Pin to a specific release tag or a commit SHA.

🟡 Minor comments (5)
.github/workflows/job.helm-cleanup.yml-41-55 (1)

41-55: ⚠️ Potential issue | 🟡 Minor

Misleading summary: "Status: Deleted" is always written even when the release was absent.

The || echo "Release may not exist..." on Line 47 silences the error, but Line 55 unconditionally appends **Status:** Deleted to the step summary. A missing release would report a false success.

Capture the exit code to report the real outcome:

🐛 Proposed fix
-      - name: Delete Helm release
-        run: |
-          echo "Deleting Helm release: ${{ inputs.release-name }} in namespace: ${{ inputs.namespace }}"
-          helm uninstall "${{ inputs.release-name }}" \
-            --namespace "${{ inputs.namespace }}" \
-            --wait \
-            --timeout 5m0s || echo "Release may not exist or already deleted"
-
-      - name: Output cleanup summary
-        run: |
-          echo "### Helm Cleanup Summary :broom:" >> $GITHUB_STEP_SUMMARY
-          echo "" >> $GITHUB_STEP_SUMMARY
-          echo "**Release:** \`${{ inputs.release-name }}\`" >> $GITHUB_STEP_SUMMARY
-          echo "**Namespace:** \`${{ inputs.namespace }}\`" >> $GITHUB_STEP_SUMMARY
-          echo "**Status:** Deleted" >> $GITHUB_STEP_SUMMARY
+      - name: Delete Helm release
+        id: helm_uninstall
+        run: |
+          echo "Deleting Helm release: ${{ inputs.release-name }} in namespace: ${{ inputs.namespace }}"
+          if helm uninstall "${{ inputs.release-name }}" \
+              --namespace "${{ inputs.namespace }}" \
+              --wait \
+              --timeout 5m0s; then
+            echo "status=Deleted" >> "$GITHUB_OUTPUT"
+          else
+            echo "status=Not found (skipped)" >> "$GITHUB_OUTPUT"
+          fi
+
+      - name: Output cleanup summary
+        run: |
+          {
+            echo "### Helm Cleanup Summary :broom:"
+            echo ""
+            echo "**Release:** \`${{ inputs.release-name }}\`"
+            echo "**Namespace:** \`${{ inputs.namespace }}\`"
+            echo "**Status:** ${{ steps.helm_uninstall.outputs.status }}"
+          } >> "$GITHUB_STEP_SUMMARY"

This also addresses the SC2086/SC2129 shellcheck warnings from static analysis.

.github/workflows/jobs/build-and-publish-images/set-image-variables.sh-31-34 (1)

31-34: ⚠️ Potential issue | 🟡 Minor

short_sha is not validated but is used in image tags for affected apps.

If short_sha is empty, affected apps will receive tags like pr-123- or staging- (trailing hyphen), which are likely invalid or at least unintended. Add short_sha to the validation guard when there are affected apps, or at minimum to the usage check.

Proposed fix
-  if [ -z "$helm_apps" ] || [ -z "$change_id" ] || [ -z "$application_name" ]; then
+  if [ -z "$helm_apps" ] || [ -z "$change_id" ] || [ -z "$short_sha" ] || [ -z "$application_name" ]; then
     echo "Usage: $0 <affected_apps_json> <helm_apps_json> <change_id> <short_sha> <timestamp> <application_name>"
     exit 1
   fi
.github/workflows/job.helm-deploy.yml-125-164 (1)

125-164: ⚠️ Potential issue | 🟡 Minor

DNS registration: quote shell variables and handle empty hostname list.

Line 136 leaves ${{ env.TEAM_NAME }} unquoted inside the kubectl call, and line 142 has an unquoted expansion inside ${..} (SC2295). Also, if $HOSTNAMES is empty (no ingresses yet), the loop silently does nothing — consider adding an explicit check or warning.

Suggested quoting fixes
-          HOSTNAMES=$(kubectl get ingress -n ${{ env.TEAM_NAME }} \
+          HOSTNAMES=$(kubectl get ingress -n "${{ env.TEAM_NAME }}" \
             -l "app.kubernetes.io/instance=${{ env.RELEASE_NAME }}" \
             -o jsonpath='{.items[*].spec.rules[*].host}')
 
+          if [ -z "$HOSTNAMES" ]; then
+            echo "::warning::No ingress hostnames found for release ${{ env.RELEASE_NAME }}"
+            exit 0
+          fi
+
           for HOST in $HOSTNAMES; do
-            RECORD_NAME="${HOST%.${ZONE}}"
+            RECORD_NAME="${HOST%."${ZONE}"}"
infrastructure/variables.tf-6-16 (1)

6-16: ⚠️ Potential issue | 🟡 Minor

Redis and Application Insights modules produce redundant resource names with default variables.

With product = "pip-ss-kv" and component = "cath", the dedicated resource group becomes pip-ss-kv-{env}-cath. However, both redis.tf line 5 and appinsights.tf line 8 hardcode a cath- prefix and then append ${var.component}, resulting in:

  • Redis: cath-cath-{env}
  • Application Insights: cath-cath-appinsights

This double prefix is likely unintended. The postgres.tf module avoids this by using only "cath-${var.env}", suggesting the hardcoded prefix may be redundant or the variable usage should be reconsidered.

.github/workflows/workflow.preview.yml-46-50 (1)

46-50: ⚠️ Potential issue | 🟡 Minor

Avoid deploying when the infra stage was cancelled or timed out.

needs.infrastructure-stage.result != 'failure' permits cancelled, so deploy could proceed after an incomplete infra run. Consider matching the main workflow’s guard (success or skipped).

💡 Suggested fix
-    if: always() && needs.build-stage.result == 'success' && needs.infrastructure-stage.result != 'failure'
+    if: |
+      always() &&
+      needs.build-stage.result == 'success' &&
+      (needs.infrastructure-stage.result == 'success' || needs.infrastructure-stage.result == 'skipped')
🧹 Nitpick comments (17)
apps/postgres/package.json (1)

32-32: http-proxy is unmaintained — consider an alternative.

http-proxy@1.18.1 is the latest version on npm, last published 6 years ago. For a simple internal sidecar this is low-risk, but given it's unmaintained you may want to replace it with Node's built-in http module to avoid carrying an unloved dependency long-term.

apps/postgres/health-server.mjs (1)

13-15: Readiness probe does not reflect Prisma Studio's actual availability.

/health/readiness returns 200 unconditionally. During the window between health-server.mjs starting and Prisma Studio becoming ready, Kubernetes may consider the pod ready and stop directing traffic to the 502 path. If accurate readiness signalling matters, consider polling localhost:5556 before returning 200.

apps/postgres/Dockerfile (2)

21-21: prisma CLI used at runtime but listed as a devDependency.

start.sh invokes npx prisma migrate deploy and npx prisma studio, so the prisma CLI must be available in the runtime image. It's currently declared as a devDependency, meaning yarn workspaces focus --production would break the container. The missing --production flag is therefore working around this, at the cost of shipping all dev dependencies in the image.

Cleaner fix: move prisma to dependencies and add --production to the focus command.

♻️ Proposed change
-    "prisma": "6.19.2"
+    "prisma": "6.19.2",

Move prisma from devDependencies to dependencies in package.json, then:

-RUN yarn workspaces focus `@hmcts/postgres`
+RUN yarn workspaces focus --production `@hmcts/postgres`

3-5: Combine corepack install steps into a single RUN layer.

Two separate RUN commands create unnecessary intermediate layers.

♻️ Proposed fix
-RUN npm install -g corepack
-RUN corepack enable
+RUN npm install -g corepack && corepack enable
.github/workflows/job.terraform-fmt.yml (1)

28-33: Consider pinning hmcts/cnp-githubactions-library/terraform-fmt to a specific tag or SHA.

The actions/checkout@v6 version is valid and follows GitHub's recommended approach of using major version tags that auto-update within the major release series. However, pinning terraform-fmt from @main to a specific tag or SHA would improve supply-chain stability.

apps/web/Dockerfile (1)

19-21: ENV NODE_ENV=production should precede yarn workspaces focus.

Minor ordering nit: the --production flag handles pruning correctly regardless, but setting the env var before the RUN keeps the intent explicit and aligns with the api/crons convention.

♻️ Proposed reorder
+ENV NODE_ENV=production
+ENV NODE_OPTIONS='--conditions=production'
+
 RUN yarn workspaces focus `@hmcts/web` --production
-ENV NODE_ENV=production
-ENV NODE_OPTIONS='--conditions=production'
.github/workflows/job.helm-cleanup.yml (1)

27-33: Staging credentials and cluster are hardcoded, making this job non-reusable.

The release-name and namespace inputs imply a generic cleanup job, but AZURE_CREDENTIALS_SDS_STG, sds-stg-00-rg, and sds-stg-00-aks hard-wire it to the staging environment. If cleanup is ever needed for dev or prod, a new duplicate job would be required. Consider parameterising the environment, or at minimum rename to make the staging scope explicit in the job contract.

.github/workflows/job.smoke-test.yml (2)

93-98: Summary step is a no-op — it only runs when everything already succeeded.

Since prior steps don't use if: always(), this step is unreachable on failure. It's harmless but adds no diagnostic value. Consider removing it, or adding if: always() and printing a conditional pass/fail summary.


31-91: Web and API health-check blocks are near-identical.

Consider extracting the retry logic into a small shared shell function or a separate script to reduce duplication. Not blocking, but would simplify future maintenance (e.g., changing retry count or adding headers).

.github/workflows/jobs/build-and-publish-images/set-image-variables.sh (1)

58-91: Pipeline subshell: while read in a pipe runs in a subshell.

The jq ... | while read construct means the loop body executes in a subshell. This is fine here because side effects are limited to appending to $GITHUB_ENV (file I/O persists) and echo (stdout). However, if you ever need to propagate a failure or accumulate state from the loop, this pattern will silently swallow it. A process substitution (while read ... < <(jq ...)) would avoid the subshell, but this is non-blocking as-is.

.github/workflows/job.detect-changes.yml (1)

104-110: Redundant PR base ref check.

Lines 105–107 check github.event.pull_request.base.ref and lines 108–110 check github.base_ref. For pull_request events, these resolve to the same value. For non-PR events, both are empty. Unless there's a specific edge case you're targeting (e.g., pull_request_target), the second branch is dead code.

.github/workflows/job.build-and-publish-images.yml (4)

46-56: Quote $GITHUB_OUTPUT to satisfy shellcheck SC2086.

Flagged by static analysis. While the GHA-provided path won't contain spaces, quoting is good practice and silences the linter.

Proposed fix
-            echo "sha=$SHA" >> $GITHUB_OUTPUT
+            echo "sha=$SHA" >> "$GITHUB_OUTPUT"
             ...
-            echo "sha=" >> $GITHUB_OUTPUT
+            echo "sha=" >> "$GITHUB_OUTPUT"

125-136: Same SC2086 quoting issue for $GITHUB_OUTPUT.

Proposed fix
-            echo "tag1=pr-${{ env.CHANGE_ID }}-${{ steps.metadata.outputs.short-sha }}" >> $GITHUB_OUTPUT
-            echo "tag2=pr-${{ env.CHANGE_ID }}" >> $GITHUB_OUTPUT
+            echo "tag1=pr-${{ env.CHANGE_ID }}-${{ steps.metadata.outputs.short-sha }}" >> "$GITHUB_OUTPUT"
+            echo "tag2=pr-${{ env.CHANGE_ID }}" >> "$GITHUB_OUTPUT"
             ...
-            echo "tag1=${{ env.CHANGE_ID }}-${{ steps.metadata.outputs.short-sha }}" >> $GITHUB_OUTPUT
-            echo "tag2=${{ env.CHANGE_ID }}" >> $GITHUB_OUTPUT
+            echo "tag1=${{ env.CHANGE_ID }}-${{ steps.metadata.outputs.short-sha }}" >> "$GITHUB_OUTPUT"
+            echo "tag2=${{ env.CHANGE_ID }}" >> "$GITHUB_OUTPUT"

138-149: Reusable action pinned to @main.

hmcts/cnp-githubactions-library/container-build-push-openid@main tracks a mutable branch. If this is an organisation convention, it's fine; otherwise consider pinning to a release tag or commit SHA for supply-chain safety.


115-119: Use fetch-depth: 1 instead of fetch-depth: 0 to reduce CI/CD execution time across matrix jobs.

The generate-build-metadata.sh script only requires the remote URL (via git config) and the current SHA, both of which are available without full history. Fetching only shallow history (depth 1) will speed up the checkout step for each matrix element without impact.

.github/workflows/stage.infrastructure.yml (1)

25-31: Environment and subscription values are hard-coded to staging.

This stage will need parameterisation (environment, subscription, aks-subscription, storage-account) when production Terraform runs are introduced. For now this is functional, but worth noting for the planned multi-environment expansion.

.github/workflows/job.helm-deploy.yml (1)

65-85: Fragile YAML parsing and unquoted variables.

Parsing Chart.yaml with grep / awk is brittle — it will break if indentation or key ordering changes. Consider using yq which is available on ubuntu-latest runners. Additionally, shellcheck flags several unquoted variable expansions (SC2086).

Suggested improvement
-          TEAM_NAME=$(grep -A 1 'annotations:' "$CHART_PATH" | grep 'team:' | awk '{print $2}' | tr -d '"')
-          APPLICATION_NAME=$(grep '^name:' "$CHART_PATH" | awk '{print $2}' | tr -d '"')
+          TEAM_NAME=$(yq '.annotations.team' "$CHART_PATH")
+          APPLICATION_NAME=$(yq '.name' "$CHART_PATH")

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6462765 and c92eab9.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (56)
  • .github/workflows/e2e.yml
  • .github/workflows/job.build-and-publish-images.yml
  • .github/workflows/job.detect-changes.yml
  • .github/workflows/job.e2e-test.yml
  • .github/workflows/job.helm-cleanup.yml
  • .github/workflows/job.helm-deploy.yml
  • .github/workflows/job.helm-publish.yml
  • .github/workflows/job.lint.yml
  • .github/workflows/job.osv-scanner.yml
  • .github/workflows/job.pr-comment.yml
  • .github/workflows/job.promote-images.yml
  • .github/workflows/job.save-successful-sha.yml
  • .github/workflows/job.smoke-test.yml
  • .github/workflows/job.terraform-fmt.yml
  • .github/workflows/job.terraform.yml
  • .github/workflows/job.test.yml
  • .github/workflows/jobs/build-and-publish-images/detect-affected-apps.sh
  • .github/workflows/jobs/build-and-publish-images/generate-build-metadata.sh
  • .github/workflows/jobs/build-and-publish-images/set-image-variables.sh
  • .github/workflows/jobs/pr-comment/get-deployment-urls.sh
  • .github/workflows/osv-scanner.yml
  • .github/workflows/stage.build.yml
  • .github/workflows/stage.cleanup.yml
  • .github/workflows/stage.deploy.yml
  • .github/workflows/stage.e2e.yml
  • .github/workflows/stage.infrastructure.yml
  • .github/workflows/stage.promote-images.yml
  • .github/workflows/stage.smoke-test.yml
  • .github/workflows/workflow.main.yml
  • .github/workflows/workflow.preview.yml
  • apps/api/Dockerfile
  • apps/api/helm/Chart.yaml
  • apps/crons/Dockerfile
  • apps/crons/helm/Chart.yaml
  • apps/crons/helm/values.yaml
  • apps/postgres/Dockerfile
  • apps/postgres/health-server.mjs
  • apps/postgres/helm/Chart.yaml
  • apps/postgres/helm/values.yaml
  • apps/postgres/package.json
  • apps/postgres/start.sh
  • apps/web/Dockerfile
  • apps/web/helm/Chart.yaml
  • helm/cath-service/Chart.yaml
  • helm/cath-service/values.preview.template.yaml
  • helm/cath-service/values.preview.yaml
  • helm/cath-service/values.template.yaml
  • infrastructure/.terraform-version
  • infrastructure/appinsights.tf
  • infrastructure/keyvault.tf
  • infrastructure/main.tf
  • infrastructure/output.tf
  • infrastructure/postgres.tf
  • infrastructure/redis.tf
  • infrastructure/state.tf
  • infrastructure/variables.tf
💤 Files with no reviewable changes (2)
  • .github/workflows/osv-scanner.yml
  • .github/workflows/e2e.yml

Comment on lines +70 to +75
run: |
echo "### Helm Chart Publish Summary :package:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Chart:** \`cath-service\`" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY
echo "**Registry:** \`oci://${{ env.REGISTRY }}/helm\`" >> $GITHUB_STEP_SUMMARY
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Quote the summary path and consolidate redirects.

actionlint reports SC2086/SC2129 here; this can fail linting and block the workflow.

Proposed change
       - name: Output publish summary
         run: |
-          echo "### Helm Chart Publish Summary :package:" >> $GITHUB_STEP_SUMMARY
-          echo "" >> $GITHUB_STEP_SUMMARY
-          echo "**Chart:** \`cath-service\`" >> $GITHUB_STEP_SUMMARY
-          echo "**Version:** \`${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY
-          echo "**Registry:** \`oci://${{ env.REGISTRY }}/helm\`" >> $GITHUB_STEP_SUMMARY
+          {
+            echo "### Helm Chart Publish Summary :package:"
+            echo ""
+            echo "**Chart:** \`cath-service\`"
+            echo "**Version:** \`${CHART_VERSION}\`"
+            echo "**Registry:** \`oci://${{ env.REGISTRY }}/helm\`"
+          } >> "$GITHUB_STEP_SUMMARY"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: |
echo "### Helm Chart Publish Summary :package:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Chart:** \`cath-service\`" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY
echo "**Registry:** \`oci://${{ env.REGISTRY }}/helm\`" >> $GITHUB_STEP_SUMMARY
run: |
{
echo "### Helm Chart Publish Summary :package:"
echo ""
echo "**Chart:** \`cath-service\`"
echo "**Version:** \`${CHART_VERSION}\`"
echo "**Registry:** \`oci://${{ env.REGISTRY }}/helm\`"
} >> "$GITHUB_STEP_SUMMARY"
🧰 Tools
🪛 actionlint (1.7.11)

[error] 70-70: shellcheck reported issue in this script: SC2086:info:1:52: Double quote to prevent globbing and word splitting

(shellcheck)


[error] 70-70: shellcheck reported issue in this script: SC2086:info:2:12: Double quote to prevent globbing and word splitting

(shellcheck)


[error] 70-70: shellcheck reported issue in this script: SC2086:info:3:39: Double quote to prevent globbing and word splitting

(shellcheck)


[error] 70-70: shellcheck reported issue in this script: SC2086:info:4:45: Double quote to prevent globbing and word splitting

(shellcheck)


[error] 70-70: shellcheck reported issue in this script: SC2086:info:5:60: Double quote to prevent globbing and word splitting

(shellcheck)


[error] 70-70: shellcheck reported issue in this script: SC2129:style:1:1: Consider using { cmd1; cmd2; } >> file instead of individual redirects

(shellcheck)

Comment on lines +43 to +46
- name: Validate Prisma schema
run: yarn workspace @hmcts/postgres-prisma exec prisma validate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

prisma validate will fail on forked PRs where DATABASE_URL is unavailable.

Secrets are not passed to workflows triggered by forked repository PRs. An empty DATABASE_URL will cause this step to fail, blocking lint on all fork contributions.

🐛 Proposed fix
       - name: Validate Prisma schema
         run: yarn workspace `@hmcts/postgres-prisma` exec prisma validate
+        continue-on-error: true
         env:
           DATABASE_URL: ${{ secrets.DATABASE_URL }}

Alternatively, guard the step: if: secrets.DATABASE_URL != '' (requires the secret to be defined, which is the safer approach).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Validate Prisma schema
run: yarn workspace @hmcts/postgres-prisma exec prisma validate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Validate Prisma schema
run: yarn workspace `@hmcts/postgres-prisma` exec prisma validate
continue-on-error: true
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}

Comment on lines +3 to +13
module "application_insights" {
source = "git::https://github.com/hmcts/terraform-module-application-insights?ref=4.x"

env = var.env
product = var.product
name = "cath-${var.component}-appinsights"

resource_group_name = azurerm_resource_group.rg.name

common_tags = var.common_tags
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

hmcts terraform-module-application-insights v4 releases tags GitHub

💡 Result:

  • The repo hmcts/terraform-module-application-insights has no GitHub Releases published (“No releases published”). [1]
  • Its default branch is 4.x (i.e., v4 is tracked as a branch, not as v4.* release tags). [1][2]
  • The Tags/Releases view shows “There aren’t any releases here”, and no v4.* release tags are listed. [3]

Sources: [1] [2] [3]


Pin the module source to a specific commit SHA.

ref=4.x is a branch reference and is mutable—the module source can change unexpectedly with new commits, weakening reproducibility and supply-chain guarantees. Since the repository has no release tags, pin to an exact commit SHA on the 4.x branch instead.

🧰 Tools
🪛 Checkov (3.2.334)

[medium] 3-13: Ensure Terraform module sources use a commit hash

(CKV_TF_1)


[high] 3-13: Ensure Terraform module sources use a tag with a version number

(CKV_TF_2)

azurerm.postgres_network = azurerm.postgres_network
}

source = "git::https://github.com/hmcts/terraform-module-postgresql-flexible?ref=master"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pin module source to a versioned tag or commit hash.

Same issue as redis.tf — using ref=master is a supply-chain and reproducibility risk.

The CATH infrastructure is in the same STG subscription as the apps,
so use AZURE_CREDENTIALS_SDS_STG and DTS-SHAREDSERVICES-STG instead
of the DEV equivalents.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/job.terraform.yml (1)

57-57: Pin to a specific commit SHA instead of @main.

The hmcts/cnp-githubactions-library repository has no published releases, so version tags are unavailable. However, the repository's documentation explicitly recommends pinning to a commit SHA for production use to ensure reproducibility and prevent unexpected failures from changes to the main branch.


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c92eab9 and 6813880.

📒 Files selected for processing (2)
  • .github/workflows/job.terraform.yml
  • .github/workflows/stage.infrastructure.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/stage.infrastructure.yml

- Add postgres-url secret to Terraform (connection string for STG)
- Add DATABASE_URL key vault reference to web, api, crons, postgres
  Helm values for STG/prod (from postgres-url in pip-ss-kv)
- Add DATABASE_URL key vault reference to preview templates
  (from db-url in pip-ss-kv, manually added to dev key vault)
- Fix crons: rename db-url to postgres-url to match Terraform secret
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
infrastructure/postgres.tf (1)

30-58: Consider setting expiration dates on Key Vault secrets.

Static analysis flags CKV_AZURE_41 - secrets without expiration dates. Adding expiration_date enables secret rotation compliance and auditing, particularly important for database credentials.

♻️ Example fix for one secret
 resource "azurerm_key_vault_secret" "postgres_password" {
   name         = "postgres-password"
   value        = module.postgresql.password
   key_vault_id = data.azurerm_key_vault.key_vault.id
+  expiration_date = timeadd(timestamp(), "8760h") # 1 year
+
+  lifecycle {
+    ignore_changes = [expiration_date]
+  }
 }

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6813880 and a960d5a.

📒 Files selected for processing (7)
  • apps/api/helm/values.yaml
  • apps/crons/helm/values.yaml
  • apps/postgres/helm/values.yaml
  • apps/web/helm/values.yaml
  • helm/cath-service/values.preview.template.yaml
  • helm/cath-service/values.preview.yaml
  • infrastructure/postgres.tf
🚧 Files skipped from review as they are similar to previous changes (2)
  • helm/cath-service/values.preview.yaml
  • apps/postgres/helm/values.yaml

Drop individual postgres-host/user/password/port secrets in favour of
the single postgres-url connection string. Prisma only needs
DATABASE_URL, not the individual components.
Resolve conflicts with Prisma 7 upgrade: keep Prisma 7 deps
(@prisma/adapter-pg, @prisma/client 7.2.0, pg) and retain
http-proxy needed for the postgres health server.
The workspace is @hmcts/postgres, not @hmcts/postgres-prisma.
SDS uses different state store names than CNP:
- Resource group: jenkins-state
- Storage account: sdsstate{nonprod|prod}
- Container: tfstate-{environment}
Resource group is jenkins-state-{env} not jenkins-state, and storage
account is sdsstate{env} not sdsstate{storage-account}.
The terraform-deploy action extracts 'cath' from Chart.yaml annotations
and passes it as -var product=cath, but Terraform uses product for key
vault and resource group naming (pip-ss-kv-stg, not cath-stg).
…ult directly

The terraform-deploy action passes product=cath (from Chart.yaml), so the
resource group needs to be created as cath-{env} rather than looked up.
The key vault remains pip-ss-kv-{env} as it's borrowed from PIP's shared
infrastructure.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

Terraform Plan for stg

Show Plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.redis.azurerm_redis_cache.redis will be updated in-place
  ~ resource "azurerm_redis_cache" "redis" {
        id                                 = "/subscriptions/74dacd4f-a248-45bb-a2f0-af700dc4cf68/resourceGroups/cath-cache-stg/providers/Microsoft.Cache/redis/cath-cath-stg"
        name                               = "cath-cath-stg"
        tags                               = {
            "application"  = "cath"
            "builtFrom"    = "hmcts/cath-service"
            "businessArea" = "Cross-Cutting"
            "environment"  = "staging"
            "managedBy"    = "cath"
        }
        # (24 unchanged attributes hidden)

      ~ redis_configuration {
          ~ maxfragmentationmemory_reserved         = 30 -> 642
          ~ maxmemory_delta                         = 30 -> 642
          ~ maxmemory_reserved                      = 30 -> 642
            # (14 unchanged attributes hidden)
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

…e condition

The postinstall hook runs `yarn db:generate` which generates the Prisma
client during `yarn install`. Without a `db:generate` script in
postgres-prisma, the client was only generated during `yarn build`,
causing a race where downstream packages could start tsc before the
generated client existed.
The import block caused Terraform apply to recreate the managed identity
with a new client ID, breaking the Key Vault CSI mount. Removed the
import since Terraform now owns the resource. Reverted preview pipeline
back to plan-only to prevent accidental infra changes from PRs.
@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@linusnorton linusnorton force-pushed the feat/deployment-pipeline branch from bd6f0d7 to 299396a Compare March 30, 2026 16:35
@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants