diff --git a/docs/Gemfile b/docs/Gemfile
new file mode 100644
index 0000000..da46d85
--- /dev/null
+++ b/docs/Gemfile
@@ -0,0 +1,36 @@
+source "https://rubygems.org"
+# Hello! This is where you manage which Jekyll version is used to run.
+# When you want to use a different version, change it below, save the
+# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
+#
+# bundle exec jekyll serve
+#
+# This will help ensure the proper Jekyll version is running.
+# Happy Jekylling!
+gem "jekyll", "~> 4.3.3"
+# This is the default theme for new Jekyll sites. You may change this to anything you like.
+gem "minima", "~> 2.5"
+# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
+# uncomment the line below. To upgrade, run `bundle update github-pages`.
+# gem "github-pages", group: :jekyll_plugins
+# If you have any plugins, put them here!
+group :jekyll_plugins do
+ gem "jekyll-feed", "~> 0.12"
+ gem "jekyll-drawio"
+end
+
+# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
+# and associated library.
+platforms :mingw, :x64_mingw, :mswin, :jruby do
+ gem "tzinfo", ">= 1", "< 3"
+ gem "tzinfo-data"
+end
+
+# Performance-booster for watching directories on Windows
+gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
+
+# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
+# do not have a Java counterpart.
+gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
+
+gem "just-the-docs"
diff --git a/docs/_collections/.gitkeep b/docs/_collections/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docs/_config.dev.yml b/docs/_config.dev.yml
new file mode 100644
index 0000000..314d4ac
--- /dev/null
+++ b/docs/_config.dev.yml
@@ -0,0 +1,2 @@
+
+baseurl: "" # the subpath of your site, e.g. /blog
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000..daf2e1e
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1,89 @@
+# Welcome to Jekyll!
+#
+# This config file is meant for settings that affect your whole blog, values
+# which you are expected to set up once and rarely edit after that. If you find
+# yourself editing this file very often, consider using Jekyll's data files
+# feature for the data you need to update frequently.
+#
+# For technical reasons, this file is *NOT* reloaded automatically when you use
+# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
+#
+# If you need help with YAML syntax, here are some quick references for you:
+# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
+# https://learnxinyminutes.com/docs/yaml/
+#
+# Site settings
+# These are used to personalize your new site. If you look in the HTML files,
+# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
+# You can create any custom variable you would like, and they will be accessible
+# in the templates via {{ site.myvariable }}.
+
+title: NHS Notify CMS
+# email: your-email@example.com
+description: >- # this means to ignore newlines until "baseurl:"
+ NHS Notify Web CMS
+baseurl: "/nhs-notify-web-cms" # the subpath of your site, e.g. /blog
+url: "https://nhsdigital.github.io" # the base hostname & protocol for your site, e.g. http://example.com
+
+collections_dir: _collections
+
+collections:
+ notify-repos:
+ output: true
+ sort_by: order
+
+# Build settings
+theme: just-the-docs
+plugins:
+ - jekyll-feed
+
+color_scheme: nhs
+mermaid:
+ # Version of mermaid library
+ # Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/
+ version: "10.9.1"
+
+aux_links:
+ "NHS Notify on GitHub":
+ - "//github.com/NHSDigital/nhs-notify"
+
+aux_links_new_tab: false
+
+# Footer "Edit this page on GitHub" link text
+gh_edit_link: true # show or hide edit this page link
+gh_edit_link_text: "Edit this page on GitHub."
+gh_edit_repository: "https://github.com/NHSDigital/nhs-notify" # the github URL for your repo
+gh_edit_branch: "main" # the branch that your docs is served from
+# gh_edit_source: docs # the source that your files originate from
+gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately
+
+nav_external_links:
+ - title: Notify Service Catalogue
+ url: https://digital.nhs.uk/services/nhs-notify
+ hide_icon: false # set to true to hide the external link icon - defaults to false
+ opens_in_new_tab: false # set to true to open this link in a new tab - defaults to false
+
+callouts:
+ warning:
+ title: Warning
+ color: red
+
+# Exclude from processing.
+# The following items will not be processed, by default.
+# Any item listed under the `exclude:` key here will be automatically added to
+# the internal "default list".
+#
+# Excluded items can be processed by explicitly listing the directories or
+# their entries' file path in the `include:` list.
+#
+# exclude:
+# - .sass-cache/
+# - .jekyll-cache/
+# - gemfiles/
+# - Gemfile
+# - Gemfile.lock
+# - node_modules/
+# - vendor/bundle/
+# - vendor/cache/
+# - vendor/gems/
+# - vendor/ruby/
diff --git a/docs/_includes/.gitkeep b/docs/_includes/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docs/_layouts/.gitkeep b/docs/_layouts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docs/_layouts/nhs-notify-default.html b/docs/_layouts/nhs-notify-default.html
new file mode 100644
index 0000000..4eaae58
--- /dev/null
+++ b/docs/_layouts/nhs-notify-default.html
@@ -0,0 +1,47 @@
+---
+layout: table_wrappers
+---
+
+
+
+
+{% include head.html %}
+
+ Skip to main content
+ {% include icons/icons.html %}
+ {% if page.nav_enabled == true %}
+ {% include components/sidebar.html %}
+ {% elsif layout.nav_enabled == true and page.nav_enabled == nil %}
+ {% include components/sidebar.html %}
+ {% elsif site.nav_enabled != false and layout.nav_enabled == nil and page.nav_enabled == nil %}
+ {% include components/sidebar.html %}
+ {% endif %}
+
+ {% include components/header.html %}
+
+ {% include components/breadcrumbs.html %}
+
+
+ {% if site.heading_anchors != false %}
+ {% include vendor/anchor_headings.html html=content beforeHeading="true" anchorBody="" anchorClass="anchor-heading" anchorAttrs="aria-labelledby=\"%html_id%\"" %}
+ {% else %}
+ {{ content }}
+ {% endif %}
+
+ {% if page.has_children == true and page.has_toc != false %}
+ {% include components/children_nav.html %}
+ {% endif %}
+
+ {% include components/footer.html %}
+
+
+ {% if site.search_enabled != false %}
+ {% include components/search_footer.html %}
+ {% endif %}
+
+
diff --git a/docs/_posts/.gitkeep b/docs/_posts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docs/_sass/color_schemes/example-variables.scss b/docs/_sass/color_schemes/example-variables.scss
new file mode 100644
index 0000000..dd87178
--- /dev/null
+++ b/docs/_sass/color_schemes/example-variables.scss
@@ -0,0 +1,137 @@
+//Reference Links
+
+$color-scheme: dark;
+$body-background-color: $grey-dk-300;
+$body-heading-color: $grey-lt-000;
+$body-text-color: $grey-lt-300;
+$link-color: $blue-000;
+$nav-child-link-color: $grey-dk-000;
+$sidebar-color: $grey-dk-300;
+$base-button-color: $grey-dk-250;
+$btn-primary-color: $blue-200;
+$code-background-color: #31343f; // OneDarkJekyll default for syntax-one-dark-vivid
+$code-linenumber-color: #dee2f7; // OneDarkJekyll .nf for syntax-one-dark-vivid
+$feedback-color: darken($sidebar-color, 3%);
+$table-background-color: $grey-dk-250;
+$search-background-color: $grey-dk-250;
+$search-result-preview-color: $grey-dk-000;
+$border-color: $grey-dk-200;
+
+
+// To be set in colour scheme
+
+// prettier-ignore
+$body-font-family: system-ui, -apple-system, blinkmacsystemfont, "Segoe UI",
+ roboto, "Helvetica Neue", arial, sans-serif, "Segoe UI Emoji" !default;
+$mono-font-family: "SFMono-Regular", menlo, consolas, monospace !default;
+$root-font-size: 16px !default; // DEPRECATED: previously base font-size for rems
+$body-line-height: 1.4 !default;
+$content-line-height: 1.6 !default;
+$body-heading-line-height: 1.25 !default;
+
+// Font size
+// `-sm` suffix is the size at the small (and above) media query
+
+$font-size-1: 0.5625rem !default;
+$font-size-1-sm: 0.625rem !default;
+$font-size-2: 0.6875rem !default; // h4 - uppercased!, h6 not uppercased, text-small
+$font-size-3: 0.75rem !default; // h5
+$font-size-4: 0.875rem !default;
+$font-size-5: 1rem !default; // h3
+$font-size-6: 1.125rem !default; // h2
+$font-size-7: 1.5rem !default;
+$font-size-8: 2rem !default; // h1
+$font-size-9: 2.25rem !default;
+$font-size-10: 2.625rem !default;
+$font-size-10-sm: 3rem !default;
+
+// Colors
+
+$white: #fff !default;
+$grey-dk-000: #959396 !default;
+$grey-dk-100: #5c5962 !default;
+$grey-dk-200: #44434d !default;
+$grey-dk-250: #302d36 !default;
+$grey-dk-300: #27262b !default;
+$grey-lt-000: #f5f6fa !default;
+$grey-lt-100: #eeebee !default;
+$grey-lt-200: #ecebed !default;
+$grey-lt-300: #e6e1e8 !default;
+$purple-000: #7253ed !default;
+$purple-100: #5e41d0 !default;
+$purple-200: #4e26af !default;
+$purple-300: #381885 !default;
+$blue-000: #2c84fa !default;
+$blue-100: #2869e6 !default;
+$blue-200: #264caf !default;
+$blue-300: #183385 !default;
+$green-000: #41d693 !default;
+$green-100: #11b584 !default;
+$green-200: #009c7b !default;
+$green-300: #026e57 !default;
+$yellow-000: #ffeb82 !default;
+$yellow-100: #fadf50 !default;
+$yellow-200: #f7d12e !default;
+$yellow-300: #e7af06 !default;
+$red-000: #f77e7e !default;
+$red-100: #f96e65 !default;
+$red-200: #e94c4c !default;
+$red-300: #dd2e2e !default;
+
+// Spacing
+
+$spacing-unit: 1rem; // 1rem == 16px
+
+$spacers: (
+ sp-0: 0,
+ sp-1: $spacing-unit * 0.25,
+ sp-2: $spacing-unit * 0.5,
+ sp-3: $spacing-unit * 0.75,
+ sp-4: $spacing-unit,
+ sp-5: $spacing-unit * 1.5,
+ sp-6: $spacing-unit * 2,
+ sp-7: $spacing-unit * 2.5,
+ sp-8: $spacing-unit * 3,
+ sp-9: $spacing-unit * 3.5,
+ sp-10: $spacing-unit * 4,
+) !default;
+$sp-1: map-get($spacers, sp-1) !default; // 0.25 rem == 4px
+$sp-2: map-get($spacers, sp-2) !default; // 0.5 rem == 8px
+$sp-3: map-get($spacers, sp-3) !default; // 0.75 rem == 12px
+$sp-4: map-get($spacers, sp-4) !default; // 1 rem == 16px
+$sp-5: map-get($spacers, sp-5) !default; // 1.5 rem == 24px
+$sp-6: map-get($spacers, sp-6) !default; // 2 rem == 32px
+$sp-7: map-get($spacers, sp-7) !default; // 2.5 rem == 40px
+$sp-8: map-get($spacers, sp-8) !default; // 3 rem == 48px
+$sp-9: map-get($spacers, sp-9) !default; // 3.5 rem == 56px
+$sp-10: map-get($spacers, sp-10) !default; // 4 rem == 64px
+
+// Borders
+
+$border: 1px solid !default;
+$border-radius: 4px !default;
+$border-color: $grey-lt-100 !default;
+
+// Grid system
+
+$gutter-spacing: $sp-6 !default;
+$gutter-spacing-sm: $sp-4 !default;
+$nav-width: 16.5rem !default;
+$nav-width-md: 15.5rem !default;
+$nav-list-item-height: $sp-6 !default;
+$nav-list-item-height-sm: $sp-8 !default;
+$nav-list-expander-right: true;
+$content-width: 50rem !default;
+$header-height: 3.75rem !default;
+$search-results-width: $content-width - $nav-width !default;
+$transition-duration: 400ms;
+
+// Media queries in pixels
+
+$media-queries: (
+ xs: 20rem,
+ sm: 31.25rem,
+ md: $content-width,
+ lg: $content-width + $nav-width,
+ xl: 87.5rem,
+) !default;
diff --git a/docs/_sass/color_schemes/nhs.scss b/docs/_sass/color_schemes/nhs.scss
new file mode 100644
index 0000000..5b45b8f
--- /dev/null
+++ b/docs/_sass/color_schemes/nhs.scss
@@ -0,0 +1,18 @@
+@import "./color_schemes/light";
+
+// Typography
+
+// prettier-ignore
+$body-font-family: "Frutiger W01", Arial, Sans-serif;
+$mono-font-family: "Frutiger W01", Arial, Sans-serif;
+
+
+
+$blue-000: #005eb8;
+$grey-dk-000: #d8dde0;
+$grey-dk-100: #f0f4f5;
+$sidebar-color: $grey-dk-000;
+$body-background-color: $grey-dk-100;
+$link-color: $blue-000;
+
+$font-size-7: 1.25rem;
diff --git a/docs/content/about.md b/docs/content/about.md
new file mode 100644
index 0000000..358a79f
--- /dev/null
+++ b/docs/content/about.md
@@ -0,0 +1,11 @@
+---
+# Feel free to add content and custom Front Matter to this file.
+# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
+
+layout: nhs-notify-page
+title: About
+nav_order: 2
+has_children: true
+---
+
+## About
diff --git a/docs/content/about/about-child.md b/docs/content/about/about-child.md
new file mode 100644
index 0000000..dd146dc
--- /dev/null
+++ b/docs/content/about/about-child.md
@@ -0,0 +1,10 @@
+---
+# Feel free to add content and custom Front Matter to this file.
+# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
+
+layout: nhs-notify-page
+title: About Child
+parent: About
+nav_order: 1
+---
+## About Child
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..5c3a18d
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,10 @@
+---
+# Feel free to add content and custom Front Matter to this file.
+# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
+
+layout: nhs-notify-home
+title: Home
+nav_order: 1
+---
+
+## NHS Notify
diff --git a/project.code-workspace b/project.code-workspace
new file mode 100644
index 0000000..ff84143
--- /dev/null
+++ b/project.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "name": "NHS Notify Repo Template",
+ "path": "."
+ }
+ ]
+}
diff --git a/scripts/config/gitleaks.toml b/scripts/config/gitleaks.toml
new file mode 100644
index 0000000..af5f0bb
--- /dev/null
+++ b/scripts/config/gitleaks.toml
@@ -0,0 +1,19 @@
+# SEE: https://github.com/gitleaks/gitleaks/#configuration
+
+[extend]
+useDefault = true # SEE: https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml
+
+[[rules]]
+description = "IPv4"
+id = "ipv4"
+regex = '''[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'''
+
+[rules.allowlist]
+regexTarget = "match"
+regexes = [
+ # Exclude the private network IPv4 addresses as well as the DNS servers for Google and OpenDNS
+ '''(127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|172\.(1[6-9]|2[0-9]|3[0-1])\.[0-9]{1,3}\.[0-9]{1,3}|192\.168\.[0-9]{1,3}\.[0-9]{1,3}|0\.0\.0\.0|255\.255\.255\.255|8\.8\.8\.8|8\.8\.4\.4|208\.67\.222\.222|208\.67\.220\.220)''',
+]
+
+[allowlist]
+paths = ['''.terraform.lock.hcl''', '''poetry.lock''', '''yarn.lock''']
diff --git a/scripts/config/grype.yaml b/scripts/config/grype.yaml
new file mode 100644
index 0000000..80c752e
--- /dev/null
+++ b/scripts/config/grype.yaml
@@ -0,0 +1,19 @@
+# If using SBOM input, automatically generate CPEs when packages have none
+add-cpes-if-none: true
+
+# ignore:
+# # This is the full set of supported rule fields:
+# - vulnerability: CVE-2008-4318
+# fix-state: unknown
+# package:
+# name: libcurl
+# version: 1.5.1
+# type: npm
+# location: "/usr/local/lib/node_modules/**"
+
+# # We can make rules to match just by vulnerability ID:
+# - vulnerability: CVE-2014-54321
+
+# # ...or just by a single package field:
+# - package:
+# type: gem
diff --git a/scripts/config/hadolint.yaml b/scripts/config/hadolint.yaml
new file mode 100644
index 0000000..d01a9ce
--- /dev/null
+++ b/scripts/config/hadolint.yaml
@@ -0,0 +1,7 @@
+# SEE: https://github.com/hadolint/hadolint#configure
+
+trustedRegistries:
+ - docker.io
+ - "*.gcr.io"
+ - "*.dkr.ecr.*.amazonaws.com"
+ - "*.azurecr.io"
diff --git a/scripts/config/markdownlint.yaml b/scripts/config/markdownlint.yaml
new file mode 100644
index 0000000..554ab55
--- /dev/null
+++ b/scripts/config/markdownlint.yaml
@@ -0,0 +1,11 @@
+# SEE: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml
+
+# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
+MD013: false
+
+# https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md
+MD024:
+ siblings_only: true
+
+# https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md
+MD033: false
diff --git a/scripts/config/pre-commit.yaml b/scripts/config/pre-commit.yaml
new file mode 100644
index 0000000..37ca637
--- /dev/null
+++ b/scripts/config/pre-commit.yaml
@@ -0,0 +1,40 @@
+repos:
+- repo: local
+ hooks:
+ - id: scan-secrets
+ name: Scan secrets
+ entry: ./scripts/githooks/scan-secrets.sh
+ args: ["check=staged-changes"]
+ language: script
+ pass_filenames: false
+- repo: local
+ hooks:
+ - id: check-file-format
+ name: Check file format
+ entry: ./scripts/githooks/check-file-format.sh
+ args: ["check=staged-changes"]
+ language: script
+ pass_filenames: false
+- repo: local
+ hooks:
+ - id: check-markdown-format
+ name: Check Markdown format
+ entry: ./scripts/githooks/check-markdown-format.sh
+ args: ["check=staged-changes"]
+ language: script
+ pass_filenames: false
+- repo: local
+ hooks:
+ - id: check-english-usage
+ name: Check English usage
+ entry: ./scripts/githooks/check-english-usage.sh
+ args: ["check=staged-changes"]
+ language: script
+ pass_filenames: false
+- repo: local
+ hooks:
+ - id: lint-terraform
+ name: Lint Terraform
+ entry: ./scripts/githooks/check-terraform-format.sh
+ language: script
+ pass_filenames: false
diff --git a/scripts/config/repository-template.yaml b/scripts/config/repository-template.yaml
new file mode 100644
index 0000000..eb37cee
--- /dev/null
+++ b/scripts/config/repository-template.yaml
@@ -0,0 +1 @@
+update-from-template:
diff --git a/scripts/config/sonar-scanner.properties b/scripts/config/sonar-scanner.properties
new file mode 100644
index 0000000..147891d
--- /dev/null
+++ b/scripts/config/sonar-scanner.properties
@@ -0,0 +1,9 @@
+# Please DO NOT set the following properties `sonar.organization` and `sonar.projectKey` in this file. They must be stored as `SONAR_ORGANISATION_KEY` and `SONAR_PROJECT_KEY` GitHub secrets.
+
+sonar.host.url=https://sonarcloud.io
+sonar.qualitygate.wait=true
+sonar.sourceEncoding=UTF-8
+sonar.sources=.
+
+#sonar.python.coverage.reportPaths=.coverage/coverage.xml
+#sonar.[javascript|typescript].lcov.reportPaths=.coverage/lcov.info
diff --git a/scripts/config/syft.yaml b/scripts/config/syft.yaml
new file mode 100644
index 0000000..e9f5f58
--- /dev/null
+++ b/scripts/config/syft.yaml
@@ -0,0 +1,83 @@
+# a list of globs to exclude from scanning. same as --exclude ; for example:
+# exclude:
+# - "/etc/**"
+# - "./out/**/*.json"
+exclude:
+ - ./.git/**
+
+# maximum number of workers used to process the list of package catalogers in parallel
+parallelism: 3
+
+# cataloging packages is exposed through the packages and power-user subcommands
+package:
+ # search within archives that do contain a file index to search against (zip)
+ # note: for now this only applies to the java package cataloger
+ # SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
+ search-indexed-archives: true
+ # search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc)
+ # note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed
+ # note: for now this only applies to the java package cataloger
+ # SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
+ search-unindexed-archives: true
+ cataloger:
+ # enable/disable cataloging of packages
+ # SYFT_PACKAGE_CATALOGER_ENABLED env var
+ enabled: true
+ # the search space to look for packages (options: all-layers, squashed)
+ # same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
+ scope: "squashed"
+
+# cataloging file contents is exposed through the power-user subcommand
+file-contents:
+ cataloger:
+ # enable/disable cataloging of secrets
+ # SYFT_FILE_CONTENTS_CATALOGER_ENABLED env var
+ enabled: true
+ # the search space to look for secrets (options: all-layers, squashed)
+ # SYFT_FILE_CONTENTS_CATALOGER_SCOPE env var
+ scope: "squashed"
+ # skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
+ # SYFT_FILE_CONTENTS_SKIP_FILES_ABOVE_SIZE env var
+ skip-files-above-size: 1048576
+ # file globs for the cataloger to match on
+ # SYFT_FILE_CONTENTS_GLOBS env var
+ globs: []
+
+# cataloging file metadata is exposed through the power-user subcommand
+file-metadata:
+ cataloger:
+ # enable/disable cataloging of file metadata
+ # SYFT_FILE_METADATA_CATALOGER_ENABLED env var
+ enabled: true
+ # the search space to look for file metadata (options: all-layers, squashed)
+ # SYFT_FILE_METADATA_CATALOGER_SCOPE env var
+ scope: "squashed"
+ # the file digest algorithms to use when cataloging files (options: "sha256", "md5", "sha1")
+ # SYFT_FILE_METADATA_DIGESTS env var
+ digests: ["sha256"]
+
+# cataloging secrets is exposed through the power-user subcommand
+secrets:
+ cataloger:
+ # enable/disable cataloging of secrets
+ # SYFT_SECRETS_CATALOGER_ENABLED env var
+ enabled: true
+ # the search space to look for secrets (options: all-layers, squashed)
+ # SYFT_SECRETS_CATALOGER_SCOPE env var
+ scope: "all-layers"
+ # show extracted secret values in the final JSON report
+ # SYFT_SECRETS_REVEAL_VALUES env var
+ reveal-values: false
+ # skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
+ # SYFT_SECRETS_SKIP_FILES_ABOVE_SIZE env var
+ skip-files-above-size: 1048576
+ # name-regex pairs to consider when searching files for secrets. Note: the regex must match single line patterns
+ # but may also have OPTIONAL multiline capture groups. Regexes with a named capture group of "value" will
+ # use the entire regex to match, but the secret value will be assumed to be entirely contained within the
+ # "value" named capture group.
+ additional-patterns: {}
+ # names to exclude from the secrets search, valid values are: "aws-access-key", "aws-secret-key", "pem-private-key",
+ # "docker-config-auth", and "generic-api-key". Note: this does not consider any names introduced in the
+ # "secrets.additional-patterns" config option.
+ # SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var
+ exclude-pattern-names: []
diff --git a/scripts/config/vale/styles/Vocab/words/accept.txt b/scripts/config/vale/styles/Vocab/words/accept.txt
new file mode 100644
index 0000000..eb9cd04
--- /dev/null
+++ b/scripts/config/vale/styles/Vocab/words/accept.txt
@@ -0,0 +1,17 @@
+Bitwarden
+Cyber
+Dependabot
+Gitleaks
+Grype
+OAuth
+Octokit
+Podman
+Python
+Syft
+Terraform
+Trufflehog
+bot
+idempotence
+onboarding
+toolchain
+[A-Z]+s
diff --git a/scripts/config/vale/styles/Vocab/words/reject.txt b/scripts/config/vale/styles/Vocab/words/reject.txt
new file mode 100644
index 0000000..fdc793e
--- /dev/null
+++ b/scripts/config/vale/styles/Vocab/words/reject.txt
@@ -0,0 +1 @@
+python
diff --git a/scripts/config/vale/vale.ini b/scripts/config/vale/vale.ini
new file mode 100644
index 0000000..171494e
--- /dev/null
+++ b/scripts/config/vale/vale.ini
@@ -0,0 +1,8 @@
+StylesPath = styles
+
+MinAlertLevel = suggestion
+
+Vocab = words
+
+[*.md]
+BasedOnStyles = Vale
diff --git a/scripts/docker/Dockerfile.metadata b/scripts/docker/Dockerfile.metadata
new file mode 100644
index 0000000..f54092e
--- /dev/null
+++ b/scripts/docker/Dockerfile.metadata
@@ -0,0 +1,22 @@
+
+# === Metadata =================================================================
+
+ARG IMAGE
+ARG TITLE
+ARG DESCRIPTION
+ARG LICENCE
+ARG GIT_URL
+ARG GIT_BRANCH
+ARG GIT_COMMIT_HASH
+ARG BUILD_DATE
+ARG BUILD_VERSION
+LABEL \
+ org.opencontainers.image.base.name=$IMAGE \
+ org.opencontainers.image.title="$TITLE" \
+ org.opencontainers.image.description="$DESCRIPTION" \
+ org.opencontainers.image.licenses="$LICENCE" \
+ org.opencontainers.image.url=$GIT_URL \
+ org.opencontainers.image.ref.name=$GIT_BRANCH \
+ org.opencontainers.image.revision=$GIT_COMMIT_HASH \
+ org.opencontainers.image.created=$BUILD_DATE \
+ org.opencontainers.image.version=$BUILD_VERSION
diff --git a/scripts/docker/dgoss.sh b/scripts/docker/dgoss.sh
new file mode 100644
index 0000000..e573a48
--- /dev/null
+++ b/scripts/docker/dgoss.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+# shellcheck disable=SC2016,SC2154,SC2166
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+# SEE: https://github.com/goss-org/goss/blob/master/extras/dgoss/dgoss
+
+set -e
+
+USAGE="USAGE: $(basename "$0") [run|edit] "
+GOSS_FILES_PATH="${GOSS_FILES_PATH:-.}"
+
+# Container runtime
+CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}"
+
+info() {
+ echo -e "INFO: $*" >&2;
+}
+error() {
+ echo -e "ERROR: $*" >&2;
+ exit 1;
+}
+
+cleanup() {
+ set +e
+ { kill "$log_pid" && wait "$log_pid"; } 2> /dev/null
+ if [ -n "$CONTAINER_LOG_OUTPUT" ]; then
+ cp "$tmp_dir/docker_output.log" "$CONTAINER_LOG_OUTPUT"
+ fi
+ rm -rf "$tmp_dir"
+ if [[ $id ]];then
+ info "Deleting container"
+ $CONTAINER_RUNTIME rm -vf "$id" > /dev/null
+ fi
+}
+
+run(){
+ # Copy in goss
+ cp "${GOSS_PATH}" "$tmp_dir/goss"
+ chmod 755 "$tmp_dir/goss"
+ [[ -e "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}" "$tmp_dir/goss.yaml" && chmod 644 "$tmp_dir/goss.yaml"
+ [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]] && cp "${GOSS_FILES_PATH}/goss_wait.yaml" "$tmp_dir" && chmod 644 "$tmp_dir/goss_wait.yaml"
+ [[ -n "${GOSS_VARS}" ]] && [[ -e "${GOSS_FILES_PATH}/${GOSS_VARS}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_VARS}" "$tmp_dir" && chmod 644 "$tmp_dir/${GOSS_VARS}"
+
+ # Switch between mount or cp files strategy
+ GOSS_FILES_STRATEGY=${GOSS_FILES_STRATEGY:="mount"}
+ case "$GOSS_FILES_STRATEGY" in
+ mount)
+ info "Starting $CONTAINER_RUNTIME container"
+ if [ "$CONTAINER_RUNTIME" == "podman" -a $# == 2 ]; then
+ id=$($CONTAINER_RUNTIME run -d -v "$tmp_dir:/goss:z" "${@:2}" sleep infinity)
+ else
+ id=$($CONTAINER_RUNTIME run -d -v "$tmp_dir:/goss:z" "${@:2}")
+ fi
+ ;;
+ cp)
+ info "Creating $CONTAINER_RUNTIME container"
+ id=$($CONTAINER_RUNTIME create "${@:2}")
+ info "Copy goss files into container"
+ $CONTAINER_RUNTIME cp "$tmp_dir/." "$id:/goss"
+ info "Starting $CONTAINER_RUNTIME container"
+ $CONTAINER_RUNTIME start "$id" > /dev/null
+ ;;
+ *) error "Wrong goss files strategy used! Correct options are \"mount\" or \"cp\"."
+ esac
+
+ $CONTAINER_RUNTIME logs -f "$id" > "$tmp_dir/docker_output.log" 2>&1 &
+ log_pid=$!
+ info "Container ID: ${id:0:8}"
+}
+
+get_docker_file() {
+ local cid=$1 # Docker container ID
+ local src=$2 # Source file path (in the container)
+ local dst=$3 # Destination file path
+
+ if $CONTAINER_RUNTIME exec "${cid}" sh -c "test -e ${src}" > /dev/null; then
+ mkdir -p "${GOSS_FILES_PATH}"
+ $CONTAINER_RUNTIME cp "${cid}:${src}" "${dst}"
+ info "Copied '${src}' from container to '${dst}'"
+ fi
+}
+
+# Main
+tmp_dir=$(mktemp -d /tmp/tmp.XXXXXXXXXX)
+chmod 777 "$tmp_dir"
+trap 'ret=$?;cleanup;exit $ret' EXIT
+
+GOSS_PATH="${GOSS_PATH:-$(which goss 2> /dev/null || true)}"
+[[ $GOSS_PATH ]] || { error "Couldn't find goss installation, please set GOSS_PATH to it"; }
+[[ ${GOSS_OPTS+x} ]] || GOSS_OPTS="--color --format documentation"
+[[ ${GOSS_WAIT_OPTS+x} ]] || GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null"
+GOSS_SLEEP=${GOSS_SLEEP:-0.2}
+
+[[ $CONTAINER_RUNTIME =~ ^(docker|podman)$ ]] || { error "Runtime must be one of docker or podman"; }
+
+case "$1" in
+ run)
+ run "$@"
+ if [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]]; then
+ info "Found goss_wait.yaml, waiting for it to pass before running tests"
+ if [[ -z "${GOSS_VARS}" ]]; then
+ if ! $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml validate $GOSS_WAIT_OPTS"; then
+ $CONTAINER_RUNTIME logs "$id" >&2
+ error "goss_wait.yaml never passed"
+ fi
+ else
+ if ! $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_WAIT_OPTS"; then
+ $CONTAINER_RUNTIME logs "$id" >&2
+ error "goss_wait.yaml never passed"
+ fi
+ fi
+ fi
+ [[ $GOSS_SLEEP ]] && { info "Sleeping for $GOSS_SLEEP"; sleep "$GOSS_SLEEP"; }
+ info "Container health"
+ if [ "true" != "$($CONTAINER_RUNTIME inspect -f '{{.State.Running}}' "$id")" ]; then
+ $CONTAINER_RUNTIME logs "$id" >&2
+ error "the container failed to start"
+ fi
+ info "Running Tests"
+ if [[ -z "${GOSS_VARS}" ]]; then
+ $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss.yaml validate $GOSS_OPTS"
+ else
+ $CONTAINER_RUNTIME exec "$id" sh -c "/goss/goss -g /goss/goss.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_OPTS"
+ fi
+ ;;
+ edit)
+ run "$@"
+ info "Run goss add/autoadd to add resources"
+ $CONTAINER_RUNTIME exec -it "$id" sh -c 'cd /goss; PATH="/goss:$PATH" exec sh'
+ get_docker_file "$id" "/goss/goss.yaml" "${GOSS_FILES_PATH}/${GOSS_FILE:-goss.yaml}"
+ get_docker_file "$id" "/goss/goss_wait.yaml" "${GOSS_FILES_PATH}/goss_wait.yaml"
+ if [[ -n "${GOSS_VARS}" ]]; then
+ get_docker_file "$id" "/goss/${GOSS_VARS}" "${GOSS_FILES_PATH}/${GOSS_VARS}"
+ fi
+ ;;
+ *)
+ error "$USAGE"
+esac
diff --git a/scripts/docker/docker.lib.sh b/scripts/docker/docker.lib.sh
new file mode 100644
index 0000000..1878710
--- /dev/null
+++ b/scripts/docker/docker.lib.sh
@@ -0,0 +1,303 @@
+#!/bin/bash
+# shellcheck disable=SC2155
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# A set of Docker functions written in Bash.
+#
+# Usage:
+# $ source ./docker.lib.sh
+#
+# Arguments (provided as environment variables):
+# DOCKER_IMAGE=ghcr.io/org/repo # Docker image name
+# DOCKER_TITLE="My Docker image" # Docker image title
+# TOOL_VERSIONS=$project_dir/.tool-versions # Path to the tool versions file
+
+# ==============================================================================
+# Functions to be used with custom images.
+
+# Build Docker image.
+# Arguments (provided as environment variables):
+# dir=[path to the Dockerfile to use, default is '.']
+function docker-build() {
+
+ local dir=${dir:-$PWD}
+
+ version-create-effective-file
+ _create-effective-dockerfile
+ # The current directory must be changed for the image build script to access
+ # assets that need to be copied
+ current_dir=$(pwd)
+ cd "$dir"
+ docker build \
+ --progress=plain \
+ --platform linux/amd64 \
+ --build-arg IMAGE="${DOCKER_IMAGE}" \
+ --build-arg TITLE="${DOCKER_TITLE}" \
+ --build-arg DESCRIPTION="${DOCKER_TITLE}" \
+ --build-arg LICENCE=MIT \
+ --build-arg GIT_URL="$(git config --get remote.origin.url)" \
+ --build-arg GIT_BRANCH="$(_get-git-branch-name)" \
+ --build-arg GIT_COMMIT_HASH="$(git rev-parse --short HEAD)" \
+ --build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S%z")" \
+ --build-arg BUILD_VERSION="$(_get-effective-version)" \
+ --tag "${DOCKER_IMAGE}:$(_get-effective-version)" \
+ --rm \
+ --file "${dir}/Dockerfile.effective" \
+ .
+ cd "$current_dir"
+ # Tag the image with all the stated versions, see the documentation for more details
+ for version in $(_get-all-effective-versions) latest; do
+ docker tag "${DOCKER_IMAGE}:$(_get-effective-version)" "${DOCKER_IMAGE}:${version}"
+ done
+ docker rmi --force "$(docker images | grep "" | awk '{print $3}')" 2> /dev/null ||:
+}
+
+# Check test Docker image.
+# Arguments (provided as environment variables):
+# args=[arguments to pass to Docker to run the container, default is none/empty]
+# cmd=[command to pass to the container for execution, default is none/empty]
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+# check=[output string to search for]
+function docker-check-test() {
+
+ local dir=${dir:-$PWD}
+
+ # shellcheck disable=SC2086,SC2154
+ docker run --rm --platform linux/amd64 \
+ ${args:-} \
+ "${DOCKER_IMAGE}:$(_get-effective-version)" 2>/dev/null \
+ ${cmd:-} \
+ | grep -q "${check}" && echo PASS || echo FAIL
+}
+
+# Run Docker image.
+# Arguments (provided as environment variables):
+# args=[arguments to pass to Docker to run the container, default is none/empty]
+# cmd=[command to pass to the container for execution, default is none/empty]
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function docker-run() {
+
+ local dir=${dir:-$PWD}
+
+ # shellcheck disable=SC2086
+ docker run --rm --platform linux/amd64 \
+ ${args:-} \
+ "${DOCKER_IMAGE}:$(dir="$dir" _get-effective-version)" \
+ ${cmd:-}
+}
+
+# Push Docker image.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function docker-push() {
+
+ local dir=${dir:-$PWD}
+
+ # Push all the image tags based on the stated versions, see the documentation for more details
+ for version in $(dir="$dir" _get-all-effective-versions) latest; do
+ docker push "${DOCKER_IMAGE}:${version}"
+ done
+}
+
+# Remove Docker resources.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function docker-clean() {
+
+ local dir=${dir:-$PWD}
+
+ for version in $(dir="$dir" _get-all-effective-versions) latest; do
+ docker rmi "${DOCKER_IMAGE}:${version}" > /dev/null 2>&1 ||:
+ done
+ rm -f \
+ .version \
+ Dockerfile.effective
+}
+
+# Create effective version from the VERSION file.
+# Arguments (provided as environment variables):
+# dir=[path to the VERSION file to use, default is '.']
+# BUILD_DATETIME=[build date and time in the '%Y-%m-%dT%H:%M:%S%z' format generated by the CI/CD pipeline, default is current date and time]
+function version-create-effective-file() {
+
+ local dir=${dir:-$PWD}
+ local version_file="$dir/VERSION"
+ local build_datetime=${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')}
+
+ if [ -f "$version_file" ]; then
+ # shellcheck disable=SC2002
+ cat "$version_file" | \
+ sed "s/\(\${yyyy}\|\$yyyy\)/$(date --date="${build_datetime}" -u +"%Y")/g" | \
+ sed "s/\(\${mm}\|\$mm\)/$(date --date="${build_datetime}" -u +"%m")/g" | \
+ sed "s/\(\${dd}\|\$dd\)/$(date --date="${build_datetime}" -u +"%d")/g" | \
+ sed "s/\(\${HH}\|\$HH\)/$(date --date="${build_datetime}" -u +"%H")/g" | \
+ sed "s/\(\${MM}\|\$MM\)/$(date --date="${build_datetime}" -u +"%M")/g" | \
+ sed "s/\(\${SS}\|\$SS\)/$(date --date="${build_datetime}" -u +"%S")/g" | \
+ sed "s/\(\${hash}\|\$hash\)/$(git rev-parse --short HEAD)/g" \
+ > "$dir/.version"
+ fi
+}
+
+# ==============================================================================
+# Functions to be used with external images.
+
+# Retrieve the Docker image version from the '.tool-versions' file and pull the
+# image if required. This function is to be used in conjunction with the
+# external images and it prevents Docker from downloading an image each time it
+# is used, since the digest is not stored locally for compressed images. To
+# optimise, the solution is to pull the image using its digest and then tag it,
+# checking this tag for existence for any subsequent use.
+# Arguments (provided as environment variables):
+# name=[full name of the Docker image]
+# match_version=[regexp to match the version, for example if the same image is used with multiple tags, default is '.*']
+# shellcheck disable=SC2001
+function docker-get-image-version-and-pull() {
+
+ # E.g. for the given entry "# docker/ghcr.io/org/image 1.2.3@sha256:hash" in
+ # the '.tool-versions' file, the following variables will be set to:
+ # name="ghcr.io/org/image"
+ # version="1.2.3@sha256:hash"
+ # tag="1.2.3"
+ # digest="sha256:hash"
+
+ # Get the image full version from the '.tool-versions' file,
+ # match it by name and version regex, if given.
+ local versions_file="${TOOL_VERSIONS:=$(git rev-parse --show-toplevel)/.tool-versions}"
+ local version="latest"
+ if [ -f "$versions_file" ]; then
+ line=$(grep "docker/${name} " "$versions_file" | sed "s/^#\s*//; s/\s*#.*$//" | grep "${match_version:-".*"}")
+ [ -n "$line" ] && version=$(echo "$line" | awk '{print $2}')
+ fi
+
+ # Split the image version into two, tag name and digest sha256.
+ local tag="$(echo "$version" | sed 's/@.*$//')"
+ local digest="$(echo "$version" | sed 's/^.*@//')"
+
+ # Check if the image exists locally already
+ if ! docker images | awk '{ print $1 ":" $2 }' | grep -q "^${name}:${tag}$"; then
+ if [ "$digest" != "latest" ]; then
+ # Pull image by the digest sha256 and tag it
+ docker pull \
+ --platform linux/amd64 \
+ "${name}@${digest}" \
+ > /dev/null 2>&1 || true
+ docker tag "${name}@${digest}" "${name}:${tag}"
+ else
+ # Pull the latest image
+ docker pull \
+ --platform linux/amd64 \
+ "${name}:latest" \
+ > /dev/null 2>&1 || true
+ fi
+ fi
+
+ echo "${name}:${version}"
+}
+
+# ==============================================================================
+# "Private" functions.
+
+# Create effective Dockerfile.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function _create-effective-dockerfile() {
+
+ local dir=${dir:-$PWD}
+
+ cp "${dir}/Dockerfile" "${dir}/Dockerfile.effective"
+ _replace-image-latest-by-specific-version
+ _append-metadata
+}
+
+# Replace image:latest by a specific version.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function _replace-image-latest-by-specific-version() {
+
+ local dir=${dir:-$PWD}
+ local versions_file="${TOOL_VERSIONS:=$(git rev-parse --show-toplevel)/.tool-versions}"
+ local dockerfile="${dir}/Dockerfile.effective"
+ local build_datetime=${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')}
+
+ if [ -f "$versions_file" ]; then
+ # First, list the entries specific for Docker to take precedence, then the rest but exclude comments
+ content=$(grep " docker/" "$versions_file"; grep -v " docker/" "$versions_file" ||: | grep -v "^#")
+ echo "$content" | while IFS= read -r line; do
+ [ -z "$line" ] && continue
+ line=$(echo "$line" | sed "s/^#\s*//; s/\s*#.*$//" | sed "s;docker/;;")
+ name=$(echo "$line" | awk '{print $1}')
+ version=$(echo "$line" | awk '{print $2}')
+ sed -i "s;\(FROM .*\)${name}:latest;\1${name}:${version};g" "$dockerfile"
+ done
+ fi
+
+ if [ -f "$dockerfile" ]; then
+ # shellcheck disable=SC2002
+ cat "$dockerfile" | \
+ sed "s/\(\${yyyy}\|\$yyyy\)/$(date --date="${build_datetime}" -u +"%Y")/g" | \
+ sed "s/\(\${mm}\|\$mm\)/$(date --date="${build_datetime}" -u +"%m")/g" | \
+ sed "s/\(\${dd}\|\$dd\)/$(date --date="${build_datetime}" -u +"%d")/g" | \
+ sed "s/\(\${HH}\|\$HH\)/$(date --date="${build_datetime}" -u +"%H")/g" | \
+ sed "s/\(\${MM}\|\$MM\)/$(date --date="${build_datetime}" -u +"%M")/g" | \
+ sed "s/\(\${SS}\|\$SS\)/$(date --date="${build_datetime}" -u +"%S")/g" | \
+ sed "s/\(\${hash}\|\$hash\)/$(git rev-parse --short HEAD)/g" \
+ > "$dockerfile.tmp"
+ mv "$dockerfile.tmp" "$dockerfile"
+ fi
+
+ # Do not ignore the issue if 'latest' is used in the effective image
+ sed -Ei "/# hadolint ignore=DL3007$/d" "${dir}/Dockerfile.effective"
+}
+
+# Append metadata to the end of Dockerfile.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function _append-metadata() {
+
+ local dir=${dir:-$PWD}
+
+ cat \
+ "$dir/Dockerfile.effective" \
+ "$(git rev-parse --show-toplevel)/scripts/docker/Dockerfile.metadata" \
+ > "$dir/Dockerfile.effective.tmp"
+ mv "$dir/Dockerfile.effective.tmp" "$dir/Dockerfile.effective"
+}
+
+# Print top Docker image version.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function _get-effective-version() {
+
+ local dir=${dir:-$PWD}
+
+ head -n 1 "${dir}/.version" 2> /dev/null ||:
+}
+
+# Print all Docker image versions.
+# Arguments (provided as environment variables):
+# dir=[path to the image directory where the Dockerfile is located, default is '.']
+function _get-all-effective-versions() {
+
+ local dir=${dir:-$PWD}
+
+ cat "${dir}/.version" 2> /dev/null ||:
+}
+
+# Print Git branch name. Check the GitHub variables first and then the local Git
+# repo.
+function _get-git-branch-name() {
+
+ local branch_name=$(git rev-parse --abbrev-ref HEAD)
+
+ if [ -n "${GITHUB_HEAD_REF:-}" ]; then
+ branch_name=$GITHUB_HEAD_REF
+ elif [ -n "${GITHUB_REF:-}" ]; then
+ # shellcheck disable=SC2001
+ branch_name=$(echo "$GITHUB_REF" | sed "s#refs/heads/##")
+ fi
+
+ echo "$branch_name"
+}
diff --git a/scripts/docker/docker.mk b/scripts/docker/docker.mk
new file mode 100644
index 0000000..a31ad9d
--- /dev/null
+++ b/scripts/docker/docker.mk
@@ -0,0 +1,83 @@
+# This file is for you! Edit it to implement your own Docker make targets.
+
+# ==============================================================================
+# Custom implementation - implementation of a make target should not exceed 5 lines of effective code.
+# In most cases there should be no need to modify the existing make targets.
+
+docker-build: # Build Docker image - optional: docker_dir|dir=[path to the Dockerfile to use, default is '.'] @Development
+ make _docker cmd="build" \
+ dir=$(or ${docker_dir}, ${dir})
+ file=$(or ${docker_dir}, ${dir})/Dockerfile.effective
+ scripts/docker/dockerfile-linter.sh
+
+docker-push: # Push Docker image - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Development
+ make _docker cmd="push" \
+ dir=$(or ${docker_dir}, ${dir})
+
+clean:: # Remove Docker resources (docker) - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Operations
+ make _docker cmd="clean" \
+ dir=$(or ${docker_dir}, ${dir})
+
+_docker: # Docker command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to the image directory where the Dockerfile is located, relative to the project's top-level directory, default is '.']
+ # 'DOCKER_IMAGE' and 'DOCKER_TITLE' are passed to the functions as environment variables
+ DOCKER_IMAGE=$(or ${DOCKER_IMAGE}, $(or ${docker_image}, $(or ${IMAGE}, $(or ${image}, ghcr.io/org/repo))))
+ DOCKER_TITLE=$(or "${DOCKER_TITLE}", $(or "${docker_title}", $(or "${TITLE}", $(or "${title}", "Service Docker image"))))
+ source scripts/docker/docker.lib.sh
+ dir=$(realpath ${dir})
+ docker-${cmd} # 'dir' is accessible by the function as environment variable
+
+# ==============================================================================
+# Quality checks - please DO NOT edit this section!
+
+docker-shellscript-lint: # Lint all Docker module shell scripts @Quality
+ for file in $$(find scripts/docker -type f -name "*.sh"); do
+ file=$${file} scripts/shellscript-linter.sh
+ done
+
+# ==============================================================================
+# Module tests and examples - please DO NOT edit this section!
+
+docker-test-suite-run: # Run Docker test suite @ExamplesAndTests
+ scripts/docker/tests/docker.test.sh
+
+docker-example-build: # Build Docker example @ExamplesAndTests
+ source scripts/docker/docker.lib.sh
+ cd scripts/docker/examples/python
+ DOCKER_IMAGE=repository-template/docker-example-python
+ DOCKER_TITLE="Repository Template Docker Python Example"
+ TOOL_VERSIONS="$(shell git rev-parse --show-toplevel)/scripts/docker/examples/python/.tool-versions.example"
+ docker-build
+
+docker-example-lint: # Lint Docker example @ExamplesAndTests
+ dockerfile=scripts/docker/examples/python/Dockerfile
+ file=$${dockerfile} scripts/docker/dockerfile-linter.sh
+
+docker-example-run: # Run Docker example @ExamplesAndTests
+ source scripts/docker/docker.lib.sh
+ cd scripts/docker/examples/python
+ DOCKER_IMAGE=repository-template/docker-example-python
+ args=" \
+ -it \
+ --publish 8000:8000 \
+ "
+ docker-run
+
+docker-example-clean: # Remove Docker example resources @ExamplesAndTests
+ source scripts/docker/docker.lib.sh
+ cd scripts/docker/examples/python
+ DOCKER_IMAGE=repository-template/docker-example-python
+ docker-clean
+
+# ==============================================================================
+
+${VERBOSE}.SILENT: \
+ _docker \
+ clean \
+ docker-build \
+ docker-example-build \
+ docker-example-clean \
+ docker-example-lint \
+ docker-example-run \
+ docker-push \
+ docker-shellscript-lint \
+ docker-test-suite-run \
diff --git a/scripts/docker/dockerfile-linter.sh b/scripts/docker/dockerfile-linter.sh
new file mode 100755
index 0000000..7e8c75f
--- /dev/null
+++ b/scripts/docker/dockerfile-linter.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Hadolint command wrapper. It will run hadolint natively if it is installed,
+# otherwise it will run it in a Docker container.
+#
+# Usage:
+# $ [options] ./dockerfile-linter.sh
+#
+# Arguments (provided as environment variables):
+# file=Dockerfile # Path to the Dockerfile to lint, relative to the project's top-level directory, default is './Dockerfile.effective'
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is 'false'
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ local file=${file:-./Dockerfile.effective}
+ if command -v hadolint > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ file="$file" run-hadolint-natively
+ else
+ file="$file" run-hadolint-in-docker
+ fi
+}
+
+# Run hadolint natively.
+# Arguments (provided as environment variables):
+# file=[path to the Dockerfile to lint, relative to the project's top-level directory]
+function run-hadolint-natively() {
+
+ # shellcheck disable=SC2001
+ hadolint "$(echo "$file" | sed "s#$PWD#.#")"
+}
+
+# Run hadolint in a Docker container.
+# Arguments (provided as environment variables):
+# file=[path to the Dockerfile to lint, relative to the project's top-level directory]
+function run-hadolint-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=hadolint/hadolint docker-get-image-version-and-pull)
+ # shellcheck disable=SC2001
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD:/workdir" \
+ --workdir /workdir \
+ "$image" \
+ hadolint \
+ --config /workdir/scripts/config/hadolint.yaml \
+ "/workdir/$(echo "$file" | sed "s#$PWD#.#")"
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/docker/examples/python/.tool-versions.example b/scripts/docker/examples/python/.tool-versions.example
new file mode 100644
index 0000000..9209311
--- /dev/null
+++ b/scripts/docker/examples/python/.tool-versions.example
@@ -0,0 +1,2 @@
+# python, SEE: https://hub.docker.com/_/python/tags
+# docker/python 3.11.4-alpine3.18@sha256:0135ae6442d1269379860b361760ad2cf6ab7c403d21935a8015b48d5bf78a86
diff --git a/scripts/docker/examples/python/Dockerfile b/scripts/docker/examples/python/Dockerfile
new file mode 100644
index 0000000..d0780aa
--- /dev/null
+++ b/scripts/docker/examples/python/Dockerfile
@@ -0,0 +1,33 @@
+# `*:latest` will be replaced with a corresponding version stored in the '.tool-versions' file
+# hadolint ignore=DL3007
+FROM python:latest as base
+
+# === Builder ==================================================================
+
+FROM base AS builder
+COPY ./assets/hello_world/requirements.txt /requirements.txt
+WORKDIR /packages
+RUN set -eux; \
+ \
+ # Install dependencies
+ pip install \
+ --requirement /requirements.txt \
+ --prefix=/packages \
+ --no-warn-script-location \
+ --no-cache-dir
+
+# === Runtime ==================================================================
+
+FROM base
+ENV \
+ LANG="C.UTF-8" \
+ LC_ALL="C.UTF-8" \
+ PYTHONDONTWRITEBYTECODE="1" \
+ PYTHONUNBUFFERED="1" \
+ TZ="UTC"
+COPY --from=builder /packages /usr/local
+COPY ./assets/hello_world /hello_world
+WORKDIR /hello_world
+USER nobody
+CMD [ "python", "app.py" ]
+EXPOSE 8000
diff --git a/scripts/docker/examples/python/Dockerfile.effective b/scripts/docker/examples/python/Dockerfile.effective
new file mode 100644
index 0000000..3f1ea6b
--- /dev/null
+++ b/scripts/docker/examples/python/Dockerfile.effective
@@ -0,0 +1,54 @@
+# `*:latest` will be replaced with a corresponding version stored in the '.tool-versions' file
+FROM python:3.11.4-alpine3.18@sha256:0135ae6442d1269379860b361760ad2cf6ab7c403d21935a8015b48d5bf78a86 as base
+
+# === Builder ==================================================================
+
+FROM base AS builder
+COPY ./assets/hello_world/requirements.txt /requirements.txt
+WORKDIR /packages
+RUN set -eux; \
+ \
+ # Install dependencies
+ pip install \
+ --requirement /requirements.txt \
+ --prefix=/packages \
+ --no-warn-script-location \
+ --no-cache-dir
+
+# === Runtime ==================================================================
+
+FROM base
+ENV \
+ LANG="C.UTF-8" \
+ LC_ALL="C.UTF-8" \
+ PYTHONDONTWRITEBYTECODE="1" \
+ PYTHONUNBUFFERED="1" \
+ TZ="UTC"
+COPY --from=builder /packages /usr/local
+COPY ./assets/hello_world /hello_world
+WORKDIR /hello_world
+USER nobody
+CMD [ "python", "app.py" ]
+EXPOSE 8000
+
+# === Metadata =================================================================
+
+ARG IMAGE
+ARG TITLE
+ARG DESCRIPTION
+ARG LICENCE
+ARG GIT_URL
+ARG GIT_BRANCH
+ARG GIT_COMMIT_HASH
+ARG BUILD_DATE
+ARG BUILD_VERSION
+LABEL \
+ org.opencontainers.image.base.name=$IMAGE \
+ org.opencontainers.image.title="$TITLE" \
+ org.opencontainers.image.description="$DESCRIPTION" \
+ org.opencontainers.image.licenses="$LICENCE" \
+ org.opencontainers.image.url=$GIT_URL \
+ org.opencontainers.image.ref.name=$GIT_BRANCH \
+ org.opencontainers.image.revision=$GIT_COMMIT_HASH \
+ org.opencontainers.image.created=$BUILD_DATE \
+ org.opencontainers.image.version=$BUILD_VERSION
diff --git a/scripts/docker/examples/python/VERSION b/scripts/docker/examples/python/VERSION
new file mode 100644
index 0000000..8acdd82
--- /dev/null
+++ b/scripts/docker/examples/python/VERSION
@@ -0,0 +1 @@
+0.0.1
diff --git a/scripts/docker/examples/python/assets/hello_world/app.py b/scripts/docker/examples/python/assets/hello_world/app.py
new file mode 100644
index 0000000..4844e89
--- /dev/null
+++ b/scripts/docker/examples/python/assets/hello_world/app.py
@@ -0,0 +1,12 @@
+from flask import Flask
+from flask_wtf.csrf import CSRFProtect
+
+app = Flask(__name__)
+csrf = CSRFProtect()
+csrf.init_app(app)
+
+@app.route("/")
+def index():
+ return "Hello World!"
+
+app.run(host='0.0.0.0', port=8000)
diff --git a/scripts/docker/examples/python/assets/hello_world/requirements.txt b/scripts/docker/examples/python/assets/hello_world/requirements.txt
new file mode 100644
index 0000000..a38fca7
--- /dev/null
+++ b/scripts/docker/examples/python/assets/hello_world/requirements.txt
@@ -0,0 +1,12 @@
+blinker==1.6.2
+click==8.1.7
+Flask-WTF==1.2.0
+Flask==2.3.3
+itsdangerous==2.1.2
+Jinja2==3.1.3
+MarkupSafe==2.1.3
+pip==23.3
+setuptools==65.5.1
+Werkzeug==3.0.1
+wheel==0.41.1
+WTForms==3.0.1
diff --git a/scripts/docker/examples/python/tests/goss.yaml b/scripts/docker/examples/python/tests/goss.yaml
new file mode 100644
index 0000000..589db37
--- /dev/null
+++ b/scripts/docker/examples/python/tests/goss.yaml
@@ -0,0 +1,8 @@
+package:
+ python:
+ installed: true
+
+command:
+ pip list | grep -i flask:
+ exit-status: 0
+ timeout: 60000
diff --git a/scripts/docker/tests/.gitignore b/scripts/docker/tests/.gitignore
new file mode 100644
index 0000000..c50e8c0
--- /dev/null
+++ b/scripts/docker/tests/.gitignore
@@ -0,0 +1 @@
+Dockerfile.effective
diff --git a/scripts/docker/tests/.tool-versions.test b/scripts/docker/tests/.tool-versions.test
new file mode 100644
index 0000000..9209311
--- /dev/null
+++ b/scripts/docker/tests/.tool-versions.test
@@ -0,0 +1,2 @@
+# python, SEE: https://hub.docker.com/_/python/tags
+# docker/python 3.11.4-alpine3.18@sha256:0135ae6442d1269379860b361760ad2cf6ab7c403d21935a8015b48d5bf78a86
diff --git a/scripts/docker/tests/Dockerfile b/scripts/docker/tests/Dockerfile
new file mode 100644
index 0000000..b5ea560
--- /dev/null
+++ b/scripts/docker/tests/Dockerfile
@@ -0,0 +1,3 @@
+# `*:latest` will be replaced with a corresponding version stored in the '.tool-versions' file
+# hadolint ignore=DL3007
+FROM python:latest
diff --git a/scripts/docker/tests/VERSION b/scripts/docker/tests/VERSION
new file mode 100644
index 0000000..fb36635
--- /dev/null
+++ b/scripts/docker/tests/VERSION
@@ -0,0 +1,3 @@
+${yyyy}${mm}${dd}-${hash}
+$yyyy.$mm.$dd-$hash
+somme-name-yyyyeah
diff --git a/scripts/docker/tests/docker.test.sh b/scripts/docker/tests/docker.test.sh
new file mode 100755
index 0000000..8f487b8
--- /dev/null
+++ b/scripts/docker/tests/docker.test.sh
@@ -0,0 +1,162 @@
+#!/bin/bash
+# shellcheck disable=SC1091,SC2034,SC2317
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Test suite for Docker functions.
+#
+# Usage:
+# $ ./docker.test.sh
+#
+# Arguments (provided as environment variables):
+# VERBOSE=true # Show all the executed commands, default is 'false'
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+ source ./scripts/docker/docker.lib.sh
+ cd ./scripts/docker/tests
+
+ DOCKER_IMAGE=repository-template/docker-test
+ DOCKER_TITLE="Repository Template Docker Test"
+
+ test-docker-suite-setup
+ tests=( \
+ test-docker-build \
+ test-docker-image-from-signature \
+ test-docker-version-file \
+ test-docker-test \
+ test-docker-run \
+ test-docker-clean \
+ test-docker-get-image-version-and-pull \
+ )
+ local status=0
+ for test in "${tests[@]}"; do
+ {
+ echo -n "$test"
+ # shellcheck disable=SC2015
+ $test && echo " PASS" || { echo " FAIL"; ((status++)); }
+ }
+ done
+ echo "Total: ${#tests[@]}, Passed: $(( ${#tests[@]} - status )), Failed: $status"
+ test-docker-suite-teardown
+ [ $status -gt 0 ] && return 1 || return 0
+}
+
+# ==============================================================================
+
+function test-docker-suite-setup() {
+
+ :
+}
+
+function test-docker-suite-teardown() {
+
+ :
+}
+
+# ==============================================================================
+
+function test-docker-build() {
+
+ # Arrange
+ export BUILD_DATETIME="2023-09-04T15:46:34+0000"
+ # Act
+ docker-build > /dev/null 2>&1
+ # Assert
+ docker image inspect "${DOCKER_IMAGE}:$(_get-effective-version)" > /dev/null 2>&1 && return 0 || return 1
+}
+
+function test-docker-image-from-signature() {
+
+ # Arrange
+ TOOL_VERSIONS="$(git rev-parse --show-toplevel)/scripts/docker/tests/.tool-versions.test"
+ cp Dockerfile Dockerfile.effective
+ # Act
+ _replace-image-latest-by-specific-version
+ # Assert
+ grep -q "FROM python:.*-alpine.*@sha256:.*" Dockerfile.effective && return 0 || return 1
+}
+
+function test-docker-version-file() {
+
+ # Arrange
+ export BUILD_DATETIME="2023-09-04T15:46:34+0000"
+ # Act
+ version-create-effective-file
+ # Assert
+ # shellcheck disable=SC2002
+ (
+ cat .version | grep -q "20230904-" &&
+ cat .version | grep -q "2023.09.04-" &&
+ cat .version | grep -q "somme-name-yyyyeah"
+ ) && return 0 || return 1
+}
+
+function test-docker-test() {
+
+ # Arrange
+ cmd="python --version"
+ check="Python"
+ # Act
+ output=$(docker-check-test)
+ # Assert
+ echo "$output" | grep -q "PASS"
+}
+
+function test-docker-run() {
+
+ # Arrange
+ cmd="python --version"
+ # Act
+ output=$(docker-run)
+ # Assert
+ echo "$output" | grep -Eq "Python [0-9]+\.[0-9]+\.[0-9]+"
+}
+
+function test-docker-clean() {
+
+ # Arrange
+ version="$(_get-effective-version)"
+ # Act
+ docker-clean
+ # Assert
+ docker image inspect "${DOCKER_IMAGE}:${version}" > /dev/null 2>&1 && return 1 || return 0
+}
+
+function test-docker-get-image-version-and-pull() {
+
+ # Arrange
+ name="ghcr.io/nhs-england-tools/github-runner-image"
+ match_version=".*-rt.*"
+ # Act
+ docker-get-image-version-and-pull > /dev/null 2>&1
+ # Assert
+ docker images \
+ --filter=reference="$name" \
+ --format "{{.Tag}}" \
+ | grep -vq ""
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/githooks/check-english-usage.sh b/scripts/githooks/check-english-usage.sh
new file mode 100755
index 0000000..b3942de
--- /dev/null
+++ b/scripts/githooks/check-english-usage.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Git hook to check prose style
+#
+# Usage:
+# $ check={all,staged-changes,working-tree-changes,branch} ./check-english-usage.sh
+#
+# Exit codes:
+# 0 - All files are formatted correctly
+# 1 - Files are not formatted correctly
+#
+# The `check` parameter controls which files are checked, so you can
+# limit the scope of the check according to what is appropriate at the
+# point the check is being applied.
+#
+# check=all: check all files in the repository
+# check=staged-changes: check only files staged for commit.
+# check=working-tree-changes: check modified, unstaged files. This is the default.
+# check=branch: check for all changes since branching from $BRANCH_NAME
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ check=${check:-working-tree-changes}
+ case $check in
+ "all")
+ filter="git ls-files"
+ ;;
+ "staged-changes")
+ filter="git diff --diff-filter=ACMRT --name-only --cached"
+ ;;
+ "working-tree-changes")
+ filter="git diff --diff-filter=ACMRT --name-only"
+ ;;
+ "branch")
+ filter="git diff --diff-filter=ACMRT --name-only ${BRANCH_NAME:-origin/main}"
+ ;;
+ *)
+ echo "Unrecognised check mode: $check" >&2 && exit 1
+ ;;
+ esac
+
+ if command -v vale > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ filter="$filter" run-vale-natively
+ else
+ filter="$filter" run-vale-in-docker
+ fi
+}
+
+# Run Vale natively.
+# Arguments (provided as environment variables):
+# filter=[git command to filter the files to check]
+function run-vale-natively() {
+
+ # shellcheck disable=SC2046
+ vale \
+ --config "$PWD/scripts/config/vale/vale.ini" \
+ $($filter)
+}
+
+# Run Vale in a Docker container.
+# Arguments (provided as environment variables):
+# filter=[git command to filter the files to check]
+function run-vale-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=jdkato/vale docker-get-image-version-and-pull)
+ # We use /dev/null here to stop `vale` from complaining that it's
+ # not been called correctly if the $filter happens to return an
+ # empty list. As long as there's a filename, even if it's one that
+ # will be ignored, `vale` is happy.
+ # shellcheck disable=SC2046,SC2086
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD:/workdir" \
+ --workdir /workdir \
+ "$image" \
+ --config /workdir/scripts/config/vale/vale.ini \
+ $($filter) /dev/null
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/githooks/check-file-format.sh b/scripts/githooks/check-file-format.sh
new file mode 100755
index 0000000..d7c9474
--- /dev/null
+++ b/scripts/githooks/check-file-format.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Pre-commit git hook to check the EditorConfig rules compliance over changed
+# files. It ensures all non-binary files across the codebase are formatted
+# according to the style defined in the `.editorconfig` file. This is a
+# editorconfig command wrapper. It will run editorconfig natively if it is
+# installed, otherwise it will run it in a Docker container.
+#
+# Usage:
+# $ [options] ./check-file-format.sh
+#
+# Options:
+# check={all,staged-changes,working-tree-changes,branch} # Check mode, default is 'working-tree-changes'
+# dry_run=true # Do not check, run dry run only, default is 'false'
+# BRANCH_NAME=other-branch-than-main # Branch to compare with, default is `origin/main`
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is `false`
+#
+# Exit codes:
+# 0 - All files are formatted correctly
+# 1 - Files are not formatted correctly
+#
+# The `check` parameter controls which files are checked, so you can
+# limit the scope of the check according to what is appropriate at the
+# point the check is being applied.
+#
+# check=all: check all files in the repository
+# check=staged-changes: check only files staged for commit.
+# check=working-tree-changes: check modified, unstaged files. This is the default.
+# check=branch: check for all changes since branching from $BRANCH_NAME
+#
+# Notes:
+# Please make sure to enable EditorConfig linting in your IDE. For the
+# Visual Studio Code editor it is `editorconfig.editorconfig` that is already
+# specified in the `./.vscode/extensions.json` file.
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ # shellcheck disable=SC2154
+ is-arg-true "${dry_run:-false}" && dry_run_opt="--dry-run"
+
+ check=${check:-working-tree-changes}
+ case $check in
+ "all")
+ filter="git ls-files"
+ ;;
+ "staged-changes")
+ filter="git diff --diff-filter=ACMRT --name-only --cached"
+ ;;
+ "working-tree-changes")
+ filter="git diff --diff-filter=ACMRT --name-only"
+ ;;
+ "branch")
+ filter="git diff --diff-filter=ACMRT --name-only ${BRANCH_NAME:-origin/main}"
+ ;;
+ *)
+ echo "Unrecognised check mode: $check" >&2 && exit 1
+ ;;
+ esac
+
+ if command -v editorconfig > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ filter="$filter" dry_run_opt="${dry_run_opt:-}" run-editorconfig-natively
+ else
+ filter="$filter" dry_run_opt="${dry_run_opt:-}" run-editorconfig-in-docker
+ fi
+}
+
+# Run editorconfig natively.
+# Arguments (provided as environment variables):
+# dry_run_opt=[dry run option]
+# filter=[git command to filter the files to check]
+function run-editorconfig-natively() {
+
+ # shellcheck disable=SC2046,SC2086
+ editorconfig \
+ --exclude '.git/' $dry_run_opt $($filter)
+}
+
+# Run editorconfig in a Docker container.
+# Arguments (provided as environment variables):
+# dry_run_opt=[dry run option]
+# filter=[git command to filter the files to check]
+function run-editorconfig-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=mstruebing/editorconfig-checker docker-get-image-version-and-pull)
+ # We use /dev/null here as a backstop in case there are no files in the state
+ # we choose. If the filter comes back empty, adding `/dev/null` onto it has
+ # the effect of preventing `ec` from treating "no files" as "all the files".
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/check \
+ "$image" \
+ sh -c "ec --exclude '.git/' $dry_run_opt \$($filter) /dev/null"
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/githooks/check-markdown-format.sh b/scripts/githooks/check-markdown-format.sh
new file mode 100755
index 0000000..698df4a
--- /dev/null
+++ b/scripts/githooks/check-markdown-format.sh
@@ -0,0 +1,109 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Pre-commit git hook to check the Markdown file formatting rules compliance
+# over changed files. This is a markdownlint command wrapper. It will run
+# markdownlint natively if it is installed, otherwise it will run it in a Docker
+# container.
+#
+# Usage:
+# $ [options] ./check-markdown-format.sh
+#
+# Options:
+# check={all,staged-changes,working-tree-changes,branch} # Check mode, default is 'working-tree-changes'
+# BRANCH_NAME=other-branch-than-main # Branch to compare with, default is `origin/main`
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is `false`
+#
+# Exit codes:
+# 0 - All files are formatted correctly
+# 1 - Files are not formatted correctly
+#
+# Notes:
+# 1) Please make sure to enable Markdown linting in your IDE. For the Visual
+# Studio Code editor it is `davidanson.vscode-markdownlint` that is already
+# specified in the `./.vscode/extensions.json` file.
+# 2) To see the full list of the rules, please visit
+# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ check=${check:-working-tree-changes}
+ case $check in
+ "all")
+ files="$(find ./ -type f -name "*.md")"
+ ;;
+ "staged-changes")
+ files="$(git diff --diff-filter=ACMRT --name-only --cached "*.md")"
+ ;;
+ "working-tree-changes")
+ files="$(git diff --diff-filter=ACMRT --name-only "*.md")"
+ ;;
+ "branch")
+ files="$( (git diff --diff-filter=ACMRT --name-only "${BRANCH_NAME:-origin/main}" "*.md"; git diff --name-only "*.md") | sort | uniq )"
+ ;;
+ esac
+
+ if [ -n "$files" ]; then
+ if command -v markdownlint > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ files="$files" run-markdownlint-natively
+ else
+ files="$files" run-markdownlint-in-docker
+ fi
+ fi
+}
+
+# Run markdownlint natively.
+# Arguments (provided as environment variables):
+# files=[files to check]
+function run-markdownlint-natively() {
+
+ # shellcheck disable=SC2086
+ markdownlint \
+ $files \
+ --config "$PWD/scripts/config/markdownlint.yaml"
+}
+
+# Run markdownlint in a Docker container.
+# Arguments (provided as environment variables):
+# files=[files to check]
+function run-markdownlint-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=ghcr.io/igorshubovych/markdownlint-cli docker-get-image-version-and-pull)
+ # shellcheck disable=SC2086
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/workdir \
+ "$image" \
+ $files \
+ --config /workdir/scripts/config/markdownlint.yaml
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/githooks/check-terraform-format.sh b/scripts/githooks/check-terraform-format.sh
new file mode 100755
index 0000000..7255e51
--- /dev/null
+++ b/scripts/githooks/check-terraform-format.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Pre-commit git hook to check format Terraform code.
+#
+# Usage:
+# $ [options] ./check-terraform-format.sh
+#
+# Options:
+# check_only=true # Do not format, run check only, default is 'false'
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is 'false'
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ local check_only=${check_only:-false}
+ check_only=$check_only terraform-fmt
+}
+
+# Format Terraform files.
+# Arguments (provided as environment variables):
+# check_only=[do not format, run check only]
+function terraform-fmt() {
+
+ local opts=
+ if is-arg-true "$check_only"; then
+ opts="-check"
+ fi
+ opts=$opts make terraform-fmt
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/githooks/scan-secrets.sh b/scripts/githooks/scan-secrets.sh
new file mode 100755
index 0000000..06155b8
--- /dev/null
+++ b/scripts/githooks/scan-secrets.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Pre-commit git hook to scan for secrets hard-coded in the codebase. This is a
+# gitleaks command wrapper. It will run gitleaks natively if it is installed,
+# otherwise it will run it in a Docker container.
+#
+# Usage:
+# $ [options] ./scan-secrets.sh
+#
+# Options:
+# check={whole-history,last-commit,staged-changes} # Type of the check to run, default is 'staged-changes'
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is 'false'
+#
+# Exit codes:
+# 0 - No leaks present
+# 1 - Leaks or error encountered
+# 126 - Unknown flag
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ if command -v gitleaks > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ dir="$PWD"
+ cmd="$(get-cmd-to-run)" run-gitleaks-natively
+ else
+ dir="/workdir"
+ cmd="$(get-cmd-to-run)" run-gitleaks-in-docker
+ fi
+}
+
+# Get Gitleaks command to execute and configuration.
+# Arguments (provided as environment variables):
+# dir=[project's top-level directory]
+function get-cmd-to-run() {
+
+ check=${check:-staged-changes}
+ case $check in
+ "whole-history")
+ cmd="detect --source $dir --verbose --redact"
+ ;;
+ "last-commit")
+ cmd="detect --source $dir --verbose --redact --log-opts -1"
+ ;;
+ "staged-changes")
+ cmd="protect --source $dir --verbose --staged"
+ ;;
+ esac
+ # Include base line file if it exists
+ if [ -f "$dir/scripts/config/.gitleaks-baseline.json" ]; then
+ cmd="$cmd --baseline-path $dir/scripts/config/.gitleaks-baseline.json"
+ fi
+ # Include the config file
+ cmd="$cmd --config $dir/scripts/config/gitleaks.toml"
+
+ echo "$cmd"
+}
+
+# Run Gitleaks natively.
+# Arguments (provided as environment variables):
+# cmd=[command to run]
+function run-gitleaks-natively() {
+
+ # shellcheck disable=SC2086
+ gitleaks $cmd
+}
+
+# Run Gitleaks in a Docker container.
+# Arguments (provided as environment variables):
+# cmd=[command to run]
+# dir=[directory to mount as a volume]
+function run-gitleaks-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=ghcr.io/gitleaks/gitleaks docker-get-image-version-and-pull)
+ # shellcheck disable=SC2086
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD:$dir" \
+ --workdir $dir \
+ "$image" \
+ $cmd
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/init.mk b/scripts/init.mk
new file mode 100644
index 0000000..373f8a4
--- /dev/null
+++ b/scripts/init.mk
@@ -0,0 +1,157 @@
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+include scripts/docker/docker.mk
+include scripts/tests/test.mk
+-include scripts/terraform/terraform.mk
+
+# ==============================================================================
+
+runner-act: # Run GitHub Actions locally - mandatory: workflow=[workflow file name], job=[job name] @Development
+ source ./scripts/docker/docker.lib.sh
+ act $(shell [[ "${VERBOSE}" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$$ ]] && echo --verbose) \
+ --container-architecture linux/amd64 \
+ --platform ubuntu-latest=$$(name="ghcr.io/nhs-england-tools/github-runner-image" docker-get-image-version-and-pull) \
+ --container-options "--privileged" \
+ --bind \
+ --pull=false \
+ --reuse \
+ --rm \
+ --defaultbranch main \
+ --workflows .github/workflows/${workflow}.yaml \
+ --job ${job}
+
+version-create-effective-file: # Create effective version file - optional: dir=[path to the VERSION file to use, default is '.'], BUILD_DATETIME=[build date and time in the '%Y-%m-%dT%H:%M:%S%z' format generated by the CI/CD pipeline, default is current date and time] @Development
+ source scripts/docker/docker.lib.sh
+ version-create-effective-file
+
+shellscript-lint-all: # Lint all shell scripts in this project, do not fail on error, just print the error messages @Quality
+ for file in $$(find . -type f -name "*.sh"); do
+ file=$${file} scripts/shellscript-linter.sh ||:
+ done
+
+githooks-config: # Trigger Git hooks on commit that are defined in this repository @Configuration
+ make _install-dependency name="pre-commit"
+ pre-commit install \
+ --config scripts/config/pre-commit.yaml \
+ --install-hooks
+
+githooks-run: # Run git hooks configured in this repository @Operations
+ pre-commit run \
+ --config scripts/config/pre-commit.yaml \
+ --all-files
+
+_install-dependency: # Install asdf dependency - mandatory: name=[listed in the '.tool-versions' file]; optional: version=[if not listed]
+ echo ${name}
+ asdf plugin add ${name} ||:
+ asdf install ${name} $(or ${version},)
+
+_install-dependencies: # Install all the dependencies listed in .tool-versions
+ for plugin in $$(grep ^[a-z] .tool-versions | sed 's/[[:space:]].*//'); do
+ make _install-dependency name="$${plugin}"
+ done
+
+clean:: # Remove all generated and temporary files (common) @Operations
+ rm -rf \
+ .scannerwork \
+ *report*.json \
+ *report*json.zip \
+ docs/diagrams/.*.bkp \
+ docs/diagrams/.*.dtmp \
+ .version
+
+config:: # Configure development environment (common) @Configuration
+ make \
+ githooks-config
+
+help: # Print help @Others
+ printf "\nUsage: \033[3m\033[93m[arg1=val1] [arg2=val2] \033[0m\033[0m\033[32mmake\033[0m\033[34m \033[0m\n\n"
+ perl -e '$(HELP_SCRIPT)' $(MAKEFILE_LIST)
+
+list-variables: # List all the variables available to make @Others
+ $(foreach v, $(sort $(.VARIABLES)),
+ $(if $(filter-out default automatic, $(origin $v)),
+ $(if $(and $(patsubst %_PASSWORD,,$v), $(patsubst %_PASS,,$v), $(patsubst %_KEY,,$v), $(patsubst %_SECRET,,$v)),
+ $(info $v=$($v) ($(value $v)) [$(flavor $v),$(origin $v)]),
+ $(info $v=****** (******) [$(flavor $v),$(origin $v)])
+ )
+ )
+ )
+
+# ==============================================================================
+
+.DEFAULT_GOAL := help
+.EXPORT_ALL_VARIABLES:
+.NOTPARALLEL:
+.ONESHELL:
+.PHONY: * # Please do not change this line! The alternative usage of it introduces unnecessary complexity and is considered an anti-pattern.
+MAKEFLAGS := --no-print-director
+SHELL := /bin/bash
+ifeq (true, $(shell [[ "${VERBOSE}" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$$ ]] && echo true))
+ .SHELLFLAGS := -cex
+else
+ .SHELLFLAGS := -ce
+endif
+
+# This script parses all the make target descriptions and renders the help output.
+HELP_SCRIPT = \
+ \
+ use Text::Wrap; \
+ %help_info; \
+ my $$max_command_length = 0; \
+ my $$terminal_width = `tput cols` || 120; chomp($$terminal_width); \
+ \
+ while(<>){ \
+ next if /^_/; \
+ \
+ if (/^([\w-_]+)\s*:.*\#(.*?)(@(\w+))?\s*$$/) { \
+ my $$command = $$1; \
+ my $$description = $$2; \
+ $$description =~ s/@\w+//; \
+ my $$category_key = $$4 // 'Others'; \
+ (my $$category_name = $$category_key) =~ s/(?<=[a-z])([A-Z])/\ $$1/g; \
+ $$category_name = lc($$category_name); \
+ $$category_name =~ s/^(.)/\U$$1/; \
+ \
+ push @{$$help_info{$$category_name}}, [$$command, $$description]; \
+ $$max_command_length = (length($$command) > 37) ? 40 : $$max_command_length; \
+ } \
+ } \
+ \
+ my $$description_width = $$terminal_width - $$max_command_length - 4; \
+ $$Text::Wrap::columns = $$description_width; \
+ \
+ for my $$category (sort { $$a eq 'Others' ? 1 : $$b eq 'Others' ? -1 : $$a cmp $$b } keys %help_info) { \
+ print "\033[1m$$category\033[0m:\n\n"; \
+ for my $$item (sort { $$a->[0] cmp $$b->[0] } @{$$help_info{$$category}}) { \
+ my $$description = $$item->[1]; \
+ my @desc_lines = split("\n", wrap("", "", $$description)); \
+ my $$first_line_description = shift @desc_lines; \
+ \
+ $$first_line_description =~ s/(\w+)(\|\w+)?=/\033[3m\033[93m$$1$$2\033[0m=/g; \
+ \
+ my $$formatted_command = $$item->[0]; \
+ $$formatted_command = substr($$formatted_command, 0, 37) . "..." if length($$formatted_command) > 37; \
+ \
+ print sprintf(" \033[0m\033[34m%-$${max_command_length}s\033[0m%s %s\n", $$formatted_command, $$first_line_description); \
+ for my $$line (@desc_lines) { \
+ $$line =~ s/(\w+)(\|\w+)?=/\033[3m\033[93m$$1$$2\033[0m=/g; \
+ print sprintf(" %-$${max_command_length}s %s\n", " ", $$line); \
+ } \
+ print "\n"; \
+ } \
+ }
+
+# ==============================================================================
+
+${VERBOSE}.SILENT: \
+ _install-dependencies \
+ _install-dependency \
+ clean \
+ config \
+ githooks-config \
+ githooks-run \
+ help \
+ list-variables \
+ runner-act \
+ shellscript-lint-all \
+ version-create-effective-file \
diff --git a/scripts/reports/create-lines-of-code-report.sh b/scripts/reports/create-lines-of-code-report.sh
new file mode 100755
index 0000000..01645c7
--- /dev/null
+++ b/scripts/reports/create-lines-of-code-report.sh
@@ -0,0 +1,99 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Count lines of code of this repository. This is a gocloc command wrapper. It
+# will run gocloc natively if it is installed, otherwise it will run it in a
+# Docker container.
+#
+# Usage:
+# $ [options] ./create-lines-of-code-report.sh
+#
+# Options:
+# BUILD_DATETIME=%Y-%m-%dT%H:%M:%S%z # Build datetime, default is `date -u +'%Y-%m-%dT%H:%M:%S%z'`
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is `false`
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ create-report
+ enrich-report
+}
+
+function create-report() {
+
+ if command -v gocloc > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ run-gocloc-natively
+ else
+ run-gocloc-in-docker
+ fi
+ # shellcheck disable=SC2002
+ cat lines-of-code-report.tmp.json \
+ | jq -r '["Language","files","blank","comment","code"],["--------"],(.languages[]|[.name,.files,.blank,.comment,.code]),["-----"],(.total|["TOTAL",.files,.blank,.comment,.code])|@tsv' \
+ | sed 's/Plain Text/Plaintext/g' \
+ | column -t
+}
+
+function run-gocloc-natively() {
+
+ gocloc --output-type=json . > lines-of-code-report.tmp.json
+}
+
+function run-gocloc-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=ghcr.io/make-ops-tools/gocloc docker-get-image-version-and-pull)
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/workdir \
+ "$image" \
+ --output-type=json \
+ . \
+ > lines-of-code-report.tmp.json
+}
+
+function enrich-report() {
+
+ build_datetime=${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')}
+ git_url=$(git config --get remote.origin.url)
+ git_branch=$(git rev-parse --abbrev-ref HEAD)
+ git_commit_hash=$(git rev-parse HEAD)
+ git_tags=$(echo \""$(git tag | tr '\n' ',' | sed 's/,$//' | sed 's/,/","/g')"\" | sed 's/""//g')
+ pipeline_run_id=${GITHUB_RUN_ID:-0}
+ pipeline_run_number=${GITHUB_RUN_NUMBER:-0}
+ pipeline_run_attempt=${GITHUB_RUN_ATTEMPT:-0}
+
+ # shellcheck disable=SC2086
+ jq \
+ '.creationInfo |= . + {"created":"'${build_datetime}'","repository":{"url":"'${git_url}'","branch":"'${git_branch}'","tags":['${git_tags}'],"commitHash":"'${git_commit_hash}'"},"pipeline":{"id":'${pipeline_run_id}',"number":'${pipeline_run_number}',"attempt":'${pipeline_run_attempt}'}}' \
+ lines-of-code-report.tmp.json \
+ > lines-of-code-report.json
+ rm -f lines-of-code-report.tmp.json
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/reports/create-sbom-report.sh b/scripts/reports/create-sbom-report.sh
new file mode 100755
index 0000000..1ed735a
--- /dev/null
+++ b/scripts/reports/create-sbom-report.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Script to generate SBOM (Software Bill of Materials) for the repository
+# content and any artefact created by the CI/CD pipeline. This is a syft command
+# wrapper. It will run syft natively if it is installed, otherwise it will run
+# it in a Docker container.
+#
+# Usage:
+# $ [options] ./create-sbom-report.sh
+#
+# Options:
+# BUILD_DATETIME=%Y-%m-%dT%H:%M:%S%z # Build datetime, default is `date -u +'%Y-%m-%dT%H:%M:%S%z'`
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is `false`
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ create-report
+ enrich-report
+}
+
+function create-report() {
+
+ if command -v syft > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ run-syft-natively
+ else
+ run-syft-in-docker
+ fi
+}
+
+function run-syft-natively() {
+
+ syft packages dir:"$PWD" \
+ --config "$PWD/scripts/config/syft.yaml" \
+ --output spdx-json="$PWD/sbom-repository-report.tmp.json"
+}
+
+function run-syft-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=ghcr.io/anchore/syft docker-get-image-version-and-pull)
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/workdir \
+ "$image" \
+ packages dir:/workdir \
+ --config /workdir/scripts/config/syft.yaml \
+ --output spdx-json=/workdir/sbom-repository-report.tmp.json
+}
+
+function enrich-report() {
+
+ build_datetime=${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')}
+ git_url=$(git config --get remote.origin.url)
+ git_branch=$(git rev-parse --abbrev-ref HEAD)
+ git_commit_hash=$(git rev-parse HEAD)
+ git_tags=$(echo \""$(git tag | tr '\n' ',' | sed 's/,$//' | sed 's/,/","/g')"\" | sed 's/""//g')
+ pipeline_run_id=${GITHUB_RUN_ID:-0}
+ pipeline_run_number=${GITHUB_RUN_NUMBER:-0}
+ pipeline_run_attempt=${GITHUB_RUN_ATTEMPT:-0}
+
+ # shellcheck disable=SC2086
+ jq \
+ '.creationInfo |= . + {"created":"'${build_datetime}'","repository":{"url":"'${git_url}'","branch":"'${git_branch}'","tags":['${git_tags}'],"commitHash":"'${git_commit_hash}'"},"pipeline":{"id":'${pipeline_run_id}',"number":'${pipeline_run_number}',"attempt":'${pipeline_run_attempt}'}}' \
+ sbom-repository-report.tmp.json \
+ > sbom-repository-report.json
+ rm -f sbom-repository-report.tmp.json
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/reports/perform-static-analysis.sh b/scripts/reports/perform-static-analysis.sh
new file mode 100755
index 0000000..2426e6d
--- /dev/null
+++ b/scripts/reports/perform-static-analysis.sh
@@ -0,0 +1,80 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Script to perform static analysis of the repository content and upload the
+# report to SonarCloud.
+#
+# Usage:
+# $ [options] ./perform-static-analysis.sh
+#
+# Expects:
+# BRANCH_NAME=branch-name # Branch to report on
+# SONAR_ORGANISATION_KEY=org-key # SonarCloud organisation key
+# SONAR_PROJECT_KEY=project-key # SonarCloud project key
+# SONAR_TOKEN=token # SonarCloud token
+#
+# Options:
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is 'false'
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ if command -v sonar-scanner > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ run-sonar-scanner-natively
+ else
+ run-sonar-scanner-in-docker
+ fi
+}
+
+function run-sonar-scanner-natively() {
+
+ sonar-scanner \
+ -Dproject.settings="$PWD/scripts/config/sonar-scanner.properties" \
+ -Dsonar.branch.name="${BRANCH_NAME:-$(git rev-parse --abbrev-ref HEAD)}" \
+ -Dsonar.organization="$SONAR_ORGANISATION_KEY" \
+ -Dsonar.projectKey="$SONAR_PROJECT_KEY" \
+ -Dsonar.token="$SONAR_TOKEN"
+}
+
+function run-sonar-scanner-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=sonarsource/sonar-scanner-cli docker-get-image-version-and-pull)
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/usr/src \
+ "$image" \
+ -Dproject.settings=/usr/src/scripts/config/sonar-scanner.properties \
+ -Dsonar.branch.name="${BRANCH_NAME:-$(git rev-parse --abbrev-ref HEAD)}" \
+ -Dsonar.organization="$SONAR_ORGANISATION_KEY" \
+ -Dsonar.projectKey="$SONAR_PROJECT_KEY" \
+ -Dsonar.token="$SONAR_TOKEN"
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/reports/scan-vulnerabilities.sh b/scripts/reports/scan-vulnerabilities.sh
new file mode 100755
index 0000000..eb68d4b
--- /dev/null
+++ b/scripts/reports/scan-vulnerabilities.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+
+# WARNING: Please, DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Script to scan an SBOM file for CVEs (Common Vulnerabilities and Exposures).
+# This is a grype command wrapper. It will run grype natively if it is
+# installed, otherwise it will run it in a Docker container.
+#
+# Usage:
+# $ [options] ./scan-vulnerabilities.sh
+#
+# Options:
+# BUILD_DATETIME=%Y-%m-%dT%H:%M:%S%z # Build datetime, default is `date -u +'%Y-%m-%dT%H:%M:%S%z'`
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is `false`
+#
+# Depends on:
+# $ ./create-sbom-report.sh
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ create-report
+ enrich-report
+}
+
+function create-report() {
+
+ if command -v grype > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ run-grype-natively
+ else
+ run-grype-in-docker
+ fi
+}
+
+function run-grype-natively() {
+
+ grype \
+ sbom:"$PWD/sbom-repository-report.json" \
+ --config "$PWD/scripts/config/grype.yaml" \
+ --output json \
+ --file "$PWD/vulnerabilities-repository-report.tmp.json"
+}
+
+function run-grype-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=ghcr.io/anchore/grype docker-get-image-version-and-pull)
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/workdir \
+ --volume /tmp/grype/db:/.cache/grype/db \
+ "$image" \
+ sbom:/workdir/sbom-repository-report.json \
+ --config /workdir/scripts/config/grype.yaml \
+ --output json \
+ --file /workdir/vulnerabilities-repository-report.tmp.json
+}
+
+function enrich-report() {
+
+ build_datetime=${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')}
+ git_url=$(git config --get remote.origin.url)
+ git_branch=$(git rev-parse --abbrev-ref HEAD)
+ git_commit_hash=$(git rev-parse HEAD)
+ git_tags=$(echo \""$(git tag | tr '\n' ',' | sed 's/,$//' | sed 's/,/","/g')"\" | sed 's/""//g')
+ pipeline_run_id=${GITHUB_RUN_ID:-0}
+ pipeline_run_number=${GITHUB_RUN_NUMBER:-0}
+ pipeline_run_attempt=${GITHUB_RUN_ATTEMPT:-0}
+
+ # shellcheck disable=SC2086
+ jq \
+ '.creationInfo |= . + {"created":"'${build_datetime}'","repository":{"url":"'${git_url}'","branch":"'${git_branch}'","tags":['${git_tags}'],"commitHash":"'${git_commit_hash}'"},"pipeline":{"id":'${pipeline_run_id}',"number":'${pipeline_run_number}',"attempt":'${pipeline_run_attempt}'}}' \
+ vulnerabilities-repository-report.tmp.json \
+ > vulnerabilities-repository-report.json
+ rm -f vulnerabilities-repository-report.tmp.json
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/shellscript-linter.sh b/scripts/shellscript-linter.sh
new file mode 100755
index 0000000..8b3fe09
--- /dev/null
+++ b/scripts/shellscript-linter.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# ShellCheck command wrapper. It will run ShellCheck natively if it is
+# installed, otherwise it will run it in a Docker container.
+#
+# Usage:
+# $ [options] ./shellscript-linter.sh
+#
+# Arguments (provided as environment variables):
+# file=shellscript # Path to the shell script to lint, relative to the project's top-level directory, default is itself
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is 'false'
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ [ -z "${file:-}" ] && echo "WARNING: 'file' variable not set, defaulting to itself"
+ local file=${file:-scripts/shellscript-linter.sh}
+ if command -v shellcheck > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ file="$file" run-shellcheck-natively
+ else
+ file="$file" run-shellcheck-in-docker
+ fi
+}
+
+# Run ShellCheck natively.
+# Arguments (provided as environment variables):
+# file=[path to the shell script to lint, relative to the project's top-level directory]
+function run-shellcheck-natively() {
+
+ # shellcheck disable=SC2001
+ shellcheck "$(echo "$file" | sed "s#$PWD#.#")"
+}
+
+# Run ShellCheck in a Docker container.
+# Arguments (provided as environment variables):
+# file=[path to the shell script to lint, relative to the project's top-level directory]
+function run-shellcheck-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=koalaman/shellcheck docker-get-image-version-and-pull)
+ # shellcheck disable=SC2001
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD:/workdir" \
+ --workdir /workdir \
+ "$image" \
+ "/workdir/$(echo "$file" | sed "s#$PWD#.#")"
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/terraform/examples/terraform-state-aws-s3/.gitignore b/scripts/terraform/examples/terraform-state-aws-s3/.gitignore
new file mode 100644
index 0000000..c831140
--- /dev/null
+++ b/scripts/terraform/examples/terraform-state-aws-s3/.gitignore
@@ -0,0 +1,41 @@
+# Ignore the lock file as this is just an example
+.terraform.lock.hcl
+# Ignore Terraform plan
+*tfplan*
+
+# SEE: https://github.com/github/gitignore/blob/main/Terraform.gitignore
+
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Exclude all .tfvars files, which are likely to contain sensitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+*.tfvars
+*.tfvars.json
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
diff --git a/scripts/terraform/examples/terraform-state-aws-s3/main.tf b/scripts/terraform/examples/terraform-state-aws-s3/main.tf
new file mode 100644
index 0000000..a4ca5b0
--- /dev/null
+++ b/scripts/terraform/examples/terraform-state-aws-s3/main.tf
@@ -0,0 +1,46 @@
+resource "aws_s3_bucket" "terraform_state_store" {
+ bucket = var.terraform_state_bucket_name
+ lifecycle {
+ prevent_destroy = false // FIXME: Normally, this should be 'true' - this is just an example
+ }
+}
+
+resource "aws_s3_bucket_versioning" "enabled" {
+ bucket = aws_s3_bucket.terraform_state_store.id
+ versioning_configuration {
+ status = "Enabled"
+ }
+}
+
+resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
+ bucket = aws_s3_bucket.terraform_state_store.id
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "AES256"
+ }
+ }
+}
+
+resource "aws_s3_bucket_public_access_block" "public_access" {
+ bucket = aws_s3_bucket.terraform_state_store.id
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
+
+resource "aws_dynamodb_table" "dynamodb_terraform_state_lock" {
+ name = var.terraform_state_table_name
+ billing_mode = "PAY_PER_REQUEST"
+ hash_key = "LockID"
+ attribute {
+ name = "LockID"
+ type = "S"
+ }
+ server_side_encryption {
+ enabled = true
+ }
+ point_in_time_recovery {
+ enabled = true
+ }
+}
diff --git a/scripts/terraform/examples/terraform-state-aws-s3/provider.tf b/scripts/terraform/examples/terraform-state-aws-s3/provider.tf
new file mode 100644
index 0000000..b64be2a
--- /dev/null
+++ b/scripts/terraform/examples/terraform-state-aws-s3/provider.tf
@@ -0,0 +1,3 @@
+provider "aws" {
+ region = "eu-west-2"
+}
diff --git a/scripts/terraform/examples/terraform-state-aws-s3/variables.tf b/scripts/terraform/examples/terraform-state-aws-s3/variables.tf
new file mode 100644
index 0000000..07f60cb
--- /dev/null
+++ b/scripts/terraform/examples/terraform-state-aws-s3/variables.tf
@@ -0,0 +1,9 @@
+variable "terraform_state_bucket_name" {
+ description = "The S3 bucket name to store Terraform state"
+ default = "repository-template-example-terraform-state-store"
+}
+
+variable "terraform_state_table_name" {
+ description = "The DynamoDB table name to acquire Terraform lock"
+ default = "repository-template-example-terraform-state-lock"
+}
diff --git a/scripts/terraform/examples/terraform-state-aws-s3/versions.tf b/scripts/terraform/examples/terraform-state-aws-s3/versions.tf
new file mode 100644
index 0000000..18fd04a
--- /dev/null
+++ b/scripts/terraform/examples/terraform-state-aws-s3/versions.tf
@@ -0,0 +1,8 @@
+terraform {
+ required_version = ">= 1.5.0"
+ required_providers {
+ aws = {
+ version = ">= 5.14.0"
+ }
+ }
+}
diff --git a/scripts/terraform/terraform.lib.sh b/scripts/terraform/terraform.lib.sh
new file mode 100644
index 0000000..7793b9b
--- /dev/null
+++ b/scripts/terraform/terraform.lib.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# A set of Terraform functions written in Bash.
+#
+# Usage:
+# $ source ./terraform.lib.sh
+
+# ==============================================================================
+# Common Terraform functions.
+
+# Initialise Terraform.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+# opts=[options to pass to the Terraform init command, default is none/empty]
+function terraform-init() {
+
+ _terraform init # 'dir' and 'opts' are passed to the function as environment variables, if set
+}
+
+# Plan Terraform changes.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+# opts=[options to pass to the Terraform plan command, default is none/empty]
+function terraform-plan() {
+
+ _terraform plan # 'dir' and 'opts' are passed to the function as environment variables, if set
+}
+
+# Apply Terraform changes.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+# opts=[options to pass to the Terraform apply command, default is none/empty]
+function terraform-apply() {
+
+ _terraform apply # 'dir' and 'opts' are passed to the function as environment variables, if set
+}
+
+# Destroy Terraform resources.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+# opts=[options to pass to the Terraform destroy command, default is none/empty]
+function terraform-destroy() {
+
+ _terraform apply -destroy # 'dir' and 'opts' are passed to the function as environment variables, if set
+}
+
+# Format Terraform code.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+# opts=[options to pass to the Terraform fmt command, default is '-recursive']
+function terraform-fmt() {
+
+ _terraform fmt -recursive # 'dir' and 'opts' are passed to the function as environment variables, if set
+}
+
+# Validate Terraform code.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+# opts=[options to pass to the Terraform validate command, default is none/empty]
+function terraform-validate() {
+
+ _terraform validate # 'dir' and 'opts' are passed to the function as environment variables, if set
+}
+
+# shellcheck disable=SC2001,SC2155
+function _terraform() {
+
+ local dir="$(echo "${dir:-$PWD}" | sed "s#$PWD#.#")"
+ local cmd="-chdir=$dir $* ${opts:-}"
+ local project_dir="$(git rev-parse --show-toplevel)"
+
+ cmd="$cmd" "$project_dir/scripts/terraform/terraform.sh"
+}
+
+# Remove Terraform files.
+# Arguments (provided as environment variables):
+# dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is '.']
+function terraform-clean() {
+
+ (
+ cd "${dir:-$PWD}"
+ rm -rf \
+ .terraform \
+ terraform.log \
+ terraform.tfplan \
+ terraform.tfstate \
+ terraform.tfstate.backup
+ )
+}
diff --git a/scripts/terraform/terraform.mk b/scripts/terraform/terraform.mk
new file mode 100644
index 0000000..120a059
--- /dev/null
+++ b/scripts/terraform/terraform.mk
@@ -0,0 +1,96 @@
+# This file is for you! Edit it to implement your own Terraform make targets.
+
+# ==============================================================================
+# Custom implementation - implementation of a make target should not exceed 5 lines of effective code.
+# In most cases there should be no need to modify the existing make targets.
+
+terraform-init: # Initialise Terraform - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform init command, default is none/empty] @Development
+ make _terraform cmd="init" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+terraform-plan: # Plan Terraform changes - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform plan command, default is none/empty] @Development
+ make _terraform cmd="plan" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+terraform-apply: # Apply Terraform changes - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform apply command, default is none/empty] @Development
+ make _terraform cmd="apply" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+terraform-destroy: # Destroy Terraform resources - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform destroy command, default is none/empty] @Development
+ make _terraform \
+ cmd="destroy" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+terraform-fmt: # Format Terraform files - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform fmt command, default is '-recursive'] @Quality
+ make _terraform cmd="fmt" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+terraform-validate: # Validate Terraform configuration - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform validate command, default is none/empty] @Quality
+ make _terraform cmd="validate" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+clean:: # Remove Terraform files (terraform) - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set] @Operations
+ make _terraform cmd="clean" \
+ dir=$(or ${terraform_dir}, ${dir}) \
+ opts=$(or ${terraform_opts}, ${opts})
+
+_terraform: # Terraform command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], opts=[options to pass to the Terraform command, default is none/empty]
+ # 'TERRAFORM_STACK' is passed to the functions as environment variable
+ TERRAFORM_STACK=$(or ${TERRAFORM_STACK}, $(or ${terraform_stack}, $(or ${STACK}, $(or ${stack}, scripts/terraform/examples/terraform-state-aws-s3))))
+ dir=$(or ${dir}, ${TERRAFORM_STACK})
+ source scripts/terraform/terraform.lib.sh
+ terraform-${cmd} # 'dir' and 'opts' are accessible by the function as environment variables, if set
+
+# ==============================================================================
+# Quality checks - please DO NOT edit this section!
+
+terraform-shellscript-lint: # Lint all Terraform module shell scripts @Quality
+ for file in $$(find scripts/terraform -type f -name "*.sh"); do
+ file=$${file} scripts/shellscript-linter.sh
+ done
+
+# ==============================================================================
+# Module tests and examples - please DO NOT edit this section!
+
+terraform-example-provision-aws-infrastructure: # Provision example of AWS infrastructure @ExamplesAndTests
+ make terraform-init
+ make terraform-plan opts="-out=terraform.tfplan"
+ make terraform-apply opts="-auto-approve terraform.tfplan"
+
+terraform-example-destroy-aws-infrastructure: # Destroy example of AWS infrastructure @ExamplesAndTests
+ make terraform-destroy opts="-auto-approve"
+
+terraform-example-clean: # Remove Terraform example files @ExamplesAndTests
+ dir=$(or ${dir}, ${TERRAFORM_STACK})
+ source scripts/terraform/terraform.lib.sh
+ terraform-clean
+ rm -f ${TERRAFORM_STACK}/.terraform.lock.hcl
+
+# ==============================================================================
+# Configuration - please DO NOT edit this section!
+
+terraform-install: # Install Terraform @Installation
+ make _install-dependency name="terraform"
+
+# ==============================================================================
+
+${VERBOSE}.SILENT: \
+ _terraform \
+ clean \
+ terraform-apply \
+ terraform-destroy \
+ terraform-example-clean \
+ terraform-example-destroy-aws-infrastructure \
+ terraform-example-provision-aws-infrastructure \
+ terraform-fmt \
+ terraform-init \
+ terraform-install \
+ terraform-plan \
+ terraform-shellscript-lint \
+ terraform-validate \
diff --git a/scripts/terraform/terraform.sh b/scripts/terraform/terraform.sh
new file mode 100755
index 0000000..73f37c1
--- /dev/null
+++ b/scripts/terraform/terraform.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+set -euo pipefail
+
+# Terraform command wrapper. It will run the command natively if Terraform is
+# installed, otherwise it will run it in a Docker container.
+#
+# Usage:
+# $ [options] ./terraform.sh
+#
+# Options:
+# cmd=command # Terraform command to execute
+# FORCE_USE_DOCKER=true # If set to true the command is run in a Docker container, default is 'false'
+# VERBOSE=true # Show all the executed commands, default is 'false'
+
+# ==============================================================================
+
+function main() {
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ if command -v terraform > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then
+ # shellcheck disable=SC2154
+ cmd=$cmd run-terraform-natively
+ else
+ cmd=$cmd run-terraform-in-docker
+ fi
+}
+
+# Run Terraform natively.
+# Arguments (provided as environment variables):
+# cmd=[Terraform command to execute]
+function run-terraform-natively() {
+
+ # shellcheck disable=SC2086
+ terraform $cmd
+}
+
+# Run Terraform in a Docker container.
+# Arguments (provided as environment variables):
+# cmd=[Terraform command to execute]
+function run-terraform-in-docker() {
+
+ # shellcheck disable=SC1091
+ source ./scripts/docker/docker.lib.sh
+
+ # shellcheck disable=SC2155
+ local image=$(name=hashicorp/terraform docker-get-image-version-and-pull)
+ # shellcheck disable=SC2086
+ docker run --rm --platform linux/amd64 \
+ --volume "$PWD":/workdir \
+ --workdir /workdir \
+ "$image" \
+ $cmd
+}
+
+# ==============================================================================
+
+function is-arg-true() {
+
+ if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# ==============================================================================
+
+is-arg-true "${VERBOSE:-false}" && set -x
+
+main "$@"
+
+exit 0
diff --git a/scripts/tests/style.sh b/scripts/tests/style.sh
new file mode 100755
index 0000000..da042fa
--- /dev/null
+++ b/scripts/tests/style.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+set -euo pipefail
+
+cd "$(git rev-parse --show-toplevel)"
+
+
+# This file is for you! Edit it to call your prose style checker.
+# It's preconfigured to use `vale`, the same as the github action,
+# except that here it only checks unstaged files first, and only if
+# those files are OK does it then go on to check staged files. This
+# is to give you fast feedback on the changes you've most recently
+# made.
+
+check=working-tree-changes ./scripts/githooks/check-english-usage.sh && \
+ check=staged-changes ./scripts/githooks/check-english-usage.sh
diff --git a/scripts/tests/test.mk b/scripts/tests/test.mk
new file mode 100644
index 0000000..aab47c6
--- /dev/null
+++ b/scripts/tests/test.mk
@@ -0,0 +1,91 @@
+# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead.
+
+# The test types listed here are both those which might run both locally and in CI, or
+# in one but not the other. All of the test types listed at
+# https://github.com/NHSDigital/software-engineering-quality-framework/blob/main/quality-checks.md
+# should be represented here with the exception of:
+# - dependency scanning, which we expect to be applied at the repository level
+# - secret scanning, which we expect to be a pre-commit hook
+# - code review, which is outside the scope of automated testing for the moment
+
+test-unit: # Run your unit tests from scripts/test/unit @Testing
+ make _test name="unit"
+
+test-lint: # Lint your code from scripts/test/lint @Testing
+ make _test name="lint"
+
+test-coverage: # Evaluate code coverage from scripts/test/coverage @Testing
+ make _test name="coverage"
+
+test-accessibility: # Run your accessibility tests from scripts/test/accessibility @Testing
+ make _test name="accessibility"
+
+test-contract: # Run your contract tests from scripts/test/contract @Testing
+ make _test name="contract"
+
+test-integration: # Run your integration tests from scripts/test/integration @Testing
+ make _test name="integration"
+
+test-load: # Run all your load tests @Testing
+ make \
+ test-capacity \
+ test-soak \
+ test-response-time
+ # You may wish to add more here, depending on your app
+
+test-capacity: # Test what load level your app fails at from scripts/test/capacity @Testing
+ make _test name="capacity"
+
+test-soak: # Test that resources don't get exhausted over time from scripts/test/soak @Testing
+ make _test name="soak"
+
+test-response-time: # Test your API response times from scripts/test/response-time @Testing
+ make _test name="response-time"
+
+test-security: # Run your security tests from scripts/test/security @Testing
+ make _test name="security"
+
+test-ui: # Run your UI tests from scripts/test/ui @Testing
+ make _test name="ui"
+
+test-ui-performance: # Run UI render tests from scripts/test/ui-performance @Testing
+ make _test name="ui-performance"
+
+test: # Run all the test tasks @Testing
+ make \
+ test-unit \
+ test-lint \
+ test-coverage \
+ test-contract \
+ test-security \
+ test-ui \
+ test-ui-performance \
+ test-integration \
+ test-accessibility \
+ test-load
+
+_test:
+ set -e
+ script="./scripts/tests/${name}.sh"
+ if [ -e "$${script}" ]; then
+ exec $${script}
+ else
+ echo "make test-${name} not implemented: $${script} not found" >&2
+ fi
+
+${VERBOSE}.SILENT: \
+ _test \
+ test \
+ test-accessibility \
+ test-capacity \
+ test-contract \
+ test-coverage \
+ test-soak \
+ test-integration \
+ test-lint \
+ test-load \
+ test-response-time \
+ test-security \
+ test-ui \
+ test-ui-performance \
+ test-unit \
diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh
new file mode 100755
index 0000000..c589be5
--- /dev/null
+++ b/scripts/tests/unit.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -euo pipefail
+
+cd "$(git rev-parse --show-toplevel)"
+
+# This file is for you! Edit it to call your unit test suite. Note that the same
+# file will be called if you run it locally as if you run it on CI.
+
+# Replace the following line with something like:
+#
+# rails test:unit
+# python manage.py test
+# npm run test
+#
+# or whatever is appropriate to your project. You should *only* run your fast
+# 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."
diff --git a/tests/.gitkeep b/tests/.gitkeep
new file mode 100644
index 0000000..e69de29