Feature/cdapi 85 #475
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |