Skip to content

Feature/cdapi 85

Feature/cdapi 85 #475

Workflow file for this run

name: Preview Environment
on:
pull_request:
types:
- opened
- synchronize
- reopened
- closed
permissions:
id-token: write
contents: read
pull-requests: write
env:
AWS_REGION: eu-west-2
PREVIEW_PREFIX: pr-
MOCK_PREFIX: mock-
PREVIEW_INT_PREFIX: int-
PYTHON_VERSION: 3.14
LAMBDA_RUNTIME: python3.14
LAMBDA_HANDLER: lambda_handler.handler
MOCK_LAMBDA_HANDLER: lambda_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 }}
PROXYGEN_API_NAME: ${{ vars.PROXYGEN_API_NAME }}
BASE_URL: "https://internal-dev.api.service.nhs.uk/${{ vars.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}"
INT_BASE_URL: "https://internal-dev.api.service.nhs.uk/${{ vars.PROXYGEN_API_NAME }}-pri-${{ github.event.pull_request.number }}"
ENV: "remote"
PR_NUMBER: ${{ github.event.pull_request.number }}
HOST: "internal-dev.api.service.nhs.uk"
jobs:
pr-preview:
name: "PR preview management"
runs-on: ubuntu-latest
outputs:
function_name: ${{ steps.names.outputs.function_name }}
preview_url: ${{ steps.names.outputs.preview_url }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
fetch-depth: 0 # Full history required for accurate sonar analysis.
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Setup Python project
uses: ./.github/actions/setup-python-project
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Package artifacts
run: |
make build
- name: Select AWS role inputs
id: role-select
env:
DEPENDABOT_AWS_ROLE_ARN: ${{ secrets.DEPENDABOT_AWS_ROLE_ARN }}
DEPENDABOT_LAMBDA_ROLE_ARN: ${{ secrets.DEPENDABOT_LAMBDA_ROLE_ARN }}
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
LAMBDA_ROLE_ARN: ${{ secrets.LAMBDA_ROLE_ARN }}
run: |
if [ "${{ github.actor }}" = "dependabot[bot]" ]; then
echo "aws_role=$DEPENDABOT_AWS_ROLE_ARN" >> "$GITHUB_OUTPUT"
echo "lambda_role=$DEPENDABOT_LAMBDA_ROLE_ARN" >> "$GITHUB_OUTPUT"
else
echo "aws_role=$AWS_ROLE_ARN" >> "$GITHUB_OUTPUT"
echo "lambda_role=$LAMBDA_ROLE_ARN" >> "$GITHUB_OUTPUT"
fi
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@b1257c400167d727708335212f95607835cd03fd
with:
role-to-assume: ${{ steps.role-select.outputs.aws_role }}
aws-region: ${{ env.AWS_REGION }}
- name: Sanitize branch name
id: branch
env:
RAW_BRANCH_NAME: "${{ github.head_ref }}"
run: |
branch=$RAW_BRANCH_NAME
if [ -z "$branch" ]; then branch="${{ github.ref_name }}"; fi
if [ "${{ github.actor }}" = "dependabot[bot]" ]; then
safe="dependabot-${{ github.event.pull_request.number }}"
else
safe=$(echo "$branch" | sed -E 's/[^a-zA-Z0-9._-]+/-/g' | tr '[:upper:]' '[:lower:]')
fi
echo "branch=$branch" >> $GITHUB_OUTPUT
echo "safe=$safe" >> $GITHUB_OUTPUT
- name: Compute function name
id: names
run: |
SAFE=${{ steps.branch.outputs.safe }}
PREFIX=${{ env.PREVIEW_PREFIX }}
MOCK_PREFIX=${{ env.MOCK_PREFIX }}
INT_PREFIX=${{ env.PREVIEW_INT_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}"
IFN="${INT_PREFIX}${SAFE}"
echo "function_name=$FN" >> "$GITHUB_OUTPUT"
echo "mock_function_name=$MFN" >> "$GITHUB_OUTPUT"
echo "int_function_name=$IFN" >> "$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"
INT_URL="https://${SAFE}.i.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
echo "preview_url=$URL" >> "$GITHUB_OUTPUT"
echo "mock_preview_url=$MOCK_URL" >> "$GITHUB_OUTPUT"
echo "int_preview_url=$INT_URL" >> "$GITHUB_OUTPUT"
# ---------- Handle application with mock ----------
- name: Create or update preview Lambda with mock (on open/sync/reopen)
if: github.event.action != 'closed'
env:
MOCK_URL: ${{ steps.names.outputs.mock_preview_url }}
TOKEN_EXPIRY_THRESHOLD: ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
JWKS_SECRET_NAME: ${{ secrets.JWKS_SECRET }}
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 }}
APIM_KEY_ID: ${{ secrets.APIM_KEY_ID }}
CLIENT_REQUEST_TIMEOUT: ${{ secrets.CLIENT_REQUEST_TIMEOUT }}
run: |
cd pathology-api/target/
FN="${{ steps.names.outputs.function_name }}"
EXPIRY_THRESHOLD="${TOKEN_EXPIRY_THRESHOLD:-30s}"
JWKS_SECRET="${JWKS_SECRET_NAME:-/cds/pathology/dev/jwks/secret}"
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}"
KEY_ID="${APIM_KEY_ID:-DEV-1}"
CLIENT_TIMEOUT="${CLIENT_REQUEST_TIMEOUT:-10s}"
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")
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 "$FN" >/dev/null 2>&1; then
wait_for_lambda_ready
aws lambda update-function-configuration --function-name "$FN" \
--handler "${{ env.LAMBDA_HANDLER }}" \
--memory-size 512 \
--timeout 30 \
--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_KEY_ID=$KEY_ID, \
APIM_TOKEN_URL=$MOCK_URL/apim/oauth2/token, \
PDM_BUNDLE_URL=$MOCK_URL/apim/check_auth, \
MNS_EVENT_URL=$MOCK_URL/mns, \
CLIENT_TIMEOUT=$CLIENT_TIMEOUT, \
JWKS_SECRET_NAME=$JWKS_SECRET}" || true
wait_for_lambda_ready
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 }}" \
--memory-size 512 \
--timeout 30 \
--environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
APIM_API_KEY_NAME=$API_KEY, \
APIM_KEY_ID=$KEY_ID, \
APIM_MTLS_CERT_NAME=$MTLS_CERT, \
APIM_MTLS_KEY_NAME=$MTLS_KEY, \
APIM_TOKEN_URL=$MOCK_URL/apim/oauth2/token, \
PDM_BUNDLE_URL=$MOCK_URL/apim/check_auth, \
MNS_EVENT_URL=$MOCK_URL/mns, \
CLIENT_TIMEOUT=$CLIENT_TIMEOUT, \
JWKS_SECRET_NAME=$JWKS_SECRET}" \
--publish
wait_for_lambda_ready
fi
- name: Delete preview Lambda with mock (on PR closed)
if: github.event.action == 'closed'
run: |
FN="${{ steps.names.outputs.function_name }}"
echo "Deleting preview function: $FN"
aws lambda delete-function --function-name "$FN" || true
- name: Output function name with mock
run: |
echo "function = ${{ steps.names.outputs.function_name }}"
echo "url = ${{ steps.names.outputs.preview_url }}"
# ---------- Handle application with integration----------
- name: Create or update preview Lambda with integration (on open/sync/reopen)
if: github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
env:
MOCK_URL: ${{ steps.names.outputs.mock_preview_url }}
TOKEN_EXPIRY_THRESHOLD: ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
JWKS_SECRET_NAME: ${{ secrets.JWKS_SECRET }}
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.int_function_name }}"
EXPIRY_THRESHOLD="${TOKEN_EXPIRY_THRESHOLD:-30s}"
JWKS_SECRET="${JWKS_SECRET_NAME:-/cds/pathology/dev/jwks/secret}"
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")
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 "$FN" >/dev/null 2>&1; then
wait_for_lambda_ready
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, \
JWKS_SECRET_NAME=$JWKS_SECRET}" || true
wait_for_lambda_ready
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, \
JWKS_SECRET_NAME=$JWKS_SECRET}" \
--publish
wait_for_lambda_ready
fi
- name: Delete preview Lambda with integration (on PR closed)
if: github.event.action == 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
run: |
FN="${{ steps.names.outputs.int_function_name }}"
echo "Deleting preview function: $FN"
aws lambda delete-function --function-name "$FN" || true
- name: Output function name with integration
if: github.event.pull_request.user.login != 'dependabot[bot]'
run: |
echo "function = ${{ steps.names.outputs.int_function_name }}"
echo "url = ${{ steps.names.outputs.int_preview_url }}"
# ---------- Handle mock endpoints ----------
- name: Get Secrets for mocks
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
with:
secret-ids: |
/cds/pathology/dev/jwks/secret
name-transformation: lowercase
- name: Create or update mock Lambda (on open/sync/reopen)
if: github.event.action != 'closed'
env:
TOKEN_EXPIRY_TIME: ${{ secrets.TOKEN_LIFETIME }}
AUTH_URL: "${{ steps.names.outputs.mock_preview_url }}/apim/oauth2/token"
JWKS_SECRET: ${{ env._cds_pathology_dev_jwks_secret }}
PUBLIC_KEY_URL: "https://example.com"
TOKEN_TABLE_NAME: "mock_services_dev"
run: |
cd mocks/target/
MFN="${{ steps.names.outputs.mock_function_name }}"
SAFE="${{ steps.branch.outputs.safe }}"
TOKEN_LIFETIME="${TOKEN_EXPIRY_TIME:-15m}"
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, \
TOKEN_LIFETIME=$TOKEN_LIFETIME, \
AUTH_URL=$AUTH_URL, \
PUBLIC_KEY_URL=$PUBLIC_KEY_URL, \
API_KEY=$JWKS_SECRET, \
TOKEN_TABLE_NAME=$TOKEN_TABLE_NAME \
}" || true
wait_for_lambda_ready
aws lambda update-function-code --function-name "$MFN" --zip-file "fileb://artifact.zip" --publish
else
aws lambda create-function --function-name "$MFN" \
--runtime "${{ env.LAMBDA_RUNTIME }}" \
--handler "${{ env.MOCK_LAMBDA_HANDLER }}" \
--zip-file "fileb://artifact.zip" \
--role "${{ steps.role-select.outputs.lambda_role }}" \
--environment "Variables={CLIENT_PUBLIC_KEY_ARN=mock, \
DDB_INDEX_TAG=$SAFE, \
TOKEN_LIFETIME=$TOKEN_LIFETIME, \
AUTH_URL=$AUTH_URL, \
PUBLIC_KEY_URL=$PUBLIC_KEY_URL, \
API_KEY=$JWKS_SECRET, \
TOKEN_TABLE_NAME=$TOKEN_TABLE_NAME, \
}" \
--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'
id: mtls-certs
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
with:
secret-ids: |
/cds/pathology/dev/mtls/client1-key-secret
/cds/pathology/dev/mtls/client1-key-public
name-transformation: lowercase
- name: Smoke test preview URL
if: github.event.action != 'closed'
id: smoke-test
env:
PREVIEW_URL: ${{ steps.names.outputs.preview_url }}
run: |
if [ -z "$PREVIEW_URL" ] || [ "$PREVIEW_URL" = "null" ]; then
echo "Preview 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 "Preview 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 "Preview responded with status $STATUS"
echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
echo "http_result=success" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Preview 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: 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: Smoke test int URL
if: github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
id: smoke-int
env:
PREVIEW_URL: ${{ steps.names.outputs.int_preview_url }}
run: |
if [ -z "$PREVIEW_URL" ] || [ "$PREVIEW_URL" = "null" ]; then
echo "Int 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 "Int 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 "Int responded with status $STATUS"
echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
echo "http_result=success" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Int 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
with:
secret-ids: /cds/pathology/dev/proxygen/proxygen-key-secret
name-transformation: lowercase
- name: Deploy preview API proxy
if: github.event.action != 'closed'
uses: ./.github/actions/proxy/deploy-proxy
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 }}"
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 }}
proxygen-api-name: ${{ env.PROXYGEN_API_NAME }}
- name: Deploy int preview API proxy
if: github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
uses: ./.github/actions/proxy/deploy-proxy
with:
mtls-secret-name: ${{ env.MTLS_SECRET_NAME }}
target-url: ${{ steps.names.outputs.int_preview_url }}
proxy-base-path: "${{ env.PROXYGEN_API_NAME }}-pri-${{ 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 }}
proxygen-api-name: ${{ env.PROXYGEN_API_NAME }}
- name: Tear down preview API proxy
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 }}"
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 }}
proxygen-api-name: ${{ env.PROXYGEN_API_NAME }}
- name: Tear down int preview API proxy
if: github.event.action == 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
uses: ./.github/actions/proxy/tear-down-proxy
with:
proxy-base-path: "${{ env.PROXYGEN_API_NAME }}-pri-${{ 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 }}
proxygen-api-name: ${{ env.PROXYGEN_API_NAME }}
- name: Retrieve Apigee Token
id: apigee-token
shell: bash
run: |
set -euo pipefail
APIGEE_TOKEN="$(proxygen pytest-nhsd-apim get-token | jq -r '.pytest_nhsd_apim_token' 2>/dev/null)"
if [ -z "$APIGEE_TOKEN" ] || [ "$APIGEE_TOKEN" = "null" ]; then
echo "::error::Failed to retrieve Apigee token"
exit 1
fi
echo "::add-mask::$APIGEE_TOKEN"
printf 'apigee-access-token=%s\n' "$APIGEE_TOKEN" >> "$GITHUB_OUTPUT"
echo "Token retrieved successfully (length: ${#APIGEE_TOKEN})"
- name: "Create coverage artefact name"
id: create-name
uses: ./.github/actions/create-artefact-name
with:
prefix: coverage
# ---------- Test suites ----------
- name: "Run unit tests"
if: github.event.action != 'closed'
uses: ./.github/actions/run-test-suite
with:
test-type: unit
env: local
- name: "Run contract tests"
if: github.event.action != 'closed'
uses: ./.github/actions/run-test-suite
with:
test-type: contract
apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }}
- name: "Run schema validation tests"
if: github.event.action != 'closed'
uses: ./.github/actions/run-test-suite
with:
test-type: schema
apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }}
- name: "Run integration tests"
if: github.event.action != 'closed'
uses: ./.github/actions/run-test-suite
with:
test-type: integration
apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }}
- name: "Run acceptance tests"
if: github.event.action != 'closed'
uses: ./.github/actions/run-test-suite
with:
test-type: acceptance
apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }}
# ---------- Coverage & reporting ----------
- name: "Download all test coverage artefacts"
if: always() && github.event.action != 'closed'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: pathology-api/test-artefacts/
merge-multiple: false
- name: "Download mock test coverage artefacts"
if: always() && github.event.action != 'closed'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
path: mocks/test-artefacts/
merge-multiple: false
- name: "Merge coverage data"
if: always() && github.event.action != 'closed'
run: make test-coverage
- name: "Rename coverage XML with unique name"
if: always() && github.event.action != 'closed'
run: |
cd pathology-api/test-artefacts
mv coverage-merged.xml "${{ steps.create-name.outputs.artefact-name }}.xml"
cd ../..
cd mocks/test-artefacts
mv coverage-merged.xml ${{ steps.create-name.outputs.artefact-name }}-mocks.xml
- name: "Upload combined coverage report"
if: always() && github.event.action != 'closed'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ steps.create-name.outputs.artefact-name }}
path: pathology-api/test-artefacts
retention-days: 30
- name: "Upload mocks coverage report"
if: always() && github.event.action != 'closed'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ steps.create-name.outputs.artefact-name }}-mocks
path: mocks/test-artefacts
retention-days: 30
- name: "Download merged coverage report"
if: always() && github.event.action != 'closed'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ steps.create-name.outputs.artefact-name }}
path: coverage-reports/
- name: "Download mock coverage report"
if: always() && github.event.action != 'closed'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: ${{ steps.create-name.outputs.artefact-name }}-mocks
path: coverage-reports/
- name: "SonarCloud Scan"
if: always() && github.event.action != 'closed' && github.actor != 'dependabot[bot]'
uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 #7.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=${{ vars.SONAR_ORGANISATION_KEY }}
-Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }}
-Dsonar.python.coverage.reportPaths=coverage-reports/${{ steps.create-name.outputs.artefact-name }}.xml,coverage-reports/${{ steps.create-name.outputs.artefact-name }}-mocks.xml
- name: Comment function name on PR
if: github.event_name == 'pull_request' && github.event.action != 'closed'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const fn = '${{ steps.names.outputs.function_name }}' || 'not-set';
const mock_fn = '${{ steps.names.outputs.mock_function_name }}' || 'not-set';
const int_fn = '${{ steps.names.outputs.int_function_name }}' || 'not-set';
const url = '${{ steps.names.outputs.preview_url }}';
const mock_url = '${{ steps.names.outputs.mock_preview_url }}';
const int_url = '${{ steps.names.outputs.int_preview_url }}';
const proxy_url = '${{ env.BASE_URL }}';
const int_proxy_url = '${{ env.INT_BASE_URL}}';
const isDependabotPr = '${{ github.event.pull_request.user.login }}' === 'dependabot[bot]';
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 smokeIntStatus = '${{ steps.smoke-int.outputs.http_status }}' || 'n/a';
const smokeIntResult = '${{ steps.smoke-int.outputs.http_result }}' || 'not-run';
const smokeLabels = {
success: ':white_check_mark: Passed',
'allowed-404': ':white_check_mark: Allowed 404',
'unexpected-status': ':x: Unexpected status',
'missing-url': ':x: Missing URL',
};
const smokeReadable = smokeLabels[smokeResult] ?? smokeResult;
const smokeMockReadable = smokeLabels[smokeMockResult] ?? smokeMockResult;
const smokeIntReadable = smokeLabels[smokeIntResult] ?? smokeIntResult;
const intUrlDisplay = isDependabotPr
? 'Skipped for Dependabot PR'
: `[${int_url}](${int_url}) — [Status](${int_url}/_status)`;
const intSmokeDisplay = isDependabotPr
? 'Skipped for Dependabot PR'
: `${smokeIntReadable} (HTTP ${smokeIntStatus})`;
const intProxyDisplay = isDependabotPr
? 'Skipped for Dependabot PR'
: `[${int_proxy_url}](${int_proxy_url})`;
const intLambdaDisplay = isDependabotPr
? 'Skipped for Dependabot PR'
: int_fn;
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
for (const comment of comments) {
const isBot = comment.user?.login === 'github-actions[bot]';
const isPreviewUpdate = comment.body?.includes('Deployment Complete');
if (isBot && isPreviewUpdate) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id,
});
}
}
const lines = [
'**Deployment Complete**',
`- Preview with mock:`,
` - URL: [${url}](${url}) — [Status](${url}/_status)`,
` - Smoke test result: ${smokeReadable} (HTTP ${smokeStatus})`,
` - Proxy URL: [${proxy_url}](${proxy_url})`,
` - Lambda function: ${fn}`,
`- Mock endpoints:`,
` - URL: [${mock_url}](${mock_url})`,
` - Smoke test result: ${smokeMockReadable} (HTTP ${smokeMockStatus})`,
` - Lambda function: ${mock_fn}`,
`- Preview with integration:`,
` - URL: ${intUrlDisplay}`,
` - Smoke test result: ${intSmokeDisplay}`,
` - Proxy URL: ${intProxyDisplay}`,
` - Lambda function: ${intLambdaDisplay}`,
];
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: lines.join('\n'),
});
# ---------- Perform trivy scan and notify ----------
- name: Prepare lambda artifact for trivy scan
if: github.event.action != 'closed'
run: |
cd pathology-api/target/
rm -rf /tmp/artifact
mkdir -p /tmp/artifact
unzip -q artifact.zip -d /tmp/artifact
- name: Trivy filesystem scan
if: github.event.action != 'closed'
uses: ./.github/actions/trivy-fs-scan
with:
filesystem-ref: /tmp/artifact
artifact-name: trivy-fs-scan-${{ steps.branch.outputs.safe }}
- name: Trivy SBOM generation
if: github.event.action != 'closed'
uses: ./.github/actions/trivy-fs-sbom
with:
fs-path: /tmp/artifact