Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c4f1f96
Environment selection
samgibsonmoj Jun 20, 2026
5b254bf
Helm deployments (with hmpps charts)
samgibsonmoj Jun 20, 2026
c1c5239
Pin helm setup
samgibsonmoj Jun 20, 2026
ed25300
Add helm validation ci step
samgibsonmoj Jun 20, 2026
51b9bbc
Health checks for redis/rabbit + templates
samgibsonmoj Jun 20, 2026
a986944
Bump sdk
samgibsonmoj Jun 20, 2026
a82a3dd
Add flag for enabling/disabling prometheus alerts
samgibsonmoj Jun 20, 2026
9da8e74
Fix for recreate strategy - most override rolling updates
samgibsonmoj Jun 20, 2026
b9614d3
Chart cleanup
samgibsonmoj Jun 20, 2026
bdb96e3
Add gating to manifests
samgibsonmoj Jun 20, 2026
8b90b6d
Add --wait-for-jobs to wait until completion
samgibsonmoj Jun 20, 2026
a7bc4fb
Cleanup prometheus alerts (temporarily disabled)
samgibsonmoj Jun 20, 2026
332ec6d
ModSec: enable WAF (detection only)
samgibsonmoj Jun 21, 2026
5d9ddd3
use pods instead of jobs for migrate/seeding
samgibsonmoj Jun 21, 2026
e0116b0
Remove/ignore Chart.lock
samgibsonmoj Jun 21, 2026
57b8dc6
Use CP's modsec defaults
samgibsonmoj Jun 22, 2026
345acaf
Translate port-forward-deployment to helm
samgibsonmoj Jun 22, 2026
90e50d1
Remove readme
samgibsonmoj Jun 22, 2026
80a69ee
Reset global.json
samgibsonmoj Jun 22, 2026
d4c3c67
Pin .NET runtime/sdk version in Dockerfile
samgibsonmoj Jun 22, 2026
57085f2
Pin dotnet container publishes to immutable digests
samgibsonmoj Jun 22, 2026
f4b2c40
Remove redundant sqlpackage install
samgibsonmoj Jun 22, 2026
b0d90fe
Add dockerfile's to individual projects
samgibsonmoj Jun 22, 2026
328a4c8
Update helper comment
samgibsonmoj Jun 23, 2026
d69fa4a
Explicit port forward deploy for environments, excluding non-dev envs
samgibsonmoj Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
**/bin
**/obj
test
**/.vs
**/.idea
**/.vscode
*.user
150 changes: 89 additions & 61 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ name: deploy

on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [dev, staging, production]
required: true

concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false

jobs:
ecr:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
Expand All @@ -25,9 +32,6 @@ jobs:
- name: Restore dependencies
run: dotnet restore

- name: Install dotnet sql package
run: dotnet tool install --global microsoft.sqlpackage --version 170.3.93

- name: Build
run: dotnet build --configuration Release --no-restore

Expand All @@ -39,40 +43,34 @@ jobs:
with:
role-to-assume: ${{ secrets.ECR_ROLE_TO_ASSUME }}
aws-region: ${{ vars.ECR_REGION }}

- name: Login to ECR
uses: aws-actions/amazon-ecr-login@33f92af657bba1882ab79d8621debd2f6769a0c9
id: login-ecr

- name: Build and Push Server.UI Container
run: |
dotnet publish src/Server.UI/Server.UI.csproj \
--configuration Release \
--no-build \
/t:PublishContainer \
/p:ContainerRegistry=${{ steps.login-ecr.outputs.registry }} \
/p:ContainerRepository=${{ vars.ECR_REPOSITORY }} \
/p:ContainerImageTag=cats-${{ github.sha }}
docker build \
-f src/Server.UI/Dockerfile \
-t ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:cats-${{ github.sha }} \
.
docker push ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:cats-${{ github.sha }}

- name: Build and Push Worker Container
run: |
dotnet publish src/Worker/Worker.csproj \
--configuration Release \
--no-build \
/t:PublishContainer \
/p:ContainerRegistry=${{ steps.login-ecr.outputs.registry }} \
/p:ContainerRepository=${{ vars.ECR_REPOSITORY }} \
/p:ContainerImageTag=worker-${{ github.sha }}

docker build \
-f src/Worker/Dockerfile \
-t ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:worker-${{ github.sha }} \
.
docker push ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:worker-${{ github.sha }}

- name: Build and Push DatabaseSeeding Container
run: |
dotnet publish src/DatabaseSeeding/DatabaseSeeding.csproj \
--configuration Release \
--no-build \
/t:PublishContainer \
/p:ContainerRegistry=${{ steps.login-ecr.outputs.registry }} \
/p:ContainerRepository=${{ vars.ECR_REPOSITORY }} \
/p:ContainerImageTag=seeder-${{ github.sha }}
docker build \
-f src/DatabaseSeeding/Dockerfile \
-t ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:seeder-${{ github.sha }} \
.
docker push ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:seeder-${{ github.sha }}

- name: Build and Push DatabaseMigrator Container
run: |
Expand All @@ -81,25 +79,16 @@ jobs:
-t ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:migrator-${{ github.sha }} \
.
docker push ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:migrator-${{ github.sha }}

- name: Generate app version
id: version
run: echo "app_version=$(date +'%Y.%m.%d').${{ github.run_number }}" >> $GITHUB_OUTPUT

- name: Generate Kubernetes Manifests
run: |
mkdir -p deploy
for file in infra/*.yml; do
envsubst < "$file" > "deploy/$(basename "$file")"
done
env:
IMAGE_TAG: ${{ github.sha }}
APP_VERSION: ${{ steps.version.outputs.app_version }}
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ vars.ECR_REPOSITORY }}
NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
DOTNET_ENVIRONMENT: "Development"

- name: Setup Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
with:
version: v3.21.2

- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CERT }}" > ca.crt
Expand All @@ -111,33 +100,72 @@ jobs:
KUBE_NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
KUBE_CLUSTER: ${{ secrets.KUBE_CLUSTER }}

- name: Run database migration
- name: Build Helm dependencies
run: |
kubectl -n ${KUBE_NAMESPACE} delete pod -l app=migrator --wait=false || true
kubectl -n ${KUBE_NAMESPACE} apply -f deploy/migrator-pod.yml
if ! kubectl -n ${KUBE_NAMESPACE} wait --for=jsonpath='{.status.phase}'=Succeeded --timeout=300s pod/migrator-${{ github.sha }}; then
echo "Migration pod did not succeed within timeout."
kubectl -n ${KUBE_NAMESPACE} describe pod/migrator-${{ github.sha }} || true
exit 1
fi
set -euo pipefail
helm repo add hmpps-helm-charts https://ministryofjustice.github.io/hmpps-helm-charts
helm dependency update ./helm_deploy/cats

- name: Run database migrations
run: |
set -euo pipefail
helm upgrade --install cats-migrate ./helm_deploy/cats \
--namespace "${KUBE_NAMESPACE}" \
--values ./helm_deploy/cats/values-${{ inputs.environment }}.yaml \
--set migrator.enabled=true \
--set serviceAccountName="${KUBE_NAMESPACE}" \
--set migrator.image.repository="${REGISTRY}/${REPOSITORY}" \
--set migrator.image.tag="migrator-${{ github.sha }}" \
--timeout 5m
kubectl -n "${KUBE_NAMESPACE}" wait --for=jsonpath='{.status.phase}'=Succeeded --timeout=300s pod -l app=migrator
env:
KUBE_NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ vars.ECR_REPOSITORY }}

- name: Run database seeding
- name: Seed the database
run: |
kubectl -n ${KUBE_NAMESPACE} delete pod -l app=seeder --wait=false || true
kubectl -n ${KUBE_NAMESPACE} apply -f deploy/seeder-pod.yml
if ! kubectl -n ${KUBE_NAMESPACE} wait --for=jsonpath='{.status.phase}'=Succeeded --timeout=300s pod/seeder-${{ github.sha }}; then
echo "Seeder pod did not succeed within timeout."
kubectl -n ${KUBE_NAMESPACE} describe pod/seeder-${{ github.sha }} || true
exit 1
fi
set -euo pipefail
helm upgrade --install cats-seed ./helm_deploy/cats \
--namespace "${KUBE_NAMESPACE}" \
--values ./helm_deploy/cats/values-${{ inputs.environment }}.yaml \
--set seeder.enabled=true \
--set serviceAccountName="${KUBE_NAMESPACE}" \
--set seeder.image.repository="${REGISTRY}/${REPOSITORY}" \
--set seeder.image.tag="seeder-${{ github.sha }}" \
--timeout 5m
kubectl -n "${KUBE_NAMESPACE}" wait --for=jsonpath='{.status.phase}'=Succeeded --timeout=300s pod -l app=seeder
env:
KUBE_NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ vars.ECR_REPOSITORY }}

- name: Deploy to Kubernetes
- name: Deploy CATS and Worker
run: |
rm -f deploy/migrator-pod.yml deploy/seeder-pod.yml
kubectl -n ${KUBE_NAMESPACE} apply -f deploy/
set -euo pipefail
IMAGE_REPOSITORY="${REGISTRY}/${REPOSITORY}"

helm upgrade --install cats ./helm_deploy/cats \
--namespace "${KUBE_NAMESPACE}" \
--values ./helm_deploy/cats/values-${{ inputs.environment }}.yaml \
--set app.enabled=true \
--set worker.enabled=true \
--set rabbitmq.enabled=true \
--set redis.enabled=true \
--set serviceAccountName="${KUBE_NAMESPACE}" \
--set app.serviceAccountName="${KUBE_NAMESPACE}" \
--set app.image.repository="${IMAGE_REPOSITORY}" \
--set app.image.tag="cats-${{ github.sha }}" \
--set app.env.Sentry__Release="${APP_VERSION}" \
--set app.env.AppConfigurationSettings__Version="${APP_VERSION}" \
--set worker.serviceAccountName="${KUBE_NAMESPACE}" \
--set worker.image.repository="${IMAGE_REPOSITORY}" \
--set worker.image.tag="worker-${{ github.sha }}" \
--set worker.env.Sentry__Release="${APP_VERSION}" \
--set worker.env.AppConfigurationSettings__Version="${APP_VERSION}" \
--atomic --wait --timeout 10m
env:
KUBE_NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ vars.ECR_REPOSITORY }}
APP_VERSION: ${{ steps.version.outputs.app_version }}
80 changes: 80 additions & 0 deletions .github/workflows/validate-helm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Validate Helm

on:
pull_request:
branches:
- main
paths:
- helm_deploy/**
- .github/workflows/validate-helm.yml

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Setup Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
with:
version: v3.21.2

- name: Build chart dependencies
run: |
helm repo add hmpps-helm-charts https://ministryofjustice.github.io/hmpps-helm-charts
helm dependency update ./helm_deploy/cats

- name: Lint and template all environments
run: |
set -euo pipefail
for env in dev staging production; do
echo "::group::helm lint ($env)"
helm lint ./helm_deploy/cats --values ./helm_deploy/cats/values-$env.yaml
echo "::endgroup::"

echo "::group::helm template app ($env)"
# Render with placeholder per-deploy values that CI normally supplies via --set,
# so templating exercises the same paths as a real deploy.
helm template cats ./helm_deploy/cats \
--namespace "cfocats-$env" \
--values ./helm_deploy/cats/values-$env.yaml \
--set app.enabled=true \
--set worker.enabled=true \
--set rabbitmq.enabled=true \
--set redis.enabled=true \
--set serviceAccountName="cfocats-$env" \
--set app.serviceAccountName="cfocats-$env" \
--set app.image.repository="example/cfocats" \
--set app.image.tag="cats-validate" \
--set worker.serviceAccountName="cfocats-$env" \
--set worker.image.repository="example/cfocats" \
--set worker.image.tag="worker-validate" \
> /dev/null
echo "::endgroup::"

echo "::group::helm template migrate ($env)"
helm template cats-migrate ./helm_deploy/cats \
--namespace "cfocats-$env" \
--values ./helm_deploy/cats/values-$env.yaml \
--set migrator.enabled=true \
--set serviceAccountName="cfocats-$env" \
--set migrator.image.repository="example/cfocats" \
--set migrator.image.tag="migrator-validate" \
> /dev/null
echo "::endgroup::"

echo "::group::helm template seed ($env)"
helm template cats-seed ./helm_deploy/cats \
--namespace "cfocats-$env" \
--values ./helm_deploy/cats/values-$env.yaml \
--set seeder.enabled=true \
--set serviceAccountName="cfocats-$env" \
--set seeder.image.repository="example/cfocats" \
--set seeder.image.tag="seeder-validate" \
> /dev/null
echo "::endgroup::"
done
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,7 @@ aspire-output/

# ls cache files for C# develop extension
*csproj.lscache

# Helm
helm_deploy/*/charts/
helm_deploy/*/Chart.lock
7 changes: 7 additions & 0 deletions helm_deploy/cats/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git/
.gitignore
*.tmproj
*.bak
*.orig
.vscode/
.idea/
33 changes: 33 additions & 0 deletions helm_deploy/cats/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: v2
name: cats
description: |
HMPPS - Case Assessment and Tracking System (CATS)
type: application

# Version of this chart. Bump on every change to the chart/values.
version: "0.1.0"

# Mirrors the application version; the running image is selected via image tags at deploy time.
appVersion: "0.1.0"

dependencies:
# Web tier (Blazor Server UI) — ingress, SignalR sticky sessions, multiple replicas.
- name: generic-service
alias: app
version: "3.17.2"
repository: https://ministryofjustice.github.io/hmpps-helm-charts
condition: app.enabled

# Background worker (Quartz jobs) — single instance, no ingress.
- name: generic-service
alias: worker
version: "3.17.2"
repository: https://ministryofjustice.github.io/hmpps-helm-charts
condition: worker.enabled

# todo: enable prometheus alerts
# https://user-guide.cloud-platform.service.justice.gov.uk/documentation/monitoring-an-app/how-to-create-alarms.html#creating-your-own-custom-alerts
# uncomment and re-run `helm dependency update ./helm_deploy/cats` to fetch it.
# - name: generic-prometheus-alerts
# version: "1.17.1"
# repository: https://ministryofjustice.github.io/hmpps-helm-charts
24 changes: 24 additions & 0 deletions helm_deploy/cats/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{{/*
Environment variables that expose the MSSQL connection details from the
rds-mssql-instance-output namespace secret, plus the composed connection string.
Used by the migrator and seeder Pods.
*/}}
Comment thread
Copilot marked this conversation as resolved.
{{- define "cats.databaseEnv" -}}
- name: DATABASE_ADDRESS
valueFrom:
secretKeyRef:
name: rds-mssql-instance-output
key: rds_instance_address
- name: DATABASE_USERNAME
valueFrom:
secretKeyRef:
name: rds-mssql-instance-output
key: database_username
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: rds-mssql-instance-output
key: database_password
- name: ConnectionStrings__CatsDb
value: {{ .Values.connectionStrings.catsDb | quote }}
{{- end -}}
Loading