Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8a56077
Create mock mock lambda artifact
neil-sproston Feb 16, 2026
7b91fd6
Setup mock endpoints
neil-sproston Feb 16, 2026
fc50e2d
Update mock Lambda artifact filename in preview environment workflow
neil-sproston Feb 16, 2026
2b4c30b
Fix mock URL generation in preview environment workflow
neil-sproston Feb 16, 2026
006ec15
Log Lambda context in handler function
neil-sproston Feb 16, 2026
16dbd47
Update mock Lambda artifact filename in preview environment workflow
neil-sproston Feb 16, 2026
5a29aca
Add smoke test for mock URL and update deployment comments
neil-sproston Feb 16, 2026
aada71f
Update Lambda handler to use mock handler and refine deployment statu…
neil-sproston Feb 16, 2026
777ad8c
Format deployment status message for better readability in preview en…
neil-sproston Feb 16, 2026
012b56a
Enhance Lambda function environment variables handling for mock deplo…
neil-sproston Feb 17, 2026
990e332
Update mock Lambda configuration to include environment variables
neil-sproston Feb 17, 2026
6147b9a
Add environment variables to Lambda configuration
neil-sproston Feb 17, 2026
0775649
Add safe branch output to mock Lambda deployment configuration
neil-sproston Feb 17, 2026
5dbaba2
Update Lambda configuration to use dynamic URLs and token expiry thre…
neil-sproston Feb 17, 2026
94431e9
[CDAPI-95]: Removed terraform lint from pre-commit and GitHub Actions.
nhsd-jack-wainwright Feb 13, 2026
0bfea19
Update mock URLs in Lambda configuration for APIM, PDM, and MNS endpo…
neil-sproston Feb 17, 2026
24a9de0
Add pragma directive to skip coverage for incoming request headers log
neil-sproston Feb 17, 2026
5bacdcb
Fix formatting of log header comment in handler function
neil-sproston Feb 17, 2026
063f54f
Enhance handler function documentation and update coverage exclusions…
neil-sproston Feb 17, 2026
ac8d9f1
Fix formatting of docstring in handler function for improved readability
neil-sproston Feb 17, 2026
6329d64
Fix formatting of docstring in handler function for consistency
neil-sproston Feb 17, 2026
f392ce4
Fix docstring formatting in handler function for clarity
neil-sproston Feb 17, 2026
4b7d1c8
Add environment variables for APIM integration in preview workflow
neil-sproston Feb 17, 2026
df69cc1
Add APIM secrets to preview environment for enhanced security
neil-sproston Feb 17, 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
20 changes: 0 additions & 20 deletions .github/actions/lint-terraform/action.yaml

This file was deleted.

185 changes: 175 additions & 10 deletions .github/workflows/preview-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ permissions:
env:
AWS_REGION: eu-west-2
PREVIEW_PREFIX: pr-
MOCK_PREFIX: mock-
PYTHON_VERSION: 3.14
LAMBDA_RUNTIME: python3.14
LAMBDA_HANDLER: lambda_handler.handler
MOCK_LAMBDA_HANDLER: handler.handler
MTLS_SECRET_NAME: ${{ vars.PREVIEW_ENV_MTLS_SECRET_NAME }}
PROXYGEN_KEY_ID: ${{ vars.PREVIEW_ENV_PROXYGEN_KEY_ID }}
PROXYGEN_CLIENT_ID: ${{ vars.PREVIEW_ENV_PROXYGEN_CLIENT_ID }}
Expand Down Expand Up @@ -50,6 +52,14 @@ jobs:
run: |
make build

# Place holder mock artifact packaging to allow testing of mock API in preview environment;
# can be extended to build a real mock Lambda if needed
- name: Package mock artifact
run: |
cd infrastructure/environments/preview
rm -f mock_artifact.zip
zip -r mock_artifact.zip .

- name: Select AWS role inputs
id: role-select
env:
Expand Down Expand Up @@ -88,25 +98,49 @@ jobs:
run: |
SAFE=${{ steps.branch.outputs.safe }}
PREFIX=${{ env.PREVIEW_PREFIX }}
MAX_FN_LEN=64
MAX_SAFE_LEN=$((MAX_FN_LEN - ${#PREFIX}))
MOCK_PREFIX=${{ env.MOCK_PREFIX }}
MAX_FN_LEN=62
MAX_PREFIX_LEN=${#PREFIX}
if [ ${#MOCK_PREFIX} -gt "$MAX_PREFIX_LEN" ]; then
MAX_PREFIX_LEN=${#MOCK_PREFIX}
fi
MAX_SAFE_LEN=$((MAX_FN_LEN - MAX_PREFIX_LEN))
if [ ${#SAFE} -gt "$MAX_SAFE_LEN" ]; then
SAFE=${SAFE:0:MAX_SAFE_LEN}
fi
FN="${PREFIX}${SAFE}"
MFN="${MOCK_PREFIX}${SAFE}"
echo "function_name=$FN" >> "$GITHUB_OUTPUT"
echo "mock_function_name=$MFN" >> "$GITHUB_OUTPUT"
URL="https://${SAFE}.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
MOCK_URL="https://${SAFE}.m.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
echo "preview_url=$URL" >> "$GITHUB_OUTPUT"
echo "mock_preview_url=$MOCK_URL" >> "$GITHUB_OUTPUT"

# ---------- Handle application ----------
- name: Create or update preview Lambda (on open/sync/reopen)
if: github.event.action != 'closed'
env:
MOCK_URL: ${{ steps.names.outputs.mock_preview_url }}
EXPIRY_THRESHOLD: ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
APIM_PRIVATE_KEY: ${{ secrets.APIM_PRIVATE_KEY }}
APIM_APIKEY: ${{ secrets.APIM_APIKEY }}
API_MTLS_CERT: ${{ secrets.API_MTLS_CERT }}
API_MTLS_KEY: ${{ secrets.API_MTLS_KEY }}
run: |
cd pathology-api/target/
FN="${{ steps.names.outputs.function_name }}"
EXPIRY_THRESHOLD="${TOKEN_EXPIRY_THRESHOLD:-840s}"
PRIVATE_KEY="${APIM_PRIVATE_KEY:-/cds/pathology/dev/apim/private-key}"
API_KEY="${APIM_APIKEY:-/cds/pathology/dev/apim/api-key}"
MTLS_CERT="${API_MTLS_CERT:-/cds/pathology/dev/mtls/client1-key-public}"
MTLS_KEY="${API_MTLS_KEY:-/cds/pathology/dev/mtls/client1-key-secret}"
echo "Deploying preview function: $FN"
wait_for_lambda_ready() {
while true; do
status=$(aws lambda get-function-configuration --function-name "$FN" --query 'LastUpdateStatus' --output text 2>/dev/null || echo "Unknown")
status=$(aws lambda get-function-configuration --function-name "$FN" \
--query 'LastUpdateStatus' \
--output text 2>/dev/null || echo "Unknown")
if [ "$status" = "Successful" ] || [ "$status" = "Unknown" ]; then
break
fi
Expand All @@ -120,15 +154,34 @@ jobs:
}
if aws lambda get-function --function-name "$FN" >/dev/null 2>&1; then
wait_for_lambda_ready
aws lambda update-function-configuration --function-name "$FN" --handler "${{ env.LAMBDA_HANDLER }}" || true
aws lambda update-function-configuration --function-name "$FN" \
--handler "${{ env.LAMBDA_HANDLER }}" \
--environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
APIM_API_KEY_NAME=$API_KEY, \
APIM_MTLS_CERT_NAME=$MTLS_CERT, \
APIM_MTLS_KEY_NAME=$MTLS_KEY, \
APIM_TOKEN_URL=$MOCK_URL/apim, \
PDM_BUNDLE_URL=$MOCK_URL/pdm, \
MNS_EVENT_URL=$MOCK_URL/mns}" || true
wait_for_lambda_ready
aws lambda update-function-code --function-name "$FN" --zip-file "fileb://artifact.zip" --publish
aws lambda update-function-code --function-name "$FN" \
--zip-file "fileb://artifact.zip" \
--publish
else
aws lambda create-function --function-name "$FN" \
--runtime "${{ env.LAMBDA_RUNTIME }}" \
--handler "${{ env.LAMBDA_HANDLER }}" \
--zip-file "fileb://artifact.zip" \
--role "${{ steps.role-select.outputs.lambda_role }}" \
--environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
APIM_API_KEY_NAME=$API_KEY, \
APIM_MTLS_CERT_NAME=$MTLS_CERT, \
APIM_MTLS_KEY_NAME=$MTLS_KEY, \
APIM_TOKEN_URL=$MOCK_URL/apim, \
PDM_BUNDLE_URL=$MOCK_URL/pdm, \
MNS_EVENT_URL=$MOCK_URL/mns}" \
--publish
wait_for_lambda_ready
fi
Expand All @@ -145,6 +198,60 @@ jobs:
echo "function = ${{ steps.names.outputs.function_name }}"
echo "url = ${{ steps.names.outputs.preview_url }}"

# ---------- Handle mock endpoints ----------
- name: Create or update mock Lambda (on open/sync/reopen)
if: github.event.action != 'closed'
run: |
cd infrastructure/environments/preview
MFN="${{ steps.names.outputs.mock_function_name }}"
SAFE="${{ steps.branch.outputs.safe }}"
echo "Deploying mock function: $MFN"
wait_for_lambda_ready() {
while true; do
status=$(aws lambda get-function-configuration --function-name "$MFN" --query 'LastUpdateStatus' --output text 2>/dev/null || echo "Unknown")
if [ "$status" = "Successful" ] || [ "$status" = "Unknown" ]; then
break
fi
if [ "$status" = "Failed" ]; then
echo "Lambda is in Failed state; check logs." >&2
exit 1
fi
echo "Lambda update status: $status — waiting..."
sleep 5
done
}
if aws lambda get-function --function-name "$MFN" >/dev/null 2>&1; then
wait_for_lambda_ready
aws lambda update-function-configuration --function-name "$MFN" \
--handler "${{ env.MOCK_LAMBDA_HANDLER }}" \
--environment "Variables={CLIENT_PUBLIC_KEY_ARN=mock, \
DDB_INDEX_TAG=$SAFE}" || true
wait_for_lambda_ready
aws lambda update-function-code --function-name "$MFN" --zip-file "fileb://mock_artifact.zip" --publish
else
aws lambda create-function --function-name "$MFN" \
--runtime "${{ env.LAMBDA_RUNTIME }}" \
--handler "${{ env.MOCK_LAMBDA_HANDLER }}" \
--zip-file "fileb://mock_artifact.zip" \
--role "${{ steps.role-select.outputs.lambda_role }}" \
--environment "Variables={CLIENT_PUBLIC_KEY_ARN=mock, \
DDB_INDEX_TAG=$SAFE}" \
--publish
wait_for_lambda_ready
fi

- name: Delete mock Lambda (on PR closed)
if: github.event.action == 'closed'
run: |
MFN="${{ steps.names.outputs.mock_function_name }}"
echo "Deleting mock function: $MFN"
aws lambda delete-function --function-name "$MFN" || true

- name: Output mock function name
run: |
echo "mock_function = ${{ steps.names.outputs.mock_function_name }}"
echo "mock_url = ${{ steps.names.outputs.mock_preview_url }}"

# ---------- Wait on AWS tasks and notify ----------
- name: Get mTLS certs for testing
if: github.event.action != 'closed'
Expand Down Expand Up @@ -207,6 +314,57 @@ jobs:
echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT"
exit 0

- name: Smoke test mock URL
if: github.event.action != 'closed'
id: smoke-mock
env:
PREVIEW_URL: ${{ steps.names.outputs.mock_preview_url }}
run: |
if [ -z "$PREVIEW_URL" ] || [ "$PREVIEW_URL" = "null" ]; then
echo "Mock URL missing"
echo "http_status=missing" >> "$GITHUB_OUTPUT"
echo "http_result=missing-url" >> "$GITHUB_OUTPUT"
exit 0
fi

# Reachability check: allow 404 (app routes might not exist yet) but fail otherwise
printf '%s' "$_cds_pathology_dev_mtls_client1_key_secret" > /tmp/client1-key.pem
printf '%s' "$_cds_pathology_dev_mtls_client1_key_public" > /tmp/client1-cert.pem
STATUS=$(curl \
--cert /tmp/client1-cert.pem \
--key /tmp/client1-key.pem \
--silent \
--output /tmp/preview.headers \
--write-out '%{http_code}' \
--head \
--max-time 30 \
-X GET "$PREVIEW_URL"/_status || true)
rm -f /tmp/client1-key.pem
rm -f /tmp/client1-cert.pem

if [ "$STATUS" = "404" ]; then
echo "Mock responded with expected 404"
echo "http_status=404" >> "$GITHUB_OUTPUT"
echo "http_result=allowed-404" >> "$GITHUB_OUTPUT"
exit 0
fi

if [[ "$STATUS" =~ ^[0-9]{3}$ ]] && [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 400 ]; then
echo "Mock responded with status $STATUS"
echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
echo "http_result=success" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Mock responded with unexpected status $STATUS"
if [ -f /tmp/preview.headers ]; then
echo "Response headers:"
cat /tmp/preview.headers
fi
echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT"
exit 0

- name: Get proxygen machine user details
id: proxygen-machine-user
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
Expand All @@ -220,7 +378,7 @@ jobs:
with:
mtls-secret-name: ${{ env.MTLS_SECRET_NAME }}
target-url: ${{ steps.names.outputs.preview_url }}
proxy-base-path: '${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}'
proxy-base-path: "${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}"
proxygen-key-secret: ${{ env._cds_pathology_dev_proxygen_proxygen_key_secret }}
proxygen-key-id: ${{ env.PROXYGEN_KEY_ID }}
proxygen-client-id: ${{ env.PROXYGEN_CLIENT_ID }}
Expand All @@ -230,7 +388,7 @@ jobs:
if: github.event.action == 'closed'
uses: ./.github/actions/proxy/tear-down-proxy
with:
proxy-base-path: '${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}'
proxy-base-path: "${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}"
proxygen-key-secret: ${{ env._cds_pathology_dev_proxygen_proxygen_key_secret }}
proxygen-key-id: ${{ env.PROXYGEN_KEY_ID }}
proxygen-client-id: ${{ env.PROXYGEN_CLIENT_ID }}
Expand All @@ -242,13 +400,17 @@ jobs:
with:
script: |
const fn = '${{ steps.names.outputs.function_name }}';
const mock_fn = '${{ steps.names.outputs.mock_function_name }}';
const url = '${{ steps.names.outputs.preview_url }}';
const mock_url = '${{ steps.names.outputs.mock_preview_url }}';
const proxy_url = 'https://internal-dev.api.service.nhs.uk/${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}';
const owner = context.repo.owner;
const repo = context.repo.repo;
const issueNumber = context.issue.number;
const smokeStatus = '${{ steps.smoke-test.outputs.http_status }}' || 'n/a';
const smokeResult = '${{ steps.smoke-test.outputs.http_result }}' || 'not-run';
const smokeMockStatus = '${{ steps.smoke-mock.outputs.http_status }}' || 'n/a';
const smokeMockResult = '${{ steps.smoke-mock.outputs.http_result }}' || 'not-run';

const smokeLabels = {
success: ':white_check_mark: Passed',
Expand All @@ -258,7 +420,7 @@ jobs:
};

const smokeReadable = smokeLabels[smokeResult] ?? smokeResult;

const smokeMockReadable = smokeLabels[smokeMockResult] ?? smokeMockResult;
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
Expand All @@ -281,10 +443,13 @@ jobs:

const lines = [
'**Deployment Complete**',
`- Preview URL: [${url}](${url}) — [Status endpoint](${url}/_status)`,
`- Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
`- Preview URL: [${url}](${url}) — [Status](${url}/_status)`,
` - Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
`- Mock URL: [${mock_url}](${mock_url})`,
` - Smoke Mock Test: ${smokeMockReadable} (HTTP ${smokeMockStatus})`,
`- Proxy URL: [${proxy_url}](${proxy_url})`,
`- Lambda Function: ${fn}`,
`- Mock Lambda Function: ${mock_fn}`,
];

await github.rest.issues.createComment({
Expand Down
9 changes: 0 additions & 9 deletions .github/workflows/stage-1-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,6 @@ jobs:
fetch-depth: 0 # Full history is needed to compare branches
- name: "Check English usage"
uses: ./.github/actions/check-english-usage
lint-terraform:
name: "Lint Terraform"
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: "Checkout code"
uses: actions/checkout@v6
- name: "Lint Terraform"
uses: ./.github/actions/lint-terraform
count-lines-of-code:
name: "Count lines of code"
runs-on: ubuntu-latest
Expand Down
2 changes: 0 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# This file is for you! Please, updated to the versions agreed by your team.

terraform 1.7.0
pre-commit 3.6.0
gitleaks 8.18.4

Expand All @@ -15,7 +14,6 @@ gitleaks 8.18.4
# docker/ghcr.io/make-ops-tools/gocloc latest@sha256:6888e62e9ae693c4ebcfed9f1d86c70fd083868acb8815fe44b561b9a73b5032 # SEE: https://github.com/make-ops-tools/gocloc/pkgs/container/gocloc
# docker/ghcr.io/nhs-england-tools/github-runner-image 20230909-321fd1e-rt@sha256:ce4fd6035dc450a50d3cbafb4986d60e77cb49a71ab60a053bb1b9518139a646 # SEE: https://github.com/nhs-england-tools/github-runner-image/pkgs/container/github-runner-image
# docker/hadolint/hadolint 2.12.0-alpine@sha256:7dba9a9f1a0350f6d021fb2f6f88900998a4fb0aaf8e4330aa8c38544f04db42 # SEE: https://hub.docker.com/r/hadolint/hadolint/tags
# docker/hashicorp/terraform 1.12.2@sha256:b3d13c9037d2bd858fe10060999aa7ca56d30daafe067d7715b29b3d4f5b162f # SEE: https://hub.docker.com/r/hashicorp/terraform/tags
# docker/koalaman/shellcheck latest@sha256:e40388688bae0fcffdddb7e4dea49b900c18933b452add0930654b2dea3e7d5c # SEE: https://hub.docker.com/r/koalaman/shellcheck/tags
# docker/mstruebing/editorconfig-checker 2.7.1@sha256:dd3ca9ea50ef4518efe9be018d669ef9cf937f6bb5cfe2ef84ff2a620b5ddc24 # SEE: https://hub.docker.com/r/mstruebing/editorconfig-checker/tags
# docker/sonarsource/sonar-scanner-cli 10.0@sha256:0bc49076468d2955948867620b2d98d67f0d59c0fd4a5ef1f0afc55cf86f2079 # SEE: https://hub.docker.com/r/sonarsource/sonar-scanner-cli/tags
40 changes: 40 additions & 0 deletions infrastructure/environments/preview/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import json
import logging
from typing import Any

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def handler(event: dict[str, Any], context: Any):
"""Handler for the preview environment. This simply returns a 200 response with
the request headers, which can be used to verify and debug how the environment
is working and to inspect the incoming request.

Args:
event (dict[str, Any]): Dictionary containing request data from API Gateway.
context (Any): AWS lambda context object (what the lambda is running in)

Returns:
dict: Diagnostic 200 response with request headers.
"""
logger.info("Lambda context: %s", context)
headers = event.get("headers", {}) or {}

# Log headers to CloudWatch
logger.info("Incoming request headers:")
for k, v in headers.items():
logger.info("%s: %s", k, v)

response_body = {
"message": "ok",
"headers": headers,
"requestContext": event.get("requestContext", {}),
}

return {
"statusCode": 200,
"headers": {"content-type": "application/json"},
"body": json.dumps(response_body, indent=2),
"isBase64Encoded": False,
}
Loading
Loading