diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6727d307cf..5a5026ee59 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 - + --- diff --git a/.github/actions/trivy-iac/action.yaml b/.github/actions/trivy-iac/action.yaml index 27075aca12..740d77ace1 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 7cad282fd5..196dc0370c 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 b73989e033..a52c1bbee4 100755 --- a/.github/scripts/dispatch_internal_repo_workflow.sh +++ b/.github/scripts/dispatch_internal_repo_workflow.sh @@ -20,7 +20,7 @@ # All arguments are required except terraformAction, and internalRef. # Example: # ./dispatch_internal_repo_workflow.sh \ -# --infraRepoName "nhs-notify-web-template-management" \ +# --infraRepoName "nhs-notify-dns" \ # --releaseVersion "v1.2.3" \ # --targetWorkflow "deploy.yaml" \ # --targetEnvironment "prod" \ @@ -86,7 +86,7 @@ while [[ $# -gt 0 ]]; do ;; esac done -# Validate required parameters + if [[ -z "$APP_PEM_FILE" ]]; then echo "[ERROR] PEM_FILE environment variable is not set or is empty." exit 1 @@ -140,7 +140,6 @@ PR_TRIGGER_PAT=$(curl --request POST \ -H "Authorization: Bearer ${JWT}" \ -H "X-GitHub-Api-Version: 2022-11-28" | jq -r '.token') - # Set default values if not provided if [[ -z "$PR_TRIGGER_PAT" ]]; then echo "[ERROR] PR_TRIGGER_PAT environment variable is not set or is empty." diff --git a/.github/workflows/scheduled-repository-template-sync.yaml b/.github/workflows/scheduled-repository-template-sync.yaml index b8484e12a2..e91148656a 100644 --- a/.github/workflows/scheduled-repository-template-sync.yaml +++ b/.github/workflows/scheduled-repository-template-sync.yaml @@ -16,10 +16,10 @@ jobs: steps: - name: Check out the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v4 - name: Check out external repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v4 with: repository: NHSDigital/nhs-notify-repository-template path: nhs-notify-repository-template diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 9794746d6d..5552785d3f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,12 +32,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v5.0.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 with: sarif_file: results.sarif diff --git a/.trivyignore b/.trivyignore index 86d7aa90ed..e69de29bb2 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,6 +0,0 @@ -# All CVEs below are tracked for remediation under the following Jira ticket: -# https://nhsd-jira.digital.nhs.uk/browse/CCM-14700 - -CVE-2026-26996 # https://avd.aquasec.com/nvd/cve-2026-26996 - minimatch - used by several dependencies, most notably eslint. we need to do the eslint v9 upgrade. -CVE-2026-27903 # https://avd.aquasec.com/nvd/cve-2026-27903 - minimatch - used by several dependencies, most notably eslint. we need to do the eslint v9 upgrade. -CVE-2026-27904 # https://avd.aquasec.com/nvd/cve-2026-27904 - minimatch - used by several dependencies, most notably eslint. we need to do the eslint v9 upgrade. diff --git a/AGENTS.md b/AGENTS.md index 55726d99af..7ff8086dd2 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. `build:archive`). +- 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`, `build:archive`) 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 build:archive`). +- 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 0000000000..dd99980a41 --- /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 0000000000..d864e2c3fa --- /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 0000000000..41e5a8f4e4 --- /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 0000000000..d307bab9f5 --- /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 0000000000..3fb1ec41e2 --- /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 0000000000..ea37d6966e --- /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 f06e0c5d6c..2851ba310f 100755 --- a/infrastructure/terraform/bin/terraform.sh +++ b/infrastructure/terraform/bin/terraform.sh @@ -428,7 +428,7 @@ fi; # Run pre.sh if [ -f "pre.sh" ]; then 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 ${?}"; + 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 6b5f90d737..9c5e690af9 100644 --- a/scripts/config/pre-commit.yaml +++ b/scripts/config/pre-commit.yaml @@ -3,11 +3,6 @@ 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/src/__tests__/utils/markdownit/fixtures/index\.ts - )$ - id: detect-aws-credentials args: [--allow-missing-credentials] - id: check-added-large-files @@ -17,12 +12,8 @@ repos: - id: forbid-new-submodules - id: mixed-line-ending - id: pretty-format-json - exclude: | - (?x)^( - .*/?package-lock.json | - packages/event-schemas/schemas/[^/]+/[^/]+\.json - )$ args: ['--autofix'] + exclude: '(^|/)package(-lock)?\.json$' # - id: ... - repo: local hooks: @@ -42,7 +33,7 @@ repos: hooks: - id: check-file-format name: Check file format - entry: /usr/bin/env check=branch exclude=frontend/src/__tests__/utils/markdownit/fixtures/index.ts ./scripts/githooks/check-file-format.sh + entry: /usr/bin/env check=branch ./scripts/githooks/check-file-format.sh language: script pass_filenames: false - repo: local diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt index 8d5dc59187..fb087d41c9 100644 --- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt +++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt @@ -1,3 +1,4 @@ +[A-Z]+s Bitwarden bot Cognito diff --git a/scripts/docker/docker.lib.sh b/scripts/docker/docker.lib.sh index cf756be585..aa8c8a2c6a 100644 --- a/scripts/docker/docker.lib.sh +++ b/scripts/docker/docker.lib.sh @@ -441,7 +441,7 @@ function docker-push-container() { 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 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}" diff --git a/scripts/docker/docker.mk b/scripts/docker/docker.mk index 1421de4a4e..d94c95c570 100644 --- a/scripts/docker/docker.mk +++ b/scripts/docker/docker.mk @@ -24,12 +24,10 @@ docker-build-and-push: # Build and push container in one workflow - required: ba 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}" @@ -42,7 +40,7 @@ docker-ecr-login: # Authenticate Docker with AWS ECR - required: AWS_ACCOUNT_ID, docker-ghcr-login: # Authenticate Docker with GitHub Container Registry - required: GITHUB_TOKEN @Development source scripts/docker/docker.lib.sh; \ - GITHUB_TOKEN="$${GITHUB_TOKEN:-}" \ + GITHUB_TOKEN="$${GITHUB_TOKEN}" \ docker-ghcr-login clean:: # Remove container image and resources - required: DOCKER_IMAGE @Development diff --git a/scripts/githooks/check-file-format.sh b/scripts/githooks/check-file-format.sh index 79e44d5718..b1e02efb91 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 } diff --git a/scripts/githooks/check-markdown-format.sh b/scripts/githooks/check-markdown-format.sh index 2c8f3ea7c5..c39a080d42 100755 --- a/scripts/githooks/check-markdown-format.sh +++ b/scripts/githooks/check-markdown-format.sh @@ -52,7 +52,7 @@ function main() { esac if [ -n "$files" ]; then - if command -v markdownlint-cli > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then + if command -v markdownlint > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then files="$files" run-markdownlint-natively else files="$files" run-markdownlint-in-docker @@ -60,13 +60,13 @@ function main() { fi } -# Run markdownlint-cli natively. +# Run markdownlint natively. # Arguments (provided as environment variables): # files=[files to check] function run-markdownlint-natively() { # shellcheck disable=SC2086 - markdownlint-cli \ + markdownlint \ $files \ --config "$PWD/scripts/config/markdownlint.yaml" } diff --git a/scripts/githooks/check-todos.sh b/scripts/githooks/check-todos.sh index 49a3663beb..83b7a80e85 100755 --- a/scripts/githooks/check-todos.sh +++ b/scripts/githooks/check-todos.sh @@ -33,7 +33,6 @@ EXCLUDED_FILES=( "Makefile" "project.code-workspace" "src/jekyll-devcontainer/src/.devcontainer/devcontainer.json" - ".eslintrc.json" ) EXCLUDED_DIRS=(