From 0336dd3fa43e503d8738347cad717173ff41e7ca Mon Sep 17 00:00:00 2001
From: Aiden Vaines <54067008+aidenvaines-cgi@users.noreply.github.com>
Date: Fri, 3 Jul 2026 12:12:16 +0000
Subject: [PATCH 1/6] CCM-18044 Adding a simple release notes page with
automation
---
.github/actions/release-notes/action.yaml | 27 ++
.../release-notes/fetch-release-notes.js | 245 ++++++++++++++++++
.github/workflows/release-notes-sync.yaml | 54 ++++
README.md | 8 +
docs/_data/release-notes.json | 193 ++++++++++++++
docs/_includes/components/timeline.html | 10 +
docs/assets/css/_nhsnotify.scss | 46 ++++
docs/pages/about/about.md | 4 +
docs/pages/about/release-notes.md | 27 ++
9 files changed, 614 insertions(+)
create mode 100644 .github/actions/release-notes/action.yaml
create mode 100644 .github/actions/release-notes/fetch-release-notes.js
create mode 100644 .github/workflows/release-notes-sync.yaml
create mode 100644 docs/_data/release-notes.json
create mode 100644 docs/_includes/components/timeline.html
create mode 100644 docs/pages/about/release-notes.md
diff --git a/.github/actions/release-notes/action.yaml b/.github/actions/release-notes/action.yaml
new file mode 100644
index 00000000..f8684fdc
--- /dev/null
+++ b/.github/actions/release-notes/action.yaml
@@ -0,0 +1,27 @@
+name: "Release notes sync"
+description: "Fetch release notes data from JIRA and update the Jekyll data cache"
+inputs:
+ output-file:
+ description: "Path to the generated release notes cache file"
+ required: false
+ default: "docs/_data/release-notes.json"
+ jql:
+ description: "JQL used to fetch release notes"
+ required: false
+ default: 'project = CCM AND "Release Notes" IS NOT EMPTY AND fixVersion IS NOT EMPTY AND updated >= -365d'
+ max-results:
+ description: "Maximum JIRA issues to fetch per page"
+ required: false
+ default: "50"
+
+runs:
+ using: "composite"
+ steps:
+ - name: "Fetch release notes cache"
+ env:
+ RELEASE_NOTES_CACHE_FILE: "${{ inputs.output-file }}"
+ RELEASE_NOTES_JQL: "${{ inputs.jql }}"
+ RELEASE_NOTES_MAX_RESULTS: "${{ inputs.max-results }}"
+ shell: bash
+ run: |
+ node ./.github/actions/release-notes/fetch-release-notes.js
diff --git a/.github/actions/release-notes/fetch-release-notes.js b/.github/actions/release-notes/fetch-release-notes.js
new file mode 100644
index 00000000..c4f24a9a
--- /dev/null
+++ b/.github/actions/release-notes/fetch-release-notes.js
@@ -0,0 +1,245 @@
+#!/usr/bin/env node
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+const DEFAULT_RELEASE_NOTES_JQL = 'project = CCM AND "Release Notes" IS NOT EMPTY AND fixVersion IS NOT EMPTY AND updated >= -365d';
+const DEFAULT_RELEASE_NOTES_CACHE_FILE = 'docs/_data/release-notes.json';
+const DEFAULT_RELEASE_NOTES_MAX_RESULTS = 50;
+
+async function main() {
+ const repoRoot = process.cwd();
+ const outputFile = process.env.RELEASE_NOTES_CACHE_FILE || DEFAULT_RELEASE_NOTES_CACHE_FILE;
+ const jiraBaseUrl = resolveJiraBaseUrl();
+
+ if (!jiraBaseUrl) {
+ throw new Error('Set JIRA_BASE_URL or JIRA_URL before running the release notes sync.');
+ }
+
+ const releaseNotesJql = process.env.RELEASE_NOTES_JQL || DEFAULT_RELEASE_NOTES_JQL;
+ const maxResults = Number.parseInt(process.env.RELEASE_NOTES_MAX_RESULTS || String(DEFAULT_RELEASE_NOTES_MAX_RESULTS), 10);
+ const startedAt = new Date();
+
+ if (!Number.isInteger(maxResults) || maxResults <= 0) {
+ throw new Error('RELEASE_NOTES_MAX_RESULTS must be a positive integer.');
+ }
+
+ if (!process.env.JIRA_TOKEN && !process.env.JIRA_AUTH_HEADER) {
+ throw new Error('Set JIRA_TOKEN or JIRA_AUTH_HEADER before running the release notes sync.');
+ }
+
+ fs.mkdirSync(path.dirname(path.resolve(repoRoot, outputFile)), { recursive: true });
+
+ console.log(`Fetching release notes from ${jiraBaseUrl}`);
+
+ // Look up the custom field ID once so the search request can read release notes text.
+ const fields = await requestFields(jiraBaseUrl);
+ const releaseNotesFieldId = resolveReleaseNotesFieldId(fields);
+
+ console.log(`Resolved Release Notes field: ${releaseNotesFieldId}`);
+
+ const issuesByKey = new Map();
+ let startAt = 0;
+ let pageNumber = 1;
+ let total = 0;
+
+ while (true) {
+ // Fetch one page of JIRA issues at a time until we have them all.
+ const payload = await requestSearch({
+ jiraBaseUrl,
+ releaseNotesFieldId,
+ jql: releaseNotesJql,
+ startAt,
+ maxResults,
+ });
+
+ total = Number(payload.total || 0);
+ const pageIssues = Array.isArray(payload.issues) ? payload.issues : [];
+ console.log(`Fetched page ${pageNumber} with ${pageIssues.length} issue(s)`);
+
+ for (const rawIssue of pageIssues) {
+ const issue = normalizeIssue(rawIssue, releaseNotesFieldId);
+ if (issue) {
+ issuesByKey.set(issue.key, issue);
+ }
+ }
+
+ if (pageIssues.length === 0 || issuesByKey.size >= total) {
+ break;
+ }
+
+ startAt += maxResults;
+ pageNumber += 1;
+ }
+
+ const output = {
+ releases: groupIssuesByFixVersion(Array.from(issuesByKey.values())),
+ };
+
+ fs.writeFileSync(path.resolve(repoRoot, outputFile), `${JSON.stringify(output, null, 2)}\n`, 'utf8');
+ console.log(`Updated release notes cache at ${outputFile}`);
+}
+
+async function requestFields(jiraBaseUrl) {
+ return fetchJson(`${jiraBaseUrl}/field`);
+}
+
+async function requestSearch({
+ jiraBaseUrl,
+ releaseNotesFieldId,
+ jql,
+ startAt,
+ maxResults,
+}) {
+ const url = new URL(`${jiraBaseUrl}/search`);
+ url.searchParams.set('jql', jql);
+ url.searchParams.set('fields', `summary,fixVersions,issuetype,updated,${releaseNotesFieldId}`);
+ url.searchParams.set('startAt', String(startAt));
+ url.searchParams.set('maxResults', String(maxResults));
+
+ return fetchJson(url.toString());
+}
+
+async function fetchJson(url) {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: buildHeaders(),
+ });
+
+ if (!response.ok) {
+ throw new Error(`JIRA request failed with status ${response.status} ${response.statusText}`);
+ }
+
+ return response.json();
+}
+
+function buildHeaders() {
+ const headers = {
+ Accept: 'application/json',
+ };
+
+ if (process.env.JIRA_AUTH_HEADER && process.env.JIRA_AUTH_HEADER.trim()) {
+ const header = process.env.JIRA_AUTH_HEADER;
+ const index = header.indexOf(':');
+ if (index <= 0) {
+ throw new Error('JIRA_AUTH_HEADER must be in the format "Header-Name: value".');
+ }
+ headers[header.slice(0, index).trim()] = header.slice(index + 1).trim();
+ return headers;
+ }
+
+ headers.Authorization = `Bearer ${process.env.JIRA_TOKEN}`;
+ return headers;
+}
+
+function resolveReleaseNotesFieldId(fields) {
+ const match = fields.find((field) => String(field.name || '').trim().toLowerCase() === 'release notes');
+ if (!match || !match.id) {
+ throw new Error("Unable to resolve the JIRA field named 'Release Notes'.");
+ }
+ return match.id;
+}
+
+function normalizeIssue(issue, releaseNotesFieldId) {
+ const fields = issue && typeof issue === 'object' ? issue.fields || {} : {};
+ const fixVersions = Array.isArray(fields.fixVersions)
+ ? fields.fixVersions
+ .map((version) => String(version && version.name ? version.name : '').trim())
+ .filter(Boolean)
+ : [];
+ const releaseNotes = extractText(fields[releaseNotesFieldId]);
+ // Keep only the fields needed by the generated JSON.
+ const normalized = {
+ key: String(issue && issue.key ? issue.key : '').trim(),
+ fix_versions: fixVersions,
+ release_notes: releaseNotes,
+ };
+
+ if (!normalized.key || !normalized.release_notes || normalized.fix_versions.length === 0) {
+ return null;
+ }
+
+ return normalized;
+}
+
+function resolveJiraBaseUrl() {
+ if (process.env.JIRA_BASE_URL && process.env.JIRA_BASE_URL.trim()) {
+ return process.env.JIRA_BASE_URL.replace(/\/$/, '');
+ }
+
+ if (process.env.JIRA_URL && process.env.JIRA_URL.trim()) {
+ return `${process.env.JIRA_URL.replace(/\/$/, '')}/rest/api/2`;
+ }
+
+ return '';
+}
+
+function extractText(value) {
+ if (value === null || value === undefined) {
+ return '';
+ }
+
+ if (typeof value === 'string') {
+ return value.trim();
+ }
+
+ if (Array.isArray(value)) {
+ return value.map(extractText).filter(Boolean).join('\n').trim();
+ }
+
+ if (typeof value === 'object') {
+ if (typeof value.text === 'string') {
+ return value.text.trim();
+ }
+ if (Array.isArray(value.content)) {
+ return value.content
+ .map(extractText)
+ .filter(Boolean)
+ .join('\n')
+ .replace(/\n{3,}/g, '\n\n')
+ .trim();
+ }
+ }
+
+ return String(value).trim();
+}
+
+function groupIssuesByFixVersion(issues) {
+ const releases = new Map();
+
+ for (const issue of issues) {
+ // Group each issue under every fix version it belongs to.
+ for (const fixVersion of issue.fix_versions) {
+ if (!releases.has(fixVersion)) {
+ releases.set(fixVersion, []);
+ }
+ releases.get(fixVersion).push({
+ key: issue.key,
+ release_notes: issue.release_notes,
+ });
+ }
+ }
+
+ return Array.from(releases, ([name, items]) => ({
+ name: formatReleaseName(name),
+ items,
+ }));
+}
+
+function formatReleaseName(name) {
+ // Only reformat kebab-case names (e.g. digital-letters-0.0.0 → Digital Letters 0.0.0).
+ // Names that already contain spaces are left as-is.
+ if (!name.includes('-') || name.includes(' ')) {
+ return name;
+ }
+
+ return name
+ .split('-')
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ');
+}
+
+main().catch((error) => {
+ console.error(error.message || error);
+ process.exit(1);
+});
diff --git a/.github/workflows/release-notes-sync.yaml b/.github/workflows/release-notes-sync.yaml
new file mode 100644
index 00000000..58397aad
--- /dev/null
+++ b/.github/workflows/release-notes-sync.yaml
@@ -0,0 +1,54 @@
+name: "Release notes sync"
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "0 6 * * 1"
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ sync-release-notes:
+ name: "Sync release notes"
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - name: "Checkout code"
+ uses: actions/checkout@v4
+
+ - name: "Update release notes data"
+ uses: ./.github/actions/release-notes
+ env:
+ JIRA_TOKEN: "${{ secrets.JIRA_TOKEN }}"
+ JIRA_BASE_URL: "${{ secrets.JIRA_URL }}/rest/api/2"
+
+ - name: "Detect release notes changes"
+ id: changes
+ shell: bash
+ run: |
+ if git diff --quiet -- docs/_data/release-notes.json; then
+ echo "has_changes=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "has_changes=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: "Create pull request"
+ if: steps.changes.outputs.has_changes == 'true'
+ uses: peter-evans/create-pull-request@v7
+ with:
+ token: "${{ secrets.GITHUB_TOKEN }}"
+ branch: "automation/release-notes-cache"
+ delete-branch: true
+ commit-message: "sync release notes"
+ title: "CCM-18043: Sync release notes"
+ body: |
+ ## Summary
+
+ This PR syncs the release notes generated from JIRA for the last year.
+
+ - Trigger: `${{ github.event_name }}`
+ - Source: `JIRA_URL secret (expanded to REST API base path)`
+ add-paths: |
+ docs/_data/release-notes.json
diff --git a/README.md b/README.md
index f4b36f33..b46ccec7 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,14 @@
- page content is inside the `/docs/pages` folder
- page css is inside `/docs/_sass` folder
+## Release notes data sync
+
+- Release notes data is generated into `docs/_data/release-notes.json`
+- The cache is refreshed by `.github/workflows/release-notes-sync.yaml`
+- The workflow supports a weekly scheduled run and manual `workflow_dispatch`
+- Set the `JIRA_TOKEN` GitHub Actions secret before enabling the workflow
+- The sync action resolves the custom `Release Notes` field dynamically from the JIRA API, so the field ID is not hard coded in the repository
+
## Getting Started - First time setup
This is only needed once.
diff --git a/docs/_data/release-notes.json b/docs/_data/release-notes.json
new file mode 100644
index 00000000..b375d17d
--- /dev/null
+++ b/docs/_data/release-notes.json
@@ -0,0 +1,193 @@
+{
+ "releases": [
+ {
+ "name": "Digital Letters 1.1.0",
+ "items": [
+ {
+ "key": "CCM-20922",
+ "release_notes": "NHS Notify are able to onboard a Trust to send a PDF letter attached to an NHS App message, and fallback to printed letters where needed."
+ }
+ ]
+ },
+ {
+ "name": "Digital Letters 1.0.1",
+ "items": [
+ {
+ "key": "CCM-18345",
+ "release_notes": "NHS Notify will have the ability to query data and produce reporting relating to digital letters within Power BI by Trust per day."
+ },
+ {
+ "key": "CCM-18337",
+ "release_notes": "Investigate and resolve the memory issues for PDM uploader for digital letters"
+ },
+ {
+ "key": "CCM-18212",
+ "release_notes": "NHS Notify are able to onboard a Trust to send a PDF letter attached to an NHS App message, and fallback to printed letters where needed."
+ }
+ ]
+ },
+ {
+ "name": "Digital Letters 1.0.0",
+ "items": [
+ {
+ "key": "CCM-18337",
+ "release_notes": "Investigate and resolve the memory issues for PDM uploader for digital letters"
+ }
+ ]
+ },
+ {
+ "name": "Release 4.67.1",
+ "items": [
+ {
+ "key": "CCM-17732",
+ "release_notes": "A timeout setting has been increased to resolve an error processing Vaccs reminders"
+ }
+ ]
+ },
+ {
+ "name": "WebUI 0.1.17",
+ "items": [
+ {
+ "key": "CCM-17623",
+ "release_notes": "A bug has been fixed that blocked Digitrials templates"
+ },
+ {
+ "key": "CCM-17502",
+ "release_notes": "The Letter Authoring (Carbone) journey now prevents uploads of files over 5MB"
+ },
+ {
+ "key": "CCM-17341",
+ "release_notes": "A bug has been fixed which caused problems with certain Word fonts"
+ },
+ {
+ "key": "CCM-17129",
+ "release_notes": "The 'Copy to Clipboard' option now indicates whether the message plan is in Draft or Production."
+ },
+ {
+ "key": "CCM-16449",
+ "release_notes": "Additional config checks have been added to the Printing and Postage option"
+ },
+ {
+ "key": "CCM-11322",
+ "release_notes": "Additional validation has been added to ensure links in pages are secure"
+ }
+ ]
+ },
+ {
+ "name": "Release 4.65.1",
+ "items": [
+ {
+ "key": "CCM-17272",
+ "release_notes": "- Memory allocation has been increased to support Digitrials' large 'aspiring' templates"
+ },
+ {
+ "key": "CCM-15512",
+ "release_notes": "- Carbone letter events can now be consumed"
+ },
+ {
+ "key": "CCM-13387",
+ "release_notes": "- We’ve added a new printSupplier field in NHS Notify Core to store the allocated supplier selected by supplier‑api for each letter.\r\nThis ensures that Core’s internal state now matches the final print supplier, improving accuracy for reporting, billing, and operational audits.\r\nDownstream systems can now use this field for correct supplier tracking and reconciliation across domains."
+ }
+ ]
+ },
+ {
+ "name": "WebUI 0.1.16",
+ "items": [
+ {
+ "key": "CCM-16488",
+ "release_notes": "Clearer warnings on the 'Get ready to approve' letter page"
+ },
+ {
+ "key": "CCM-16455",
+ "release_notes": "A warning message has been added to tell users they will be unable to edit a campaign after creating a message plan"
+ },
+ {
+ "key": "CCM-14768",
+ "release_notes": "Message Plan letter templates can now be previewed in a new tab"
+ },
+ {
+ "key": "CCM-12653",
+ "release_notes": "A bug which stopped buttons working correctly after a double click has been fixed"
+ }
+ ]
+ },
+ {
+ "name": "Release 4.64.1",
+ "items": [
+ {
+ "key": "CCM-15626",
+ "release_notes": "A change has been made to support letters which use large templates that were previously prone to timing out."
+ },
+ {
+ "key": "CCM-15207",
+ "release_notes": "We’ve defined and registered JSON Schemas for the message and channel status events published by NHS Notify Core.\r\n\r\nThis provides a versioned contract for the callbacks bounded context, allowing subscribers to validate event payloads reliably.\r\n\r\nThese schemas support the migration to an event‑driven callbacks domain, ensuring consistent communication across services.\r\n\r\nWe’ve improved internal event handling and validation processes to strengthen system integrity."
+ },
+ {
+ "key": "CCM-15037",
+ "release_notes": "Preparatory work to support the introduction of British Sign Language"
+ }
+ ]
+ },
+ {
+ "name": "Release 4.63.0",
+ "items": [
+ {
+ "key": "CCM-15266",
+ "release_notes": "Additional monitoring and alerting added"
+ }
+ ]
+ },
+ {
+ "name": "WebUI 0.1.15",
+ "items": [
+ {
+ "key": "CCM-15036",
+ "release_notes": "Users are now warned that they may lose their chosen printing and postage option when switching campaign"
+ },
+ {
+ "key": "CCM-15018",
+ "release_notes": "Users are now informed of the process for requesting digital proofs"
+ },
+ {
+ "key": "CCM-14747",
+ "release_notes": "British Sign Language letter templates can now be included in a message plan"
+ }
+ ]
+ },
+ {
+ "name": "WebUI 0.1.14",
+ "items": [
+ {
+ "key": "CCM-14264",
+ "release_notes": "An 'Email Only' option has been added to the routing config"
+ },
+ {
+ "key": "CCM-14211",
+ "release_notes": "DOCX files can now be uploaded"
+ },
+ {
+ "key": "CCM-8353",
+ "release_notes": "In the NHS App, test messages can now be sent from the template preview page"
+ }
+ ]
+ },
+ {
+ "name": "Supplier Api 1.1.5",
+ "items": [
+ {
+ "key": "CCM-13906",
+ "release_notes": "A change has been made to prioritise urgent letters so that they are processed ahead of BAU letters"
+ }
+ ]
+ },
+ {
+ "name": "WebUI 0.1.18",
+ "items": [
+ {
+ "key": "CCM-8366",
+ "release_notes": "Users can now send a test message to their NHS App test account"
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/_includes/components/timeline.html b/docs/_includes/components/timeline.html
new file mode 100644
index 00000000..05b5df4d
--- /dev/null
+++ b/docs/_includes/components/timeline.html
@@ -0,0 +1,10 @@
+
+ {% for item in include.items %}
+ -
+
+
+
{{ item.release_notes | markdownify }}
+
+
+ {% endfor %}
+
diff --git a/docs/assets/css/_nhsnotify.scss b/docs/assets/css/_nhsnotify.scss
index 7eb7b14b..aaa1983f 100644
--- a/docs/assets/css/_nhsnotify.scss
+++ b/docs/assets/css/_nhsnotify.scss
@@ -210,3 +210,49 @@
.nhsuk-header__account-item:last-child {
outline: none;
}
+
+.nhsnotify-timeline {
+ list-style: none;
+ margin: 0 0 40px;
+ padding: 0;
+ position: relative;
+}
+
+.nhsnotify-timeline::before {
+ background-color: $color_nhsuk-grey-3;
+ bottom: 8px;
+ content: "";
+ left: 5px;
+ position: absolute;
+ top: 8px;
+ width: 2px;
+}
+
+.nhsnotify-timeline__item {
+ margin: 0 0 24px;
+ padding: 0 0 0 24px;
+ position: relative;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.nhsnotify-timeline__marker {
+ background-color: $color_nhsuk-white;
+ border: 2px solid $color_nhsuk-grey-3;
+ border-radius: 50%;
+ height: 12px;
+ left: 0;
+ position: absolute;
+ top: 4px;
+ width: 12px;
+}
+
+.nhsnotify-timeline__content {
+ padding-left: 8px;
+}
+
+.nhsnotify-timeline__body > :last-child {
+ margin-bottom: 0;
+}
diff --git a/docs/pages/about/about.md b/docs/pages/about/about.md
index 5c67535a..73a7d59a 100644
--- a/docs/pages/about/about.md
+++ b/docs/pages/about/about.md
@@ -118,6 +118,10 @@ To send messages automatically, you'll need a developer to integrate with the NH
[Learn more about NHS Notify’s security features.]({% link pages/footer/security.md %})
+### Keep up to date
+
+You can also view improvements and changes tot he services by reviewing our [release notes]({% link pages/about/release-notes.md %}).
+
### Get support when you need it
NHS Notify is available 24 hours a day, 365 days a year and supported from 8am to 6pm, Monday to Friday excluding bank holidays.
diff --git a/docs/pages/about/release-notes.md b/docs/pages/about/release-notes.md
new file mode 100644
index 00000000..de41d8e9
--- /dev/null
+++ b/docs/pages/about/release-notes.md
@@ -0,0 +1,27 @@
+---
+layout: page
+title: Release notes
+parent: About
+nav_order: 3
+permalink: /about/release-notes/
+---
+
+This page lists release notes from JIRA, grouped by release name.
+
+{% assign releases = site.data["release-notes"].releases %}
+
+{% if releases and releases.size > 0 %}
+{% for release in releases %}
+
+## {{ release.name }}
+
+{% if release.items and release.items.size > 0 %}
+{% include components/timeline.html items=release.items %}
+{% else %}
+No release notes available for this release.
+{% endif %}
+
+{% endfor %}
+{% else %}
+No release notes data is currently available.
+{% endif %}
From cc4c2002136ad11375c891ab809b66d43d434470 Mon Sep 17 00:00:00 2001
From: Aiden Vaines <54067008+aidenvaines-cgi@users.noreply.github.com>
Date: Fri, 3 Jul 2026 12:35:21 +0000
Subject: [PATCH 2/6] CCM-18044 PR Fixes
---
.../pages/using-nhs-notify/upload-a-letter.md | 2 +-
.../config/vocabularies/words/accept.txt | 79 ++++++++++---------
2 files changed, 41 insertions(+), 40 deletions(-)
diff --git a/docs/pages/using-nhs-notify/upload-a-letter.md b/docs/pages/using-nhs-notify/upload-a-letter.md
index 9090e6d8..f40088a8 100644
--- a/docs/pages/using-nhs-notify/upload-a-letter.md
+++ b/docs/pages/using-nhs-notify/upload-a-letter.md
@@ -191,7 +191,7 @@ The address is a personalisation field and is set automatically.
The recipient's name is always included as the first line of the address.
-If your letter is about a child, use the [parent or guardian letter template](#download-our-blank-letter-template). This template includes 'Parent or guardian of' in the first line of the address.
+If your letter is about a child, use the [parent or guardian letter template](#download-a-blank-word-letter-template). This template includes 'Parent or guardian of' in the first line of the address.
### NHS logo
diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt
index b31a6a50..1052a4f3 100644
--- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt
+++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt
@@ -1,53 +1,54 @@
-Bitwarden
+:
+[A-Z]+s
[cC]yber
+[iI]nset
+[Uu][Rr][Ll]
+APIM
+Bitwarden
+bot
+bundler
+Burkina
+clientRef
+Cohorting
+ctrl
Dependabot
+endfor
+endif
+fullName
+Futuna
Gitleaks
Grype
+idempotence
+Maarten
+Marino
+namePrefix
+Notify's
+Noto
+npm
OAuth
Octokit
+onboarding
+pending_enrichment
+permanent_failure
+phoneNumber
Podman
+precompiled
Python
+realtime
+Rica
+rollout
+Sao
+SCAL
+Sint
+src
Syft
+technical_failure
+temporary_failure
Terraform
-Trufflehog
-bot
-idempotence
-onboarding
+Tokelau
toolchain
-bundler
-endfor
-npm
-src
-[iI]nset
-urlset
-[A-Z]+s
-Noto
-[Uu][Rr][Ll]
-ctrl
-pending_enrichment
+Trufflehog
unnotified
-permanent_failure
-temporary_failure
-technical_failure
-precompiled
+urlset
validation_failed
-fullName
-namePrefix
-clientRef
-Cohorting
-Sint
-Maarten
-Burkina
-Sao
-Marino
-Rica
-Futuna
-Tokelau
Wayfinder
-phoneNumber
-:
-SCAL
-APIM
-realtime
-Notify's
-rollout
From 5b4226b819e03aac391878ce89e5db20e6b78f1d Mon Sep 17 00:00:00 2001
From: Aiden Vaines <54067008+aidenvaines-cgi@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:14:16 +0000
Subject: [PATCH 3/6] CCM-18044 Stakeholder Fixes
---
.github/actions/release-notes/action.yaml | 5 +
.../release-notes/fetch-release-notes.js | 77 ++++++++++--
docs/_data/release-notes.json | 116 +++++++++++-------
docs/pages/about/about.md | 2 +-
docs/pages/about/release-notes.md | 7 +-
5 files changed, 150 insertions(+), 57 deletions(-)
diff --git a/.github/actions/release-notes/action.yaml b/.github/actions/release-notes/action.yaml
index f8684fdc..f5cc71e5 100644
--- a/.github/actions/release-notes/action.yaml
+++ b/.github/actions/release-notes/action.yaml
@@ -13,6 +13,10 @@ inputs:
description: "Maximum JIRA issues to fetch per page"
required: false
default: "50"
+ project-key:
+ description: "JIRA project key used to look up fix version release dates"
+ required: false
+ default: "CCM"
runs:
using: "composite"
@@ -22,6 +26,7 @@ runs:
RELEASE_NOTES_CACHE_FILE: "${{ inputs.output-file }}"
RELEASE_NOTES_JQL: "${{ inputs.jql }}"
RELEASE_NOTES_MAX_RESULTS: "${{ inputs.max-results }}"
+ RELEASE_NOTES_PROJECT_KEY: "${{ inputs.project-key }}"
shell: bash
run: |
node ./.github/actions/release-notes/fetch-release-notes.js
diff --git a/.github/actions/release-notes/fetch-release-notes.js b/.github/actions/release-notes/fetch-release-notes.js
index c4f24a9a..693e1eab 100644
--- a/.github/actions/release-notes/fetch-release-notes.js
+++ b/.github/actions/release-notes/fetch-release-notes.js
@@ -6,6 +6,7 @@ const path = require('node:path');
const DEFAULT_RELEASE_NOTES_JQL = 'project = CCM AND "Release Notes" IS NOT EMPTY AND fixVersion IS NOT EMPTY AND updated >= -365d';
const DEFAULT_RELEASE_NOTES_CACHE_FILE = 'docs/_data/release-notes.json';
const DEFAULT_RELEASE_NOTES_MAX_RESULTS = 50;
+const DEFAULT_RELEASE_NOTES_PROJECT_KEY = 'CCM';
async function main() {
const repoRoot = process.cwd();
@@ -18,6 +19,7 @@ async function main() {
const releaseNotesJql = process.env.RELEASE_NOTES_JQL || DEFAULT_RELEASE_NOTES_JQL;
const maxResults = Number.parseInt(process.env.RELEASE_NOTES_MAX_RESULTS || String(DEFAULT_RELEASE_NOTES_MAX_RESULTS), 10);
+ const releaseNotesProjectKey = process.env.RELEASE_NOTES_PROJECT_KEY || DEFAULT_RELEASE_NOTES_PROJECT_KEY;
const startedAt = new Date();
if (!Number.isInteger(maxResults) || maxResults <= 0) {
@@ -35,6 +37,8 @@ async function main() {
// Look up the custom field ID once so the search request can read release notes text.
const fields = await requestFields(jiraBaseUrl);
const releaseNotesFieldId = resolveReleaseNotesFieldId(fields);
+ const projectVersions = await requestProjectVersions(jiraBaseUrl, releaseNotesProjectKey);
+ const releaseDatesByName = buildReleaseDateMap(projectVersions);
console.log(`Resolved Release Notes field: ${releaseNotesFieldId}`);
@@ -73,7 +77,7 @@ async function main() {
}
const output = {
- releases: groupIssuesByFixVersion(Array.from(issuesByKey.values())),
+ releases: groupIssuesByFixVersion(Array.from(issuesByKey.values()), releaseDatesByName),
};
fs.writeFileSync(path.resolve(repoRoot, outputFile), `${JSON.stringify(output, null, 2)}\n`, 'utf8');
@@ -84,6 +88,11 @@ async function requestFields(jiraBaseUrl) {
return fetchJson(`${jiraBaseUrl}/field`);
}
+async function requestProjectVersions(jiraBaseUrl, projectKey) {
+ const encodedProjectKey = encodeURIComponent(projectKey);
+ return fetchJson(`${jiraBaseUrl}/project/${encodedProjectKey}/versions`);
+}
+
async function requestSearch({
jiraBaseUrl,
releaseNotesFieldId,
@@ -140,6 +149,25 @@ function resolveReleaseNotesFieldId(fields) {
return match.id;
}
+function buildReleaseDateMap(versions) {
+ const releaseDatesByName = new Map();
+
+ if (!Array.isArray(versions)) {
+ return releaseDatesByName;
+ }
+
+ for (const version of versions) {
+ const name = String(version && version.name ? version.name : '').trim();
+ const releaseDate = String(version && version.releaseDate ? version.releaseDate : '').trim();
+
+ if (name && releaseDate) {
+ releaseDatesByName.set(name, releaseDate);
+ }
+ }
+
+ return releaseDatesByName;
+}
+
function normalizeIssue(issue, releaseNotesFieldId) {
const fields = issue && typeof issue === 'object' ? issue.fields || {} : {};
const fixVersions = Array.isArray(fields.fixVersions)
@@ -204,7 +232,7 @@ function extractText(value) {
return String(value).trim();
}
-function groupIssuesByFixVersion(issues) {
+function groupIssuesByFixVersion(issues, releaseDatesByName) {
const releases = new Map();
for (const issue of issues) {
@@ -220,20 +248,49 @@ function groupIssuesByFixVersion(issues) {
}
}
- return Array.from(releases, ([name, items]) => ({
- name: formatReleaseName(name),
+ return Array.from(releases, ([rawName, items]) => ({
+ name: formatReleaseName(rawName),
+ jira_name: rawName,
+ release_date: releaseDatesByName.get(rawName) || null,
items,
- }));
+ })).sort(compareReleasesByDateDesc);
+}
+
+function compareReleasesByDateDesc(left, right) {
+ const leftDate = left.release_date ? Date.parse(left.release_date) : Number.NaN;
+ const rightDate = right.release_date ? Date.parse(right.release_date) : Number.NaN;
+ const leftHasDate = Number.isFinite(leftDate);
+ const rightHasDate = Number.isFinite(rightDate);
+
+ if (leftHasDate && rightHasDate && leftDate !== rightDate) {
+ return rightDate - leftDate;
+ }
+
+ if (leftHasDate && !rightHasDate) {
+ return -1;
+ }
+
+ if (!leftHasDate && rightHasDate) {
+ return 1;
+ }
+
+ return left.name.localeCompare(right.name);
}
function formatReleaseName(name) {
- // Only reformat kebab-case names (e.g. digital-letters-0.0.0 → Digital Letters 0.0.0).
- // Names that already contain spaces are left as-is.
- if (!name.includes('-') || name.includes(' ')) {
- return name;
+ const trimmedName = String(name || '').trim();
+
+ // Jira names prefixed with "Release" are displayed as "Core".
+ if (/^release\s+/i.test(trimmedName)) {
+ return trimmedName.replace(/^release\s+/i, 'Core ');
+ }
+
+ // Reformat kebab-case names (e.g. digital-letters-0.0.0 → Digital Letters 0.0.0).
+ if (!trimmedName.includes('-') || trimmedName.includes(' ')) {
+ return trimmedName;
}
- return name
+ return trimmedName
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
diff --git a/docs/_data/release-notes.json b/docs/_data/release-notes.json
index b375d17d..824fdf4e 100644
--- a/docs/_data/release-notes.json
+++ b/docs/_data/release-notes.json
@@ -2,6 +2,8 @@
"releases": [
{
"name": "Digital Letters 1.1.0",
+ "jira_name": "digital-letters-1.1.0",
+ "release_date": "2026-07-02",
"items": [
{
"key": "CCM-20922",
@@ -9,8 +11,21 @@
}
]
},
+ {
+ "name": "Digital Letters 1.0.0",
+ "jira_name": "digital-letters-1.0.0",
+ "release_date": "2026-06-17",
+ "items": [
+ {
+ "key": "CCM-18337",
+ "release_notes": "Investigate and resolve the memory issues for PDM uploader for digital letters"
+ }
+ ]
+ },
{
"name": "Digital Letters 1.0.1",
+ "jira_name": "digital-letters-1.0.1",
+ "release_date": "2026-06-17",
"items": [
{
"key": "CCM-18345",
@@ -27,25 +42,31 @@
]
},
{
- "name": "Digital Letters 1.0.0",
+ "name": "Core 4.67.1",
+ "jira_name": "Release 4.67.1",
+ "release_date": "2026-05-28",
"items": [
{
- "key": "CCM-18337",
- "release_notes": "Investigate and resolve the memory issues for PDM uploader for digital letters"
+ "key": "CCM-17732",
+ "release_notes": "A timeout setting has been increased to resolve an error processing Vaccs reminders"
}
]
},
{
- "name": "Release 4.67.1",
+ "name": "WebUI 0.1.18",
+ "jira_name": "WebUI 0.1.18",
+ "release_date": "2026-05-28",
"items": [
{
- "key": "CCM-17732",
- "release_notes": "A timeout setting has been increased to resolve an error processing Vaccs reminders"
+ "key": "CCM-8366",
+ "release_notes": "Users can now send a test message to their NHS App test account"
}
]
},
{
"name": "WebUI 0.1.17",
+ "jira_name": "WebUI 0.1.17",
+ "release_date": "2026-05-21",
"items": [
{
"key": "CCM-17623",
@@ -73,25 +94,10 @@
}
]
},
- {
- "name": "Release 4.65.1",
- "items": [
- {
- "key": "CCM-17272",
- "release_notes": "- Memory allocation has been increased to support Digitrials' large 'aspiring' templates"
- },
- {
- "key": "CCM-15512",
- "release_notes": "- Carbone letter events can now be consumed"
- },
- {
- "key": "CCM-13387",
- "release_notes": "- We’ve added a new printSupplier field in NHS Notify Core to store the allocated supplier selected by supplier‑api for each letter.\r\nThis ensures that Core’s internal state now matches the final print supplier, improving accuracy for reporting, billing, and operational audits.\r\nDownstream systems can now use this field for correct supplier tracking and reconciliation across domains."
- }
- ]
- },
{
"name": "WebUI 0.1.16",
+ "jira_name": "WebUI 0.1.16",
+ "release_date": "2026-05-01",
"items": [
{
"key": "CCM-16488",
@@ -112,33 +118,28 @@
]
},
{
- "name": "Release 4.64.1",
+ "name": "Core 4.65.1",
+ "jira_name": "Release 4.65.1",
+ "release_date": "2026-04-29",
"items": [
{
- "key": "CCM-15626",
- "release_notes": "A change has been made to support letters which use large templates that were previously prone to timing out."
+ "key": "CCM-17272",
+ "release_notes": "- Memory allocation has been increased to support Digitrials' large 'aspiring' templates"
},
{
- "key": "CCM-15207",
- "release_notes": "We’ve defined and registered JSON Schemas for the message and channel status events published by NHS Notify Core.\r\n\r\nThis provides a versioned contract for the callbacks bounded context, allowing subscribers to validate event payloads reliably.\r\n\r\nThese schemas support the migration to an event‑driven callbacks domain, ensuring consistent communication across services.\r\n\r\nWe’ve improved internal event handling and validation processes to strengthen system integrity."
+ "key": "CCM-15512",
+ "release_notes": "- Carbone letter events can now be consumed"
},
{
- "key": "CCM-15037",
- "release_notes": "Preparatory work to support the introduction of British Sign Language"
- }
- ]
- },
- {
- "name": "Release 4.63.0",
- "items": [
- {
- "key": "CCM-15266",
- "release_notes": "Additional monitoring and alerting added"
+ "key": "CCM-13387",
+ "release_notes": "- We’ve added a new printSupplier field in NHS Notify Core to store the allocated supplier selected by supplier‑api for each letter.\r\nThis ensures that Core’s internal state now matches the final print supplier, improving accuracy for reporting, billing, and operational audits.\r\nDownstream systems can now use this field for correct supplier tracking and reconciliation across domains."
}
]
},
{
"name": "WebUI 0.1.15",
+ "jira_name": "WebUI 0.1.15",
+ "release_date": "2026-04-22",
"items": [
{
"key": "CCM-15036",
@@ -156,6 +157,8 @@
},
{
"name": "WebUI 0.1.14",
+ "jira_name": "WebUI 0.1.14",
+ "release_date": "2026-04-17",
"items": [
{
"key": "CCM-14264",
@@ -172,20 +175,43 @@
]
},
{
- "name": "Supplier Api 1.1.5",
+ "name": "Core 4.64.1",
+ "jira_name": "Release 4.64.1",
+ "release_date": "2026-04-15",
"items": [
{
- "key": "CCM-13906",
- "release_notes": "A change has been made to prioritise urgent letters so that they are processed ahead of BAU letters"
+ "key": "CCM-15626",
+ "release_notes": "A change has been made to support letters which use large templates that were previously prone to timing out."
+ },
+ {
+ "key": "CCM-15207",
+ "release_notes": "We’ve defined and registered JSON Schemas for the message and channel status events published by NHS Notify Core.\r\n\r\nThis provides a versioned contract for the callbacks bounded context, allowing subscribers to validate event payloads reliably.\r\n\r\nThese schemas support the migration to an event‑driven callbacks domain, ensuring consistent communication across services.\r\n\r\nWe’ve improved internal event handling and validation processes to strengthen system integrity."
+ },
+ {
+ "key": "CCM-15037",
+ "release_notes": "Preparatory work to support the introduction of British Sign Language"
}
]
},
{
- "name": "WebUI 0.1.18",
+ "name": "Core 4.63.0",
+ "jira_name": "Release 4.63.0",
+ "release_date": "2026-04-01",
"items": [
{
- "key": "CCM-8366",
- "release_notes": "Users can now send a test message to their NHS App test account"
+ "key": "CCM-15266",
+ "release_notes": "Additional monitoring and alerting added"
+ }
+ ]
+ },
+ {
+ "name": "Supplier Api 1.1.5",
+ "jira_name": "supplier-api-1.1.5",
+ "release_date": "2026-03-26",
+ "items": [
+ {
+ "key": "CCM-13906",
+ "release_notes": "A change has been made to prioritise urgent letters so that they are processed ahead of BAU letters"
}
]
}
diff --git a/docs/pages/about/about.md b/docs/pages/about/about.md
index 73a7d59a..e79eeba8 100644
--- a/docs/pages/about/about.md
+++ b/docs/pages/about/about.md
@@ -120,7 +120,7 @@ To send messages automatically, you'll need a developer to integrate with the NH
### Keep up to date
-You can also view improvements and changes tot he services by reviewing our [release notes]({% link pages/about/release-notes.md %}).
+You can also view improvements and changes to the services by reviewing our [release notes]({% link pages/about/release-notes.md %}).
### Get support when you need it
diff --git a/docs/pages/about/release-notes.md b/docs/pages/about/release-notes.md
index de41d8e9..d6cb9806 100644
--- a/docs/pages/about/release-notes.md
+++ b/docs/pages/about/release-notes.md
@@ -6,7 +6,7 @@ nav_order: 3
permalink: /about/release-notes/
---
-This page lists release notes from JIRA, grouped by release name.
+This page lists release notes for NHS Notify services.
{% assign releases = site.data["release-notes"].releases %}
@@ -15,6 +15,11 @@ This page lists release notes from JIRA, grouped by release name.
## {{ release.name }}
+{% if release.release_date %}
+
+Released on {{ release.release_date | date: "%d %B %Y" }}
+{% endif %}
+
{% if release.items and release.items.size > 0 %}
{% include components/timeline.html items=release.items %}
{% else %}
From 4e2a746bc12e48613b61b6867ce62e4997f3d4ca Mon Sep 17 00:00:00 2001
From: Aiden Vaines <54067008+aidenvaines-cgi@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:26:57 +0000
Subject: [PATCH 4/6] CCM-18044 Stakeholder Fixes
---
.../release-notes/fetch-release-notes.js | 5 ++-
docs/_data/release-notes.json | 32 +++++++++----------
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/.github/actions/release-notes/fetch-release-notes.js b/.github/actions/release-notes/fetch-release-notes.js
index 693e1eab..d6f0247e 100644
--- a/.github/actions/release-notes/fetch-release-notes.js
+++ b/.github/actions/release-notes/fetch-release-notes.js
@@ -274,7 +274,10 @@ function compareReleasesByDateDesc(left, right) {
return 1;
}
- return left.name.localeCompare(right.name);
+ return right.name.localeCompare(left.name, undefined, {
+ numeric: true,
+ sensitivity: 'base',
+ });
}
function formatReleaseName(name) {
diff --git a/docs/_data/release-notes.json b/docs/_data/release-notes.json
index 824fdf4e..4233fdc8 100644
--- a/docs/_data/release-notes.json
+++ b/docs/_data/release-notes.json
@@ -11,17 +11,6 @@
}
]
},
- {
- "name": "Digital Letters 1.0.0",
- "jira_name": "digital-letters-1.0.0",
- "release_date": "2026-06-17",
- "items": [
- {
- "key": "CCM-18337",
- "release_notes": "Investigate and resolve the memory issues for PDM uploader for digital letters"
- }
- ]
- },
{
"name": "Digital Letters 1.0.1",
"jira_name": "digital-letters-1.0.1",
@@ -42,13 +31,13 @@
]
},
{
- "name": "Core 4.67.1",
- "jira_name": "Release 4.67.1",
- "release_date": "2026-05-28",
+ "name": "Digital Letters 1.0.0",
+ "jira_name": "digital-letters-1.0.0",
+ "release_date": "2026-06-17",
"items": [
{
- "key": "CCM-17732",
- "release_notes": "A timeout setting has been increased to resolve an error processing Vaccs reminders"
+ "key": "CCM-18337",
+ "release_notes": "Investigate and resolve the memory issues for PDM uploader for digital letters"
}
]
},
@@ -63,6 +52,17 @@
}
]
},
+ {
+ "name": "Core 4.67.1",
+ "jira_name": "Release 4.67.1",
+ "release_date": "2026-05-28",
+ "items": [
+ {
+ "key": "CCM-17732",
+ "release_notes": "A timeout setting has been increased to resolve an error processing Vaccs reminders"
+ }
+ ]
+ },
{
"name": "WebUI 0.1.17",
"jira_name": "WebUI 0.1.17",
From c9d3f6a166b5b84b5fad818a4acdd6d56681dc5a Mon Sep 17 00:00:00 2001
From: Aiden Vaines <54067008+aidenvaines-cgi@users.noreply.github.com>
Date: Fri, 3 Jul 2026 14:18:53 +0000
Subject: [PATCH 5/6] CCM-18044 Stakeholder Fixes
---
.../config/vale/styles/config/vocabularies/words/accept.txt | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt
index c7f670bd..1f1300de 100644
--- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt
+++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt
@@ -1,5 +1,3 @@
-:
-[A-Z]+s
[cC]yber
[iI]nset
[Uu][Rr][Ll]
@@ -14,6 +12,7 @@ Codespace
Codespaces
Cohorting
ctrl
+css
Dependabot
endfor
endif
From 6549e96f9f320b83b97b9553e0b4bc81ba7397ba Mon Sep 17 00:00:00 2001
From: Aiden Vaines <54067008+aidenvaines-cgi@users.noreply.github.com>
Date: Fri, 3 Jul 2026 14:24:04 +0000
Subject: [PATCH 6/6] CCM-18044 PR Fixes
---
.github/workflows/release-notes-sync.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release-notes-sync.yaml b/.github/workflows/release-notes-sync.yaml
index 58397aad..577e0a65 100644
--- a/.github/workflows/release-notes-sync.yaml
+++ b/.github/workflows/release-notes-sync.yaml
@@ -36,7 +36,7 @@ jobs:
- name: "Create pull request"
if: steps.changes.outputs.has_changes == 'true'
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: "${{ secrets.GITHUB_TOKEN }}"
branch: "automation/release-notes-cache"