From 366911d3c3ce332d79c0c3de57718741a49cfe6d Mon Sep 17 00:00:00 2001 From: Maciej Murawski Date: Fri, 5 Dec 2025 12:52:57 +0000 Subject: [PATCH] feat: DevTest workflows set, triggered by a commit with devtest tag --- .../cicd-1-pull-request-devtest.yaml | 164 +++++++++++++- .../stage-3-build-images-devtest.yaml | 202 ++++++++++++++++++ .github/workflows/stage-4-deploy-devtest.yaml | 118 ++++++++++ infrastructure/tf-core/diagnostic_settings.tf | 4 + .../tf-core/environments/development.tfvars | 2 +- 5 files changed, 479 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/stage-3-build-images-devtest.yaml create mode 100644 .github/workflows/stage-4-deploy-devtest.yaml diff --git a/.github/workflows/cicd-1-pull-request-devtest.yaml b/.github/workflows/cicd-1-pull-request-devtest.yaml index c9b312ff74..c07ee39658 100644 --- a/.github/workflows/cicd-1-pull-request-devtest.yaml +++ b/.github/workflows/cicd-1-pull-request-devtest.yaml @@ -1,20 +1,164 @@ -name: CI/CD pull request - devtest +name: 'CI/CD pull request - devtest' on: push: tags: - 'devtest' +permissions: + contents: read + id-token: write + pull-requests: write + jobs: - print-debug-info: + metadata: + name: "Set CI/CD metadata" runs-on: ubuntu-latest + timeout-minutes: 1 + permissions: + pull-requests: read + outputs: + build_datetime_london: ${{ steps.variables.outputs.build_datetime_london }} + build_datetime: ${{ steps.variables.outputs.build_datetime }} + build_timestamp: ${{ steps.variables.outputs.build_timestamp }} + build_epoch: ${{ steps.variables.outputs.build_epoch }} + nodejs_version: ${{ steps.variables.outputs.nodejs_version }} + python_version: ${{ steps.variables.outputs.python_version }} + terraform_version: ${{ steps.variables.outputs.terraform_version }} + environment_tag: ${{ steps.variables.outputs.environment_tag }} + version: ${{ steps.variables.outputs.version }} + does_pull_request_exist: ${{ steps.pr_exists.outputs.does_pull_request_exist }} steps: - - name: Display Information + - name: "Checkout code" + uses: actions/checkout@v4 + with: + submodules: 'true' + - name: "Set CI/CD variables" + id: variables + run: | + datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') + BUILD_DATETIME=$datetime make version-create-effective-file + echo "build_datetime_london=$(TZ=Europe/London date --date=$datetime +'%Y-%m-%dT%H:%M:%S%z')" >> $GITHUB_OUTPUT + echo "build_datetime=$datetime" >> $GITHUB_OUTPUT + echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT + echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT + echo "nodejs_version=$(grep "^nodejs" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + echo "python_version=$(grep "^nodejs" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + echo "terraform_version=$(grep "^terraform" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "environment_tag=development" >> $GITHUB_OUTPUT + - name: "Check if pull request exists for this branch" + id: pr_exists + env: + GH_TOKEN: ${{ github.token }} + run: | + branch_name=${GITHUB_HEAD_REF:-$(echo $GITHUB_REF | sed 's#refs/heads/##')} + echo "Current branch is '$branch_name'" + if gh pr list --head $branch_name | grep -q .; then + echo "Pull request exists" + echo "does_pull_request_exist=true" >> $GITHUB_OUTPUT + else + echo "Pull request doesn't exist" + echo "does_pull_request_exist=false" >> $GITHUB_OUTPUT + fi + - name: "List variables" run: | - echo "Workflow triggered by tag!" - echo "--------------------------------------" - echo "Tag/Ref Name : $GITHUB_REF" - echo "Commit Hash : $GITHUB_SHA" - echo "Triggered By : $GITHUB_ACTOR" - echo "Event Name : $GITHUB_EVENT_NAME" - echo "--------------------------------------" + export BUILD_DATETIME_LONDON="${{ steps.variables.outputs.build_datetime_london }}" + export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" + export BUILD_TIMESTAMP="${{ steps.variables.outputs.build_timestamp }}" + export BUILD_EPOCH="${{ steps.variables.outputs.build_epoch }}" + export NODEJS_VERSION="${{ steps.variables.outputs.nodejs_version }}" + export PYTHON_VERSION="${{ steps.variables.outputs.python_version }}" + export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}" + export ENVIRONMENT_TAG="${{ steps.variables.outputs.environment_tag }}" + export VERSION="${{ steps.variables.outputs.version }}" + export DOES_PULL_REQUEST_EXIST="${{ steps.pr_exists.outputs.does_pull_request_exist }}" + make list-variables + commit-stage: # Recommended maximum execution time is 2 minutes + name: "Commit stage" + needs: [metadata] + uses: ./.github/workflows/stage-1-commit.yaml + with: + build_datetime: "${{ needs.metadata.outputs.build_datetime }}" + build_timestamp: "${{ needs.metadata.outputs.build_timestamp }}" + build_epoch: "${{ needs.metadata.outputs.build_epoch }}" + nodejs_version: "${{ needs.metadata.outputs.nodejs_version }}" + python_version: "${{ needs.metadata.outputs.python_version }}" + terraform_version: "${{ needs.metadata.outputs.terraform_version }}" + version: "${{ needs.metadata.outputs.version }}" + test-stage: # Recommended maximum execution time is 5 minutes + name: 'Test stage' + needs: [metadata] + uses: ./.github/workflows/stage-2-test.yaml + with: + unit_test_dir: tests/UnitTests + app_dir: application/CohortManager + build_datetime: '${{ needs.metadata.outputs.build_datetime }}' + build_timestamp: '${{ needs.metadata.outputs.build_timestamp }}' + build_epoch: '${{ needs.metadata.outputs.build_epoch }}' + nodejs_version: '${{ needs.metadata.outputs.nodejs_version }}' + python_version: '${{ needs.metadata.outputs.python_version }}' + terraform_version: '${{ needs.metadata.outputs.terraform_version }}' + version: '${{ needs.metadata.outputs.version }}' + secrets: inherit + analysis-stage: # Recommended maximum execution time is 5 minutes + name: "Analysis stage" + needs: [metadata, commit-stage, test-stage] + uses: ./.github/workflows/stage-2-analyse.yaml + secrets: + sonar_token: ${{ secrets.SONAR_TOKEN }} + with: + unit_test_dir: tests/UnitTests + build_datetime: "${{ needs.metadata.outputs.build_datetime }}" + build_timestamp: "${{ needs.metadata.outputs.build_timestamp }}" + build_epoch: "${{ needs.metadata.outputs.build_epoch }}" + nodejs_version: "${{ needs.metadata.outputs.nodejs_version }}" + python_version: "${{ needs.metadata.outputs.python_version }}" + terraform_version: "${{ needs.metadata.outputs.terraform_version }}" + version: "${{ needs.metadata.outputs.version }}" + build-image-stage: # Recommended maximum execution time is 3 minutes + name: "Image build stage" + needs: [metadata, commit-stage, test-stage, analysis-stage] + uses: ./.github/workflows/stage-3-build-images-devtest.yaml + secrets: + client_id: ${{ secrets.AZURE_CLIENT_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + acr_name: ${{ secrets.ACR_NAME }} + with: + docker_compose_file: application/CohortManager/compose.yaml + excluded_containers_csv_list: azurite,azurite-setup,sql-server + environment_tag: ${{ needs.metadata.outputs.environment_tag }} + function_app_source_code_path: application/CohortManager/src + project_name: cohort-manager + build_all_images: true + deploy-stage: + if: github.event_name == 'push' + name: Deploy DevTest environment for commit ${{ github.sha }} + needs: [metadata, build-image-stage] + permissions: + id-token: write + contents: read + uses: ./.github/workflows/stage-4-deploy-devtest.yaml + secrets: + client_id: ${{ secrets.AZURE_CLIENT_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + with: + environments: "[\"development\"]" + commit_sha: ${{ github.sha }} + name: Validate PR title + runs-on: ubuntu-latest + permissions: + pull-requests: write + env: + GITHUB_TOKEN: ${{ github.token }} + if: github.event_name == 'push' + steps: + - uses: amannn/action-semantic-pull-request@v5 + id: validate + - uses: thollander/actions-comment-pull-request@v2 + if: ${{ failure() && steps.validate.conclusion == 'failure' }} + with: + message: | + Your Pull Request title must meet the conventional commit standards, please see the following documentation - https://www.conventionalcommits.org/en/v1.0.0/#specification diff --git a/.github/workflows/stage-3-build-images-devtest.yaml b/.github/workflows/stage-3-build-images-devtest.yaml new file mode 100644 index 0000000000..1d3b604b90 --- /dev/null +++ b/.github/workflows/stage-3-build-images-devtest.yaml @@ -0,0 +1,202 @@ +name: 'Docker Image CI - devtest' + +on: + workflow_call: + inputs: + environment_tag: + description: Environment of the deployment + required: true + type: string + default: development + docker_compose_file: + description: The path of the compose.yaml file needed to build docker images + required: true + type: string + function_app_source_code_path: + description: The source path of the function app source code for the docker builds + required: true + type: string + project_name: + description: The name of the project + required: true + type: string + excluded_containers_csv_list: + description: Excluded containers in a comma separated list + required: true + type: string + build_all_images: + description: Build all images (true) or only changed ones (false) + required: false + type: boolean + default: false + + secrets: + client_id: + description: 'The Azure Client ID.' + required: true + tenant_id: + description: 'The Azure Tenant ID.' + required: true + subscription_id: + description: 'The Azure Subscription ID.' + required: true + acr_name: + description: 'The name of the Azure Container Registry.' + required: true + +jobs: + get-functions: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + id-token: write + outputs: + FUNC_NAMES: ${{ steps.get-function-names.outputs.FUNC_NAMES }} + DOCKER_COMPOSE_DIR: ${{ steps.get-function-names.outputs.DOCKER_COMPOSE_DIR }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout dtos-devops-templates repository + uses: actions/checkout@v4 + with: + repository: NHSDigital/dtos-devops-templates + path: templates + ref: main + + - name: Determine which Docker container(s) to build + id: get-function-names + env: + COMPOSE_FILES_CSV: ${{ inputs.docker_compose_file }} + EXCLUDED_CONTAINERS_CSV: ${{ inputs.excluded_containers_csv_list }} + SOURCE_CODE_PATH: ${{ inputs.function_app_source_code_path }} + MANUAL_BUILD_ALL: ${{ inputs.build_all_images || false }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash scripts/deployment/get-docker-names.sh + + build-and-push: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: read + needs: get-functions + strategy: + matrix: + function: ${{ fromJSON(needs.get-functions.outputs.FUNC_NAMES) }} + if: needs.get-functions.outputs.FUNC_NAMES != '[]' + outputs: + pr_num_tag: ${{ env.PR_NUM_TAG }} + short_commit_hash: ${{ env.COMMIT_HASH_TAG }} + devtest_commit_hash: ${{ env.DEVTEST_HASH_TAG }} + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 1 + submodules: 'true' + + - name: Checkout dtos-devops-templates repository + uses: actions/checkout@v4 + with: + repository: NHSDigital/dtos-devops-templates + path: templates + ref: main + + - name: Az CLI login + uses: azure/login@v2 + with: + client-id: ${{ secrets.client_id }} + tenant-id: ${{ secrets.tenant_id }} + subscription-id: ${{ secrets.subscription_id }} + + - name: Azure Container Registry login + env: + ACR_NAME: ${{ secrets.acr_name }} + run: az acr login --name ${ACR_NAME} + + - name: Create Tags + env: + GH_TOKEN: ${{ github.token }} + ENVIRONMENT_TAG: ${{ inputs.environment_tag }} + continue-on-error: false + run: | + echo "The branch is: ${GITHUB_REF}" + + if [[ "${GITHUB_REF}" == refs/pull/*/merge ]]; then + PR_NUM_TAG=$(echo "${GITHUB_REF}" | sed 's/refs\/pull\/\([0-9]*\)\/merge/\1/') + else + PULLS_JSON=$(gh api /repos/{owner}/{repo}/commits/${GITHUB_SHA}/pulls) + ORIGINATING_BRANCH=$(echo ${PULLS_JSON} | jq -r '.[].head.ref' | python3 -c "import sys, urllib.parse; print(urllib.parse.quote_plus(sys.stdin.read().strip()))") + echo "ORIGINATING_BRANCH: ${ORIGINATING_BRANCH}" + PR_NUM_TAG=$(echo ${PULLS_JSON} | jq -r '.[].number') + fi + + echo "PR_NUM_TAG: pr${PR_NUM_TAG}" + echo "PR_NUM_TAG=pr${PR_NUM_TAG}" >> ${GITHUB_ENV} + + SHORT_COMMIT_HASH=$(git rev-parse --short ${GITHUB_SHA}) + echo "Commit hash tag: ${SHORT_COMMIT_HASH}" + echo "COMMIT_HASH_TAG=${SHORT_COMMIT_HASH}" >> ${GITHUB_ENV} + + DEVTEST_COMMIT_HASH="devtest_${SHORT_COMMIT_HASH}" + echo "Commit devtest hash tag: ${DEVTEST_COMMIT_HASH}" + echo "DEVTEST_HASH_TAG=${DEVTEST_COMMIT_HASH}" >> ${GITHUB_ENV} + + echo "ENVIRONMENT_TAG=${ENVIRONMENT_TAG}" >> ${GITHUB_ENV} + + - name: Build and Push Image + working-directory: ${{ steps.get-function-names.outputs.DOCKER_COMPOSE_DIR }} + continue-on-error: false + env: + COMPOSE_FILE: ${{ inputs.docker_compose_file }} + PROJECT_NAME: ${{ inputs.project_name }} + ACR_NAME: ${{ secrets.acr_name }} + run: | + function=${{ matrix.function }} + + echo PROJECT_NAME: ${PROJECT_NAME} + + if [ -z "${function}" ]; then + echo "Function variable is empty. Skipping Docker build." + exit 0 + fi + + # Build the image + docker compose -f ${COMPOSE_FILE//,/ -f } -p ${PROJECT_NAME} --profile "*" build --no-cache --pull ${function} + + repo_name="${ACR_NAME}.azurecr.io/${PROJECT_NAME}-${function}" + echo $(repo_name) + + # Tag the image + echo "Tag the image:" + docker tag ${PROJECT_NAME}-${function}:latest "$repo_name:${DEVTEST_HASH_TAG}" + # docker tag ${PROJECT_NAME}-${function}:latest "$repo_name:${PR_NUM_TAG}" + # docker tag ${PROJECT_NAME}-${function}:latest "$repo_name:${ENVIRONMENT_TAG}" + + # If this variable is set, the create-sbom-report.sh script will scan this docker image instead. + export CHECK_DOCKER_IMAGE=${PROJECT_NAME}-${function}:latest + export FORCE_USE_DOCKER=true + + export PR_NUM_TAG=${PR_NUM_TAG} + echo "PR_NUM_TAG=${PR_NUM_TAG}" >> ${GITHUB_ENV} + + # Push the image to the repository + docker push "${repo_name}:${DEVTEST_HASH_TAG}" + + - name: Cleanup the docker images + env: + PROJECT_NAME: ${{ inputs.project_name }} + ACR_NAME: ${{ secrets.acr_name }} + run: | + function=${{ matrix.function }} + repo_name="${ACR_NAME}.azurecr.io/${PROJECT_NAME}-${function}" + + # Remove the images + docker rmi "${repo_name}:${DEVTEST_HASH_TAG}" + # docker rmi "${repo_name}:${PR_NUM_TAG}" + # docker rmi "${repo_name}:${ENVIRONMENT_TAG}" + docker rmi ${PROJECT_NAME}-${function}:latest diff --git a/.github/workflows/stage-4-deploy-devtest.yaml b/.github/workflows/stage-4-deploy-devtest.yaml new file mode 100644 index 0000000000..96f156e349 --- /dev/null +++ b/.github/workflows/stage-4-deploy-devtest.yaml @@ -0,0 +1,118 @@ +name: Deployment stage + +on: + workflow_call: + inputs: + environments: + description: List of environments to deploy to (String array) + required: true + type: string + commit_sha: + description: Commit SHA used to fetch ADO pipeline and docker image + required: true + type: string + pr_number: + description: Pull request number when used in a pull request + required: false + type: string + + secrets: + client_id: + description: 'The Azure Client ID.' + required: true + tenant_id: + description: 'The Azure Tenant ID.' + required: true + subscription_id: + description: 'The Azure Subscription ID.' + required: true + + workflow_dispatch: + inputs: + environments: + description: List of environments to deploy to (String array) + required: true + type: string + commit_sha: + description: Commit SHA used to fetch ADO pipeline and docker image + required: true + type: string + pr_number: + description: Pull request number when used in a pull request + required: false + type: string + + secrets: + client_id: + description: 'The Azure Client ID.' + required: true + tenant_id: + description: 'The Azure Tenant ID.' + required: true + subscription_id: + description: 'The Azure Subscription ID.' + required: true + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + environment: ${{ fromJson(inputs.environments) }} + max-parallel: 1 + environment: ${{ matrix.environment }} + concurrency: deploy-${{ matrix.environment }}-${{ github.ref }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 + with: + client-id: ${{ secrets.client_id }} + tenant-id: ${{ secrets.tenant_id }} + subscription-id: ${{ secrets.subscription_id }} + + - name: Make script executable + run: chmod +x scripts/bash/wait_ado_pipeline.sh + + - name: Call deployment pipeline + shell: bash + env: + COMMIT_SHA: ${{ inputs.commit_sha }} + ENVIRONMENT_NAME: ${{ matrix.environment }} + run: | + set -euo pipefail + # Define common variables + organisation='https://dev.azure.com/nhse-dtos' + project_name='dtos-cohort-manager' + + # Define which tests to run based on the environment + declare -A test_types=( + ["development"]="" + ) + + # Derive the short SHA from the provided commit + git_short_sha=$(git rev-parse --short "$COMMIT_SHA") + devtest_short_sha="devtest_${git_short_sha}" + + # Prepare parameters as separate key=value tokens + param_image="dockerImageTag=${devtest_short_sha}" + param_tests="testTypes=[${test_types[$ENVIRONMENT_NAME]}]" + + echo "Starting Azure devops pipeline \"Deploy to Azure - Core ${ENVIRONMENT_NAME}\"..." + RUN_ID=$(az pipelines run \ + --commit-id "$COMMIT_SHA" \ + --name "Deploy to Azure - Core ${ENVIRONMENT_NAME}" \ + --org "${organisation}" \ + --project "${project_name}" \ + --parameters "$param_image" "$param_tests" \ + --output tsv --query id) + + echo "Click here to view the ADO pipeline: ${organisation}/${project_name}/_build/results?buildId=${RUN_ID}" + + scripts/bash/wait_ado_pipeline.sh "$RUN_ID" "${organisation}" "${project_name}" 1800 diff --git a/infrastructure/tf-core/diagnostic_settings.tf b/infrastructure/tf-core/diagnostic_settings.tf index 8309155bac..ff59480f8e 100644 --- a/infrastructure/tf-core/diagnostic_settings.tf +++ b/infrastructure/tf-core/diagnostic_settings.tf @@ -1,4 +1,8 @@ locals { + # ACR + monitor_diagnostic_setting_acr_enabled_logs = ["ContainerRegistryRepositoryEvents", "ContainerRegistryLoginEvents"] + monitor_diagnostic_setting_acr_metrics = ["AllMetrics"] + # APPSERVICEPLAN monitor_diagnostic_setting_appserviceplan_metrics = ["AllMetrics"] diff --git a/infrastructure/tf-core/environments/development.tfvars b/infrastructure/tf-core/environments/development.tfvars index e45814c918..391a7ad8e0 100644 --- a/infrastructure/tf-core/environments/development.tfvars +++ b/infrastructure/tf-core/environments/development.tfvars @@ -867,7 +867,7 @@ function_apps = { ] env_vars_static = { AcceptableLatencyThresholdMs = "500" - MaxRetryCount=3 + MaxRetryCount = 3 } }