diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f519b68..18a06ba 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,6 +2,11 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu # provide DOCKER_GID via build args if you need to force group id to match host ARG DOCKER_GID +ARG TARGETARCH +ENV TARGETARCH=${TARGETARCH} + +ARG ASDF_VERSION +COPY .tool-versions.asdf /tmp/.tool-versions.asdf # specify DOCKER_GID to force container docker group id to match host RUN if [ -n "${DOCKER_GID}" ]; then \ @@ -27,12 +32,35 @@ RUN apt-get update \ libreadline-dev libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev \ xz-utils tk-dev liblzma-dev netcat-traditional libyaml-dev -USER vscode +# Download correct AWS CLI for arch +RUN if [ "$TARGETARCH" = "arm64" ] || [ "$TARGETARCH" == "aarch64" ]; then \ + wget -O /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip"; \ + else \ + wget -O /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"; \ + fi && \ + unzip /tmp/awscliv2.zip -d /tmp/aws-cli && \ + /tmp/aws-cli/aws/install && \ + rm /tmp/awscliv2.zip && rm -rf /tmp/aws-cli + +# Download correct SAM CLI for arch +RUN if [ "$TARGETARCH" = "arm64" ] || [ "$TARGETARCH" == "aarch64" ]; then \ + wget -O /tmp/aws-sam-cli.zip "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-arm64.zip"; \ + else \ + wget -O /tmp/aws-sam-cli.zip "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip"; \ + fi && \ + unzip /tmp/aws-sam-cli.zip -d /tmp/aws-sam-cli && \ + /tmp/aws-sam-cli/install && \ + rm /tmp/aws-sam-cli.zip && rm -rf /tmp/aws-sam-cli # Install ASDF -RUN git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.3 && \ - echo '. $HOME/.asdf/asdf.sh' >> ~/.bashrc && \ - echo '. $HOME/.asdf/completions/asdf.bash' >> ~/.bashrc +RUN ASDF_VERSION=$(awk '!/^#/ && NF {print $1; exit}' /tmp/.tool-versions.asdf) && \ + if [ "$TARGETARCH" = "arm64" ] || [ "$TARGETARCH" = "aarch64" ]; then \ + wget -O /tmp/asdf.tar.gz https://github.com/asdf-vm/asdf/releases/download/v${ASDF_VERSION}/asdf-v${ASDF_VERSION}-linux-arm64.tar.gz; \ + else \ + wget -O /tmp/asdf.tar.gz https://github.com/asdf-vm/asdf/releases/download/v${ASDF_VERSION}/asdf-v${ASDF_VERSION}-linux-amd64.tar.gz; \ + fi && \ + tar -xvzf /tmp/asdf.tar.gz && \ + mv asdf /usr/bin ENV PATH="$PATH:/home/vscode/.asdf/bin/:/workspaces/eps-prescription-tracker-ui/node_modules/.bin:/workspaces/eps-common-workflows/.venv/bin" @@ -49,5 +77,4 @@ ADD .tool-versions /workspaces/eps-common-workflows/.tool-versions ADD .tool-versions /home/vscode/.tool-versions RUN asdf install python && \ - asdf install && \ - asdf reshim nodejs + asdf install diff --git a/.gitallowed b/.gitallowed index e7593cf..10c7e75 100644 --- a/.gitallowed +++ b/.gitallowed @@ -1,3 +1,4 @@ token: ?"?\$\{\{\s*secrets\.GITHUB_TOKEN\s*\}\}"? .*\.gitallowed.* id-token: write +password: \${{ secrets\.GITHUB_TOKEN }} diff --git a/.github/scripts/check_ecr_image_scan_results.sh b/.github/scripts/check_ecr_image_scan_results.sh new file mode 100755 index 0000000..a43e1d5 --- /dev/null +++ b/.github/scripts/check_ecr_image_scan_results.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -e + +AWS_MAX_ATTEMPTS=20 +export AWS_MAX_ATTEMPTS + +if [ -z "${REPOSITORY_NAME}" ]; then + echo "REPOSITORY_NAME not set" + exit 1 +fi + +if [ -z "${IMAGE_TAG}" ]; then + echo "IMAGE_TAG not set" + exit 1 +fi + +if [ -z "${AWS_REGION}" ]; then + echo "AWS_REGION not set" + exit 1 +fi + +if [ -z "${ACCOUNT_ID}" ]; then + echo "ACCOUNT_ID not set" + exit 1 +fi + +IMAGE_DIGEST=$(aws ecr describe-images \ + --repository-name "$REPOSITORY_NAME" \ + --image-ids imageTag="$IMAGE_TAG" \ + --query 'imageDetails[0].imageDigest' \ + --output text) + +RESOURCE_ARN="arn:aws:ecr:${AWS_REGION}:${ACCOUNT_ID}:repository/${REPOSITORY_NAME}/${IMAGE_DIGEST}" + +echo "Monitoring scan for ${REPOSITORY_NAME}:${IMAGE_TAG}" +echo "Resource ARN: ${RESOURCE_ARN}" +echo + +# Wait for ECR scan to reach COMPLETE +STATUS="" +echo "Waiting for ECR scan to complete..." +for i in {1..30}; do + echo "Checking scan status. Attempt ${i}" + STATUS=$(aws ecr describe-image-scan-findings \ + --repository-name "$REPOSITORY_NAME" \ + --image-id imageDigest="$IMAGE_DIGEST" \ + --query 'imageScanStatus.status' \ + --output text 2>/dev/null || echo "NONE") + + if [[ "$STATUS" == "COMPLETE" ]]; then + echo "ECR scan completed." + break + fi + + if [[ "$STATUS" == "FAILED" ]]; then + echo "Scan failed." + exit 1 + fi + + echo "SCAN IS NOT YET COMPLETE. Waiting 10 seconds before checking again..." + sleep 10 +done + +if [[ "$STATUS" != "COMPLETE" ]]; then + echo "Timeout waiting for ECR scan to complete." + exit 1 +fi + +# Wait for Inspector2 findings to appear & stabilize +# this is in place as scan may show as complete but findings have not yet stabilize +echo +echo "Waiting for Inspector2 findings to stabilize..." + +PREV_HASH="" +for i in {1..12}; do # ~2 minutes max + FINDINGS=$(aws inspector2 list-findings \ + --filter-criteria "{ + \"resourceId\": [{\"comparison\": \"EQUALS\", \"value\": \"${RESOURCE_ARN}\"}], + \"findingStatus\": [{\"comparison\": \"EQUALS\", \"value\": \"ACTIVE\"}] + }" \ + --output json 2>/dev/null || echo "{}") + + CURR_HASH=$(echo "$FINDINGS" | sha256sum) + COUNT=$(echo "$FINDINGS" | jq '.findings | length') + + if [[ "$COUNT" -gt 0 && "$CURR_HASH" == "$PREV_HASH" ]]; then + echo "Findings stabilized ($COUNT findings)." + break + fi + + PREV_HASH="$CURR_HASH" + echo "Attempt: ${i}. Still waiting... (${COUNT} findings so far)" + sleep 10 +done + +# Extract counts and display findings +echo +echo "Final Inspector2 findings with suppressions removed:" +echo + +echo "$FINDINGS" | jq '{ + findings: [ + .findings[]? | { + severity: .severity, + title: .title, + package: .packageVulnerabilityDetails.vulnerablePackages[0].name, + sourceUrl: .packageVulnerabilityDetails.sourceUrl, + recommendation: (.remediation.recommendation.text // "N/A") + } + ] +}' + +echo + +# Check for critical/high severity +CRITICAL_COUNT=$(echo "$FINDINGS" | jq '[.findings[]? | select(.severity=="CRITICAL")] | length') +HIGH_COUNT=$(echo "$FINDINGS" | jq '[.findings[]? | select(.severity=="HIGH")] | length') + +if (( CRITICAL_COUNT > 0 || HIGH_COUNT > 0 )); then + echo "${CRITICAL_COUNT} CRITICAL and ${HIGH_COUNT} HIGH vulnerabilities detected!" + echo + echo "Critical/High vulnerabilities:" + echo "$FINDINGS" | jq -r ' + .findings[]? | + select(.severity=="CRITICAL" or .severity=="HIGH") |{ + severity: .severity, + title: .title, + package: .packageVulnerabilityDetails.vulnerablePackages[0].name, + sourceUrl: .packageVulnerabilityDetails.sourceUrl, + recommendation: (.remediation.recommendation.text // "N/A") + }' + echo + echo "Failing pipeline due to Critical/High vulnerabilities." + exit 2 +else + echo "No Critical or High vulnerabilities found." + exit 0 +fi diff --git a/.github/workflows/build_and_push_docker_image.yml b/.github/workflows/build_and_push_docker_image.yml new file mode 100644 index 0000000..64f3295 --- /dev/null +++ b/.github/workflows/build_and_push_docker_image.yml @@ -0,0 +1,184 @@ +name: Build and push docker image + +on: + workflow_call: + secrets: + PUSH_IMAGE_ROLE: + required: true + inputs: + container_ecr: + type: string + description: "The name of the ECR repository to push the container image to." + required: true + container_image_tag: + type: string + description: "The tag to use for the container image." + required: true + docker_file: + type: string + description: "The Dockerfile to use for building the container image." + required: true + check_ecr_image_scan_results_script_tag: + type: string + description: "The tag to download check_ecr_image_scan_results.sh script." + required: false + default: "dev_container_build" + +jobs: + build_image_amd64: + permissions: + id-token: write + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Build container + run: | + docker build -f "${DOCKER_FILE}" -t amd64-image . + env: + DOCKER_FILE: ${{ inputs.docker_file }} + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 + id: connect-aws-deploy + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.PUSH_IMAGE_ROLE }} + role-session-name: dev-container-build-amd64 + output-credentials: true + + - name: Retrieve AWS Account ID + id: retrieve-deploy-account-id + run: | + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "account_id=$ACCOUNT_ID" >> "$GITHUB_OUTPUT" + + - name: Login to Amazon ECR + run: | + aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin ${{ steps.retrieve-deploy-account-id.outputs.account_id }}.dkr.ecr.eu-west-2.amazonaws.com + + - name: Push amd64 image to Amazon ECR + env: + ECR_REPOSITORY: ${{ inputs.container_ecr }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + run: | + docker tag "amd64-image" "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" + docker push "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" + + - name: Check dev container scan results + env: + REPOSITORY_NAME: ${{ inputs.container_ecr }} + IMAGE_TAG: ${{ inputs.container_image_tag }}-amd64 + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + SCRIPT_TAG: ${{ inputs.check_ecr_image_scan_results_script_tag }} + run: | + curl -L "https://raw.githubusercontent.com/NHSDigital/eps-common-workflows/refs/heads/${SCRIPT_TAG}/.github/scripts/check_ecr_image_scan_results.sh" -o /tmp/check_ecr_image_scan_results.sh + chmod +x /tmp/check_ecr_image_scan_results.sh + sleep 30 + /tmp/check_ecr_image_scan_results.sh + + build_image_arm64: + permissions: + id-token: write + runs-on: ubuntu-22.04-arm + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Build container + run: | + docker build -f "${DOCKER_FILE}" -t arm64-image . + env: + DOCKER_FILE: ${{ inputs.docker_file }} + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 + id: connect-aws-deploy + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.PUSH_IMAGE_ROLE }} + role-session-name: dev-container-build-arm64 + output-credentials: true + + - name: Retrieve AWS Account ID + id: retrieve-deploy-account-id + run: | + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "account_id=$ACCOUNT_ID" >> "$GITHUB_OUTPUT" + + - name: Login to Amazon ECR + run: | + aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin ${{ steps.retrieve-deploy-account-id.outputs.account_id }}.dkr.ecr.eu-west-2.amazonaws.com + + - name: Push ARM64 image to Amazon ECR + env: + ECR_REPOSITORY: ${{ inputs.container_ecr }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + run: | + docker tag "arm64-image" "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" + docker push "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" + - name: Check dev container scan results + env: + REPOSITORY_NAME: ${{ inputs.container_ecr }} + IMAGE_TAG: ${{ inputs.container_image_tag }}-arm64 + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + SCRIPT_TAG: ${{ inputs.check_ecr_image_scan_results_script_tag }} + run: | + curl -L "https://raw.githubusercontent.com/NHSDigital/eps-common-workflows/refs/heads/${SCRIPT_TAG}/.github/scripts/check_ecr_image_scan_results.sh" -o /tmp/check_ecr_image_scan_results.sh + chmod +x /tmp/check_ecr_image_scan_results.sh + sleep 30 + /tmp/check_ecr_image_scan_results.sh + + create_multi_arch_manifest: + permissions: + id-token: write + runs-on: ubuntu-22.04 + needs: [build_image_amd64, build_image_arm64] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.PUSH_IMAGE_ROLE }} + role-session-name: multi-arch-manifest + output-credentials: true + + - name: Retrieve AWS Account ID + id: retrieve-deploy-account-id + run: | + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "account_id=$ACCOUNT_ID" >> "$GITHUB_OUTPUT" + + - name: Login to Amazon ECR + run: | + aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin ${{ steps.retrieve-deploy-account-id.outputs.account_id }}.dkr.ecr.eu-west-2.amazonaws.com + + - name: Create and push multi-architecture manifest for tag + env: + ECR_REPOSITORY: ${{ inputs.container_ecr }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + run: | + # Create manifest list combining both architectures + docker buildx imagetools create -t "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}" \ + "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" \ + "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" + + - name: Verify multi-architecture manifest + env: + ECR_REPOSITORY: ${{ inputs.container_ecr }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + run: | + echo "=== Verifying multi-architecture manifest ===" + docker buildx imagetools inspect "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}" diff --git a/.github/workflows/publish_nhsd_git_secrets_to_github.yml b/.github/workflows/publish_nhsd_git_secrets_to_github.yml new file mode 100644 index 0000000..99dcf49 --- /dev/null +++ b/.github/workflows/publish_nhsd_git_secrets_to_github.yml @@ -0,0 +1,98 @@ +# +name: Create and publish a Docker image + +on: + workflow_call: + secrets: + PUSH_IMAGE_ROLE: + required: true + inputs: + ecr_name: + type: string + description: "The name of the ECR repository to pull the git-secrets image from." + required: true + container_image_tag: + type: string + description: "The tag of an existing image to publish to github." + required: true + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + # + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 + id: connect-aws-deploy + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.PUSH_IMAGE_ROLE }} + role-session-name: dev-container-build-x64 + output-credentials: true + - name: Retrieve AWS Account ID + id: retrieve-deploy-account-id + run: | + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "account_id=$ACCOUNT_ID" >> "$GITHUB_OUTPUT" + + - name: Login to Amazon ECR + run: | + aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin ${{ steps.retrieve-deploy-account-id.outputs.account_id }}.dkr.ecr.eu-west-2.amazonaws.com + + - name: pull image + run: | + docker pull "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" + docker pull "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" + env: + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + ECR_REPOSITORY: ${{ inputs.ecr_name }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + + - name: Log in to the Container registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag and push amd64 image + run: | + IMAGE_NAME_LOWER=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker tag "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}-amd64" + docker push "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}-amd64" + env: + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + ECR_REPOSITORY: ${{ inputs.ecr_name }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + + - name: Tag and push arm64 image + run: | + IMAGE_NAME_LOWER=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker tag "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}-arm64" + docker push "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}-arm64" + env: + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + ECR_REPOSITORY: ${{ inputs.ecr_name }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + + - name: Create and push multi-arch manifest + run: | + IMAGE_NAME_LOWER=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker manifest create "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}" \ + --amend "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}-amd64" \ + --amend "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}-arm64" + docker manifest push "${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${IMAGE_TAG}" + env: + IMAGE_TAG: ${{ inputs.container_image_tag }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1728ec3..f1e3be5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,6 +8,45 @@ env: BRANCH_NAME: ${{ github.event.pull_request.head.ref }} jobs: + get_issue_number_and_commit_id: + runs-on: ubuntu-22.04 + outputs: + issue_number: ${{ steps.get_issue_number.outputs.result }} + version: ${{ steps.get_issue_number.outputs.version_number }} + commit_id: ${{ steps.commit_id.outputs.commit_id }} + sha_short: ${{ steps.commit_id.outputs.sha_short }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ env.BRANCH_NAME }} + + - uses: actions/github-script@v8 + name: get issue number + id: get_issue_number + with: + script: | + if (context.issue.number) { + // Return issue number if present + return context.issue.number; + } else { + // Otherwise return issue number from commit + return ( + await github.rest.repos.listPullRequestsAssociatedWithCommit({ + commit_sha: context.sha, + owner: context.repo.owner, + repo: context.repo.repo, + }) + ).data[0].number; + } + result-encoding: string + - name: Get Commit ID + id: commit_id + run: | + # echo "commit_id=${{ github.sha }}" >> "$GITHUB_ENV" + echo "commit_id=${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" dependabot-auto-approve-and-merge: needs: quality_checks uses: ./.github/workflows/dependabot-auto-approve-and-merge.yml @@ -35,11 +74,35 @@ jobs: echo "TAG_FORMAT=$TAG_FORMAT" >> "$GITHUB_OUTPUT" quality_checks: uses: ./.github/workflows/quality-checks.yml - needs: [get_asdf_version] + needs: [get_asdf_version, get_issue_number_and_commit_id] with: asdfVersion: ${{ needs.get_asdf_version.outputs.asdf_version }} + dev_container_ecr: dev-container-common-workflows + dev_container_image_tag: PR-${{ needs.get_issue_number_and_commit_id.outputs.issue_number }}-${{ needs.get_issue_number_and_commit_id.outputs.sha_short }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} + + build_nhsd_git_secrets: + uses: ./.github/workflows/build_and_push_docker_image.yml + needs: [get_asdf_version, get_issue_number_and_commit_id] + with: + container_ecr: git-secrets + container_image_tag: PR-${{ needs.get_issue_number_and_commit_id.outputs.issue_number }}-${{ needs.get_issue_number_and_commit_id.outputs.sha_short }}-nhsd-git-secrets + docker_file: dockerfiles/nhsd-git-secrets.dockerfile + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} + + publish_nhsd_git_secrets_to_github: + needs: + [get_asdf_version, get_issue_number_and_commit_id, build_nhsd_git_secrets] + uses: ./.github/workflows/publish_nhsd_git_secrets_to_github.yml + with: + ecr_name: git-secrets + container_image_tag: PR-${{ needs.get_issue_number_and_commit_id.outputs.issue_number }}-${{ needs.get_issue_number_and_commit_id.outputs.sha_short }}-nhsd-git-secrets + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} + tag_release: needs: [quality_checks, get_asdf_version] uses: ./.github/workflows/tag-release.yml diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 6e961ef..6638732 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -5,6 +5,8 @@ on: secrets: SONAR_TOKEN: required: false + PUSH_IMAGE_ROLE: + required: true inputs: install_java: type: boolean @@ -23,7 +25,19 @@ on: type: boolean description: Toggle to reinstall poetry on top of python version installed by asdf. default: false - + dev_container_ecr: + type: string + description: "The name of the ECR repository to push the dev container image to." + required: true + dev_container_image_tag: + type: string + description: "The tag to use for the dev container image." + required: true + check_ecr_image_scan_results_script_tag: + type: string + description: "The tag to download check_ecr_image_scan_results.sh script." + required: false + default: "dev_container_build" jobs: quality_checks: runs-on: ubuntu-22.04 @@ -37,7 +51,6 @@ jobs: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 with: - ref: ${{ env.BRANCH_NAME }} fetch-depth: 0 # Must be done before anything installs, or it will check dependencies for secrets too. @@ -295,11 +308,10 @@ jobs: rm -rf /tmp/ruleset rm -rf cfn_guard_output - wget -O /tmp/ruleset.zip https://github.com/aws-cloudformation/aws-guard-rules-registry/releases/download/1.0.2/ruleset-build-v1.0.2.zip >/dev/null 2>&1 - unzip /tmp/ruleset.zip -d /tmp/ruleset/ >/dev/null 2>&1 - - curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh >/dev/null 2>&1 - + wget -O /tmp/ruleset.zip https://github.com/aws-cloudformation/aws-guard-rules-registry/releases/download/1.0.2/ruleset-build-v1.0.2.zip + unzip /tmp/ruleset.zip -d /tmp/ruleset/ + curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh -o /tmp/install-guard.sh + sh /tmp/install-guard.sh mkdir -p cfn_guard_output - name: Run cfn-guard script for sam templates @@ -407,3 +419,13 @@ jobs: with: name: cfn_guard_output path: cfn_guard_output + + build_and_push_dev_container_image: + uses: ./.github/workflows/build_and_push_docker_image.yml + with: + container_ecr: ${{ inputs.dev_container_ecr }} + container_image_tag: ${{ inputs.dev_container_image_tag }} + docker_file: ".devcontainer/Dockerfile" + check_ecr_image_scan_results_script_tag: ${{ inputs.check_ecr_image_scan_results_script_tag }} + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.PUSH_IMAGE_ROLE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73fd7e9..83824bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,24 @@ env: BRANCH_NAME: ${{ github.event.ref.BRANCH_NAME }} jobs: + get_commit_id: + runs-on: ubuntu-22.04 + outputs: + commit_id: ${{ steps.commit_id.outputs.commit_id }} + sha_short: ${{ steps.commit_id.outputs.sha_short }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ env.BRANCH_NAME }} + + - name: Get Commit ID + id: commit_id + run: | + # echo "commit_id=${{ github.sha }}" >> "$GITHUB_ENV" + echo "commit_id=${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" get_asdf_version: runs-on: ubuntu-22.04 outputs: @@ -26,12 +44,24 @@ jobs: TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) echo "TAG_FORMAT=$TAG_FORMAT" >> "$GITHUB_OUTPUT" quality_checks: - needs: [get_asdf_version] + needs: [get_asdf_version, get_commit_id] uses: ./.github/workflows/quality-checks.yml with: asdfVersion: ${{ needs.get_asdf_version.outputs.asdf_version }} + dev_container_ecr: dev-container-common-workflows + dev_container_image_tag: release-${{ needs.get_commit_id.outputs.sha_short }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} + build_nhsd_git_secrets: + uses: ./.github/workflows/build_and_push_docker_image.yml + needs: [get_asdf_version, get_commit_id] + with: + container_ecr: git-secrets + container_image_tag: release-${{ needs.get_commit_id.outputs.sha_short }} + docker_file: dockerfiles/nhsd-git-secrets.dockerfile + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} tag_release: needs: [quality_checks, get_asdf_version] uses: ./.github/workflows/tag-release.yml @@ -42,3 +72,22 @@ jobs: publish_package: false tag_format: ${{ needs.get_asdf_version.outputs.tag_format }} secrets: inherit + tag_latest_dev_container: + needs: [quality_checks, get_commit_id, tag_release] + uses: ./.github/workflows/tag_latest_container_images.yml + with: + ecr_name: dev-container-common-workflows + container_image_tag: release-${{ needs.get_commit_id.outputs.sha_short }} + version_tag_to_apply: ${{ needs.tag_release.outputs.version_tag }} + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} + + tag_latest_nhsd_git_secrets: + needs: [quality_checks, get_commit_id, tag_release] + uses: ./.github/workflows/tag_latest_container_images.yml + with: + ecr_name: git-secrets + container_image_tag: release-${{ needs.get_commit_id.outputs.sha_short }} + version_tag_to_apply: ${{ needs.tag_release.outputs.version_tag }} + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} diff --git a/.github/workflows/tag_latest_container_images.yml b/.github/workflows/tag_latest_container_images.yml new file mode 100644 index 0000000..6f72069 --- /dev/null +++ b/.github/workflows/tag_latest_container_images.yml @@ -0,0 +1,104 @@ +name: Tag Latest container images + +on: + workflow_call: + secrets: + PUSH_IMAGE_ROLE: + required: true + inputs: + ecr_name: + type: string + description: "The name of the ECR repository to push the dev container image to." + required: true + container_image_tag: + type: string + description: "The tag of an existing image to apply version tag to." + required: true + version_tag_to_apply: + type: string + description: "The version tag to apply to the latest dev container image." + required: true +jobs: + tag_latest_container_images: + permissions: + id-token: write + runs-on: ubuntu-22.04 + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.PUSH_IMAGE_ROLE }} + role-session-name: multi-arch-manifest + output-credentials: true + + - name: Retrieve AWS Account ID + id: retrieve-deploy-account-id + run: | + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "account_id=$ACCOUNT_ID" >> "$GITHUB_OUTPUT" + + - name: Login to Amazon ECR + run: | + aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin ${{ steps.retrieve-deploy-account-id.outputs.account_id }}.dkr.ecr.eu-west-2.amazonaws.com + + - name: Create and push multi-architecture manifest for tag + env: + ECR_REPOSITORY: ${{ inputs.ecr_name }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + VERSION_TAG_TO_APPLY: ${{ inputs.version_tag_to_apply }} + run: | + # Create manifest list combining both architectures + docker buildx imagetools create -t "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:latest" \ + "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" \ + "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" + docker buildx imagetools create -t "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${VERSION_TAG_TO_APPLY}" \ + "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-amd64" \ + "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}-arm64" + + - name: Tag images for individual architectures + env: + ECR_REPOSITORY: ${{ inputs.ecr_name }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + VERSION_TAG_TO_APPLY: ${{ inputs.version_tag_to_apply }} + run: | + # Get the image manifest + MANIFEST=$(aws ecr batch-get-image \ + --repository-name "${ECR_REPOSITORY}" \ + --image-ids imageTag="${IMAGE_TAG}-amd64" \ + --output text --query 'images[].imageManifest') + + # Put the image with a new tag using the same manifest + aws ecr put-image --repository-name "${ECR_REPOSITORY}" \ + --image-manifest "$MANIFEST" \ + --image-tag "${VERSION_TAG_TO_APPLY}-amd64" + aws ecr put-image --repository-name "${ECR_REPOSITORY}" \ + --image-manifest "$MANIFEST" \ + --image-tag latest-amd64 + + MANIFEST=$(aws ecr batch-get-image \ + --repository-name "${ECR_REPOSITORY}" \ + --image-ids imageTag="${IMAGE_TAG}-arm64" \ + --output text --query 'images[].imageManifest') + + # Put the image with a new tag using the same manifest + aws ecr put-image --repository-name "${ECR_REPOSITORY}" \ + --image-manifest "$MANIFEST" \ + --image-tag "${VERSION_TAG_TO_APPLY}-arm64" + aws ecr put-image --repository-name "${ECR_REPOSITORY}" \ + --image-manifest "$MANIFEST" \ + --image-tag latest-arm64 + + - name: Verify multi-architecture manifest + env: + ECR_REPOSITORY: ${{ inputs.ecr_name }} + IMAGE_TAG: ${{ inputs.container_image_tag }} + ACCOUNT_ID: ${{ steps.retrieve-deploy-account-id.outputs.account_id }} + run: | + echo "=== Verifying multi-architecture manifest ===" + docker buildx imagetools inspect "${ACCOUNT_ID}.dkr.ecr.eu-west-2.amazonaws.com/${ECR_REPOSITORY}:latest" diff --git a/README.md b/README.md index bd91fa5..80cce73 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} ``` +This repository provides reusable GitHub Actions workflows for EPS repositories: ## tag release @@ -178,11 +179,172 @@ jobs: ## Secret scanning docker -The secret scanning also has a dockerfile, which can be run against a repo in order to scan it manually (or as part of pre-commit hooks). This can be done like so: + +# Quality Checks Workflow Usage + +## Inputs + +The workflow accepts the following input parameters: + +### `install_java` +- **Type**: boolean +- **Required**: false +- **Default**: false +- **Description**: If true, the action will install Java into the runner, separately from ASDF. + +### `run_sonar` +- **Type**: boolean +- **Required**: false +- **Default**: true +- **Description**: Toggle to run SonarCloud code analysis on this repository. + +### `asdfVersion` +- **Type**: string +- **Required**: true +- **Description**: The version of ASDF to use for managing runtime versions. + +### `reinstall_poetry` +- **Type**: boolean +- **Required**: false +- **Default**: false +- **Description**: Toggle to reinstall Poetry on top of the Python version installed by ASDF. + +### `dev_container_ecr` +- **Type**: string +- **Required**: true +- **Description**: The name of the ECR repository to push the dev container image to. + +### `dev_container_image_tag` +- **Type**: string +- **Required**: true +- **Description**: The tag to use for the dev container image. + +### `check_ecr_image_scan_results_script_tag` +- **Type**: string +- **Required**: false +- **Default**: "main" +- **Description**: The git ref to download the check_ecr_image_scan_results.sh script from. + +## Required Makefile targets + +In order to run, these `make` commands must be present. They may be mocked, if they are not relevant to the project. + +- `install` +- `lint` +- `test` +- `check-licenses` +- `cdk-synth` - only needed if packages/cdk folder exists + +## Secrets + +The workflow requires the following secrets: + +### `SONAR_TOKEN` +- **Required**: false +- **Description**: Required for the SonarCloud Scan step, which analyzes your code for quality and security issues using SonarCloud. + +### `PUSH_IMAGE_ROLE` +- **Required**: true +- **Description**: AWS IAM role ARN used to authenticate and push dev container images to ECR. + +## Example Workflow Call + +To use this workflow in your repository, call it from another workflow file: + +```yaml +name: Quality Checks + +on: + push: + branches: + - main + - develop + +jobs: + quality_checks: + uses: NHSDigital/eps-workflow-quality-checks/.github/workflows/quality-checks.yml@4.0.2 + with: + asdfVersion: "v0.14.1" + dev_container_ecr: "your-ecr-repo-name" + dev_container_image_tag: "latest" + # Optional inputs + install_java: false + run_sonar: true + reinstall_poetry: false + check_ecr_image_scan_results_script_tag: "dev_container_build" + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} +``` + +# Tag Latest Dev Container Workflow + +This repository also provides a reusable workflow [`tag_latest_dev_container.yml`](./.github/workflows/tag_latest_dev_container.yml) for tagging dev container images with version tags and `latest` in ECR. + +## Purpose + +This workflow takes existing dev container images (built for both x64 and arm64 architectures) and applies additional tags to them, including: +- A custom version tag (e.g., `v1.0.0`) +- The `latest` tag +- Architecture-specific tags (e.g., `v1.0.0-amd64`, `latest-arm64`) + +## Inputs + +### `dev_container_ecr` +- **Type**: string +- **Required**: true +- **Description**: The name of the ECR repository containing the dev container images. + +### `dev_container_image_tag` +- **Type**: string +- **Required**: true +- **Description**: The current tag of the dev container images to be re-tagged (should exist for both `-amd64` and `-arm64` suffixes). + +### `version_tag_to_apply` +- **Type**: string +- **Required**: true +- **Description**: The version tag to apply to the dev container images (e.g., `v1.0.0`). + +## Secrets + +### `PUSH_IMAGE_ROLE` +- **Required**: true +- **Description**: AWS IAM role ARN used to authenticate and push images to ECR. + +## Example Usage + +```yaml +name: Tag Dev Container as Latest + +on: + release: + types: [published] + +jobs: + tag_dev_container: + uses: NHSDigital/eps-workflow-quality-checks/.github/workflows/tag_latest_dev_container.yml@main + with: + dev_container_ecr: "your-ecr-repo-name" + dev_container_image_tag: release-${{ needs.get_commit_id.outputs.sha_short }} # The tag applied as part of the quality-checks workflow + version_tag_to_apply: ${{ needs.tag_release.outputs.version_tag }} # The git tag created by tag_release workflow + secrets: + PUSH_IMAGE_ROLE: ${{ secrets.DEV_CONTAINER_PUSH_IMAGE_ROLE }} +``` + +## Git secrets +There is a dockerfile at ([`nhsd-git-secrets.dockerfile`](./dockerfiles/nhsd-git-secrets.dockerfile)) that builds a docker image that is used to run git secrets. This image is pushed to ECR as part of this projects release pipeline. +This can be manually built and used to scan manually (or as part of pre-commit hooks). ```bash docker build -f https://raw.githubusercontent.com/NHSDigital/eps-workflow-quality-checks/refs/tags/v3.0.0/dockerfiles/nhsd-git-secrets.dockerfile -t git-secrets . docker run -v /path/to/repo:/src git-secrets --scan-history . ``` +Or it can be pulled from ECR +```bash +export AWS_PROFILE=prescription-dev +aws sso login --sso-session sso-session +aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin 591291862413.dkr.ecr.eu-west-2.amazonaws.com +docker pull 591291862413.dkr.ecr.eu-west-2.amazonaws.com/dev-container-git-secrets:latest +``` For usage of the script, see the [source repo](https://github.com/NHSDigital/software-engineering-quality-framework/blob/main/tools/nhsd-git-secrets/git-secrets). Generally, you will either need `--scan -r .` or `--scan-history .`. The arguments default to `--scan -r .`, i.e. scanning the current state of the code. In order to enable the pre-commit hook for secret scanning (to prevent developers from committing secrets in the first place), add the following to the `.devcontainer/devcontainer.json` file: