From 2ae0dc77c249d8b6c32363f51db143a09db0ea4b Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Tue, 7 Apr 2026 14:22:39 +0100 Subject: [PATCH 1/2] fix(DF-922): fall back to current definition when versioned endpoint fails When a submission carries a versionNumber but the versioned definition endpoint returns not found, the listener now catches the error and falls back to fetching the current definition instead of crashing. --- src/lib/manager.js | 28 ++++++++++++----- src/lib/manager.test.js | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/lib/manager.js b/src/lib/manager.js index d4ed5e3..36d581c 100644 --- a/src/lib/manager.js +++ b/src/lib/manager.js @@ -1,11 +1,14 @@ import { FormStatus } from '@defra/forms-model' import { config } from '~/src/config/index.js' +import { createLogger } from '~/src/helpers/logging/logger.js' import { getJson } from '~/src/lib/fetch.js' const managerUrl = config.get('managerUrl') +const logger = createLogger() + /** - * Gets the form definition from the Forms Manager API∂ + * Gets the form definition from the Forms Manager API * @param {string} formId * @param {FormStatus} formStatus * @param {number|undefined} versionNumber - Optional specific version to fetch @@ -16,16 +19,27 @@ export async function getFormDefinition(formId, formStatus, versionNumber) { throw new Error('Missing MANAGER_URL') } - const statusPath = formStatus === FormStatus.Draft ? FormStatus.Draft : '' - const formUrl = - versionNumber !== undefined - ? new URL( + if (versionNumber !== undefined) { + try { + const { body } = await getJson( + new URL( `/forms/${formId}/versions/${versionNumber}/definition`, managerUrl ) - : new URL(`/forms/${formId}/definition/${statusPath}`, managerUrl) + ) + return body + } catch (err) { + logger.warn( + err, + `[getFormDefinition] Version ${versionNumber} not found for form ${formId}, falling back to current definition` + ) + } + } - const { body } = await getJson(formUrl) + const statusPath = formStatus === FormStatus.Draft ? FormStatus.Draft : '' + const { body } = await getJson( + new URL(`/forms/${formId}/definition/${statusPath}`, managerUrl) + ) return body } diff --git a/src/lib/manager.test.js b/src/lib/manager.test.js index f99e089..4dcb04d 100644 --- a/src/lib/manager.test.js +++ b/src/lib/manager.test.js @@ -4,6 +4,14 @@ import { buildDefinition, buildMetaData } from '@defra/forms-model/stubs' import { getJson } from '~/src/lib/fetch.js' import { getFormDefinition, getFormMetadata } from '~/src/lib/manager.js' jest.mock('~/src/lib/fetch.js') +jest.mock('~/src/helpers/logging/logger.js', () => ({ + createLogger: () => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn() + }) +})) jest.mock('~/src/config/index.js', () => ({ config: { get: jest.fn().mockReturnValueOnce('http://forms-manager') @@ -110,6 +118,64 @@ describe('Manager', () => { expect(definition).toEqual(expectedDefinition) }) + it('should fall back to current definition when versioned endpoint fails', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + const versionNumber = 5 + jest + .mocked(getJson) + .mockRejectedValueOnce(new Error('Version not found')) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Live, + versionNumber + ) + expect(getJson).toHaveBeenCalledTimes(2) + expect(getJson).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/5/definition' + }) + ) + expect(getJson).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) + + it('should fall back to draft definition when versioned endpoint fails for draft status', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + const versionNumber = 2 + jest + .mocked(getJson) + .mockRejectedValueOnce(new Error('Version not found')) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Draft, + versionNumber + ) + expect(getJson).toHaveBeenCalledTimes(2) + expect(getJson).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/2/definition' + }) + ) + expect(getJson).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/draft' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) + it('should throw if no manager url set', async () => { const expectedDefinition = buildDefinition() const formId = '68a890909ab460290c289409' From 167795047321d8ebaf7c45d13eb0b79d032c16f4 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Wed, 8 Apr 2026 10:58:01 +0100 Subject: [PATCH 2/2] allow workflow to be tiggered by dispatch --- .github/workflows/publish.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 870c3bc..8cefa92 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,11 +8,17 @@ on: paths: - '.browserslistrc' - 'babel.config.*' + - 'postcss.config.*' + - 'webpack.config.*' - 'Dockerfile' - 'src/**' - '!**/*.test.*' workflow_dispatch: + inputs: + version: + description: 'Version number manual override' + required: false concurrency: group: publish @@ -24,7 +30,7 @@ permissions: env: AWS_REGION: eu-west-2 - AWS_ACCOUNT_ID: "094954420758" + AWS_ACCOUNT_ID: '094954420758' jobs: build: @@ -33,9 +39,10 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check out code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Build and Publish uses: DEFRA/cdp-build-action/build@main with: github-token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ inputs.version }}