Feature/cdapi 95 #160
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- | |
| 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 |