From 2616700505190164967d8d8cf8f0f78a6d05af75 Mon Sep 17 00:00:00 2001 From: aidenvaines-bjss <54067008+aidenvaines-bjss@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:23:47 +0000 Subject: [PATCH] Drift from template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/actions/trivy-iac/action.yaml | 2 +- .github/actions/trivy-package/action.yaml | 2 +- .../dispatch_internal_repo_workflow.sh | 33 +++- .trivyignore | 5 - AGENTS.md | 6 +- containers/example-app/build.sh | 15 ++ containers/example-app/docker/Dockerfile | 11 ++ containers/example-app/jest.config.ts | 49 ++++++ .../example-app/src/__tests__/server.test.ts | 61 ++++++++ containers/example-app/src/server.ts | 23 +++ containers/example-app/tsconfig.json | 7 + infrastructure/terraform/bin/terraform.sh | 8 +- scripts/config/pre-commit.yaml | 17 +- scripts/config/trivy.yaml | 1 + scripts/docker/docker.lib.sh | 146 ++++++++++++++++++ scripts/docker/docker.mk | 105 ++++++------- scripts/githooks/check-file-format.sh | 4 +- scripts/githooks/check-todos.sh | 4 +- scripts/init.mk | 2 +- 20 files changed, 401 insertions(+), 102 deletions(-) create mode 100755 containers/example-app/build.sh create mode 100644 containers/example-app/docker/Dockerfile create mode 100644 containers/example-app/jest.config.ts create mode 100644 containers/example-app/src/__tests__/server.test.ts create mode 100644 containers/example-app/src/server.ts create mode 100644 containers/example-app/tsconfig.json diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 812a8ca0..5a5026ee 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,7 @@ - [ ] I have added tests to cover my changes - [ ] I have updated the documentation accordingly - [ ] This PR is a result of pair or mob programming -- [ ] If I have used the 'skip-trivy-package' label I have done so responsibly and in the knowledge that this is being fixed as part of a separate ticket/PR. + --- diff --git a/.github/actions/trivy-iac/action.yaml b/.github/actions/trivy-iac/action.yaml index 27075aca..740d77ac 100644 --- a/.github/actions/trivy-iac/action.yaml +++ b/.github/actions/trivy-iac/action.yaml @@ -1,4 +1,4 @@ -# TODO - Re-visit Trivy usage https://nhsd-jira.digital.nhs.uk/browse/CCM-15549 +#TODO - Re-visit Trivy usage https://nhsd-jira.digital.nhs.uk/browse/CCM-15549 # name: "Trivy IaC Scan" # description: "Scan Terraform IaC using Trivy" # runs: diff --git a/.github/actions/trivy-package/action.yaml b/.github/actions/trivy-package/action.yaml index 7cad282f..196dc037 100644 --- a/.github/actions/trivy-package/action.yaml +++ b/.github/actions/trivy-package/action.yaml @@ -1,4 +1,4 @@ -# TODO - Re-visit Trivy usage https://nhsd-jira.digital.nhs.uk/browse/CCM-15549 +#TODO - Re-visit Trivy usage https://nhsd-jira.digital.nhs.uk/browse/CCM-15549 # name: "Trivy Package Scan" # description: "Scan project packages using Trivy" # runs: diff --git a/.github/scripts/dispatch_internal_repo_workflow.sh b/.github/scripts/dispatch_internal_repo_workflow.sh index 042d440b..a52c1bbe 100755 --- a/.github/scripts/dispatch_internal_repo_workflow.sh +++ b/.github/scripts/dispatch_internal_repo_workflow.sh @@ -11,19 +11,26 @@ # --targetComponent \ # --targetAccountGroup \ # --terraformAction \ -# --internalRef +# --internalRef \ +# --overrides \ +# --overrideProjectName \ +# --overrideRoleName + # # All arguments are required except terraformAction, and internalRef. # Example: # ./dispatch_internal_repo_workflow.sh \ -# --infraRepoName "nhs-notify-iam-webauth" \ +# --infraRepoName "nhs-notify-dns" \ # --releaseVersion "v1.2.3" \ # --targetWorkflow "deploy.yaml" \ # --targetEnvironment "prod" \ # --targetComponent "web" \ # --targetAccountGroup "core" \ # --terraformAction "apply" \ -# --internalRef "main" +# --internalRef "main" \ +# --overrides "tf_var=someString" \ +# --overrideProjectName nhs \ +# --overrideRoleName nhs-service-iam-role set -e @@ -65,6 +72,14 @@ while [[ $# -gt 0 ]]; do overrides="$2" shift 2 ;; + --overrideProjectName) # Override the project name (optional) + overrideProjectName="$2" + shift 2 + ;; + --overrideRoleName) # Override the role name (optional) + overrideRoleName="$2" + shift 2 + ;; *) echo "[ERROR] Unknown argument: $1" exit 1 @@ -149,6 +164,9 @@ echo " targetAccountGroup: $targetAccountGroup" echo " terraformAction: $terraformAction" echo " internalRef: $internalRef" echo " overrides: $overrides" +echo " overrideProjectName: $overrideProjectName" +echo " overrideRoleName: $overrideRoleName" +echo " targetProject: $targetProject" DISPATCH_EVENT=$(jq -ncM \ --arg infraRepoName "$infraRepoName" \ @@ -159,11 +177,17 @@ DISPATCH_EVENT=$(jq -ncM \ --arg terraformAction "$terraformAction" \ --arg targetWorkflow "$targetWorkflow" \ --arg overrides "$overrides" \ + --arg overrideProjectName "$overrideProjectName" \ + --arg overrideRoleName "$overrideRoleName" \ + --arg targetProject "$targetProject" \ '{ "ref": "'"$internalRef"'", "inputs": ( (if $infraRepoName != "" then { "infraRepoName": $infraRepoName } else {} end) + (if $terraformAction != "" then { "terraformAction": $terraformAction } else {} end) + + (if $overrideProjectName != "" then { "overrideProjectName": $overrideProjectName } else {} end) + + (if $overrideRoleName != "" then { "overrideRoleName": $overrideRoleName } else {} end) + + (if $targetProject != "" then { "targetProject": $targetProject } else {} end) + { "releaseVersion": $releaseVersion, "targetEnvironment": $targetEnvironment, @@ -176,7 +200,6 @@ DISPATCH_EVENT=$(jq -ncM \ echo "[INFO] Triggering workflow '$targetWorkflow' in nhs-notify-internal..." -set -x trigger_response=$(curl -s -L \ --fail \ -X POST \ @@ -185,7 +208,6 @@ trigger_response=$(curl -s -L \ -H "X-GitHub-Api-Version: 2022-11-28" \ "https://api.github.com/repos/NHSDigital/nhs-notify-internal/actions/workflows/$targetWorkflow/dispatches" \ -d "$DISPATCH_EVENT" 2>&1) -set +x if [[ $? -ne 0 ]]; then echo "[ERROR] Failed to trigger workflow. Response: $trigger_response" @@ -200,6 +222,7 @@ sleep 10 # Wait a few seconds before checking for the presence of the api to acc workflow_run_url="" for _ in {1..18}; do + response=$(curl -s -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${PR_TRIGGER_PAT}" \ diff --git a/.trivyignore b/.trivyignore index ac523628..e69de29b 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,5 +0,0 @@ -CVE-2026-1615 # https://avd.aquasec.com/nvd/cve-2026-1615 - jsonpath - fixed version not available. This is a dev-dependecy brought in by pa11y. pa11y will be removed soon (TODO: CCM-13084) -CVE-2026-27699 # https://avd.aquasec.com/nvd/cve-2026-26996 - basic-ftp - brought in via pa11y which will be removed imminently - TODO CCM-13084 remove this - -# All CVEs below are tracked for remediation under the following Jira ticket: -# https://nhsd-jira.digital.nhs.uk/browse/CCM-14700 diff --git a/AGENTS.md b/AGENTS.md index c85e1afd..7ff8086d 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/containers/example-app/build.sh b/containers/example-app/build.sh new file mode 100755 index 00000000..dd99980a --- /dev/null +++ b/containers/example-app/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail + +rm -rf dist + +npx esbuild \ + --bundle \ + --minify \ + --sourcemap \ + --target=es2022 \ + --platform=node \ + --entry-names=[name] \ + --outdir=dist \ + src/server.ts diff --git a/containers/example-app/docker/Dockerfile b/containers/example-app/docker/Dockerfile new file mode 100644 index 00000000..d864e2c3 --- /dev/null +++ b/containers/example-app/docker/Dockerfile @@ -0,0 +1,11 @@ +ARG BASE_IMAGE + +FROM ${BASE_IMAGE} + +WORKDIR /app + +COPY dist/ . + +EXPOSE 8080 + +CMD ["node", "server.js"] diff --git a/containers/example-app/jest.config.ts b/containers/example-app/jest.config.ts new file mode 100644 index 00000000..41e5a8f4 --- /dev/null +++ b/containers/example-app/jest.config.ts @@ -0,0 +1,49 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: './.reports/unit/coverage', + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'babel', + + coverageThreshold: { + global: { + branches: 0, + functions: 100, + lines: 90, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ['/__tests__/'], + transform: { '^.+\\.ts$': 'ts-jest' }, + testPathIgnorePatterns: ['.build'], + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + 'default', + [ + 'jest-html-reporter', + { + pageTitle: 'Test Report', + outputPath: './.reports/unit/test-report.html', + includeFailureMsg: true, + }, + ], + ], + + // The test environment that will be used for testing + testEnvironment: 'node', +}; + +export default config; diff --git a/containers/example-app/src/__tests__/server.test.ts b/containers/example-app/src/__tests__/server.test.ts new file mode 100644 index 00000000..d307bab9 --- /dev/null +++ b/containers/example-app/src/__tests__/server.test.ts @@ -0,0 +1,61 @@ +import http from 'http'; +import { createRequestHandler, startServer } from '../server'; + +describe('example-app server', () => { + describe('createRequestHandler', () => { + it('returns a request handler function', () => { + const handler = createRequestHandler(); + expect(typeof handler).toBe('function'); + }); + + it('responds with 200 status and JSON body', (done) => { + const handler = createRequestHandler(); + const mockReq = {} as http.IncomingMessage; + const mockRes = { + writeHead: jest.fn(), + end: jest.fn(), + } as unknown as http.ServerResponse; + + handler(mockReq, mockRes); + + expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); + expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ status: 'ok' })); + done(); + }); + }); + + describe('startServer', () => { + let server: http.Server; + const port = 8888; + + afterEach((done) => { + if (server) { + server.close(done); + } else { + done(); + } + }); + + it('starts server on specified port and responds correctly', (done) => { + server = startServer(port); + + // Wait a bit for server to start + setTimeout(() => { + http.get(`http://localhost:${port}`, (res) => { + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toBe('application/json'); + + let body = ''; + res.on('data', (chunk) => { + body += chunk; + }); + + res.on('end', () => { + expect(JSON.parse(body)).toEqual({ status: 'ok' }); + done(); + }); + }); + }, 100); + }); + }); +}); diff --git a/containers/example-app/src/server.ts b/containers/example-app/src/server.ts new file mode 100644 index 00000000..3fb1ec41 --- /dev/null +++ b/containers/example-app/src/server.ts @@ -0,0 +1,23 @@ +// Placeholder HTTP server for AppRunner. Replace with real application code. +import http from 'http'; + +export const createRequestHandler = () => { + return (_req: http.IncomingMessage, res: http.ServerResponse) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); + }; +}; + +export const startServer = (port: number = Number(process.env.PORT ?? 8080)) => { + const server = http.createServer(createRequestHandler()); + server.listen(port, () => { + console.log(`Placeholder app listening on port ${port}`); + }); + return server; +}; + +/* istanbul ignore next */ +// Only start server on local/direct run +if (require.main === module) { + startServer(); +} diff --git a/containers/example-app/tsconfig.json b/containers/example-app/tsconfig.json new file mode 100644 index 00000000..ea37d696 --- /dev/null +++ b/containers/example-app/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "src/**/*", + "jest.config.ts" + ] +} diff --git a/infrastructure/terraform/bin/terraform.sh b/infrastructure/terraform/bin/terraform.sh index 659b535c..2851ba31 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/scripts/config/pre-commit.yaml b/scripts/config/pre-commit.yaml index 9d30700a..9c5e690a 100644 --- a/scripts/config/pre-commit.yaml +++ b/scripts/config/pre-commit.yaml @@ -3,32 +3,17 @@ repos: rev: v5.0.0 # Use the ref you want to point at hooks: - id: trailing-whitespace - exclude: | - (?x)^( - frontend/src/__tests__/.*\.tsx\.snap | - frontend/.*\.min\..* - )$ - id: detect-aws-credentials args: [--allow-missing-credentials] - id: check-added-large-files - id: check-symlinks - id: detect-private-key - id: end-of-file-fixer - exclude: | - (?x)^( - frontend/src/__tests__/.*\.tsx\.snap | - frontend/.*\.min\..* | - .*\.bin - )$ - id: forbid-new-submodules - id: mixed-line-ending - id: pretty-format-json args: ['--autofix'] - exclude: | - (?x)^( - .*/?package-lock.json | - lambdas/jwks-key-rotation/src/__tests__/utils/test-public-key.jwks.json - )$ + exclude: '(^|/)package(-lock)?\.json$' # - id: ... - repo: local hooks: diff --git a/scripts/config/trivy.yaml b/scripts/config/trivy.yaml index a4eff466..29707dbb 100644 --- a/scripts/config/trivy.yaml +++ b/scripts/config/trivy.yaml @@ -4,3 +4,4 @@ exit-code: 1 # When issues are found scan: skip-files: - "**/.terraform/**/*" + - "**/node_modules/**/*" diff --git a/scripts/docker/docker.lib.sh b/scripts/docker/docker.lib.sh index 18787105..aa8c8a2c 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-main-acct-admail}" + 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 a31ad9db..d94c95c5 100644 --- a/scripts/docker/docker.mk +++ b/scripts/docker/docker.mk @@ -1,30 +1,52 @@ # 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}}" \ + CONTAINER_NAME="$${CONTAINER_NAME:-${container_name}}" \ + dir="$${dir}" \ + docker-calculate-image-name); \ + echo "Building and pushing: $${DOCKER_IMAGE}"; \ + ${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 +56,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/githooks/check-file-format.sh b/scripts/githooks/check-file-format.sh index 632e536b..b1e02efb 100755 --- a/scripts/githooks/check-file-format.sh +++ b/scripts/githooks/check-file-format.sh @@ -67,10 +67,8 @@ function main() { esac if command -v editorconfig-checker > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then - echo "Running editorconfig-checker natively" filter="$filter" dry_run_opt="${dry_run_opt:-}" run-editorconfig-natively else - echo "Running editorconfig-checker in Docker" filter="$filter" dry_run_opt="${dry_run_opt:-}" run-editorconfig-in-docker fi } @@ -103,7 +101,7 @@ function run-editorconfig-in-docker() { docker run --rm --platform linux/amd64 \ --volume "$PWD":/check \ "$image" \ - sh -c "set -x; ec --exclude '.git/' $dry_run_opt \$($filter) /dev/null" + sh -c "ec --exclude '.git/' $dry_run_opt \$($filter) /dev/null" } # ============================================================================== diff --git a/scripts/githooks/check-todos.sh b/scripts/githooks/check-todos.sh index 4135cb2a..83b7a80e 100755 --- a/scripts/githooks/check-todos.sh +++ b/scripts/githooks/check-todos.sh @@ -120,7 +120,7 @@ function search_todos() { # If the file is excluded, skip it if [ "$skip" = false ] && [ -f "$file" ]; then - file_todos=$(grep -nHiE '\bTODO(:| )' "$file" || true) + file_todos=$(grep -nHiE '\bTODO\b' "$file" || true) [ -n "$file_todos" ] && todos+="$file_todos\n" fi done @@ -136,7 +136,7 @@ function filter_todos_with_valid_jira_ticket() { while IFS= read -r line; do # Only lines with TODO but without a valid JIRA ticket - if grep -qnHiE '\bTODO(:| )' <<< "$line"; then + if grep -qnHiE '\bTODO\b' <<< "$line"; then if ! [[ "$line" =~ $jira_regex ]]; then todos_without_ticket+="$line\n" fi diff --git a/scripts/init.mk b/scripts/init.mk index 416ff5e0..885d2d33 100644 --- a/scripts/init.mk +++ b/scripts/init.mk @@ -46,7 +46,7 @@ _install-dependency: # Install asdf dependency - mandatory: name=[listed in the asdf install ${name} $(or ${version},) _install-dependencies: # Install all the dependencies listed in .tool-versions - for plugin in $$(grep '^[a-z]' .tool-versions | cut -f1 -d' '); do \ + for plugin in $$(grep ^[a-z] .tool-versions | sed 's/[[:space:]].*//'); do $(MAKE) _install-dependency name=$${plugin}; \ done