Skip to content

Feature/cdapi 95

Feature/cdapi 95 #160

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-
PYTHON_VERSION: 3.14
LAMBDA_RUNTIME: python3.14
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 }}
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
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: "Setup Python project"
uses: ./.github/actions/setup-python-project
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Package artifact
run: |
make build
- name: Select AWS role inputs
id: role-select
run: |
if [ "${{ github.actor }}" = "dependabot[bot]" ]; then
echo "aws_role=${{ secrets.DEPENDABOT_AWS_ROLE_ARN }}" >> "$GITHUB_OUTPUT"
echo "lambda_role=${{ secrets.DEPENDABOT_LAMBDA_ROLE_ARN }}" >> "$GITHUB_OUTPUT"
else
echo "aws_role=${{ secrets.AWS_ROLE_ARN }}" >> "$GITHUB_OUTPUT"
echo "lambda_role=${{ secrets.LAMBDA_ROLE_ARN }}" >> "$GITHUB_OUTPUT"
fi
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@a7a2c1125c67f40a1e95768f4e4a7d8f019f87af
with:
role-to-assume: ${{ steps.role-select.outputs.aws_role }}
aws-region: ${{ env.AWS_REGION }}
- name: Sanitize branch name
id: branch
run: |
branch="${{ github.head_ref }}"
if [ -z "$branch" ]; then branch="${{ github.ref_name }}"; fi
safe=$(echo "$branch" | sed -E 's/[^a-zA-Z0-9._-]+/-/g' | tr '[:upper:]' '[:lower:]')
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 }}
MAX_FN_LEN=64
MAX_SAFE_LEN=$((MAX_FN_LEN - ${#PREFIX}))
if [ ${#SAFE} -gt "$MAX_SAFE_LEN" ]; then
SAFE=${SAFE:0:MAX_SAFE_LEN}
fi
FN="${PREFIX}${SAFE}"
echo "function_name=$FN" >> "$GITHUB_OUTPUT"
URL="https://${SAFE}.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
echo "preview_url=$URL" >> "$GITHUB_OUTPUT"
- name: Create or update preview Lambda (on open/sync/reopen)
if: github.event.action != 'closed'
run: |
cd pathology-api/target/
FN="${{ steps.names.outputs.function_name }}"
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 }}" || 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 }}" \
--publish
wait_for_lambda_ready
fi
- name: Delete preview Lambda (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
run: |
echo "function = ${{ steps.names.outputs.function_name }}"
echo "url = ${{ steps.names.outputs.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: 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: 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: 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 }}';
const url = '${{ steps.names.outputs.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 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 { 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 URL: [${url}](${url}) — [Status endpoint](${url}/_status)`,
`- Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
`- Proxy URL: [${proxy_url}](${proxy_url})`,
`- Lambda Function: ${fn}`,
];
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