From e7aa9f0aad42bf2a426a8a4058e207f56a1f7f04 Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Fri, 22 May 2026 17:25:54 +0100 Subject: [PATCH 01/13] CCM-18086: Add cache to acceptance tests --- .../acceptance-tests-setup/action.yaml | 62 +++++++++++++++++++ .github/actions/acceptance-tests/action.yaml | 18 ++---- .github/actions/next-build-cache/action.yaml | 36 +++++++++++ .github/actions/playwright-cache/action.yaml | 34 ++++++++++ scripts/tests/api.sh | 4 +- scripts/tests/backend.sh | 4 +- scripts/tests/event.sh | 4 +- scripts/tests/ui-accessibility.sh | 4 +- scripts/tests/ui-component.sh | 4 +- scripts/tests/ui-e2e.sh | 4 +- scripts/tests/ui-user-timeout.sh | 4 +- 11 files changed, 144 insertions(+), 34 deletions(-) create mode 100644 .github/actions/acceptance-tests-setup/action.yaml create mode 100644 .github/actions/next-build-cache/action.yaml create mode 100644 .github/actions/playwright-cache/action.yaml diff --git a/.github/actions/acceptance-tests-setup/action.yaml b/.github/actions/acceptance-tests-setup/action.yaml new file mode 100644 index 0000000000..19e75193d2 --- /dev/null +++ b/.github/actions/acceptance-tests-setup/action.yaml @@ -0,0 +1,62 @@ +name: Acceptance tests +description: "Run acceptance tests for this repo" + +inputs: + testType: + description: Type of test to run + required: true + + targetComponent: + description: Name of the component under test + required: true + + skipRestore: + description: 'Skips restoring from caches' + required: false + default: true + +runs: + using: "composite" + + steps: + - name: Fetch terraform output + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + name: terraform-output-${{ inputs.targetComponent }} + + - name: "Restore node_modules from cache" + uses: ./.github/actions/node-modules-cache + with: + node_version: "22.22.0" + skip_restore: "${{ inputs.skipRestore }}" + + - name: "Restore playwright dependencies from cache" + uses: ./.github/actions/playwright-cache + with: + playwright_version: "1.59.1" + skip_restore: "${{ inputs.skipRestore }}" + + - name: "Restore next build from cache" + uses: ./.github/actions/next-build-cache + with: + node_version: "22.22.0" + skip_restore: "${{ inputs.skipRestore }}" + + - name: Generate outputs file + shell: bash + run: | + root_dir=${GITHUB_WORKSPACE} + mv ./terraform_output.json ./sandbox_tf_outputs.json + npm run generate-outputs sandbox-output + + - name: Run test - ${{ inputs.testType }} + shell: bash + run: | + make test-${{ inputs.testType }} + + - name: Archive test results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + if: always() + with: + name: ${{ inputs.testType }} - test report + path: "tests/acceptance-test-report" diff --git a/.github/actions/acceptance-tests/action.yaml b/.github/actions/acceptance-tests/action.yaml index 2d310f770a..c07d452366 100644 --- a/.github/actions/acceptance-tests/action.yaml +++ b/.github/actions/acceptance-tests/action.yaml @@ -23,21 +23,13 @@ runs: using: "composite" steps: - - name: Fetch terraform output - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + - name: Acceptance tests setup + uses: ./.github/actions/acceptance-tests-setup with: - name: terraform-output-${{ inputs.targetComponent }} - - name: "Repo setup" - shell: bash - run: | - npm ci + skipRestore: false + testType: "${{ inputs.testType }}" + targetComponent: "${{ inputs.targetComponent }}" - - name: Generate outputs file - shell: bash - run: | - root_dir=${GITHUB_WORKSPACE} - mv ./terraform_output.json ./sandbox_tf_outputs.json - npm run generate-outputs sandbox-output - name: Run test - ${{ inputs.testType }} shell: bash diff --git a/.github/actions/next-build-cache/action.yaml b/.github/actions/next-build-cache/action.yaml new file mode 100644 index 0000000000..811e79baff --- /dev/null +++ b/.github/actions/next-build-cache/action.yaml @@ -0,0 +1,36 @@ +name: 'Node modules cache + setup' +description: 'Run nextjs build, restoring from cache if possible' + +inputs: + node_version: + description: 'Node.js version' + required: true + cache_lock_path: + description: 'Path(s) to package-lock.json for cache key' + required: false + default: '**/package-lock.json' + skip_restore: + description: 'Skips restoring next build' + required: false + default: false + +runs: + using: 'composite' + steps: + + - name: 'Restore next build from cache' + id: next-build-cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + frontend/.next + key: ${{ runner.os }}-node-${{ inputs.node_version }}-${{ hashFiles(inputs.cache_lock_path) }} + restore-keys: | + ${{ runner.os }}-next-build-${{ inputs.node_version }}- + lookup-only: ${{ inputs.skip_restore }} + + - name: 'Install dependencies (cache miss)' + if: steps.node-modules-cache.outputs.cache-hit != 'true' + shell: bash + run: | + npm run build diff --git a/.github/actions/playwright-cache/action.yaml b/.github/actions/playwright-cache/action.yaml new file mode 100644 index 0000000000..b2cc1e8c13 --- /dev/null +++ b/.github/actions/playwright-cache/action.yaml @@ -0,0 +1,34 @@ +name: 'Playwright browser cache' +description: 'Cache Playwright browser binaries and install system dependencies' + +inputs: + playwright_version: + description: 'Playwright version to cache.' + required: true + default: '' + skip_restore: + description: 'Skips restoring playwright browser cache' + required: false + default: false + +runs: + using: 'composite' + steps: + + - name: 'Restore Playwright browsers from cache' + id: playwright-cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ inputs.playwright_version }} + lookup-only: ${{ inputs.skip_restore }} + + - name: 'Install Playwright browsers (cache miss)' + if: steps.playwright-cache.outputs.cache-hit != 'true' + shell: bash + run: npx playwright install --with-deps + + - name: 'Install Playwright system dependencies (cache hit)' + if: steps.playwright-cache.outputs.cache-hit == 'true' + shell: bash + run: npx playwright install-deps diff --git a/scripts/tests/api.sh b/scripts/tests/api.sh index 01beadfb7d..8e7550eb90 100755 --- a/scripts/tests/api.sh +++ b/scripts/tests/api.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:api || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" diff --git a/scripts/tests/backend.sh b/scripts/tests/backend.sh index 8b8440d45b..ad371ae3e5 100755 --- a/scripts/tests/backend.sh +++ b/scripts/tests/backend.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:backend || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" diff --git a/scripts/tests/event.sh b/scripts/tests/event.sh index 881be48eb3..35e94d02bc 100755 --- a/scripts/tests/event.sh +++ b/scripts/tests/event.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:event || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" diff --git a/scripts/tests/ui-accessibility.sh b/scripts/tests/ui-accessibility.sh index d139b5d5f8..e81e33bbdb 100755 --- a/scripts/tests/ui-accessibility.sh +++ b/scripts/tests/ui-accessibility.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:accessibility || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" diff --git a/scripts/tests/ui-component.sh b/scripts/tests/ui-component.sh index eb028b139d..39b2dbc5e7 100755 --- a/scripts/tests/ui-component.sh +++ b/scripts/tests/ui-component.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:component || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" diff --git a/scripts/tests/ui-e2e.sh b/scripts/tests/ui-e2e.sh index a8e40b9480..14ddc9282b 100755 --- a/scripts/tests/ui-e2e.sh +++ b/scripts/tests/ui-e2e.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:e2e || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" diff --git a/scripts/tests/ui-user-timeout.sh b/scripts/tests/ui-user-timeout.sh index 5aa840909c..a588b1c39a 100755 --- a/scripts/tests/ui-user-timeout.sh +++ b/scripts/tests/ui-user-timeout.sh @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -npx playwright install --with-deps > /dev/null -cd tests/test-team +cd "$(git rev-parse --show-toplevel)/tests/test-team" TEST_EXIT_CODE=0 npm run test:user-timeout || TEST_EXIT_CODE=$? echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" From 4cc00965988cade8323255d9fe8fc7ebcb07bcca Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Fri, 22 May 2026 18:24:20 +0100 Subject: [PATCH 02/13] CCM-18086: Various fixes --- .github/actions/acceptance-tests-setup/action.yaml | 14 +++++++------- .github/actions/next-build-cache/action.yaml | 4 +--- .github/actions/node-modules-cache/action.yaml | 2 -- .../config/accessibility/accessibility.config.ts | 7 +------ .../test-team/config/component/component.config.ts | 7 +------ tests/test-team/config/e2e/e2e.config.ts | 7 +------ .../config/user-timeout/user-timeout.config.ts | 9 +-------- 7 files changed, 12 insertions(+), 38 deletions(-) diff --git a/.github/actions/acceptance-tests-setup/action.yaml b/.github/actions/acceptance-tests-setup/action.yaml index 19e75193d2..ff18bc6630 100644 --- a/.github/actions/acceptance-tests-setup/action.yaml +++ b/.github/actions/acceptance-tests-setup/action.yaml @@ -24,6 +24,13 @@ runs: with: name: terraform-output-${{ inputs.targetComponent }} + - name: Generate outputs file + shell: bash + run: | + root_dir=${GITHUB_WORKSPACE} + mv ./terraform_output.json ./sandbox_tf_outputs.json + npm run generate-outputs sandbox-output + - name: "Restore node_modules from cache" uses: ./.github/actions/node-modules-cache with: @@ -42,13 +49,6 @@ runs: node_version: "22.22.0" skip_restore: "${{ inputs.skipRestore }}" - - name: Generate outputs file - shell: bash - run: | - root_dir=${GITHUB_WORKSPACE} - mv ./terraform_output.json ./sandbox_tf_outputs.json - npm run generate-outputs sandbox-output - - name: Run test - ${{ inputs.testType }} shell: bash run: | diff --git a/.github/actions/next-build-cache/action.yaml b/.github/actions/next-build-cache/action.yaml index 811e79baff..6350bf617a 100644 --- a/.github/actions/next-build-cache/action.yaml +++ b/.github/actions/next-build-cache/action.yaml @@ -25,12 +25,10 @@ runs: path: | frontend/.next key: ${{ runner.os }}-node-${{ inputs.node_version }}-${{ hashFiles(inputs.cache_lock_path) }} - restore-keys: | - ${{ runner.os }}-next-build-${{ inputs.node_version }}- lookup-only: ${{ inputs.skip_restore }} - name: 'Install dependencies (cache miss)' if: steps.node-modules-cache.outputs.cache-hit != 'true' shell: bash run: | - npm run build + INCLUDE_AUTH_PAGES=true npm run build diff --git a/.github/actions/node-modules-cache/action.yaml b/.github/actions/node-modules-cache/action.yaml index 5862cd0c7c..934d06bb77 100644 --- a/.github/actions/node-modules-cache/action.yaml +++ b/.github/actions/node-modules-cache/action.yaml @@ -33,8 +33,6 @@ runs: node_modules **/node_modules key: ${{ runner.os }}-node-${{ inputs.node_version }}-${{ hashFiles(inputs.cache_lock_path) }} - restore-keys: | - ${{ runner.os }}-node-${{ inputs.node_version }}- lookup-only: ${{ inputs.skip_restore }} - name: 'Install dependencies (cache miss)' diff --git a/tests/test-team/config/accessibility/accessibility.config.ts b/tests/test-team/config/accessibility/accessibility.config.ts index 8b7291b954..d4446c1d4b 100644 --- a/tests/test-team/config/accessibility/accessibility.config.ts +++ b/tests/test-team/config/accessibility/accessibility.config.ts @@ -2,11 +2,6 @@ import path from 'node:path'; import { defineConfig, devices } from '@playwright/test'; import baseConfig from '../playwright.config'; -const buildCommand = [ - 'INCLUDE_AUTH_PAGES=true', - 'npm run build && npm run start', -].join(' '); - export default defineConfig({ ...baseConfig, fullyParallel: true, @@ -46,7 +41,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { timeout: 4 * 60 * 1000, // 4 minutes - command: buildCommand, + command: 'npm run start', cwd: path.resolve(__dirname, '../../../..'), url: 'http://localhost:3000/templates/create-and-submit-templates', reuseExistingServer: !process.env.CI, diff --git a/tests/test-team/config/component/component.config.ts b/tests/test-team/config/component/component.config.ts index b8e6883298..b0ea81d550 100644 --- a/tests/test-team/config/component/component.config.ts +++ b/tests/test-team/config/component/component.config.ts @@ -2,11 +2,6 @@ import path from 'node:path'; import { defineConfig, devices } from '@playwright/test'; import baseConfig from '../playwright.config'; -const buildCommand = [ - 'INCLUDE_AUTH_PAGES=true', - 'npm run build && npm run start', -].join(' '); - export default defineConfig({ ...baseConfig, @@ -46,7 +41,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { timeout: 4 * 60 * 1000, // 4 minutes - command: buildCommand, + command: 'npm run start', cwd: path.resolve(__dirname, '../../../..'), url: 'http://localhost:3000/templates/create-and-submit-templates', reuseExistingServer: !process.env.CI, diff --git a/tests/test-team/config/e2e/e2e.config.ts b/tests/test-team/config/e2e/e2e.config.ts index 1f40fe915d..f2a61b8ba7 100644 --- a/tests/test-team/config/e2e/e2e.config.ts +++ b/tests/test-team/config/e2e/e2e.config.ts @@ -2,11 +2,6 @@ import path from 'node:path'; import { defineConfig, devices } from '@playwright/test'; import baseConfig from '../playwright.config'; -const buildCommand = [ - 'INCLUDE_AUTH_PAGES=true', - 'npm run build && npm run start', -].join(' '); - export default defineConfig({ ...baseConfig, fullyParallel: true, @@ -46,7 +41,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { timeout: 4 * 60 * 1000, // 4 minutes - command: buildCommand, + command: 'npm run start', cwd: path.resolve(__dirname, '../../../..'), url: 'http://localhost:3000/templates/create-and-submit-templates', reuseExistingServer: !process.env.CI, diff --git a/tests/test-team/config/user-timeout/user-timeout.config.ts b/tests/test-team/config/user-timeout/user-timeout.config.ts index bc3871d8da..b902e5827a 100644 --- a/tests/test-team/config/user-timeout/user-timeout.config.ts +++ b/tests/test-team/config/user-timeout/user-timeout.config.ts @@ -2,13 +2,6 @@ import path from 'node:path'; import { defineConfig, devices } from '@playwright/test'; import baseConfig from '../playwright.config'; -const buildCommand = [ - 'INCLUDE_AUTH_PAGES=true', - 'NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS=25', - 'NEXT_PUBLIC_PROMPT_SECONDS_BEFORE_LOGOUT=5', - 'npm run build && npm run start', -].join(' '); - export default defineConfig({ ...baseConfig, @@ -50,7 +43,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { timeout: 4 * 60 * 1000, // 4 minutes - command: buildCommand, + command: 'npm run start', cwd: path.resolve(__dirname, '../../../..'), url: 'http://localhost:3000/templates/create-and-submit-templates', reuseExistingServer: !process.env.CI, From 6a6ec369a3cd9ac0ab1ae4a27068ac982a0df9fa Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Fri, 22 May 2026 18:36:45 +0100 Subject: [PATCH 03/13] CCM-18086: ordering --- .github/actions/acceptance-tests-setup/action.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/acceptance-tests-setup/action.yaml b/.github/actions/acceptance-tests-setup/action.yaml index ff18bc6630..097daeb510 100644 --- a/.github/actions/acceptance-tests-setup/action.yaml +++ b/.github/actions/acceptance-tests-setup/action.yaml @@ -24,6 +24,12 @@ runs: with: name: terraform-output-${{ inputs.targetComponent }} + - name: "Restore node_modules from cache" + uses: ./.github/actions/node-modules-cache + with: + node_version: "22.22.0" + skip_restore: "${{ inputs.skipRestore }}" + - name: Generate outputs file shell: bash run: | @@ -31,12 +37,6 @@ runs: mv ./terraform_output.json ./sandbox_tf_outputs.json npm run generate-outputs sandbox-output - - name: "Restore node_modules from cache" - uses: ./.github/actions/node-modules-cache - with: - node_version: "22.22.0" - skip_restore: "${{ inputs.skipRestore }}" - - name: "Restore playwright dependencies from cache" uses: ./.github/actions/playwright-cache with: From 96a6cda4a178abfd1ccc8c8be9f9580495859979 Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Fri, 22 May 2026 19:24:38 +0100 Subject: [PATCH 04/13] CCM-18086: internal ref --- .../actions/acceptance-tests-setup/action.yaml | 15 --------------- .github/actions/acceptance-tests/action.yaml | 1 - .github/workflows/stage-4-acceptance.yaml | 1 + 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/actions/acceptance-tests-setup/action.yaml b/.github/actions/acceptance-tests-setup/action.yaml index 097daeb510..b229e2201d 100644 --- a/.github/actions/acceptance-tests-setup/action.yaml +++ b/.github/actions/acceptance-tests-setup/action.yaml @@ -2,9 +2,6 @@ name: Acceptance tests description: "Run acceptance tests for this repo" inputs: - testType: - description: Type of test to run - required: true targetComponent: description: Name of the component under test @@ -48,15 +45,3 @@ runs: with: node_version: "22.22.0" skip_restore: "${{ inputs.skipRestore }}" - - - name: Run test - ${{ inputs.testType }} - shell: bash - run: | - make test-${{ inputs.testType }} - - - name: Archive test results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - if: always() - with: - name: ${{ inputs.testType }} - test report - path: "tests/acceptance-test-report" diff --git a/.github/actions/acceptance-tests/action.yaml b/.github/actions/acceptance-tests/action.yaml index c07d452366..9855f37f6c 100644 --- a/.github/actions/acceptance-tests/action.yaml +++ b/.github/actions/acceptance-tests/action.yaml @@ -27,7 +27,6 @@ runs: uses: ./.github/actions/acceptance-tests-setup with: skipRestore: false - testType: "${{ inputs.testType }}" targetComponent: "${{ inputs.targetComponent }}" diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml index 25a6b1f935..2dea343e19 100644 --- a/.github/workflows/stage-4-acceptance.yaml +++ b/.github/workflows/stage-4-acceptance.yaml @@ -47,3 +47,4 @@ jobs: --targetEnvironment "pr${{ inputs.pr_number }}" \ --targetAccountGroup "nhs-notify-template-management-dev" \ --targetComponent "sbx" + --internalRef "feature/CCM-18086_acceptance-tests-setup" From cf98362d397e998ba98f2d0c9413f1f0bb1963ea Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Fri, 22 May 2026 19:44:34 +0100 Subject: [PATCH 05/13] CCM-18086: fix --- .github/actions/next-build-cache/action.yaml | 2 +- .github/workflows/stage-4-acceptance.yaml | 2 +- tests/test-team/config/api/api.setup.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/actions/next-build-cache/action.yaml b/.github/actions/next-build-cache/action.yaml index 6350bf617a..dc5072cec1 100644 --- a/.github/actions/next-build-cache/action.yaml +++ b/.github/actions/next-build-cache/action.yaml @@ -24,7 +24,7 @@ runs: with: path: | frontend/.next - key: ${{ runner.os }}-node-${{ inputs.node_version }}-${{ hashFiles(inputs.cache_lock_path) }} + key: ${{ runner.os }}-next-build-${{ inputs.node_version }}-${{ hashFiles(inputs.cache_lock_path) }} lookup-only: ${{ inputs.skip_restore }} - name: 'Install dependencies (cache miss)' diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml index 2dea343e19..38a706e659 100644 --- a/.github/workflows/stage-4-acceptance.yaml +++ b/.github/workflows/stage-4-acceptance.yaml @@ -46,5 +46,5 @@ jobs: --targetWorkflow "dispatch-contextual-tests-dynamic-env.yaml" \ --targetEnvironment "pr${{ inputs.pr_number }}" \ --targetAccountGroup "nhs-notify-template-management-dev" \ - --targetComponent "sbx" + --targetComponent "sbx" \ --internalRef "feature/CCM-18086_acceptance-tests-setup" diff --git a/tests/test-team/config/api/api.setup.ts b/tests/test-team/config/api/api.setup.ts index 1d89bceade..fa00c77d72 100644 --- a/tests/test-team/config/api/api.setup.ts +++ b/tests/test-team/config/api/api.setup.ts @@ -4,6 +4,8 @@ import { BackendConfigHelper } from 'nhs-notify-web-template-management-util-bac import { getTestContext } from 'helpers/context/context'; setup('api test setup', async () => { + setup.setTimeout(60_000); + const backendConfig = BackendConfigHelper.fromTerraformOutputsFile( path.join(__dirname, '..', '..', '..', '..', 'sandbox_tf_outputs.json') ); From fdab001d1d3a2eb4ea13c6efb783a14625b33745 Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 09:17:18 +0100 Subject: [PATCH 06/13] CCM-18086: Fix skipRestore --- .github/actions/acceptance-tests-setup/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/acceptance-tests-setup/action.yaml b/.github/actions/acceptance-tests-setup/action.yaml index b229e2201d..507a91b1be 100644 --- a/.github/actions/acceptance-tests-setup/action.yaml +++ b/.github/actions/acceptance-tests-setup/action.yaml @@ -25,7 +25,7 @@ runs: uses: ./.github/actions/node-modules-cache with: node_version: "22.22.0" - skip_restore: "${{ inputs.skipRestore }}" + skip_restore: false # We need to always restore the node modules because they might be needed for the build step - name: Generate outputs file shell: bash From 4fcd5e880009ec7d601094c24b1094fc1ab683dd Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 10:47:33 +0100 Subject: [PATCH 07/13] CCM-18086: Fix pipeline yaml --- .github/actions/next-build-cache/action.yaml | 4 ++-- .../routing-config.event.spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/next-build-cache/action.yaml b/.github/actions/next-build-cache/action.yaml index dc5072cec1..075f5f6634 100644 --- a/.github/actions/next-build-cache/action.yaml +++ b/.github/actions/next-build-cache/action.yaml @@ -24,11 +24,11 @@ runs: with: path: | frontend/.next - key: ${{ runner.os }}-next-build-${{ inputs.node_version }}-${{ hashFiles(inputs.cache_lock_path) }} + key: ${{ runner.os }}-next-build-${{ github.run_id }} lookup-only: ${{ inputs.skip_restore }} - name: 'Install dependencies (cache miss)' - if: steps.node-modules-cache.outputs.cache-hit != 'true' + if: steps.next-build-cache.outputs.cache-hit != 'true' shell: bash run: | INCLUDE_AUTH_PAGES=true npm run build diff --git a/tests/test-team/template-mgmt-event-tests/routing-config.event.spec.ts b/tests/test-team/template-mgmt-event-tests/routing-config.event.spec.ts index a74e8cbf29..28ca2141f2 100644 --- a/tests/test-team/template-mgmt-event-tests/routing-config.event.spec.ts +++ b/tests/test-team/template-mgmt-event-tests/routing-config.event.spec.ts @@ -146,7 +146,7 @@ test.describe('Event publishing - Routing Config', () => { ); expect(events).toHaveLength(2); - }).toPass({ timeout: 60_000 }); + }).toPass({ timeout: 90_000 }); }); test('Expect a draft event and a deleted event', async ({ @@ -228,7 +228,7 @@ test.describe('Event publishing - Routing Config', () => { ); expect(events).toHaveLength(2); - }).toPass({ timeout: 60_000 }); + }).toPass({ timeout: 90_000 }); }); test('Expect routing config and template completed events on submit', async ({ @@ -261,7 +261,7 @@ test.describe('Event publishing - Routing Config', () => { ]), }); expect(seedEvents).toHaveLength(3); - }).toPass({ timeout: 60_000 }); + }).toPass({ timeout: 90_000 }); const start = new Date(); @@ -389,6 +389,6 @@ test.describe('Event publishing - Routing Config', () => { }), }) ); - }).toPass({ timeout: 60_000 }); + }).toPass({ timeout: 90_000 }); }); }); From 2c9028ba0f52eb1d22eec77fec2b93f1daed2ad9 Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 11:09:21 +0100 Subject: [PATCH 08/13] CCM-18086: Add .env file to build cache --- .github/actions/next-build-cache/action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/next-build-cache/action.yaml b/.github/actions/next-build-cache/action.yaml index 075f5f6634..7c8723ee6f 100644 --- a/.github/actions/next-build-cache/action.yaml +++ b/.github/actions/next-build-cache/action.yaml @@ -24,6 +24,7 @@ runs: with: path: | frontend/.next + frontend/.env key: ${{ runner.os }}-next-build-${{ github.run_id }} lookup-only: ${{ inputs.skip_restore }} From 3f65737008c6b16fe85ef4707c0682abc2ee674e Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 11:31:07 +0100 Subject: [PATCH 09/13] CCM-18086: Test config changes --- tests/test-team/config/backend/backend.setup.ts | 2 ++ tests/test-team/helpers/client/client-helper.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test-team/config/backend/backend.setup.ts b/tests/test-team/config/backend/backend.setup.ts index 40c767ab89..6a263216cd 100644 --- a/tests/test-team/config/backend/backend.setup.ts +++ b/tests/test-team/config/backend/backend.setup.ts @@ -4,6 +4,8 @@ import { BackendConfigHelper } from 'nhs-notify-web-template-management-util-bac import { getTestContext } from 'helpers/context/context'; setup('backend test setup', async () => { + setup.setTimeout(60_000); + const backendConfig = BackendConfigHelper.fromTerraformOutputsFile( path.join(__dirname, '..', '..', '..', '..', 'sandbox_tf_outputs.json') ); diff --git a/tests/test-team/helpers/client/client-helper.ts b/tests/test-team/helpers/client/client-helper.ts index 66bbe2e415..8be423e38f 100644 --- a/tests/test-team/helpers/client/client-helper.ts +++ b/tests/test-team/helpers/client/client-helper.ts @@ -150,7 +150,7 @@ export const testClients: Record = { export class ClientConfigurationHelper { private readonly ssmClient = new SSMClient({ region: 'eu-west-2', - retryMode: 'standard', + retryMode: 'adaptive', maxAttempts: 10, }); From b016aef32187fa6514aa6a2272ec288a88cdc9af Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 15:11:28 +0100 Subject: [PATCH 10/13] CCM-18086: Seed clients serially --- tests/test-team/helpers/client/client-helper.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test-team/helpers/client/client-helper.ts b/tests/test-team/helpers/client/client-helper.ts index 8be423e38f..dc4afd5f40 100644 --- a/tests/test-team/helpers/client/client-helper.ts +++ b/tests/test-team/helpers/client/client-helper.ts @@ -161,16 +161,15 @@ export class ClientConfigurationHelper { } async setup() { - return Promise.all( - Object.entries(testClients).map(async ([clientKey, value]) => { + // seed clients serially to avoid overwhelming SSM + for (const [clientKey, value] of Object.entries(testClients)) { if (value !== undefined) { await this.putClient( ClientConfigurationHelper.clientIdFromKey(clientKey as ClientKey), value ); } - }) - ); + } } async teardown() { From 6e0f950c494f5746d580e92e24cc963614dc9287 Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 15:17:13 +0100 Subject: [PATCH 11/13] CCM-18086: Fix linting --- tests/test-team/helpers/client/client-helper.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test-team/helpers/client/client-helper.ts b/tests/test-team/helpers/client/client-helper.ts index dc4afd5f40..49737814ed 100644 --- a/tests/test-team/helpers/client/client-helper.ts +++ b/tests/test-team/helpers/client/client-helper.ts @@ -163,12 +163,12 @@ export class ClientConfigurationHelper { async setup() { // seed clients serially to avoid overwhelming SSM for (const [clientKey, value] of Object.entries(testClients)) { - if (value !== undefined) { - await this.putClient( - ClientConfigurationHelper.clientIdFromKey(clientKey as ClientKey), - value - ); - } + if (value !== undefined) { + await this.putClient( + ClientConfigurationHelper.clientIdFromKey(clientKey as ClientKey), + value + ); + } } } From fc8a0abc891b7f079c09cfbcedf93345247153be Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 17:07:42 +0100 Subject: [PATCH 12/13] CCM-18086: Use file lock instead of mutex --- package-lock.json | 43 ++++++++++++++++++- .../test-team/helpers/context/context-file.ts | 43 ++++++++++++------- tests/test-team/package.json | 4 ++ 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34ab466e2a..d9e8575153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8543,6 +8543,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/proper-lockfile": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", + "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "*" + } + }, "node_modules/@types/punycode": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@types/punycode/-/punycode-2.1.4.tgz", @@ -8570,6 +8580,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sinon": { "version": "17.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", @@ -13428,7 +13445,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -18296,6 +18312,17 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -19131,6 +19158,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -19674,7 +19710,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC" }, "node_modules/sinon": { @@ -22082,7 +22117,11 @@ "nhs-notify-web-template-management-types": "^0.0.1", "nhs-notify-web-template-management-util-backend-config": "^0.0.1", "pdf-parse": "^2.4.5", + "proper-lockfile": "^4.1.2", "zod": "^4.0.17" + }, + "devDependencies": { + "@types/proper-lockfile": "^4.1.4" } }, "tests/test-team/node_modules/glob": { diff --git a/tests/test-team/helpers/context/context-file.ts b/tests/test-team/helpers/context/context-file.ts index ec25fae0a4..644bfaaa91 100644 --- a/tests/test-team/helpers/context/context-file.ts +++ b/tests/test-team/helpers/context/context-file.ts @@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'; import fs from 'node:fs'; import path from 'node:path'; -import { Mutex } from 'async-mutex'; +import lockfile from 'proper-lockfile'; import type { LetterVariant } from 'nhs-notify-web-template-management-types'; import type { ClientConfiguration } from '../client/client-helper'; import type { TestUserContext } from '../auth/cognito-auth-helper'; @@ -16,8 +16,6 @@ type TestContextNamespace = { export class TestContextFile { public readonly path: string; - private mutex = new Mutex(); - constructor(filepath: string) { this.path = path.resolve(filepath); @@ -31,7 +29,7 @@ export class TestContextFile { } async setUser(id: string, value: Partial) { - await this.mutex.runExclusive(() => { + await this.withFileLock(async () => { const data = this.read(); const nsData = this.parseNamespace(data[process.env.PLAYWRIGHT_RUN_ID]); @@ -45,19 +43,19 @@ export class TestContextFile { } async getUser(id: string): Promise { - return this.mutex.runExclusive( + return this.withFileLock( () => this.read()[process.env.PLAYWRIGHT_RUN_ID]?.users?.[id] ?? null ); } async getAllUsers(): Promise { - return this.mutex.runExclusive(() => + return this.withFileLock(() => Object.values(this.read()[process.env.PLAYWRIGHT_RUN_ID]?.users ?? {}) ); } async setClient(id: string, value: Partial) { - await this.mutex.runExclusive(() => { + await this.withFileLock(async () => { const data = this.read(); const nsData = this.parseNamespace(data[process.env.PLAYWRIGHT_RUN_ID]); @@ -71,25 +69,25 @@ export class TestContextFile { } async getClient(id: string): Promise { - return this.mutex.runExclusive( + return this.withFileLock( () => this.read()[process.env.PLAYWRIGHT_RUN_ID]?.clients?.[id] ?? null ); } async getAllClientIds(): Promise { - return this.mutex.runExclusive(() => + return this.withFileLock(() => Object.keys(this.read()[process.env.PLAYWRIGHT_RUN_ID]?.clients ?? {}) ); } async getAllClients(): Promise<[string, ClientConfiguration][]> { - return this.mutex.runExclusive(() => + return this.withFileLock(() => Object.entries(this.read()[process.env.PLAYWRIGHT_RUN_ID]?.clients ?? {}) ); } async setLetterVariant(id: string, value: LetterVariant) { - await this.mutex.runExclusive(() => { + await this.withFileLock(async () => { const data = this.read(); const nsData = this.parseNamespace(data[process.env.PLAYWRIGHT_RUN_ID]); @@ -103,7 +101,7 @@ export class TestContextFile { } async setLetterVariants(values: Record) { - await this.mutex.runExclusive(() => { + await this.withFileLock(async () => { const data = this.read(); const nsData = this.parseNamespace(data[process.env.PLAYWRIGHT_RUN_ID]); @@ -115,14 +113,14 @@ export class TestContextFile { } async getLetterVariant(id: string): Promise { - return this.mutex.runExclusive( + return this.withFileLock( () => this.read()[process.env.PLAYWRIGHT_RUN_ID]?.letterVariants?.[id] ?? null ); } async getAllLetterVariants(): Promise { - return this.mutex.runExclusive(() => { + return this.withFileLock(() => { return Object.values( this.read()[process.env.PLAYWRIGHT_RUN_ID]?.letterVariants ?? {} ); @@ -130,12 +128,27 @@ export class TestContextFile { } async destroyNamespace(): Promise { - return this.mutex.runExclusive(() => { + return this.withFileLock(() => { const data = this.read(); Reflect.deleteProperty(data, process.env.PLAYWRIGHT_RUN_ID); this.write(data); }); } + /** + * Acquires a file lock for the duration of the callback, then releases it. + * Ensures exclusive access across processes. + */ + private async withFileLock(fn: () => Promise | T): Promise { + let release: (() => Promise) | undefined; + try { + release = await lockfile.lock(this.path, { + retries: { retries: 10, factor: 1.5, minTimeout: 50, maxTimeout: 500 }, + }); + return await fn(); + } finally { + if (release) await release(); + } + } private parseNamespace( namespace: TestContextNamespace | undefined diff --git a/tests/test-team/package.json b/tests/test-team/package.json index 4f631a5078..03187ed37b 100644 --- a/tests/test-team/package.json +++ b/tests/test-team/package.json @@ -27,8 +27,12 @@ "nhs-notify-web-template-management-types": "^0.0.1", "nhs-notify-web-template-management-util-backend-config": "^0.0.1", "pdf-parse": "^2.4.5", + "proper-lockfile": "^4.1.2", "zod": "^4.0.17" }, + "devDependencies": { + "@types/proper-lockfile": "^4.1.4" + }, "name": "nhs-notify-web-template-management-ui-tests", "private": true, "scripts": { From 9887547faa1039ba755ce3f11a8ce8bae335c187 Mon Sep 17 00:00:00 2001 From: Chris Elliott Date: Tue, 26 May 2026 17:29:05 +0100 Subject: [PATCH 13/13] CCM-18086: Fix tests --- tests/test-team/helpers/context/context-file.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test-team/helpers/context/context-file.ts b/tests/test-team/helpers/context/context-file.ts index 644bfaaa91..6590ad8652 100644 --- a/tests/test-team/helpers/context/context-file.ts +++ b/tests/test-team/helpers/context/context-file.ts @@ -26,6 +26,8 @@ export class TestContextFile { } catch { fs.mkdirSync(dir, { recursive: true }); } + + fs.closeSync(fs.openSync(this.path, 'a')); } async setUser(id: string, value: Partial) { @@ -134,6 +136,7 @@ export class TestContextFile { this.write(data); }); } + /** * Acquires a file lock for the duration of the callback, then releases it. * Ensures exclusive access across processes.