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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<!-- - [ ] 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. TODO - Re-visit Trivy usage https://nhsd-jira.digital.nhs.uk/browse/CCM-15549 -->

---

Expand Down
2 changes: 1 addition & 1 deletion .github/actions/trivy-iac/action.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/trivy-package/action.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
33 changes: 28 additions & 5 deletions .github/scripts/dispatch_internal_repo_workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@
# --targetComponent <component> \
# --targetAccountGroup <group> \
# --terraformAction <action> \
# --internalRef <ref>
# --internalRef <ref> \
# --overrides <overrides> \
# --overrideProjectName <name> \
# --overrideRoleName <name>

#
# 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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" \
Expand All @@ -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,
Expand All @@ -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 \
Expand All @@ -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"
Expand All @@ -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}" \
Expand Down
5 changes: 0 additions & 5 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,22 @@ 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`.

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

Expand Down
15 changes: 15 additions & 0 deletions containers/example-app/build.sh
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions containers/example-app/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ARG BASE_IMAGE

FROM ${BASE_IMAGE}

WORKDIR /app

COPY dist/ .

EXPOSE 8080

CMD ["node", "server.js"]
49 changes: 49 additions & 0 deletions containers/example-app/jest.config.ts
Original file line number Diff line number Diff line change
@@ -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;
61 changes: 61 additions & 0 deletions containers/example-app/src/__tests__/server.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
23 changes: 23 additions & 0 deletions containers/example-app/src/server.ts
Original file line number Diff line number Diff line change
@@ -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();
}
7 changes: 7 additions & 0 deletions containers/example-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"src/**/*",
"jest.config.ts"
]
}
8 changes: 4 additions & 4 deletions infrastructure/terraform/bin/terraform.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 1 addition & 16 deletions scripts/config/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions scripts/config/trivy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ exit-code: 1 # When issues are found
scan:
skip-files:
- "**/.terraform/**/*"
- "**/node_modules/**/*"
Loading
Loading