1717 AWS_REGION : eu-west-2
1818 PREVIEW_PREFIX : pr-
1919 MOCK_PREFIX : mock-
20+ PREVIEW_INT_PREFIX : int-
2021 PYTHON_VERSION : 3.14
2122 LAMBDA_RUNTIME : python3.14
2223 LAMBDA_HANDLER : lambda_handler.handler
2526 PROXYGEN_KEY_ID : ${{ vars.PREVIEW_ENV_PROXYGEN_KEY_ID }}
2627 PROXYGEN_CLIENT_ID : ${{ vars.PREVIEW_ENV_PROXYGEN_CLIENT_ID }}
2728 PROXYGEN_API_NAME : ${{ vars.PROXYGEN_API_NAME }}
28- BASE_URL : ' https://internal-dev.api.service.nhs.uk/${{ vars.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}'
29+ BASE_URL : " https://internal-dev.api.service.nhs.uk/${{ vars.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}"
30+ INT_BASE_URL : " https://internal-dev.api.service.nhs.uk/${{ vars.PROXYGEN_API_NAME }}-pri-${{ github.event.pull_request.number }}"
2931 ENV : " remote"
3032 PR_NUMBER : ${{ github.event.pull_request.number }}
3133 HOST : " internal-dev.api.service.nhs.uk"
9395 run : |
9496 branch=$RAW_BRANCH_NAME
9597 if [ -z "$branch" ]; then branch="${{ github.ref_name }}"; fi
96- safe=$(echo "$branch" | sed -E 's/[^a-zA-Z0-9._-]+/-/g' | tr '[:upper:]' '[:lower:]')
98+
99+ if [ "${{ github.actor }}" = "dependabot[bot]" ]; then
100+ safe="dependabot-${{ github.event.pull_request.number }}"
101+ else
102+ safe=$(echo "$branch" | sed -E 's/[^a-zA-Z0-9._-]+/-/g' | tr '[:upper:]' '[:lower:]')
103+ fi
104+
97105 echo "branch=$branch" >> $GITHUB_OUTPUT
98106 echo "safe=$safe" >> $GITHUB_OUTPUT
99107
@@ -103,6 +111,7 @@ jobs:
103111 SAFE=${{ steps.branch.outputs.safe }}
104112 PREFIX=${{ env.PREVIEW_PREFIX }}
105113 MOCK_PREFIX=${{ env.MOCK_PREFIX }}
114+ INT_PREFIX=${{ env.PREVIEW_INT_PREFIX }}
106115 MAX_FN_LEN=62
107116 MAX_PREFIX_LEN=${#PREFIX}
108117 if [ ${#MOCK_PREFIX} -gt "$MAX_PREFIX_LEN" ]; then
@@ -114,20 +123,24 @@ jobs:
114123 fi
115124 FN="${PREFIX}${SAFE}"
116125 MFN="${MOCK_PREFIX}${SAFE}"
126+ IFN="${INT_PREFIX}${SAFE}"
117127 echo "function_name=$FN" >> "$GITHUB_OUTPUT"
118128 echo "mock_function_name=$MFN" >> "$GITHUB_OUTPUT"
129+ echo "int_function_name=$IFN" >> "$GITHUB_OUTPUT"
119130 URL="https://${SAFE}.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
120131 MOCK_URL="https://${SAFE}.m.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
132+ INT_URL="https://${SAFE}.i.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
121133 echo "preview_url=$URL" >> "$GITHUB_OUTPUT"
122134 echo "mock_preview_url=$MOCK_URL" >> "$GITHUB_OUTPUT"
135+ echo "int_preview_url=$INT_URL" >> "$GITHUB_OUTPUT"
123136
124- # ---------- Handle application ----------
125- - name : Create or update preview Lambda (on open/sync/reopen)
137+ # ---------- Handle application with mock ----------
138+ - name : Create or update preview Lambda with mock (on open/sync/reopen)
126139 if : github.event.action != 'closed'
127140 env :
128141 MOCK_URL : ${{ steps.names.outputs.mock_preview_url }}
129- EXPIRY_THRESHOLD : ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
130- JWKS_SECRET : ${{ secrets.JWKS_SECRET }}
142+ TOKEN_EXPIRY_THRESHOLD : ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
143+ JWKS_SECRET_NAME : ${{ secrets.JWKS_SECRET }}
131144 APIM_PRIVATE_KEY : ${{ secrets.APIM_PRIVATE_KEY }}
132145 APIM_APIKEY : ${{ secrets.APIM_APIKEY }}
133146 API_MTLS_CERT : ${{ secrets.API_MTLS_CERT }}
@@ -194,18 +207,104 @@ jobs:
194207 wait_for_lambda_ready
195208 fi
196209
197- - name : Delete preview Lambda (on PR closed)
210+ - name : Delete preview Lambda with mock (on PR closed)
198211 if : github.event.action == 'closed'
199212 run : |
200213 FN="${{ steps.names.outputs.function_name }}"
201214 echo "Deleting preview function: $FN"
202215 aws lambda delete-function --function-name "$FN" || true
203216
204- - name : Output function name
217+ - name : Output function name with mock
205218 run : |
206219 echo "function = ${{ steps.names.outputs.function_name }}"
207220 echo "url = ${{ steps.names.outputs.preview_url }}"
208221
222+ # ---------- Handle application with integration----------
223+ - name : Create or update preview Lambda with integration (on open/sync/reopen)
224+ if : github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
225+ env :
226+ MOCK_URL : ${{ steps.names.outputs.mock_preview_url }}
227+ TOKEN_EXPIRY_THRESHOLD : ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
228+ JWKS_SECRET_NAME : ${{ secrets.JWKS_SECRET }}
229+ APIM_PRIVATE_KEY : ${{ secrets.APIM_PRIVATE_KEY }}
230+ APIM_APIKEY : ${{ secrets.APIM_APIKEY }}
231+ API_MTLS_CERT : ${{ secrets.API_MTLS_CERT }}
232+ API_MTLS_KEY : ${{ secrets.API_MTLS_KEY }}
233+ run : |
234+ cd pathology-api/target/
235+ FN="${{ steps.names.outputs.int_function_name }}"
236+ EXPIRY_THRESHOLD="${TOKEN_EXPIRY_THRESHOLD:-30s}"
237+ JWKS_SECRET="${JWKS_SECRET_NAME:-/cds/pathology/dev/jwks/secret}"
238+ PRIVATE_KEY="${APIM_PRIVATE_KEY:-/cds/pathology/dev/apim/private-key}"
239+ API_KEY="${APIM_APIKEY:-/cds/pathology/dev/apim/api-key}"
240+ MTLS_CERT="${API_MTLS_CERT:-/cds/pathology/dev/mtls/client1-key-public}"
241+ MTLS_KEY="${API_MTLS_KEY:-/cds/pathology/dev/mtls/client1-key-secret}"
242+ echo "Deploying preview function: $FN"
243+ wait_for_lambda_ready() {
244+ while true; do
245+ status=$(aws lambda get-function-configuration --function-name "$FN" \
246+ --query 'LastUpdateStatus' \
247+ --output text 2>/dev/null || echo "Unknown")
248+ if [ "$status" = "Successful" ] || [ "$status" = "Unknown" ]; then
249+ break
250+ fi
251+ if [ "$status" = "Failed" ]; then
252+ echo "Lambda is in Failed state; check logs." >&2
253+ exit 1
254+ fi
255+ echo "Lambda update status: $status — waiting..."
256+ sleep 5
257+ done
258+ }
259+ if aws lambda get-function --function-name "$FN" >/dev/null 2>&1; then
260+ wait_for_lambda_ready
261+ aws lambda update-function-configuration --function-name "$FN" \
262+ --handler "${{ env.LAMBDA_HANDLER }}" \
263+ --environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
264+ APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
265+ APIM_API_KEY_NAME=$API_KEY, \
266+ APIM_MTLS_CERT_NAME=$MTLS_CERT, \
267+ APIM_MTLS_KEY_NAME=$MTLS_KEY, \
268+ APIM_TOKEN_URL=$MOCK_URL/apim, \
269+ PDM_BUNDLE_URL=$MOCK_URL/pdm, \
270+ MNS_EVENT_URL=$MOCK_URL/mns, \
271+ JWKS_SECRET_NAME=$JWKS_SECRET}" || true
272+ wait_for_lambda_ready
273+ aws lambda update-function-code --function-name "$FN" \
274+ --zip-file "fileb://artifact.zip" \
275+ --publish
276+ else
277+ aws lambda create-function --function-name "$FN" \
278+ --runtime "${{ env.LAMBDA_RUNTIME }}" \
279+ --handler "${{ env.LAMBDA_HANDLER }}" \
280+ --zip-file "fileb://artifact.zip" \
281+ --role "${{ steps.role-select.outputs.lambda_role }}" \
282+ --environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
283+ APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
284+ APIM_API_KEY_NAME=$API_KEY, \
285+ APIM_MTLS_CERT_NAME=$MTLS_CERT, \
286+ APIM_MTLS_KEY_NAME=$MTLS_KEY, \
287+ APIM_TOKEN_URL=$MOCK_URL/apim, \
288+ PDM_BUNDLE_URL=$MOCK_URL/pdm, \
289+ MNS_EVENT_URL=$MOCK_URL/mns, \
290+ JWKS_SECRET_NAME=$JWKS_SECRET}" \
291+ --publish
292+ wait_for_lambda_ready
293+ fi
294+
295+ - name : Delete preview Lambda with integration (on PR closed)
296+ if : github.event.action == 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
297+ run : |
298+ FN="${{ steps.names.outputs.int_function_name }}"
299+ echo "Deleting preview function: $FN"
300+ aws lambda delete-function --function-name "$FN" || true
301+
302+ - name : Output function name with integration
303+ if : github.event.pull_request.user.login != 'dependabot[bot]'
304+ run : |
305+ echo "function = ${{ steps.names.outputs.int_function_name }}"
306+ echo "url = ${{ steps.names.outputs.int_preview_url }}"
307+
209308 # ---------- Handle mock endpoints ----------
210309 - name : Create or update mock Lambda (on open/sync/reopen)
211310 if : github.event.action != 'closed'
@@ -378,6 +477,57 @@ jobs:
378477 echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT"
379478 exit 0
380479
480+ - name : Smoke test int URL
481+ if : github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
482+ id : smoke-int
483+ env :
484+ PREVIEW_URL : ${{ steps.names.outputs.int_preview_url }}
485+ run : |
486+ if [ -z "$PREVIEW_URL" ] || [ "$PREVIEW_URL" = "null" ]; then
487+ echo "Int URL missing"
488+ echo "http_status=missing" >> "$GITHUB_OUTPUT"
489+ echo "http_result=missing-url" >> "$GITHUB_OUTPUT"
490+ exit 0
491+ fi
492+
493+ # Reachability check: allow 404 (app routes might not exist yet) but fail otherwise
494+ printf '%s' "$_cds_pathology_dev_mtls_client1_key_secret" > /tmp/client1-key.pem
495+ printf '%s' "$_cds_pathology_dev_mtls_client1_key_public" > /tmp/client1-cert.pem
496+ STATUS=$(curl \
497+ --cert /tmp/client1-cert.pem \
498+ --key /tmp/client1-key.pem \
499+ --silent \
500+ --output /tmp/preview.headers \
501+ --write-out '%{http_code}' \
502+ --head \
503+ --max-time 30 \
504+ -X GET "$PREVIEW_URL"/_status || true)
505+ rm -f /tmp/client1-key.pem
506+ rm -f /tmp/client1-cert.pem
507+
508+ if [ "$STATUS" = "404" ]; then
509+ echo "Int responded with expected 404"
510+ echo "http_status=404" >> "$GITHUB_OUTPUT"
511+ echo "http_result=allowed-404" >> "$GITHUB_OUTPUT"
512+ exit 0
513+ fi
514+
515+ if [[ "$STATUS" =~ ^[0-9]{3}$ ]] && [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 400 ]; then
516+ echo "Int responded with status $STATUS"
517+ echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
518+ echo "http_result=success" >> "$GITHUB_OUTPUT"
519+ exit 0
520+ fi
521+
522+ echo "Int responded with unexpected status $STATUS"
523+ if [ -f /tmp/preview.headers ]; then
524+ echo "Response headers:"
525+ cat /tmp/preview.headers
526+ fi
527+ echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
528+ echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT"
529+ exit 0
530+
381531 - name : Get proxygen machine user details
382532 id : proxygen-machine-user
383533 uses : aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
@@ -397,6 +547,18 @@ jobs:
397547 proxygen-client-id : ${{ env.PROXYGEN_CLIENT_ID }}
398548 proxygen-api-name : ${{ env.PROXYGEN_API_NAME }}
399549
550+ - name : Deploy int preview API proxy
551+ if : github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
552+ uses : ./.github/actions/proxy/deploy-proxy
553+ with :
554+ mtls-secret-name : ${{ env.MTLS_SECRET_NAME }}
555+ target-url : ${{ steps.names.outputs.int_preview_url }}
556+ proxy-base-path : " ${{ env.PROXYGEN_API_NAME }}-pri-${{ github.event.pull_request.number }}"
557+ proxygen-key-secret : ${{ env._cds_pathology_dev_proxygen_proxygen_key_secret }}
558+ proxygen-key-id : ${{ env.PROXYGEN_KEY_ID }}
559+ proxygen-client-id : ${{ env.PROXYGEN_CLIENT_ID }}
560+ proxygen-api-name : ${{ env.PROXYGEN_API_NAME }}
561+
400562 - name : Tear down preview API proxy
401563 if : github.event.action == 'closed'
402564 uses : ./.github/actions/proxy/tear-down-proxy
@@ -407,6 +569,16 @@ jobs:
407569 proxygen-client-id : ${{ env.PROXYGEN_CLIENT_ID }}
408570 proxygen-api-name : ${{ env.PROXYGEN_API_NAME }}
409571
572+ - name : Tear down int preview API proxy
573+ if : github.event.action == 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
574+ uses : ./.github/actions/proxy/tear-down-proxy
575+ with :
576+ proxy-base-path : " ${{ env.PROXYGEN_API_NAME }}-pri-${{ github.event.pull_request.number }}"
577+ proxygen-key-secret : ${{ env._cds_pathology_dev_proxygen_proxygen_key_secret }}
578+ proxygen-key-id : ${{ env.PROXYGEN_KEY_ID }}
579+ proxygen-client-id : ${{ env.PROXYGEN_CLIENT_ID }}
580+ proxygen-api-name : ${{ env.PROXYGEN_API_NAME }}
581+
410582 - name : Retrieve Apigee Token
411583 id : apigee-token
412584 shell : bash
@@ -495,7 +667,7 @@ jobs:
495667 name : ${{ steps.create-name.outputs.artefact-name }}
496668 path : coverage-reports/
497669 - name : " SonarCloud Scan"
498- if : always() && github.event.action != 'closed'
670+ if : always() && github.event.action != 'closed' && github.actor != 'dependabot[bot]'
499671 uses : SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # 7.0.0
500672 env :
501673 SONAR_TOKEN : ${{ secrets.SONAR_TOKEN }}
@@ -510,18 +682,24 @@ jobs:
510682 uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
511683 with :
512684 script : |
513- const fn = '${{ steps.names.outputs.function_name }}';
514- const mock_fn = '${{ steps.names.outputs.mock_function_name }}';
685+ const fn = '${{ steps.names.outputs.function_name }}' || 'not-set';
686+ const mock_fn = '${{ steps.names.outputs.mock_function_name }}' || 'not-set';
687+ const int_fn = '${{ steps.names.outputs.int_function_name }}' || 'not-set';
515688 const url = '${{ steps.names.outputs.preview_url }}';
516689 const mock_url = '${{ steps.names.outputs.mock_preview_url }}';
690+ const int_url = '${{ steps.names.outputs.int_preview_url }}';
517691 const proxy_url = '${{ env.BASE_URL }}';
692+ const int_proxy_url = '${{ env.INT_BASE_URL}}';
693+ const isDependabotPr = '${{ github.event.pull_request.user.login }}' === 'dependabot[bot]';
518694 const owner = context.repo.owner;
519695 const repo = context.repo.repo;
520696 const issueNumber = context.issue.number;
521697 const smokeStatus = '${{ steps.smoke-test.outputs.http_status }}' || 'n/a';
522698 const smokeResult = '${{ steps.smoke-test.outputs.http_result }}' || 'not-run';
523699 const smokeMockStatus = '${{ steps.smoke-mock.outputs.http_status }}' || 'n/a';
524700 const smokeMockResult = '${{ steps.smoke-mock.outputs.http_result }}' || 'not-run';
701+ const smokeIntStatus = '${{ steps.smoke-int.outputs.http_status }}' || 'n/a';
702+ const smokeIntResult = '${{ steps.smoke-int.outputs.http_result }}' || 'not-run';
525703
526704 const smokeLabels = {
527705 success: ':white_check_mark: Passed',
@@ -532,6 +710,19 @@ jobs:
532710
533711 const smokeReadable = smokeLabels[smokeResult] ?? smokeResult;
534712 const smokeMockReadable = smokeLabels[smokeMockResult] ?? smokeMockResult;
713+ const smokeIntReadable = smokeLabels[smokeIntResult] ?? smokeIntResult;
714+ const intUrlDisplay = isDependabotPr
715+ ? 'Skipped for Dependabot PR'
716+ : `[${int_url}](${int_url}) — [Status](${int_url}/_status)`;
717+ const intSmokeDisplay = isDependabotPr
718+ ? 'Skipped for Dependabot PR'
719+ : `${smokeIntReadable} (HTTP ${smokeIntStatus})`;
720+ const intProxyDisplay = isDependabotPr
721+ ? 'Skipped for Dependabot PR'
722+ : `[${int_proxy_url}](${int_proxy_url})`;
723+ const intLambdaDisplay = isDependabotPr
724+ ? 'Skipped for Dependabot PR'
725+ : int_fn;
535726 const { data: comments } = await github.rest.issues.listComments({
536727 owner,
537728 repo,
@@ -554,13 +745,20 @@ jobs:
554745
555746 const lines = [
556747 '**Deployment Complete**',
557- `- Preview URL: [${url}](${url}) — [Status](${url}/_status)`,
558- ` - Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
559- `- Mock URL: [${mock_url}](${mock_url})`,
560- ` - Smoke Mock Test: ${smokeMockReadable} (HTTP ${smokeMockStatus})`,
561- `- Proxy URL: [${proxy_url}](${proxy_url})`,
562- `- Lambda Function: ${fn}`,
563- `- Mock Lambda Function: ${mock_fn}`,
748+ `- Preview with mock:`,
749+ ` - URL: [${url}](${url}) — [Status](${url}/_status)`,
750+ ` - Smoke test result: ${smokeReadable} (HTTP ${smokeStatus})`,
751+ ` - Proxy URL: [${proxy_url}](${proxy_url})`,
752+ ` - Lambda function: ${fn}`,
753+ `- Mock endpoints:`,
754+ ` - URL: [${mock_url}](${mock_url})`,
755+ ` - Smoke test result: ${smokeMockReadable} (HTTP ${smokeMockStatus})`,
756+ ` - Lambda function: ${mock_fn}`,
757+ `- Preview with integration:`,
758+ ` - URL: ${intUrlDisplay}`,
759+ ` - Smoke test result: ${intSmokeDisplay}`,
760+ ` - Proxy URL: ${intProxyDisplay}`,
761+ ` - Lambda function: ${intLambdaDisplay}`,
564762 ];
565763
566764 await github.rest.issues.createComment({
0 commit comments