diff --git a/.env.template b/.env.template index dfc86b8cce..3e1511f5c4 100644 --- a/.env.template +++ b/.env.template @@ -4,4 +4,3 @@ # Deployment related GITHUB_TOKEN=Github Personal Access Token with packages:read permissions -GITHUB_ACTOR=Github username associated with the token diff --git a/.tool-versions b/.tool-versions index 58b6eff99f..654b910d17 100644 --- a/.tool-versions +++ b/.tool-versions @@ -5,7 +5,7 @@ nodejs 22.22.0 pre-commit 3.6.0 terraform 1.10.1 terraform-docs 0.19.0 -trivy 0.69.2 +trivy 0.69.3 vale 3.6.0 python 3.13.2 diff --git a/AGENTS.md b/AGENTS.md index c85e1afdf5..55726d99af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,14 +26,14 @@ Agents should look for a nested `AGENTS.md` in or near these areas before making The root `package.json` is the orchestration manifestgit co for this repo. It does not ship application code; it wires up shared dev tooling and delegates to workspace-level projects. - Workspaces: Declares the set of npm workspaces (e.g. under `lambdas/`, `utils/`, `tests/`, `scripts/`). Agents should add a new workspace path here when introducing a new npm project. -- Scripts: Provides top-level commands that fan out across workspaces using `--workspaces` (lint, typecheck, unit tests) and project-specific runners (e.g. `lambda-build`). +- Scripts: Provides top-level commands that fan out across workspaces using `--workspaces` (lint, typecheck, unit tests) and project-specific runners (e.g. `build:archive`). - Dev tool dependencies: Centralises Jest, TypeScript, ESLint configurations and plugins to keep versions consistent across workspaces. Workspace projects should rely on these unless a local override is strictly needed. - Overrides/resolutions: Pins transitive dependencies (e.g. Jest/react-is) to avoid ecosystem conflicts. Agents must not remove overrides without verifying tests across all workspaces. Agent guidance: - Before adding or removing a workspace, update the root `workspaces` array and ensure CI scripts still succeed with `npm run lint`, `npm run typecheck`, and `npm run test:unit` at the repo root. -- When adding repo-wide scripts, keep names consistent with existing patterns (e.g. `lint`, `lint:fix`, `typecheck`, `test:unit`, `lambda-build`) and prefer `--workspaces` fan-out. +- When adding repo-wide scripts, keep names consistent with existing patterns (e.g. `lint`, `lint:fix`, `typecheck`, `test:unit`, `build:archive`) and prefer `--workspaces` fan-out. - Do not publish from the root. If adding a new workspace intended for publication, mark that workspace package as `private: false` and keep the root as private. - Validate changes by running the repo pre-commit hooks: `make githooks-run`. @@ -41,7 +41,7 @@ Success criteria for changes affecting the root `package.json`: - `npm run lint`, `npm run typecheck`, and `npm run test:unit` pass at the repo root. - Workspace discovery is correct (new projects appear under `npm run typecheck --workspaces`). -- No regression in lambda build tooling (`npm run lambda-build`). +- No regression in lambda build tooling (`npm run build:archive`). ## What Agents Can / Can’t Do diff --git a/infrastructure/terraform/bin/terraform.sh b/infrastructure/terraform/bin/terraform.sh index 659b535c2e..f06e0c5d6c 100755 --- a/infrastructure/terraform/bin/terraform.sh +++ b/infrastructure/terraform/bin/terraform.sh @@ -391,8 +391,8 @@ rm -rf ${component_path}/.terraform; # Run global pre.sh if [ -f "pre.sh" ]; then - source pre.sh "${region}" "${environment}" "${action}" \ - || error_and_die "Global pre script execution failed with exit code ${?}"; + PROJECT="${project}" REGION="${region}" COMPONENT="${component}" AWS_ACCOUNT_ID="${aws_account_id}" ENVIRONMENT="${environment}" ACTION="${action}" \ + source pre.sh || error_and_die "Global pre script execution failed with exit code ${?}"; fi; # Make sure we're running in the component directory @@ -427,8 +427,8 @@ fi; # Run pre.sh if [ -f "pre.sh" ]; then - source pre.sh "${region}" "${environment}" "${action}" \ - || error_and_die "Component pre script execution failed with exit code ${?}"; + PROJECT="${project}" REGION="${region}" COMPONENT="${component}" AWS_ACCOUNT_ID="${aws_account_id}" ENVIRONMENT="${environment}" ACTION="${action}" \ + source pre.sh || error_and_die "Component pre script execution failed with exit code ${?}"; fi; # Pull down secret TFVAR file from S3 diff --git a/infrastructure/terraform/components/app/pre.sh b/infrastructure/terraform/components/app/pre.sh old mode 100755 new mode 100644 index e7ce6cd62a..4f984816fc --- a/infrastructure/terraform/components/app/pre.sh +++ b/infrastructure/terraform/components/app/pre.sh @@ -1,8 +1,44 @@ +#!/bin/bash + +# This script is run before Terraform executable commands. +# It ensures all Node.js dependencies are installed, generates any required dependencies, +# and builds all Lambda functions in the workspace before Terraform provisions infrastructure. # pre.sh runs in the same shell as terraform.sh, not in a subshell -# any variables set or changed, any change of directory will persist once this script exits and returns control to terraform.sh -REGION=$1 -ENVIRONMENT=$2 -ACTION=$3 + +: "${PROJECT:?PROJECT is required}" +: "${AWS_REGION:?AWS_REGION is required}" +: "${COMPONENT:?COMPONENT is required}" +: "${ENVIRONMENT:?ENVIRONMENT is required}" +: "${AWS_ACCOUNT_ID:?AWS_ACCOUNT_ID is required}" +: "${ACTION:?ACTION is required}" +REPO_ROOT="$(git rev-parse --show-toplevel)" + +echo "Running app pre.sh" +echo "ENVIRONMENT=$ENVIRONMENT" +echo "ACTION=$ACTION" +echo "PROJECT=$PROJECT" +echo "COMPONENT=$COMPONENT" +echo "AWS_REGION=$REGION" +echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" +echo "REPO_ROOT=$REPO_ROOT" + +# Calculate container image prefix from PROJECT, ENVIRONMENT, COMPONENT +CONTAINER_IMAGE_PREFIX="${PROJECT}-${ENVIRONMENT}-${COMPONENT}" +echo "CONTAINER_IMAGE_PREFIX: ${CONTAINER_IMAGE_PREFIX}" + +# Translate ACTION to PUBLISH_CONTAINER_IMAGE (build) +if [ "${ACTION}" = "plan" ]; then + PUBLISH_CONTAINER_IMAGE="false" +else + PUBLISH_CONTAINER_IMAGE="true" +fi + +if [ -f "${REPO_ROOT}/.env" ]; then + set -a + # shellcheck disable=SC1090 + . "${REPO_ROOT}/.env" + set +a +fi # Helper function for error handling run_or_fail() { @@ -13,28 +49,35 @@ run_or_fail() { fi } -echo "Running app pre.sh" -echo "REGION=$REGION" -echo "ENVIRONMENT=$ENVIRONMENT" -echo "ACTION=$ACTION" +# Switch to repo root +pushd "${REPO_ROOT}" || exit 1 +# Calculate git-based version suffix +SHORT_SHA="$(git rev-parse --short HEAD)" GIT_TAG="$(git describe --tags --exact-match 2>/dev/null || true)" + if [ -n "${GIT_TAG}" ]; then RELEASE_VERSION="${GIT_TAG#v}" - export TF_VAR_container_image_tag_suffix="release-${RELEASE_VERSION}-$(git rev-parse --short HEAD)" - echo "On tag: $GIT_TAG, image tag suffixes will be: release-${RELEASE_VERSION}-$(git rev-parse --short HEAD)" + CONTAINER_IMAGE_SUFFIX="release-${RELEASE_VERSION}-${SHORT_SHA}" + echo "On tag: $GIT_TAG, image suffix: ${CONTAINER_IMAGE_SUFFIX}" else - export TF_VAR_container_image_tag_suffix="sha-$(git rev-parse --short HEAD)" - echo "Not on a tag, image tag suffix will be: sha-$(git rev-parse --short HEAD)" + CONTAINER_IMAGE_SUFFIX="sha-${SHORT_SHA}" + echo "Not on a tag, image suffix: ${CONTAINER_IMAGE_SUFFIX}" fi -# change to monorepo root -cd $(git rev-parse --show-toplevel) +# Export for Terraform +export TF_VAR_container_image_tag_suffix="${CONTAINER_IMAGE_SUFFIX}" run_or_fail npm ci run_or_fail npm run generate-dependencies --workspaces --if-present -run_or_fail npm run lambda-build --workspaces --if-present +run_or_fail npm run build:archive --workspaces --if-present +run_or_fail env \ + CONTAINER_IMAGE_PREFIX="${CONTAINER_IMAGE_PREFIX}" \ + CONTAINER_IMAGE_SUFFIX="${CONTAINER_IMAGE_SUFFIX}" \ + AWS_ACCOUNT_ID="${AWS_ACCOUNT_ID}" \ + AWS_REGION="${REGION}" \ + PUBLISH_CONTAINER_IMAGE="${PUBLISH_CONTAINER_IMAGE}" \ + npm run build:container --workspaces --if-present run_or_fail lambdas/layers/pdfjs/build.sh -# revert back to original directory -cd - +popd || exit 1 # Return to working directory diff --git a/infrastructure/terraform/components/sbx/pre.sh b/infrastructure/terraform/components/sbx/pre.sh old mode 100755 new mode 100644 index 1661385d95..6d84788302 --- a/infrastructure/terraform/components/sbx/pre.sh +++ b/infrastructure/terraform/components/sbx/pre.sh @@ -1,8 +1,44 @@ +#!/bin/bash + +# This script is run before Terraform executable commands. +# It ensures all Node.js dependencies are installed, generates any required dependencies, +# and builds all Lambda functions in the workspace before Terraform provisions infrastructure. # pre.sh runs in the same shell as terraform.sh, not in a subshell -# any variables set or changed, and change of directory will persist once this script exits and returns control to terraform.sh -REGION=$1 -ENVIRONMENT=$2 -ACTION=$3 + +: "${PROJECT:?PROJECT is required}" +: "${AWS_REGION:?AWS_REGION is required}" +: "${COMPONENT:?COMPONENT is required}" +: "${ENVIRONMENT:?ENVIRONMENT is required}" +: "${AWS_ACCOUNT_ID:?AWS_ACCOUNT_ID is required}" +: "${ACTION:?ACTION is required}" +REPO_ROOT="$(git rev-parse --show-toplevel)" + +echo "Running sbx pre.sh" +echo "ENVIRONMENT=$ENVIRONMENT" +echo "ACTION=$ACTION" +echo "PROJECT=$PROJECT" +echo "COMPONENT=$COMPONENT" +echo "AWS_REGION=$REGION" +echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" +echo "REPO_ROOT=$REPO_ROOT" + +# Calculate container image prefix from PROJECT, ENVIRONMENT, COMPONENT +CONTAINER_IMAGE_PREFIX="${PROJECT}-${ENVIRONMENT}-${COMPONENT}" +echo "CONTAINER_IMAGE_PREFIX: ${CONTAINER_IMAGE_PREFIX}" + +# Translate ACTION to PUBLISH_CONTAINER_IMAGE (build) +if [ "${ACTION}" = "plan" ]; then + PUBLISH_CONTAINER_IMAGE="false" +else + PUBLISH_CONTAINER_IMAGE="true" +fi + +if [ -f "${REPO_ROOT}/.env" ]; then + set -a + # shellcheck disable=SC1090 + . "${REPO_ROOT}/.env" + set +a +fi # Helper function for error handling run_or_fail() { @@ -13,23 +49,24 @@ run_or_fail() { fi } -echo "Running sandbox pre.sh" -echo "REGION=$REGION" -echo "ENVIRONMENT=$ENVIRONMENT" -echo "ACTION=$ACTION" +# Switch to repo root +pushd "${REPO_ROOT}" || exit 1 +# Calculate git-based version suffix +SHORT_SHA="$(git rev-parse --short HEAD)" GIT_TAG="$(git describe --tags --exact-match 2>/dev/null || true)" + if [ -n "${GIT_TAG}" ]; then RELEASE_VERSION="${GIT_TAG#v}" - export TF_VAR_container_image_tag_suffix="release-${RELEASE_VERSION}-$(git rev-parse --short HEAD)" - echo "On tag: $GIT_TAG, image tag suffixes will be: release-${RELEASE_VERSION}-$(git rev-parse --short HEAD)" + CONTAINER_IMAGE_SUFFIX="release-${RELEASE_VERSION}-${SHORT_SHA}" + echo "On tag: $GIT_TAG, image suffix: ${CONTAINER_IMAGE_SUFFIX}" else - export TF_VAR_container_image_tag_suffix="sha-$(git rev-parse --short HEAD)" - echo "Not on a tag, image tag suffix will be: sha-$(git rev-parse --short HEAD)" + CONTAINER_IMAGE_SUFFIX="sha-${SHORT_SHA}" + echo "Not on a tag, image suffix: ${CONTAINER_IMAGE_SUFFIX}" fi -# change to monorepo root -cd $(git rev-parse --show-toplevel) +# Export for Terraform +export TF_VAR_container_image_tag_suffix="${CONTAINER_IMAGE_SUFFIX}" case "${ACTION}" in apply) @@ -43,7 +80,14 @@ case "${ACTION}" in fi run_or_fail npm run generate-dependencies --workspaces --if-present - run_or_fail npm run lambda-build --workspaces --if-present + run_or_fail npm run build:archive --workspaces --if-present + run_or_fail env \ + CONTAINER_IMAGE_PREFIX="${CONTAINER_IMAGE_PREFIX}" \ + CONTAINER_IMAGE_SUFFIX="${CONTAINER_IMAGE_SUFFIX}" \ + AWS_ACCOUNT_ID="${AWS_ACCOUNT_ID}" \ + AWS_REGION="${AWS_REGION}" \ + PUBLISH_CONTAINER_IMAGE="${PUBLISH_CONTAINER_IMAGE}" \ + npm run build:container --workspaces --if-present run_or_fail lambdas/layers/pdfjs/build.sh ;; plan) @@ -54,5 +98,4 @@ case "${ACTION}" in ;; esac -# revert back to original directory -cd - +popd || exit 1 # Return to working directory diff --git a/lambdas/authorizer/package.json b/lambdas/authorizer/package.json index bba6a2a47f..df39362d1c 100644 --- a/lambdas/authorizer/package.json +++ b/lambdas/authorizer/package.json @@ -22,7 +22,7 @@ "name": "nhs-notify-templates-api-authorizer", "private": true, "scripts": { - "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", + "build:archive": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "test:unit": "jest", diff --git a/lambdas/backend-api/package.json b/lambdas/backend-api/package.json index aa0c5129da..2ea640d7de 100644 --- a/lambdas/backend-api/package.json +++ b/lambdas/backend-api/package.json @@ -41,7 +41,7 @@ "name": "nhs-notify-backend-api", "private": true, "scripts": { - "lambda-build": "./build.sh", + "build:archive": "./build.sh", "lint": "eslint .", "lint:fix": "eslint . --fix", "test:unit": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest", diff --git a/lambdas/cognito-triggers/package.json b/lambdas/cognito-triggers/package.json index 1955b1c0fa..53c4196086 100644 --- a/lambdas/cognito-triggers/package.json +++ b/lambdas/cognito-triggers/package.json @@ -8,7 +8,7 @@ "name": "cognito-triggers", "private": true, "scripts": { - "lambda-build": "./build.sh", + "build:archive": "./build.sh", "lint": "eslint ./src", "lint:fix": "eslint ./src --fix", "test:unit": "jest", diff --git a/lambdas/download-authorizer/package.json b/lambdas/download-authorizer/package.json index 75c5c8c412..ecd86381cc 100644 --- a/lambdas/download-authorizer/package.json +++ b/lambdas/download-authorizer/package.json @@ -18,7 +18,7 @@ "name": "nhs-notify-download-authorizer", "private": true, "scripts": { - "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", + "build:archive": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "test:unit": "jest", diff --git a/lambdas/event-publisher/package.json b/lambdas/event-publisher/package.json index 2ee384d5ef..ac30cee53e 100644 --- a/lambdas/event-publisher/package.json +++ b/lambdas/event-publisher/package.json @@ -24,7 +24,7 @@ "name": "nhs-notify-templates-event-publisher", "private": true, "scripts": { - "lambda-build": "./build.sh", + "build:archive": "./build.sh", "lint": "eslint .", "lint:fix": "eslint . --fix", "test:unit": "jest", diff --git a/lambdas/letter-preview-renderer/docker/lambda/Dockerfile b/lambdas/letter-preview-renderer/docker/Dockerfile similarity index 100% rename from lambdas/letter-preview-renderer/docker/lambda/Dockerfile rename to lambdas/letter-preview-renderer/docker/Dockerfile diff --git a/lambdas/letter-preview-renderer/package.json b/lambdas/letter-preview-renderer/package.json index 9b71650194..ecd17bded7 100644 --- a/lambdas/letter-preview-renderer/package.json +++ b/lambdas/letter-preview-renderer/package.json @@ -30,7 +30,7 @@ "name": "nhs-notify-templates-letter-preview-renderer", "private": true, "scripts": { - "lambda-build": "../../scripts/lambda-container-build/docker.sh --base-image ghcr.io/nhsdigital/nhs-notify/libreoffice-amet-node-22:latest", + "build:container": "cd ../.. && make docker-build-and-push base_image=ghcr.io/nhsdigital/nhs-notify/libreoffice-amet-node-22:latest dir=lambdas/letter-preview-renderer", "lint": "eslint .", "lint:fix": "eslint . --fix", "test:unit": "jest", diff --git a/lambdas/sftp-letters/package.json b/lambdas/sftp-letters/package.json index 2c35e9a463..476005b0f9 100644 --- a/lambdas/sftp-letters/package.json +++ b/lambdas/sftp-letters/package.json @@ -36,7 +36,7 @@ "name": "nhs-notify-sftp-letters-lambdas", "private": true, "scripts": { - "lambda-build": "./build.sh", + "build:archive": "./build.sh", "lint": "eslint .", "lint:fix": "eslint . --fix", "test:unit": "jest", diff --git a/package-lock.json b/package-lock.json index 2141c4d2b8..f2acdfee2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7719,13 +7719,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", - "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.23.tgz", + "integrity": "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -7738,19 +7738,19 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", - "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "version": "3.973.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.23.tgz", + "integrity": "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/xml-builder": "^3.972.15", + "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", @@ -7807,15 +7807,15 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", - "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.24.tgz", + "integrity": "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.11", + "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", @@ -7826,44 +7826,44 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { - "version": "3.996.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", - "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "version": "3.996.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.13.tgz", + "integrity": "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -7875,13 +7875,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", + "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", + "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" @@ -7932,12 +7932,12 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", - "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.10.tgz", + "integrity": "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", @@ -7957,13 +7957,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", - "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", + "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.4.1", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { @@ -12859,9 +12859,9 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", - "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", + "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -12876,9 +12876,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.11", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.11.tgz", - "integrity": "sha512-952rGf7hBRnhUIaeLp6q4MptKW8sPFe5VvkoZ5qIzFAtx6c/QZ/54FS3yootsyUSf9gJX/NBqEBNdNR7jMIlpQ==", + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -12887,7 +12887,7 @@ "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -13096,13 +13096,13 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.25", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.25.tgz", - "integrity": "sha512-dqjLwZs2eBxIUG6Qtw8/YZ4DvzHGIf0DA18wrgtfP6a50UIO7e2nY0FPdcbv5tVJKqWCCU5BmGMOUwT7Puan+A==", + "version": "4.4.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", + "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.11", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", @@ -13115,15 +13115,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.42", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.42.tgz", - "integrity": "sha512-vbwyqHRIpIZutNXZpLAozakzamcINaRCpEy1MYmK6xBeW3xN+TyPRA123GjXnuxZIjc9848MRRCugVMTXxC4Eg==", + "version": "4.4.44", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", + "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -13135,12 +13135,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.14.tgz", - "integrity": "sha512-+CcaLoLa5apzSRtloOyG7lQvkUw2ZDml3hRh4QiG9WyEPfW5Ke/3tPOPiPjUneuT59Tpn8+c3RVaUvvkkwqZwg==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.11", + "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" @@ -13178,9 +13178,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.16", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.16.tgz", - "integrity": "sha512-ULC8UCS/HivdCB3jhi+kLFYe4B5gxH2gi9vHBfEIiRrT2jfKiZNiETJSlzRtE6B26XbBHjPtc8iZKSNqMol9bw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", @@ -13291,17 +13291,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.5.tgz", - "integrity": "sha512-UqwYawyqSr/aog8mnLnfbPurS0gi4G7IYDcD28cUIBhsvWs1+rQcL2IwkUQ+QZ7dibaoRzhNF99fAQ9AUcO00w==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", + "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.11", - "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -13398,13 +13398,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.41", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.41.tgz", - "integrity": "sha512-M1w1Ux0rSVvBOxIIiqbxvZvhnjQ+VUjJrugtORE90BbadSTH+jsQL279KRL3Hv0w69rE7EuYkV/4Lepz/NBW9g==", + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", + "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -13413,16 +13413,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.44", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.44.tgz", - "integrity": "sha512-YPze3/lD1KmWuZsl9JlfhcgGLX7AXhSoaCDtiPntUjNW5/YY0lOHjkcgxyE9x/h5vvS1fzDifMGjzqnNlNiqOQ==", + "version": "4.2.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", + "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.11", + "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -13484,13 +13484,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.19", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.19.tgz", - "integrity": "sha512-v4sa+3xTweL1CLO2UP0p7tvIMH/Rq1X4KKOxd568mpe6LSLMQCnDHs4uv7m3ukpl3HvcN2JH6jiCS0SNRXKP/w==", + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", diff --git a/scripts/docker/docker.lib.sh b/scripts/docker/docker.lib.sh index 187871050b..cf756be585 100644 --- a/scripts/docker/docker.lib.sh +++ b/scripts/docker/docker.lib.sh @@ -301,3 +301,149 @@ function _get-git-branch-name() { echo "$branch_name" } + +# ============================================================================== +# NHS Notify Project-Specific Functions - Container Support + +# Get git-based version suffix for containers. +# Returns either "release--" for tagged commits +# or "sha-" for untagged commits. +function docker-get-git-version-suffix() { + + local short_sha=$(git rev-parse --short HEAD) + local git_tag=$(git describe --tags --exact-match 2>/dev/null || true) + + if [ -n "$git_tag" ]; then + local release_version="${git_tag#v}" + echo "release-${release_version}-${short_sha}" + else + echo "sha-${short_sha}" + fi +} + +# Authenticate Docker with AWS ECR. +# Arguments (provided as environment variables): +# AWS_ACCOUNT_ID=[AWS account ID] +# AWS_REGION=[AWS region, e.g., eu-west-2] +function docker-ecr-login() { + + if [ -z "${AWS_ACCOUNT_ID:-}" ]; then + echo "Error: AWS_ACCOUNT_ID environment variable is required" >&2 + return 1 + fi + + if [ -z "${AWS_REGION:-}" ]; then + echo "Error: AWS_REGION environment variable is required" >&2 + return 1 + fi + + echo "Authenticating Docker with ECR..." + aws ecr get-login-password --region "${AWS_REGION}" | \ + docker login --username AWS --password-stdin \ + "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" +} + +# Authenticate Docker with GitHub Container Registry. +# Arguments (provided as environment variables): +# GITHUB_TOKEN=[GitHub personal access token with packages:read/write scope] +function docker-ghcr-login() { + + local ghcr_username="${GITHUB_ACTOR:-}" + + if [ -z "${GITHUB_TOKEN:-}" ]; then + echo "Error: GITHUB_TOKEN environment variable is required" >&2 + return 1 + fi + + if [ -z "${ghcr_username}" ]; then + ghcr_username="$(git config user.name 2>/dev/null || true)" + fi + + if [ -z "${ghcr_username}" ]; then + echo "Error: unable to determine GHCR username. Set GITHUB_ACTOR or configure git user.name" >&2 + return 1 + fi + + echo "Authenticating Docker with GitHub Container Registry..." + echo "${GITHUB_TOKEN}" | docker login ghcr.io --username "${ghcr_username}" --password-stdin +} + +# Build container image. +# Arguments (provided as environment variables): +# BASE_IMAGE=[base Docker image, e.g., node:22-alpine] +# dir=[path to container directory, default is '.'] +# DOCKER_IMAGE=[full ECR image URI with tag] +# Prerequisites: +# - Container directory must have build.sh script +# - Container directory must have docker/lambda/Dockerfile +function docker-build-container() { + + local dir=${dir:-$PWD} + + if [ -z "${BASE_IMAGE:-}" ]; then + echo "Error: BASE_IMAGE environment variable is required" >&2 + return 1 + fi + + if [ ! -f "${dir}/build.sh" ]; then + echo "Error: build.sh not found in ${dir}" >&2 + return 1 + fi + + if [ ! -f "${dir}/docker/Dockerfile" ]; then + echo "Error: docker/Dockerfile not found in ${dir}" >&2 + return 1 + fi + + # Run the container build script first + echo "Running build.sh in ${dir}..." + current_dir=$(pwd) + cd "$dir" + chmod +x ./build.sh + ./build.sh + + # Build the Docker image + echo "Building container image..." + docker buildx build \ + -f docker/Dockerfile \ + --platform=linux/amd64 \ + --provenance=false \ + --sbom=false \ + --build-arg BASE_IMAGE="${BASE_IMAGE}" \ + -t "${DOCKER_IMAGE}" \ + --load \ + . + + cd "$current_dir" +} + +# Push container image to ECR. +# Arguments (provided as environment variables): +# DOCKER_IMAGE=[full ECR image URI with tag] +# PUBLISH_CONTAINER_IMAGE=[true to push, false to skip, default is true] +function docker-push-container() { + + if [ "${PUBLISH_CONTAINER_IMAGE:-true}" = "true" ]; then + echo "Pushing to ECR..." + echo "Pushing ${DOCKER_IMAGE}..." + docker push "${DOCKER_IMAGE}" + echo "Push complete." + else + echo "PUBLISH_CONTAINER_IMAGE is false. Skipping push." + echo "Built image is available locally as: ${DOCKER_IMAGE}" + fi +} + +# Calculate and print Docker image name for NHS Notify containers. +# Arguments (provided as environment variables): +# CONTAINER_IMAGE_PREFIX, AWS_ACCOUNT_ID, AWS_REGION (required) +# CONTAINER_IMAGE_SUFFIX, ECR_REPO, CONTAINER_NAME, dir (optional) +function docker-calculate-image-name() { + local dir=${dir:-$PWD} + local container_name="${CONTAINER_NAME:-$(basename "$dir")}" + local ecr_repo="${ECR_REPO:-nhs-notify-main-acct}" + local image_suffix="${CONTAINER_IMAGE_SUFFIX:-$(docker-get-git-version-suffix)}" + local image_tag="${CONTAINER_IMAGE_PREFIX}-${container_name}" + local ecr_repo_uri="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ecr_repo}" + echo "${ecr_repo_uri}:${image_tag}-${image_suffix}" +} diff --git a/scripts/docker/docker.mk b/scripts/docker/docker.mk index a31ad9dbab..1421de4a4e 100644 --- a/scripts/docker/docker.mk +++ b/scripts/docker/docker.mk @@ -1,30 +1,54 @@ # This file is for you! Edit it to implement your own Docker make targets. # ============================================================================== -# Custom implementation - implementation of a make target should not exceed 5 lines of effective code. -# In most cases there should be no need to modify the existing make targets. +# NHS Notify Container Support (ECR with container name prefix) -docker-build: # Build Docker image - optional: docker_dir|dir=[path to the Dockerfile to use, default is '.'] @Development - make _docker cmd="build" \ - dir=$(or ${docker_dir}, ${dir}) - file=$(or ${docker_dir}, ${dir})/Dockerfile.effective - scripts/docker/dockerfile-linter.sh +docker-build: # Build container image - required: DOCKER_IMAGE, base_image=[base image]; optional: dir=[container directory] @Development + source scripts/docker/docker.lib.sh; \ + dir=$(or ${dir}, .); \ + BASE_IMAGE="${base_image}" \ + DOCKER_IMAGE="$${DOCKER_IMAGE}" \ + dir="$${dir}" \ + docker-build-container -docker-push: # Push Docker image - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Development - make _docker cmd="push" \ - dir=$(or ${docker_dir}, ${dir}) +docker-push: # Push container image to registry - required: DOCKER_IMAGE @Development + source scripts/docker/docker.lib.sh; \ + DOCKER_IMAGE="$${DOCKER_IMAGE}" \ + docker-push-container -clean:: # Remove Docker resources (docker) - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Operations - make _docker cmd="clean" \ - dir=$(or ${docker_dir}, ${dir}) +docker-build-and-push: # Build and push container in one workflow - required: base_image=[base image]; optional: dir=[container directory], ecr_repo=[ECR repo name], container_name=[container name] @Development + @dir=$(or ${dir}, .); \ + export DOCKER_IMAGE=$$(source scripts/docker/docker.lib.sh && \ + CONTAINER_IMAGE_PREFIX="$${CONTAINER_IMAGE_PREFIX}" \ + CONTAINER_IMAGE_SUFFIX="$${CONTAINER_IMAGE_SUFFIX:-}" \ + AWS_ACCOUNT_ID="$${AWS_ACCOUNT_ID}" \ + AWS_REGION="$${AWS_REGION}" \ + ECR_REPO="$${ECR_REPO:-${ecr_repo}}" \ + GITHUB_TOKEN="$${GITHUB_TOKEN:-}" \ + CONTAINER_NAME="$${CONTAINER_NAME:-${container_name}}" \ + dir="$${dir}" \ + docker-calculate-image-name); \ + echo "Building and pushing: $${DOCKER_IMAGE}"; \ + ${MAKE} docker-ghcr-login; \ + ${MAKE} docker-ecr-login; \ + ${MAKE} docker-build base_image=${base_image} dir="$${dir}" DOCKER_IMAGE="$${DOCKER_IMAGE}"; \ + ${MAKE} docker-push DOCKER_IMAGE="$${DOCKER_IMAGE}" -_docker: # Docker command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to the image directory where the Dockerfile is located, relative to the project's top-level directory, default is '.'] - # 'DOCKER_IMAGE' and 'DOCKER_TITLE' are passed to the functions as environment variables - DOCKER_IMAGE=$(or ${DOCKER_IMAGE}, $(or ${docker_image}, $(or ${IMAGE}, $(or ${image}, ghcr.io/org/repo)))) - DOCKER_TITLE=$(or "${DOCKER_TITLE}", $(or "${docker_title}", $(or "${TITLE}", $(or "${title}", "Service Docker image")))) - source scripts/docker/docker.lib.sh - dir=$(realpath ${dir}) - docker-${cmd} # 'dir' is accessible by the function as environment variable +docker-ecr-login: # Authenticate Docker with AWS ECR - required: AWS_ACCOUNT_ID, AWS_REGION @Development + source scripts/docker/docker.lib.sh; \ + AWS_ACCOUNT_ID="$${AWS_ACCOUNT_ID}" \ + AWS_REGION="$${AWS_REGION}" \ + docker-ecr-login + +docker-ghcr-login: # Authenticate Docker with GitHub Container Registry - required: GITHUB_TOKEN @Development + source scripts/docker/docker.lib.sh; \ + GITHUB_TOKEN="$${GITHUB_TOKEN:-}" \ + docker-ghcr-login + +clean:: # Remove container image and resources - required: DOCKER_IMAGE @Development + source scripts/docker/docker.lib.sh; \ + DOCKER_IMAGE="$${DOCKER_IMAGE:-}" \ + docker-clean # ============================================================================== # Quality checks - please DO NOT edit this section! @@ -34,50 +58,13 @@ docker-shellscript-lint: # Lint all Docker module shell scripts @Quality file=$${file} scripts/shellscript-linter.sh done -# ============================================================================== -# Module tests and examples - please DO NOT edit this section! - -docker-test-suite-run: # Run Docker test suite @ExamplesAndTests - scripts/docker/tests/docker.test.sh - -docker-example-build: # Build Docker example @ExamplesAndTests - source scripts/docker/docker.lib.sh - cd scripts/docker/examples/python - DOCKER_IMAGE=repository-template/docker-example-python - DOCKER_TITLE="Repository Template Docker Python Example" - TOOL_VERSIONS="$(shell git rev-parse --show-toplevel)/scripts/docker/examples/python/.tool-versions.example" - docker-build - -docker-example-lint: # Lint Docker example @ExamplesAndTests - dockerfile=scripts/docker/examples/python/Dockerfile - file=$${dockerfile} scripts/docker/dockerfile-linter.sh - -docker-example-run: # Run Docker example @ExamplesAndTests - source scripts/docker/docker.lib.sh - cd scripts/docker/examples/python - DOCKER_IMAGE=repository-template/docker-example-python - args=" \ - -it \ - --publish 8000:8000 \ - " - docker-run - -docker-example-clean: # Remove Docker example resources @ExamplesAndTests - source scripts/docker/docker.lib.sh - cd scripts/docker/examples/python - DOCKER_IMAGE=repository-template/docker-example-python - docker-clean - # ============================================================================== ${VERBOSE}.SILENT: \ - _docker \ clean \ docker-build \ - docker-example-build \ - docker-example-clean \ - docker-example-lint \ - docker-example-run \ + docker-build-and-push \ + docker-ecr-login \ + docker-ghcr-login \ docker-push \ docker-shellscript-lint \ - docker-test-suite-run \ diff --git a/scripts/docker/tests/.gitignore b/scripts/docker/tests/.gitignore deleted file mode 100644 index c50e8c0a82..0000000000 --- a/scripts/docker/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Dockerfile.effective diff --git a/scripts/docker/tests/.tool-versions.test b/scripts/docker/tests/.tool-versions.test deleted file mode 100644 index 9209311623..0000000000 --- a/scripts/docker/tests/.tool-versions.test +++ /dev/null @@ -1,2 +0,0 @@ -# python, SEE: https://hub.docker.com/_/python/tags -# docker/python 3.11.4-alpine3.18@sha256:0135ae6442d1269379860b361760ad2cf6ab7c403d21935a8015b48d5bf78a86 diff --git a/scripts/docker/tests/Dockerfile b/scripts/docker/tests/Dockerfile deleted file mode 100644 index b5ea56060e..0000000000 --- a/scripts/docker/tests/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -# `*:latest` will be replaced with a corresponding version stored in the '.tool-versions' file -# hadolint ignore=DL3007 -FROM python:latest diff --git a/scripts/docker/tests/VERSION b/scripts/docker/tests/VERSION deleted file mode 100644 index fb366351ec..0000000000 --- a/scripts/docker/tests/VERSION +++ /dev/null @@ -1,3 +0,0 @@ -${yyyy}${mm}${dd}-${hash} -$yyyy.$mm.$dd-$hash -somme-name-yyyyeah diff --git a/scripts/docker/tests/docker.test.sh b/scripts/docker/tests/docker.test.sh deleted file mode 100755 index 8f487b8f9a..0000000000 --- a/scripts/docker/tests/docker.test.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC1091,SC2034,SC2317 - -# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead. - -set -euo pipefail - -# Test suite for Docker functions. -# -# Usage: -# $ ./docker.test.sh -# -# Arguments (provided as environment variables): -# VERBOSE=true # Show all the executed commands, default is 'false' - -# ============================================================================== - -function main() { - - cd "$(git rev-parse --show-toplevel)" - source ./scripts/docker/docker.lib.sh - cd ./scripts/docker/tests - - DOCKER_IMAGE=repository-template/docker-test - DOCKER_TITLE="Repository Template Docker Test" - - test-docker-suite-setup - tests=( \ - test-docker-build \ - test-docker-image-from-signature \ - test-docker-version-file \ - test-docker-test \ - test-docker-run \ - test-docker-clean \ - test-docker-get-image-version-and-pull \ - ) - local status=0 - for test in "${tests[@]}"; do - { - echo -n "$test" - # shellcheck disable=SC2015 - $test && echo " PASS" || { echo " FAIL"; ((status++)); } - } - done - echo "Total: ${#tests[@]}, Passed: $(( ${#tests[@]} - status )), Failed: $status" - test-docker-suite-teardown - [ $status -gt 0 ] && return 1 || return 0 -} - -# ============================================================================== - -function test-docker-suite-setup() { - - : -} - -function test-docker-suite-teardown() { - - : -} - -# ============================================================================== - -function test-docker-build() { - - # Arrange - export BUILD_DATETIME="2023-09-04T15:46:34+0000" - # Act - docker-build > /dev/null 2>&1 - # Assert - docker image inspect "${DOCKER_IMAGE}:$(_get-effective-version)" > /dev/null 2>&1 && return 0 || return 1 -} - -function test-docker-image-from-signature() { - - # Arrange - TOOL_VERSIONS="$(git rev-parse --show-toplevel)/scripts/docker/tests/.tool-versions.test" - cp Dockerfile Dockerfile.effective - # Act - _replace-image-latest-by-specific-version - # Assert - grep -q "FROM python:.*-alpine.*@sha256:.*" Dockerfile.effective && return 0 || return 1 -} - -function test-docker-version-file() { - - # Arrange - export BUILD_DATETIME="2023-09-04T15:46:34+0000" - # Act - version-create-effective-file - # Assert - # shellcheck disable=SC2002 - ( - cat .version | grep -q "20230904-" && - cat .version | grep -q "2023.09.04-" && - cat .version | grep -q "somme-name-yyyyeah" - ) && return 0 || return 1 -} - -function test-docker-test() { - - # Arrange - cmd="python --version" - check="Python" - # Act - output=$(docker-check-test) - # Assert - echo "$output" | grep -q "PASS" -} - -function test-docker-run() { - - # Arrange - cmd="python --version" - # Act - output=$(docker-run) - # Assert - echo "$output" | grep -Eq "Python [0-9]+\.[0-9]+\.[0-9]+" -} - -function test-docker-clean() { - - # Arrange - version="$(_get-effective-version)" - # Act - docker-clean - # Assert - docker image inspect "${DOCKER_IMAGE}:${version}" > /dev/null 2>&1 && return 1 || return 0 -} - -function test-docker-get-image-version-and-pull() { - - # Arrange - name="ghcr.io/nhs-england-tools/github-runner-image" - match_version=".*-rt.*" - # Act - docker-get-image-version-and-pull > /dev/null 2>&1 - # Assert - docker images \ - --filter=reference="$name" \ - --format "{{.Tag}}" \ - | grep -vq "" -} - -# ============================================================================== - -function is-arg-true() { - - if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then - return 0 - else - return 1 - fi -} - -# ============================================================================== - -is-arg-true "${VERBOSE:-false}" && set -x - -main "$@" - -exit 0 diff --git a/scripts/lambda-container-build/docker.sh b/scripts/lambda-container-build/docker.sh deleted file mode 100755 index 85b00958d0..0000000000 --- a/scripts/lambda-container-build/docker.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash - -# Fail fast on errors, unset variables, and pipeline failures. -set -euo pipefail - -# Ensure build.sh is executable and build the lambda artifacts before producing the Docker image. -chmod +x ./build.sh -./build.sh - - -# Parse arguments -BASE_IMAGE="" -while [[ $# -gt 0 ]]; do - case $1 in - --base-image) - BASE_IMAGE="$2" - shift 2 - ;; - *) - echo "Unknown argument: $1" >&2 - exit 1 - ;; - esac -done - -if [[ -z "$BASE_IMAGE" ]]; then - echo "Error: --base-image parameter is required." >&2 - exit 1 -fi - -CSI="${PROJECT}-${ENVIRONMENT}-${COMPONENT}" -ECR_REPO="${ECR_REPO:-nhs-notify-main-acct}" -GHCR_LOGIN_TOKEN="${GITHUB_TOKEN}" -GHCR_LOGIN_USER="${GITHUB_ACTOR}" -LAMBDA_NAME="${LAMBDA_NAME:-$(basename "$PWD")}" - -## Set image tag suffix based on git metadata. -# Publish exactly one suffix: -# - release-- when HEAD is tagged -# - sha- otherwise -echo "Checking git metadata for image tag suffixes..." -SHORT_SHA="$(git rev-parse --short HEAD)" -SHA_SUFFIX="sha-${SHORT_SHA}" -GIT_TAG="$(git describe --tags --exact-match 2>/dev/null || true)" - -if [ -n "$GIT_TAG" ]; then - RELEASE_VERSION="${GIT_TAG#v}" - RELEASE_SUFFIX="release-${RELEASE_VERSION}-${SHORT_SHA}" - FINAL_SUFFIX="${RELEASE_SUFFIX}" - echo "On tag: $GIT_TAG" - echo "Publishing suffix: $FINAL_SUFFIX" -else - echo "Not on a tag" - FINAL_SUFFIX="${SHA_SUFFIX}" - echo "Publishing suffix: $FINAL_SUFFIX" -fi - -export IMAGE_TAG_SUFFIX="$FINAL_SUFFIX" - -## Check if we are running in the context of a Terraform apply or plan, and set PUBLISH_LAMBDA_IMAGE accordingly. We only want to push images to ECR on apply, not on plan. -echo "Checking if ACTION is 'apply' to set PUBLISH_LAMBDA_IMAGE..." -if [ "$ACTION" = "apply" ]; then - echo "Setting PUBLISH_LAMBDA_IMAGE to true for apply action" - export PUBLISH_LAMBDA_IMAGE="true" -else - echo "Not setting PUBLISH_LAMBDA_IMAGE for action ($ACTION)" -fi - -# Ensure required AWS/ECR configuration is present. -echo "BASE_IMAGE: ${BASE_IMAGE:-}" -echo "AWS_ACCOUNT_ID: ${AWS_ACCOUNT_ID:-}" -echo "AWS_REGION: ${AWS_REGION:-}" -echo "COMPONENT: ${COMPONENT:-}" -echo "CSI: ${CSI:-}" -echo "ECR_REPO: ${ECR_REPO:-}" -echo "ENVIRONMENT: ${ENVIRONMENT:-}" -echo "GHCR_LOGIN_TOKEN: ${GHCR_LOGIN_TOKEN:-}" -echo "GHCR_LOGIN_USER: ${GHCR_LOGIN_USER:-}" -echo "IMAGE_TAG_SUFFIX: ${IMAGE_TAG_SUFFIX:-}" -echo "LAMBDA_NAME: ${LAMBDA_NAME:-}" - -# Authenticate Docker with AWS ECR using an ephemeral login token. -aws ecr get-login-password --region "${AWS_REGION}" | docker login --username AWS --password-stdin "${AWS_ACCOUNT_ID}".dkr.ecr."${AWS_REGION}".amazonaws.com - -# Authenticate to GitHub Container Registry for base images. -if [ -n "${GHCR_LOGIN_USER:-}" ] && [ -n "${GHCR_LOGIN_TOKEN:-}" ]; then - echo "Attempting GHCR login as ${GHCR_LOGIN_USER}..." - if echo "${GHCR_LOGIN_TOKEN}" | docker login ghcr.io --username "${GHCR_LOGIN_USER}" --password-stdin; then - echo "GHCR login successful." - else - echo "GHCR login failed!" >&2 - exit 1 - fi -fi - -# Namespace tag by CSI and lambda name to avoid cross-environment collisions. -IMAGE_TAG="${CSI}-${LAMBDA_NAME}" - -# Compose the full ECR image references. -ECR_REPO_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}" - -# Final tag name we will produce. -IMAGE_TAG_FINAL="${ECR_REPO_URI}:${IMAGE_TAG}-${FINAL_SUFFIX}" - -echo "Will build and tag image: ${IMAGE_TAG_FINAL}" - -# Build and tag the Docker image for the lambda. -# --load makes the built image available to the local docker daemon (single-platform). -docker buildx build \ - -f docker/lambda/Dockerfile \ - --platform=linux/amd64 \ - --provenance=false \ - --sbom=false \ - --build-arg BASE_IMAGE="${BASE_IMAGE}" \ - -t "${IMAGE_TAG_FINAL}" \ - --load \ - . - -# Push the image tag to ECR on apply only. The Terraform configuration references the deterministic suffixed tag. -if [ "${PUBLISH_LAMBDA_IMAGE:-false}" = "true" ]; then - echo "PUBLISH_LAMBDA_IMAGE is set to true. Pushing Docker images to ECR..." - - echo "Pushing ${IMAGE_TAG_FINAL}..." - docker push "${IMAGE_TAG_FINAL}" - - echo "Push complete." -else - echo "PUBLISH_LAMBDA_IMAGE is not set to true (likely TF Plan). Skipping Docker push." - exit 0 -fi