Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/cicd-1-pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
pull_request:
types: [opened, reopened, synchronize]

permissions:
contents: read
id-token: write

jobs:
metadata:
name: "Set CI/CD metadata"
Expand Down Expand Up @@ -118,14 +122,15 @@ jobs:
build-image-stage: # Recommended maximum execution time is 3 minutes
name: "Image build stage"
needs: [metadata, commit-stage, test-stage, analysis-stage]
uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build-images.yaml@main
uses: ./.github/workflows/stage-3-build-images.yaml
if: needs.metadata.outputs.does_pull_request_exist == 'true' || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened'))
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
secrets: inherit
acceptance-stage: # Recommended maximum execution time is 10 minutes
name: "Acceptance stage"
Expand All @@ -141,6 +146,17 @@ jobs:
terraform_version: "${{ needs.metadata.outputs.terraform_version }}"
version: "${{ needs.metadata.outputs.version }}"
secrets: inherit
deploy-stage:
if: github.ref == 'refs/heads/main' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'deploy')
name: Deploy pr-${{ github.event.pull_request.number }} for commit ${{ github.event.pull_request.head.sha }}
needs: [acceptance-stage]
permissions:
id-token: write
uses: ./.github/workflows/stage-4-deploy.yaml
with:
environments: "[\"sandbox\"]"
commit_sha: ${{ github.event.pull_request.head.sha }}
secrets: inherit
validate-title-stage:
name: Validate PR title
runs-on: ubuntu-latest
Expand All @@ -152,7 +168,6 @@ jobs:
steps:
- uses: amannn/action-semantic-pull-request@v5
id: validate

- uses: thollander/actions-comment-pull-request@v2
if: ${{ failure() && steps.validate.conclusion == 'failure' }}
with:
Expand Down
279 changes: 279 additions & 0 deletions .github/workflows/stage-3-build-images.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
name: Docker Image CI

on:
push:
branches:
- main

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

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/deployments/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 }}
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
if: github.ref == 'refs/heads/main'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Azure Container Registry login
if: github.ref == 'refs/heads/main'
run: az acr login --name ${{ secrets.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}

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 }}
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="${{ secrets.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:${COMMIT_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
if [ "${GITHUB_REF}" == 'refs/heads/main' ]; then
docker push "${repo_name}:${COMMIT_HASH_TAG}"
if [ "${PR_NUM_TAG}" != 'pr' ]; then
docker push "${repo_name}:${PR_NUM_TAG}"
fi
docker push "${repo_name}:${ENVIRONMENT_TAG}"
fi

export SBOM_REPOSITORY_REPORT="sbom-${function}-repository-report"
echo "SBOM_REPOSITORY_REPORT=$SBOM_REPOSITORY_REPORT" >> $GITHUB_ENV
bash -x ${GITHUB_WORKSPACE}/templates/scripts/reports/create-sbom-report.sh

export VULNERABILITIES_REPOSITORY_REPORT="vulnerabilities-${function}-repository-report"
echo "VULNERABILITIES_REPOSITORY_REPORT=$VULNERABILITIES_REPOSITORY_REPORT" >> $GITHUB_ENV
bash -x ${GITHUB_WORKSPACE}/templates/scripts/reports/scan-vulnerabilities.sh

curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin

SCAN_RESULTS=$(grype "${PROJECT_NAME}-${function}:latest" --scope all-layers)
# ANSI color codes
RED="\033[0;31m"
RESET="\033[0m"

# Define your log file
VULNERABILITIES_SUMMARY_LOGFILE="${PROJECT_NAME}-${function}-vulnerabilities-summary.txt"
echo "VULNERABILITIES_SUMMARY_LOGFILE=$VULNERABILITIES_SUMMARY_LOGFILE" >> $GITHUB_ENV

# Clear existing log file (or create if it doesn't exist)
> "$VULNERABILITIES_SUMMARY_LOGFILE"

for SEVERITY in CRITICAL HIGH MEDIUM; do
{
echo ""
echo "${PROJECT_NAME}-${function}: vulnerabilities"
echo -e "=== ${RED}${SEVERITY}${RESET} Vulnerabilities list ==="
# If grep finds nothing, we print a fallback message
echo "$SCAN_RESULTS" | grep -i "$SEVERITY" || echo "No $SEVERITY vulnerabilities found."
} | tee -a "$VULNERABILITIES_SUMMARY_LOGFILE"
done

# Remove the image
docker rmi "${repo_name}:${COMMIT_HASH_TAG}"
docker rmi "${repo_name}:${PR_NUM_TAG}"
docker rmi "${repo_name}:${ENVIRONMENT_TAG}"
docker rmi ${PROJECT_NAME}-${function}:latest

- name: Compress SBOM report
shell: bash
run: |
echo SBOM_REPOSITORY_REPORT: ${SBOM_REPOSITORY_REPORT}
zip "${SBOM_REPOSITORY_REPORT}.json.zip" "${SBOM_REPOSITORY_REPORT}.json"

- name: Upload SBOM report as an artefact
uses: actions/upload-artifact@v4
with:
name: ${{ env.SBOM_REPOSITORY_REPORT }}.json.zip
path: ./${{ env.SBOM_REPOSITORY_REPORT }}.json.zip
retention-days: 21

- name: Compress vulnerabilities report
shell: bash
run: |
echo VULNERABILITIES_REPOSITORY_REPORT: ${VULNERABILITIES_REPOSITORY_REPORT}
zip ${VULNERABILITIES_REPOSITORY_REPORT}.json.zip ${VULNERABILITIES_REPOSITORY_REPORT}.json

- name: Upload vulnerabilities report as an artefact
uses: actions/upload-artifact@v4
with:
name: ${{ env.VULNERABILITIES_REPOSITORY_REPORT }}.json.zip
path: ./${{ env.VULNERABILITIES_REPOSITORY_REPORT }}.json.zip
retention-days: 21

- name: Upload vulnerabilities summary report as an artefact
uses: actions/upload-artifact@v4
with:
name: ${{ env.VULNERABILITIES_SUMMARY_LOGFILE }}
path: ./${{ env.VULNERABILITIES_SUMMARY_LOGFILE }}
retention-days: 21

aggregate-json:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Download SBOM JSON artifacts
uses: actions/download-artifact@v4
with:
path: ./downloaded-artifacts

- name: Combine sbom report JSON files
run: |
zip sbom-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip downloaded-artifacts/**/sbom*.json.zip

- name: Combine vulnerabilities report JSON files
run: |
zip vulnerabilities-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip downloaded-artifacts/**/vulnerabilities*.json.zip
zip vulnerabilities-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip downloaded-artifacts/**/*vulnerabilities-summary*.txt

- name: Upload sbom zip file
uses: actions/upload-artifact@v4
with:
name: aggregated-sbom-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip
path: sbom-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip

- name: Upload repository zip file
uses: actions/upload-artifact@v4
with:
name: aggregated-vulnerabilities-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip
path: vulnerabilities-repository-report-${{ needs.build-and-push.outputs.PR_NUM_TAG }}.zip
27 changes: 17 additions & 10 deletions .github/workflows/stage-4-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
strategy:
matrix:
environment: ${{ fromJson(inputs.environments) }}
Expand All @@ -46,12 +49,15 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5

- uses: azure/login@bbcc074a232a35d7283353c18aabf0de1d557775
- uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Make script executable
run: chmod +x scripts/bash/wait_ado_pipeline.sh

- name: Call deployment pipeline
run: |
# Define common variables
Expand All @@ -68,15 +74,16 @@ jobs:
["prod"]=""
)

parameters='dockerImageTag=${{inputs.commit_sha}} retagImages=false testTypes=[${test_types[matrix.environment]}]'
git_short_sha=$(git rev-parse --short ${{inputs.commit_sha}})
parameters="dockerImageTag=${git_short_sha} testTypes=[${test_types[${{ matrix.environment }}]}]"

echo "Starting Azure devops pipeline \"Deploy to Azure - Core ${{ matrix.environment }}\"..."
# RUN_ID=$(az pipelines run \
# --commit-id ${{inputs.commit_sha}} \
# --name "Deploy to Azure - Core ${{ matrix.environment }}" \
# --org "${organisation}" \
# --project "${project_name}" \
# --parameters $parameters \
# --output tsv --query id)
RUN_ID=$(az pipelines run \
--commit-id ${{inputs.commit_sha}} \
--name "Deploy to Azure - Core ${{ matrix.environment }}" \
--org "${organisation}" \
--project "${project_name}" \
--parameters $parameters \
--output tsv --query id)

# scripts/bash/wait_ado_pipeline.sh "$RUN_ID" https://dev.azure.com/nhse-dtos "${project_name}"
scripts/bash/wait_ado_pipeline.sh "$RUN_ID" https://dev.azure.com/nhse-dtos "${project_name}" 900
2 changes: 1 addition & 1 deletion scripts/bash/wait_ado_pipeline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ set -euo pipefail
RUN_ID="$1"
ORG_URL="$2"
PROJECT="$3"
TIMEOUT_SECONDS="${4:-300}" # Default to 5 minutes if not provided

SLEEP_TIME=15
TIMEOUT_SECONDS=300

echo "Waiting for Azure DevOps pipeline run $RUN_ID to complete..."

Expand Down
Loading