diff --git a/infrastructure/modules/inspector/README.md b/infrastructure/modules/inspector/README.md
index af666eb..e83c7a9 100644
--- a/infrastructure/modules/inspector/README.md
+++ b/infrastructure/modules/inspector/README.md
@@ -24,7 +24,7 @@ No providers.
| Name | Source | Version |
| ---- | ------ | ------- |
| [inspector](#module\_inspector) | cloudposse/inspector/aws | 0.4.0 |
-| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.0.0 |
+| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.1.0 |
## Resources
diff --git a/infrastructure/modules/inspector/context.tf b/infrastructure/modules/inspector/context.tf
index e5d757e..713b9bc 100644
--- a/infrastructure/modules/inspector/context.tf
+++ b/infrastructure/modules/inspector/context.tf
@@ -21,7 +21,7 @@
#
module "this" {
- source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.0.0"
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.1.0"
service = var.service
project = var.project
diff --git a/infrastructure/modules/kms/README.md b/infrastructure/modules/kms/README.md
index 7d4ddf1..c2f7d64 100644
--- a/infrastructure/modules/kms/README.md
+++ b/infrastructure/modules/kms/README.md
@@ -43,7 +43,7 @@ No providers.
| Name | Source | Version |
| ---- | ------ | ------- |
| [kms\_key](#module\_kms\_key) | terraform-aws-modules/kms/aws | 4.2.0 |
-| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.0.0 |
+| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.1.0 |
## Resources
diff --git a/infrastructure/modules/kms/context.tf b/infrastructure/modules/kms/context.tf
index e5d757e..713b9bc 100644
--- a/infrastructure/modules/kms/context.tf
+++ b/infrastructure/modules/kms/context.tf
@@ -21,7 +21,7 @@
#
module "this" {
- source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.0.0"
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.1.0"
service = var.service
project = var.project
diff --git a/infrastructure/modules/tags/exports/README.md b/infrastructure/modules/tags/exports/README.md
index ba05dee..8c8ef89 100644
--- a/infrastructure/modules/tags/exports/README.md
+++ b/infrastructure/modules/tags/exports/README.md
@@ -15,7 +15,7 @@ No providers.
| Name | Source | Version |
| ---- | ------ | ------- |
-| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.0.0 |
+| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.1.0 |
## Resources
diff --git a/infrastructure/modules/tags/exports/context.tf b/infrastructure/modules/tags/exports/context.tf
index 89b68c6..c2990c8 100644
--- a/infrastructure/modules/tags/exports/context.tf
+++ b/infrastructure/modules/tags/exports/context.tf
@@ -21,7 +21,7 @@
#
module "this" {
- source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.0.0"
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.1.0"
service = var.service
project = var.project
diff --git a/scripts/release/update-tags-module-version.cjs b/scripts/release/update-tags-module-version.cjs
index 5d137bc..af0c49f 100644
--- a/scripts/release/update-tags-module-version.cjs
+++ b/scripts/release/update-tags-module-version.cjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
- * Update references to the local tags module between semantic-release versions.
+ * Update local module source version references between semantic-release versions.
*
* Why this exists:
* - Keeping release-time string replacements in a standalone script is easier
@@ -14,13 +14,24 @@
*
* Called by semantic-release exec plugin as:
* node scripts/release/update-tags-module-version.cjs "${lastRelease.version}" "${nextRelease.version}"
+ *
+ * Optional control markers:
+ * - semantic-release:pin => do not auto-update that .tf line
+ * - semantic-release:unpin => update now and then remove this marker on that .tf line
+ *
+ * Notes:
+ * - README.md rows are always updated regardless of markers, because README
+ * content is generated by terraform-docs and marker semantics are only
+ * intended for hand-edited Terraform source lines.
*/
-const fs = require("fs");
-const path = require("path");
+const fs = require("node:fs");
+const path = require("node:path");
const MODULES_ROOT = path.join("infrastructure", "modules");
-const TARGET_FILE_NAMES = new Set(["context.tf", "readme.md"]);
+const PIN_TOKEN = "semantic-release:pin";
+const UNPIN_TOKEN = "semantic-release:unpin";
+const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
const [lastVersion, nextVersion] = process.argv.slice(2);
@@ -53,18 +64,106 @@ function listFilesRecursively(dirPath) {
}
/**
- * Replace all occurrences of release-pinned tags module references.
+ * Ensure a semantic version has a leading v (for example 1.2.3 -> v1.2.3).
+ */
+function withVPrefix(version) {
+ return version.startsWith("v") ? version : `v${version}`;
+}
+
+/**
+ * Build equivalent from/to version pairs so replacements work for both:
+ * - plain versions (1.2.3)
+ * - v-prefixed tags (v1.2.3)
+ */
+function buildVersionPairs(fromVersion, toVersion) {
+ const pairs = [
+ { from: fromVersion, to: toVersion },
+ { from: withVPrefix(fromVersion), to: withVPrefix(toVersion) }
+ ];
+
+ // De-duplicate if the input was already v-prefixed.
+ return pairs.filter(
+ (pair, index, all) =>
+ all.findIndex((item) => item.from === pair.from && item.to === pair.to) === index
+ );
+}
+
+/**
+ * Return true for files that may contain version-pinned local module references:
+ * - Terraform source files (.tf)
+ * - Module README files (README.md / readme.md)
+ */
+function isTargetFile(filePath) {
+ const fileExt = path.extname(filePath).toLowerCase();
+ const baseName = path.basename(filePath).toLowerCase();
+
+ return fileExt === ".tf" || baseName === "readme.md";
+}
+
+/**
+ * Escape user-provided values before embedding into a regular expression.
*/
-function updateContent(content, fromVersion, toVersion) {
- return content
- .replaceAll(
- `//infrastructure/modules/tags?ref=${fromVersion}`,
- `//infrastructure/modules/tags?ref=${toVersion}`
- )
- .replaceAll(
- `//infrastructure/modules/tags | ${fromVersion} |`,
- `//infrastructure/modules/tags | ${toVersion} |`
- );
+function escapeRegex(value) {
+ return value.replace(REGEX_SPECIAL_CHARS, String.raw`\$&`);
+}
+
+/**
+ * Replace all release-pinned local module source references, for example:
+ * git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/?ref=
+ * and README table rows such as:
+ * | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ | |
+ *
+ * Set applyPinningControls=true only for .tf files.
+ */
+function updateContentWithMode(content, fromVersion, toVersion, applyPinningControls) {
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
+ const lines = content.split(/\r?\n/);
+ const updatedLines = [];
+
+ for (const line of lines) {
+ const hasPin = line.toLowerCase().includes(PIN_TOKEN);
+ const hasUnpin = line.toLowerCase().includes(UNPIN_TOKEN);
+
+ // Explicitly pinned .tf lines are never auto-updated.
+ if (applyPinningControls && hasPin && !hasUnpin) {
+ updatedLines.push(line);
+ continue;
+ }
+
+ // "Unpin" updates this .tf line once and removes the marker.
+ let updated = applyPinningControls && hasUnpin
+ ? line
+ .replace(/\s*#\s*semantic-release:unpin\b/gi, "")
+ .replace(/\s*\/\/\s*semantic-release:unpin\b/gi, "")
+ .replace(/\s*/gi, "")
+ .replace(/\s*semantic-release:unpin\b/gi, "")
+ .replace(/\s+$/, "")
+ : line;
+
+ for (const pair of buildVersionPairs(fromVersion, toVersion)) {
+ const sourcePrefixPattern = String.raw`(git::https://github\.com/NHSDigital/screening-terraform-modules-aws\.git//infrastructure/modules/[^?\s"']+\?ref=)`;
+ const readmePrefixPattern = String.raw`(\|\s*git::https://github\.com/NHSDigital/screening-terraform-modules-aws\.git//infrastructure/modules/[^|\s]+\s*\|\s*)`;
+ const readmeSuffixPattern = String.raw`(\s*\|)`;
+
+ const sourcePattern = new RegExp(
+ sourcePrefixPattern + escapeRegex(pair.from),
+ "g"
+ );
+
+ const readmeTablePattern = new RegExp(
+ readmePrefixPattern + escapeRegex(pair.from) + readmeSuffixPattern,
+ "g"
+ );
+
+ updated = updated
+ .replace(sourcePattern, `$1${pair.to}`)
+ .replace(readmeTablePattern, `$1${pair.to}$2`);
+ }
+
+ updatedLines.push(updated);
+ }
+
+ return updatedLines.join(eol);
}
if (!fs.existsSync(MODULES_ROOT)) {
@@ -76,13 +175,16 @@ const allFiles = listFilesRecursively(MODULES_ROOT);
let updatedFilesCount = 0;
for (const filePath of allFiles) {
- const fileName = path.basename(filePath).toLowerCase();
-
- // Only process files where these references are expected.
- if (!TARGET_FILE_NAMES.has(fileName)) continue;
+ if (!isTargetFile(filePath)) continue;
const original = fs.readFileSync(filePath, "utf8");
- const updated = updateContent(original, lastVersion, nextVersion);
+ const applyPinningControls = path.extname(filePath).toLowerCase() === ".tf";
+ const updated = updateContentWithMode(
+ original,
+ lastVersion,
+ nextVersion,
+ applyPinningControls
+ );
// Avoid touching unchanged files to keep release commits clean.
if (updated === original) continue;
@@ -92,5 +194,5 @@ for (const filePath of allFiles) {
}
console.log(
- `Updated tags module references from ${lastVersion} to ${nextVersion} in ${updatedFilesCount} file(s).`
+ `Updated local module source references from ${lastVersion} to ${nextVersion} in ${updatedFilesCount} file(s).`
);
diff --git a/scripts/tests/release-updater.sh b/scripts/tests/release-updater.sh
new file mode 100755
index 0000000..4a366d5
--- /dev/null
+++ b/scripts/tests/release-updater.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+
+set -euo pipefail
+
+cd "$(git rev-parse --show-toplevel)"
+
+# Build an isolated fixture so this smoke test never mutates real repo files.
+tmp_dir="$(mktemp -d)"
+trap 'rm -rf "${tmp_dir}"' EXIT
+
+mkdir -p "${tmp_dir}/infrastructure/modules/example"
+
+cat > "${tmp_dir}/infrastructure/modules/example/main.tf" <<'EOF'
+module "plain" {
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=2.0.0"
+}
+
+module "prefixed" {
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/inspector?ref=v2.0.0"
+}
+
+module "pinned" {
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/kms?ref=v2.0.0" # semantic-release:pin
+}
+
+module "unpinned_now" {
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/vpc?ref=v2.0.0" # semantic-release:unpin
+}
+EOF
+
+cat > "${tmp_dir}/infrastructure/modules/example/README.md" <<'EOF'
+| Name | Source | Version |
+| ---- | ------ | ------- |
+| this | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | 2.0.0 |
+| this2 | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/inspector | v2.0.0 |
+| this3 | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/kms | v2.0.0 |
+| this4 | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/vpc | v2.0.0 |
+EOF
+
+# Show the key lines before update for quick local debugging.
+echo "Before update:"
+grep -nE "source =|\| git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/" \
+ "${tmp_dir}/infrastructure/modules/example/main.tf" \
+ "${tmp_dir}/infrastructure/modules/example/README.md"
+
+(
+ cd "${tmp_dir}"
+ node "${OLDPWD}/scripts/release/update-tags-module-version.cjs" 2.0.0 2.1.0
+)
+
+# Assertions: both plain and v-prefixed references must be updated.
+grep -q "tags?ref=2.1.0" "${tmp_dir}/infrastructure/modules/example/main.tf"
+grep -q "inspector?ref=v2.1.0" "${tmp_dir}/infrastructure/modules/example/main.tf"
+grep -q "modules/tags | 2.1.0 |" "${tmp_dir}/infrastructure/modules/example/README.md"
+grep -q "modules/inspector | v2.1.0 |" "${tmp_dir}/infrastructure/modules/example/README.md"
+
+# Assertions: pinned .tf references remain unchanged.
+grep -q "kms?ref=v2.0.0\" # semantic-release:pin" "${tmp_dir}/infrastructure/modules/example/main.tf"
+grep -q "modules/kms | v2.1.0 | " "${tmp_dir}/infrastructure/modules/example/README.md"
+
+# Assertions: .tf unpin marker updates now and removes marker.
+grep -q "vpc?ref=v2.1.0\"" "${tmp_dir}/infrastructure/modules/example/main.tf"
+if grep -q "semantic-release:unpin" "${tmp_dir}/infrastructure/modules/example/main.tf"; then
+ echo "Smoke test failed: .tf unpin marker should be removed after update." >&2
+ exit 1
+fi
+
+# Assertions: README markers do not control behavior; value updates and marker text remains.
+grep -q "modules/vpc | v2.1.0 | " "${tmp_dir}/infrastructure/modules/example/README.md"
+
+# Guard against old values remaining in the fixture.
+if grep -nE "\?ref=(2\.0\.0|v2\.0\.0)|\|\s*(2\.0\.0|v2\.0\.0)\s*\|" "${tmp_dir}/infrastructure/modules/example/main.tf" "${tmp_dir}/infrastructure/modules/example/README.md" | grep -vq "semantic-release:pin"; then
+ echo "Smoke test failed: old version references are still present." >&2
+ exit 1
+fi
+
+echo "After update:"
+grep -nE "source =|\| git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/" \
+ "${tmp_dir}/infrastructure/modules/example/main.tf" \
+ "${tmp_dir}/infrastructure/modules/example/README.md"
+
+echo "release-updater smoke test passed"
diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh
index c589be5..2ac7073 100755
--- a/scripts/tests/unit.sh
+++ b/scripts/tests/unit.sh
@@ -17,4 +17,4 @@ cd "$(git rev-parse --show-toplevel)"
# tests from here. If you want to run other test suites, see the predefined
# tasks in scripts/test.mk.
-echo "Unit tests are not yet implemented. See scripts/tests/unit.sh for more."
+./scripts/tests/release-updater.sh