diff --git a/MANIFEST.in b/MANIFEST.in index d56abe20b..a399bbdcc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ graft docs include *.rst global-exclude *.pyc include pyproject.toml +include resource-versions.json recursive-exclude news * exclude news exclude .coveragerc @@ -10,6 +11,7 @@ exclude .coveragerc exclude package.json exclude .prettierrc.json exclude Makefile +prune scripts exclude babel.config.js exclude webpack.config.js exclude pnpm-lock.yaml diff --git a/Makefile b/Makefile index 6f4c79e0f..7efbf636b 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,70 @@ BUNDLE_DIR = src/plone/staticresources/static/bundle-plone BOOTSTRAP_ICONS_DIR = src/plone/staticresources/static/icons-bootstrap -BOOTSTRAP_ICONS_BASENAME = bootstrap-icons-$(patsubst v%,%,$(BOOTSTRAP_ICONS_VERSION)) COUNTRY_FLAGS_DIR = src/plone/staticresources/static/icons-country-flags -# Run each recipe in one shell so an early exit skips its remaining commands. -# Keep stopping immediately when a command fails. -.ONESHELL: -.SHELLFLAGS = -ec - - -# Prerequisites: +# Prerequisites (checked by `make check-dependencies`): # - make # - curl # - git # - jq +# - tar +# - python3 (including the venv and ensurepip modules) +REQUIRED_COMMANDS = curl git jq tar python3 .PHONY: all all: update-mockup update-bootstrap-icons update-country-flags +# Show the versions of the installed resources. +.PHONY: versions +versions: + @jq . resource-versions.json + + .PHONY: clean clean: rm -Rf .venv -./.venv/bin/python install: - `which python3` -m venv .venv - ./.venv/bin/pip install lxml +# Check that all commands required by the update scripts are available and +# print installation hints for the missing ones. +.PHONY: check-dependencies +check-dependencies: + @missing=""; \ + for cmd in $(REQUIRED_COMMANDS); do \ + command -v "$$cmd" > /dev/null 2>&1 || missing="$$missing $$cmd"; \ + done; \ + if [ -n "$$missing" ]; then \ + echo "❌ Missing required commands:$$missing" >&2; \ + echo " Install them with your package manager, e.g.:" >&2; \ + echo " macOS (Homebrew): brew install$$missing" >&2; \ + echo " Debian/Ubuntu: sudo apt install$$missing" >&2; \ + echo " Fedora: sudo dnf install$$missing" >&2; \ + exit 1; \ + fi; \ + if ! python3 -c "import venv, ensurepip" > /dev/null 2>&1; then \ + echo "❌ python3 cannot create virtual environments (venv/ensurepip missing)." >&2; \ + echo " Debian/Ubuntu: sudo apt install python3-venv" >&2; \ + exit 1; \ + fi + + +# Create the virtual environment only if it is missing or broken, e.g. when it +# was created by a Python interpreter which no longer exists. `--clear` avoids +# mixing different Python versions in the same virtual environment. +.PHONY: install +install: check-dependencies + @if ! ./.venv/bin/python -c "import lxml" > /dev/null 2>&1 \ + || ! ./.venv/bin/towncrier --version > /dev/null 2>&1; then \ + echo "🐍 Create virtual environment in .venv with `command -v python3`."; \ + python3 -m venv --clear .venv \ + && ./.venv/bin/pip install --quiet lxml towncrier; \ + fi -# Download a Mockup universal bundle from GitHub releases and replace -# the existing one. +# Download the Mockup universal bundle with the npm dist-tag `latest` from the +# npm registry and replace the existing one. # # You can use the `MOCKUP_VERSION` environment variable to download a # specific version, e.g. a pre-release version which would not be picked up @@ -39,168 +72,38 @@ clean: # `MOCKUP_VERSION=5.7.0-alpha.2 make update-mockup` # .PHONY: update-mockup -update-mockup: -ifndef MOCKUP_VERSION - @echo "🧪 Get the latest Mockup version from GitHub (no pre-release)." - - @# If no MOCKUP_VERSION environment variable is defined, - @# Get the latest version from the GitHub API. - # Also see: https://stackoverflow.com/a/42040905/1337474 - $(eval MOCKUP_VERSION := $(shell curl https://api.github.com/repos/plone/mockup/releases/latest -s | jq .tag_name -r)) - @echo "🏷️ Mockup version is: $(MOCKUP_VERSION)" - -endif - @echo "🧪 Copy bundle from GitHub." - - @# Download the Mockup bundle. - wget https://github.com/plone/mockup/releases/download/$(MOCKUP_VERSION)/mockup-bundle-$(MOCKUP_VERSION).zip 1> /dev/null 2> /dev/null - @unzip mockup-bundle-$(MOCKUP_VERSION).zip > /dev/null - @# Replace the old Mockup bundle with the new one. - @rm -Rf $(BUNDLE_DIR) - @mv mockup-bundle-$(MOCKUP_VERSION) $(BUNDLE_DIR) - @# Cleanup. - @rm mockup-bundle-$(MOCKUP_VERSION).zip - - @echo "🧪 Check for resource changes." - @git add $(BUNDLE_DIR) - @if git diff --cached --quiet -- $(BUNDLE_DIR); then - echo "ℹ️ Mockup is unchanged; no changelog entry or commit created." - exit 0 - fi - - @echo "🧪 Git add and commit." - @# Add changelog entry - @towncrier create +update-mockup.feature \ - --content "Update Mockup to $(MOCKUP_VERSION)." - @# Add with a `*` in case a number was appended due to a naming conflict. - @git add news/+update-mockup.feature* +update-mockup: install + @sh scripts/update_mockup.sh "$(BUNDLE_DIR)" "$(MOCKUP_VERSION)" latest - @# commit - @git commit -m"Update Mockup to $(MOCKUP_VERSION)." > /dev/null - @# Spit out info. - @echo "" - @echo "📦 Mockup static folder size is: " - @cd $(BUNDLE_DIR) && du -sh - @echo "" - @echo "🚀 Updated Mockup to $(MOCKUP_VERSION)." - @echo "" +# Download the newest Mockup pre-release from the npm registry: the highest +# version among the dist-tags `alpha`, `beta` and `rc`, which is higher than +# `latest`. Use `MOCKUP_VERSION` to select a specific version instead. +.PHONY: update-mockup-prerelease +update-mockup-prerelease: install + @sh scripts/update_mockup.sh "$(BUNDLE_DIR)" "$(MOCKUP_VERSION)" prerelease -# Download bootstrap-icons from GitHub releases and replace the existing one. +# Download bootstrap-icons from the npm registry and replace the existing one. +# Only the SVG icons are copied. # # You can use the `BOOTSTRAP_ICONS_VERSION` environment variable to download a # specific version, e.g. a pre-release version which would not be picked up # automatically: -# `BOOTSTRAP_ICONS_VERSION=5.7.0-alpha.2 make update-bootstrap-icons` +# `BOOTSTRAP_ICONS_VERSION=1.13.1 make update-bootstrap-icons` # .PHONY: update-bootstrap-icons update-bootstrap-icons: install -ifndef BOOTSTRAP_ICONS_VERSION - @echo "🧪 Get the latest Bootstrap Icons version from GitHub (no pre-release)." - - @# If no BOOTSTRAP_ICONS_VERSION environment variable is defined, - @# Get the latest version from the GitHub API. - # Also see: https://stackoverflow.com/a/42040905/1337474 - $(eval BOOTSTRAP_ICONS_VERSION := $(shell curl https://api.github.com/repos/twbs/icons/releases/latest -s | jq .tag_name -r)) - @echo "🏷️ Bootstrap Icons version is: $(BOOTSTRAP_ICONS_VERSION)" - -endif - @echo "🧪 Copy bootstrap-icons from GitHub." - - @# Download the Bootstrap Icons bundle. - wget https://github.com/twbs/icons/releases/download/$(BOOTSTRAP_ICONS_VERSION)/$(BOOTSTRAP_ICONS_BASENAME).zip 1> /dev/null 2> /dev/null - @unzip $(BOOTSTRAP_ICONS_BASENAME).zip > /dev/null - @# Replace the old Bootstrap Icons bundle with the new one. - @rm -Rf $(BOOTSTRAP_ICONS_DIR) - @mv $(BOOTSTRAP_ICONS_BASENAME) $(BOOTSTRAP_ICONS_DIR) - @# Cleanup. - @rm $(BOOTSTRAP_ICONS_BASENAME).zip - - @echo "✳️ Register bootstrap icons" - ./.venv/bin/python src/plone/staticresources/_scripts/register_icons.py - ./.venv/bin/python src/plone/staticresources/_scripts/iconmap_json.py - - @echo "🧪 Check for resource changes." - @git add $(BOOTSTRAP_ICONS_DIR) - @git add src/plone/staticresources/profiles/default/registry/icons_bootstrap.xml - @git add src/plone/staticresources/static/iconmap.json - @if git diff --cached --quiet -- $(BOOTSTRAP_ICONS_DIR) \ - src/plone/staticresources/profiles/default/registry/icons_bootstrap.xml \ - src/plone/staticresources/static/iconmap.json; then - echo "ℹ️ Bootstrap Icons are unchanged; no changelog entry or commit created." - exit 0 - fi - - @echo "🧪 Git add and commit." - @# Add changelog entry - @towncrier create +update-bootstrap-icons.feature \ - --content "Update Bootstrap Icons to $(BOOTSTRAP_ICONS_VERSION)." - @# Add with a `*` in case a number was appended due to a naming conflict. - @git add news/+update-bootstrap-icons.feature* + @sh scripts/update_bootstrap_icons.sh "$(BOOTSTRAP_ICONS_DIR)" "$(BOOTSTRAP_ICONS_VERSION)" - @# commit - @git commit -m"Update Bootstrap Icons to $(BOOTSTRAP_ICONS_VERSION)." > /dev/null - @# Spit out info. - @echo "" - @echo "📦 Bootstrap Icons static folder size is: " - @cd $(BOOTSTRAP_ICONS_DIR) && du -sh - @echo "" - @echo "🚀 Updated Bootstrap Icons to $(BOOTSTRAP_ICONS_VERSION)." - @echo "‼️ Don't forget to create an upgrade step for the new icons (see upgrade 217 for an example) ‼️" - @echo "" - - -# Download country-flags from GitHub and replace the existing one. +# Download country-flags from the main branch on GitHub and replace the +# existing one. # -# Country flags have no releases, so resolve main to an exact commit. +# Country flags have no releases and the npm package is outdated, so we cannot +# download a specific version. +# Anyways, the package.json file include a version specifier, which is used for +# the changelog entry and commit message. .PHONY: update-country-flags update-country-flags: install - @echo "🧪 Copy country-flags from GitHub." - - @# Get the commit hash and download that exact revision. - @COUNTRY_FLAGS_REVISION=$$(curl -fsSL https://api.github.com/repos/hampusborgos/country-flags/commits/main | jq -er '.sha | strings | select(test("^[0-9a-f]{40}$$"))') - wget https://github.com/hampusborgos/country-flags/archive/$$COUNTRY_FLAGS_REVISION.zip -O country-flags.zip 1> /dev/null 2> /dev/null - @unzip country-flags.zip > /dev/null - @# Replace the old country flags with the new ones. - @rm -Rf $(COUNTRY_FLAGS_DIR) - @mv country-flags-$$COUNTRY_FLAGS_REVISION $(COUNTRY_FLAGS_DIR) - @# Cleanup. - @rm country-flags.zip - - @echo "🔰 Register flag icons" - ./.venv/bin/python src/plone/staticresources/_scripts/register_flag_icons.py - ./.venv/bin/python src/plone/staticresources/_scripts/iconmap_json.py - - @echo "🧪 Check for resource changes." - @git add $(COUNTRY_FLAGS_DIR) - @git add src/plone/staticresources/profiles/default/registry/icons_country_flags.xml - @git add src/plone/staticresources/profiles/default/registry/icons_language_flags.xml - @git add src/plone/staticresources/static/iconmap.json - @if git diff --cached --quiet -- $(COUNTRY_FLAGS_DIR) \ - src/plone/staticresources/profiles/default/registry/icons_country_flags.xml \ - src/plone/staticresources/profiles/default/registry/icons_language_flags.xml \ - src/plone/staticresources/static/iconmap.json; then - echo "ℹ️ Country flags are unchanged; no changelog entry or commit created." - exit 0 - fi - - @echo "🧪 Git add and commit." - @# Add changelog entry - @towncrier create +update-country-flags.feature \ - --content "Update country flags icons to commit $$COUNTRY_FLAGS_REVISION." - @# Add with a `*` in case a number was appended due to a naming conflict. - @git add news/+update-country-flags.feature* - - @# commit - @git commit -m"Update country flags icons." > /dev/null - - @# Spit out info. - @echo "" - @echo "📦 Country flags icons static folder size is: " - @cd $(COUNTRY_FLAGS_DIR) && du -sh - @echo "" - @echo "🚀 Updated country flags icons." - @echo "‼️ Don't forget to create an upgrade step for the new icons (see upgrade 217 for an example) ‼️" - @echo "" + @sh scripts/update_country_flags.sh "$(COUNTRY_FLAGS_DIR)" diff --git a/README.rst b/README.rst index 3179e4e67..8d3c8e4eb 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,23 @@ Note on Version 3.x: Version 3.0.0 switched to a PEP 420 native namespace package and requires ``Plone>=6.2`` and Python 3.10 or later. +Prerequisites +------------- + +The resource update targets below work on Linux and macOS. Install the +following tools and make sure they are available on your ``PATH``: + +- curl +- git +- jq +- make +- Python 3 with venv support (on Debian/Ubuntu install ``python3-venv``). +- tar + +Run ``make check-dependencies`` to verify them; the update targets run this +check automatically and list any missing commands with installation hints. + + How to upgrade the resources in this package -------------------------------------------- @@ -36,6 +53,29 @@ Run:: This downloads and installs the latest versions of these packages. The commits and changelog entries are automatically made. +Mockup (``@plone/mockup``) and Bootstrap Icons (``bootstrap-icons``) are +downloaded from the npm registry. ``make update-mockup`` installs the version +with the npm dist-tag ``latest``. +Country flags are downloaded from the ``main`` branch of the GitHub repository, +because the npm package is outdated. +None of the targets use the rate limited GitHub API. + +Run ``make update-mockup-prerelease`` to install the newest Mockup pre-release: +the highest version among the npm dist-tags ``alpha``, ``beta`` and ``rc``, +which is higher than ``latest``. Versions are compared by semver, so stale +pre-release tags of older release lines are ignored. +An explicit ``MOCKUP_VERSION`` overrides automatic selection for either target. + +Run ``make -k all`` to update all resources and continue with the other +updates if one fails. The command still reports failure if any update fails. + +The update targets invoke POSIX shell scripts in ``scripts/`` from the +repository root. + +The versions of the installed resources are recorded in +``resource-versions.json`` and updated together with the resources. +Run ``make versions`` to show them. + See the Makefile for more information. Then submit a Pull Request and run the tests on Jenkins. diff --git a/news/+country-flags-version.bugfix.rst b/news/+country-flags-version.bugfix.rst new file mode 100644 index 000000000..99e4e690b --- /dev/null +++ b/news/+country-flags-version.bugfix.rst @@ -0,0 +1 @@ +Makefile update-country-flags: Use package version for commit message and changelog entry. diff --git a/news/+ignore-unnecessary.feature.rst b/news/+ignore-unnecessary.feature.rst new file mode 100644 index 000000000..f32ae5230 --- /dev/null +++ b/news/+ignore-unnecessary.feature.rst @@ -0,0 +1,6 @@ +Ignore unnecessary resources from original icon packages. + +When updating icons with update-country-flags some resources (.gitignore files, +scripts) are installed, which we do not use nor want. These are git-ignored and +not added to the repository. update-bootstrap-icons only copies the SVG icons, +so the unused CSS and SCSS files and fonts are not installed at all. diff --git a/news/+make-compatibility.bugfix.rst b/news/+make-compatibility.bugfix.rst new file mode 100644 index 000000000..fc028bdf8 --- /dev/null +++ b/news/+make-compatibility.bugfix.rst @@ -0,0 +1,16 @@ +Makefile: Improve compatibility with macOS. + +The previous Makefile used `.ONESHELL` and `.SHELLFLAGS`, features of GNU make +3.82. macOS ships with GNU make 3.81 and those features are not available. For +better compatibility (and maybe even maintainability), move the shell commands +into their own shell scripts. + +Download Mockup and Bootstrap Icons from the npm registry instead of GitHub +releases. `make update-mockup` uses the npm dist-tag `latest`, +`make update-mockup-prerelease` the newest pre-release among the dist-tags +`alpha`, `beta` and `rc`. The Mockup update fails if the npm package contains +no built bundle. Country flags are downloaded as a tarball of the `main` branch, +which is resolved via `git ls-remote`. The update scripts do not use the rate +limited GitHub API, `wget` or `unzip` anymore. + +@thet @petschki diff --git a/news/+towncrier-venv.bugfix.rst b/news/+towncrier-venv.bugfix.rst new file mode 100644 index 000000000..15dca9c1c --- /dev/null +++ b/news/+towncrier-venv.bugfix.rst @@ -0,0 +1,5 @@ +Makefile: Install towncrier in the venv and reuse it. + +This removes the need to have towncrier installed globally. + +@thet diff --git a/news/+update-mockup-prerelease.feature.rst b/news/+update-mockup-prerelease.feature.rst new file mode 100644 index 000000000..c9e749628 --- /dev/null +++ b/news/+update-mockup-prerelease.feature.rst @@ -0,0 +1 @@ +Makefile: Add update-mockup-prerelease target for the newest Mockup pre-release (npm dist-tag `alpha`, `beta` or `rc`). @thet diff --git a/resource-versions.json b/resource-versions.json new file mode 100644 index 000000000..c48af8898 --- /dev/null +++ b/resource-versions.json @@ -0,0 +1,15 @@ +{ + "bootstrap-icons": { + "source": "https://www.npmjs.com/package/bootstrap-icons/v/1.13.1", + "version": "1.13.1" + }, + "country-flags": { + "revision": "c09927e63705529bbf59ca6684cd9b23225dddad", + "source": "https://github.com/hampusborgos/country-flags/tree/c09927e63705529bbf59ca6684cd9b23225dddad", + "version": "1.2.10" + }, + "mockup": { + "source": "https://www.npmjs.com/package/@plone/mockup/v/5.7.0-alpha.3", + "version": "5.7.0-alpha.3" + } +} diff --git a/scripts/npm.sh b/scripts/npm.sh new file mode 100644 index 000000000..585786e50 --- /dev/null +++ b/scripts/npm.sh @@ -0,0 +1,67 @@ +# Helpers for downloading packages from the npm registry. +# Source this file from the update scripts; requires curl, jq and tar. + +NPM_REGISTRY=https://registry.npmjs.org + +# npm_fetch PACKAGE REF DEST_DIR +# +# Resolve REF (a dist-tag like `latest` or `alpha`, or an exact version) and +# extract the package tarball into DEST_DIR. The package contents end up in +# DEST_DIR/package. Sets NPM_VERSION to the resolved version. +npm_fetch() { + npm_package=$1 + npm_ref=$2 + npm_dest=$3 + + if ! npm_meta=$(curl -fsSL "${NPM_REGISTRY}/${npm_package}/${npm_ref}"); then + echo "❌ Cannot resolve '${npm_ref}' for ${npm_package} on ${NPM_REGISTRY}." >&2 + echo " Check that the version or dist-tag exists: ${NPM_REGISTRY}/${npm_package}" >&2 + exit 1 + fi + NPM_VERSION=$(printf '%s' "$npm_meta" | jq -er '.version | strings | select(length > 0)') + npm_tarball=$(printf '%s' "$npm_meta" | jq -er '.dist.tarball | strings | select(length > 0)') + + if ! curl -fsSL "$npm_tarball" | tar -xzf - -C "$npm_dest"; then + echo "❌ Cannot download or extract ${npm_tarball}." >&2 + exit 1 + fi +} + +# npm_prerelease PACKAGE +# +# Find the newest pre-release among the dist-tags `alpha`, `beta` and `rc`. +# Only versions higher than `latest` are considered, so stale pre-release tags +# of older release lines are ignored. Versions are compared by semver, not by +# publish date, as maintenance releases may be published after pre-releases. +# Sets NPM_PRERELEASE_TAG and NPM_PRERELEASE_VERSION; returns 1 if there is no +# such pre-release. +npm_prerelease() { + npm_package=$1 + + if ! npm_meta=$(curl -fsSL -H "Accept: application/vnd.npm.install-v1+json" \ + "${NPM_REGISTRY}/${npm_package}"); then + echo "❌ Cannot get the dist-tags of ${npm_package} from ${NPM_REGISTRY}." >&2 + exit 1 + fi + npm_prerelease=$(printf '%s' "$npm_meta" | jq -r ' + # Semver sort key: releases sort after their pre-releases, numeric + # identifiers before alphanumeric ones (numbers < strings < objects). + def semver_key: + sub("\\+.*$"; "") + | capture("^(?
[0-9]+\\.[0-9]+\\.[0-9]+)(-(?
.*))?$")
+            | [(.main | split(".") | map(tonumber)),
+               (if .pre then .pre | split(".") | map(tonumber? // .) else [{}] end)];
+        ."dist-tags" as $tags
+        | ($tags.latest | semver_key) as $latest
+        | [$tags | to_entries[]
+           | select(.key == "alpha" or .key == "beta" or .key == "rc")
+           | select((.value | semver_key) > $latest)]
+        | if length == 0 then empty else max_by(.value | semver_key) | "\(.key) \(.value)" end
+    ')
+    if [ -z "$npm_prerelease" ]; then
+        echo "❌ No ${npm_package} pre-release (dist-tag alpha, beta or rc) newer than latest found." >&2
+        return 1
+    fi
+    NPM_PRERELEASE_TAG=${npm_prerelease%% *}
+    NPM_PRERELEASE_VERSION=${npm_prerelease#* }
+}
diff --git a/scripts/update_bootstrap_icons.sh b/scripts/update_bootstrap_icons.sh
new file mode 100644
index 000000000..25e83cca4
--- /dev/null
+++ b/scripts/update_bootstrap_icons.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+# Invoked by make from the repository root.
+set -e
+
+BOOTSTRAP_ICONS_DIR=${1:?BOOTSTRAP_ICONS_DIR is required}
+BOOTSTRAP_ICONS_VERSION=${2:-}
+
+. "$(dirname "$0")/npm.sh"
+. "$(dirname "$0")/version_info.sh"
+
+TMP_DIR=$(mktemp -d)
+trap 'rm -Rf "$TMP_DIR"' EXIT
+
+if [ -n "${BOOTSTRAP_ICONS_VERSION}" ]; then
+    # Accept GitHub tag names like `v1.13.1`, too.
+    echo "🧪 Download Bootstrap Icons ${BOOTSTRAP_ICONS_VERSION#v} from npm."
+    npm_fetch bootstrap-icons "${BOOTSTRAP_ICONS_VERSION#v}" "$TMP_DIR"
+else
+    echo "🧪 Download the latest Bootstrap Icons from npm (no pre-release)."
+    npm_fetch bootstrap-icons latest "$TMP_DIR"
+fi
+BOOTSTRAP_ICONS_VERSION=$NPM_VERSION
+echo "🏷️  Bootstrap Icons version is: ${BOOTSTRAP_ICONS_VERSION}"
+
+# Replace the old Bootstrap Icons with the new ones. Only the SVG icons are
+# used; the sprite and the icon font are not copied.
+rm -Rf "${BOOTSTRAP_ICONS_DIR}"
+mv "$TMP_DIR/package/icons" "${BOOTSTRAP_ICONS_DIR}"
+
+echo "✳️ Register bootstrap icons"
+./.venv/bin/python src/plone/staticresources/_scripts/register_icons.py
+./.venv/bin/python src/plone/staticresources/_scripts/iconmap_json.py
+
+echo "🧪 Check for resource changes."
+git add "${BOOTSTRAP_ICONS_DIR}"
+git add src/plone/staticresources/profiles/default/registry/icons_bootstrap.xml
+git add src/plone/staticresources/static/iconmap.json
+if git diff --cached --quiet -- "${BOOTSTRAP_ICONS_DIR}" \
+    src/plone/staticresources/profiles/default/registry/icons_bootstrap.xml \
+    src/plone/staticresources/static/iconmap.json; then
+    echo "ℹ️ Bootstrap Icons are unchanged; no changelog entry or commit created."
+    exit 0
+fi
+
+echo "🧪 Git add and commit."
+set_version_info bootstrap-icons "$(jq -n --arg version "$BOOTSTRAP_ICONS_VERSION" '{
+    version: $version,
+    source: "https://www.npmjs.com/package/bootstrap-icons/v/\($version)"
+}')"
+# Add changelog entry
+./.venv/bin/towncrier create +update-bootstrap-icons.feature \
+    --content "Update Bootstrap Icons to ${BOOTSTRAP_ICONS_VERSION}."
+# Add with a `*` in case a number was appended due to a naming conflict.
+git add news/+update-bootstrap-icons.feature*
+
+# commit
+git commit -m"Update Bootstrap Icons to ${BOOTSTRAP_ICONS_VERSION}." > /dev/null
+
+# Spit out info.
+echo ""
+echo "📦 Bootstrap Icons static folder size is: "
+cd "${BOOTSTRAP_ICONS_DIR}"
+du -sh
+echo ""
+echo "🚀 Updated Bootstrap Icons to ${BOOTSTRAP_ICONS_VERSION}."
+echo "‼️ Don't forget to create an upgrade step for the new icons (see upgrade 217 for an example) ‼️"
+echo ""
diff --git a/scripts/update_country_flags.sh b/scripts/update_country_flags.sh
new file mode 100644
index 000000000..509eaa06b
--- /dev/null
+++ b/scripts/update_country_flags.sh
@@ -0,0 +1,81 @@
+#!/bin/sh
+# Invoked by make from the repository root.
+set -e
+
+COUNTRY_FLAGS_DIR=${1:?COUNTRY_FLAGS_DIR is required}
+
+COUNTRY_FLAGS_REPO=https://github.com/hampusborgos/country-flags
+
+. "$(dirname "$0")/version_info.sh"
+
+TMP_DIR=$(mktemp -d)
+trap 'rm -Rf "$TMP_DIR"' EXIT
+
+echo "🧪 Copy country-flags from GitHub."
+
+# The npm package svg-country-flags is outdated, so get the commit hash of the
+# main branch (without using the rate limited GitHub API) and download that
+# exact revision.
+COUNTRY_FLAGS_REVISION=$(git ls-remote "$COUNTRY_FLAGS_REPO" refs/heads/main | cut -f1)
+if ! printf '%s' "$COUNTRY_FLAGS_REVISION" | grep -Eq '^[0-9a-f]{40}$'; then
+    echo "❌ Cannot get the main branch revision of ${COUNTRY_FLAGS_REPO}." >&2
+    exit 1
+fi
+if ! curl -fsSL "${COUNTRY_FLAGS_REPO}/archive/${COUNTRY_FLAGS_REVISION}.tar.gz" \
+    | tar -xzf - -C "$TMP_DIR"; then
+    echo "❌ Cannot download or extract country-flags ${COUNTRY_FLAGS_REVISION}." >&2
+    exit 1
+fi
+COUNTRY_FLAGS_SRC="$TMP_DIR/country-flags-${COUNTRY_FLAGS_REVISION}"
+# Use the package version for the changelog and commit message.
+COUNTRY_FLAGS_VERSION=$(jq -er '.version | strings | select(length > 0)' "${COUNTRY_FLAGS_SRC}/package.json")
+echo "🏷️  Country flags version is: ${COUNTRY_FLAGS_VERSION}"
+# Replace the old country flags with the new ones.
+rm -Rf "${COUNTRY_FLAGS_DIR}"
+mv "${COUNTRY_FLAGS_SRC}" "${COUNTRY_FLAGS_DIR}"
+
+echo "🔰 Register flag icons"
+./.venv/bin/python src/plone/staticresources/_scripts/register_flag_icons.py
+./.venv/bin/python src/plone/staticresources/_scripts/iconmap_json.py
+
+echo "🧪 Check for resource changes."
+git add "${COUNTRY_FLAGS_DIR}"
+git add src/plone/staticresources/profiles/default/registry/icons_country_flags.xml
+git add src/plone/staticresources/profiles/default/registry/icons_language_flags.xml
+git add src/plone/staticresources/static/iconmap.json
+if git diff --cached --quiet -- "${COUNTRY_FLAGS_DIR}" \
+    src/plone/staticresources/profiles/default/registry/icons_country_flags.xml \
+    src/plone/staticresources/profiles/default/registry/icons_language_flags.xml \
+    src/plone/staticresources/static/iconmap.json; then
+    echo "ℹ️ Country flags are unchanged; no changelog entry or commit created."
+    exit 0
+fi
+
+echo "🧪 Git add and commit."
+# The upstream version is rarely bumped, so also record the exact revision.
+set_version_info country-flags "$(jq -n \
+    --arg version "$COUNTRY_FLAGS_VERSION" \
+    --arg revision "$COUNTRY_FLAGS_REVISION" \
+    --arg repo "$COUNTRY_FLAGS_REPO" '{
+    version: $version,
+    revision: $revision,
+    source: "\($repo)/tree/\($revision)"
+}')"
+# Add changelog entry
+./.venv/bin/towncrier create +update-country-flags.feature \
+    --content "Update country flags icons to ${COUNTRY_FLAGS_VERSION}."
+# Add with a `*` in case a number was appended due to a naming conflict.
+git add news/+update-country-flags.feature*
+
+# commit
+git commit -m"Update country flags icons to ${COUNTRY_FLAGS_VERSION}." > /dev/null
+
+# Spit out info.
+echo ""
+echo "📦 Country flags icons static folder size is: "
+cd "${COUNTRY_FLAGS_DIR}"
+du -sh
+echo ""
+echo "🚀 Updated country flags icons to ${COUNTRY_FLAGS_VERSION}."
+echo "‼️ Don't forget to create an upgrade step for the new icons (see upgrade 217 for an example) ‼️"
+echo ""
diff --git a/scripts/update_mockup.sh b/scripts/update_mockup.sh
new file mode 100644
index 000000000..283df72f9
--- /dev/null
+++ b/scripts/update_mockup.sh
@@ -0,0 +1,81 @@
+#!/bin/sh
+# Invoked by make from the repository root.
+set -e
+
+BUNDLE_DIR=${1:?BUNDLE_DIR is required}
+MOCKUP_VERSION=${2:-}
+# `latest` for the latest stable release or `prerelease` for the newest
+# pre-release. Ignored if MOCKUP_VERSION is set.
+MOCKUP_CHANNEL=${3:-latest}
+
+. "$(dirname "$0")/npm.sh"
+. "$(dirname "$0")/version_info.sh"
+
+TMP_DIR=$(mktemp -d)
+trap 'rm -Rf "$TMP_DIR"' EXIT
+
+if [ -n "${MOCKUP_VERSION}" ]; then
+    echo "🧪 Download Mockup ${MOCKUP_VERSION} from npm."
+    npm_fetch @plone/mockup "${MOCKUP_VERSION}" "$TMP_DIR"
+elif [ "${MOCKUP_CHANNEL}" = prerelease ]; then
+    echo "🧪 Find the newest Mockup pre-release on npm."
+    if ! npm_prerelease @plone/mockup; then
+        echo "   Use e.g. MOCKUP_VERSION=5.7.0-alpha.3 to select a specific version." >&2
+        exit 1
+    fi
+    echo "🧪 Download Mockup ${NPM_PRERELEASE_VERSION} (dist-tag '${NPM_PRERELEASE_TAG}') from npm."
+    npm_fetch @plone/mockup "${NPM_PRERELEASE_VERSION}" "$TMP_DIR"
+else
+    echo "🧪 Download the latest Mockup release from npm."
+    npm_fetch @plone/mockup latest "$TMP_DIR"
+fi
+MOCKUP_VERSION=$NPM_VERSION
+echo "🏷️  Mockup version is: ${MOCKUP_VERSION}"
+
+# The npm package also contains the sources. Make sure it contains the webpack
+# built bundle, which is registered in the resource registry.
+MOCKUP_DIST="$TMP_DIR/package/dist"
+for file in bundle.min.js remote.min.js; do
+    if [ ! -s "${MOCKUP_DIST}/${file}" ]; then
+        echo "❌ The npm package of Mockup ${MOCKUP_VERSION} contains no built bundle (dist/${file} is missing or empty)." >&2
+        exit 1
+    fi
+done
+if [ -z "$(ls -A "${MOCKUP_DIST}/chunks" 2> /dev/null)" ]; then
+    echo "❌ The npm package of Mockup ${MOCKUP_VERSION} contains no built bundle (dist/chunks/ is missing or empty)." >&2
+    exit 1
+fi
+
+# Replace the old Mockup bundle with the new one.
+rm -Rf "${BUNDLE_DIR}"
+mv "${MOCKUP_DIST}" "${BUNDLE_DIR}"
+
+echo "🧪 Check for resource changes."
+git add "${BUNDLE_DIR}"
+if git diff --cached --quiet -- "${BUNDLE_DIR}"; then
+    echo "ℹ️ Mockup is unchanged; no changelog entry or commit created."
+    exit 0
+fi
+
+echo "🧪 Git add and commit."
+set_version_info mockup "$(jq -n --arg version "$MOCKUP_VERSION" '{
+    version: $version,
+    source: "https://www.npmjs.com/package/@plone/mockup/v/\($version)"
+}')"
+# Add changelog entry
+./.venv/bin/towncrier create +update-mockup.feature \
+    --content "Update Mockup to ${MOCKUP_VERSION}."
+# Add with a `*` in case a number was appended due to a naming conflict.
+git add news/+update-mockup.feature*
+
+# commit
+git commit -m"Update Mockup to ${MOCKUP_VERSION}." > /dev/null
+
+# Spit out info.
+echo ""
+echo "📦 Mockup static folder size is: "
+cd "${BUNDLE_DIR}"
+du -sh
+echo ""
+echo "🚀 Updated Mockup to ${MOCKUP_VERSION}."
+echo ""
diff --git a/scripts/version_info.sh b/scripts/version_info.sh
new file mode 100644
index 000000000..1d2757bb8
--- /dev/null
+++ b/scripts/version_info.sh
@@ -0,0 +1,15 @@
+# Helpers for the resource-versions.json file, which records the versions of
+# the installed resources. Source this file from the update scripts.
+
+VERSION_INFO_FILE=resource-versions.json
+
+# set_version_info KEY JSON
+#
+# Set the entry KEY to the JSON object and stage the file.
+set_version_info() {
+    [ -f "$VERSION_INFO_FILE" ] || echo '{}' > "$VERSION_INFO_FILE"
+    jq --sort-keys --arg key "$1" --argjson value "$2" '.[$key] = $value' \
+        "$VERSION_INFO_FILE" > "${VERSION_INFO_FILE}.tmp"
+    mv "${VERSION_INFO_FILE}.tmp" "$VERSION_INFO_FILE"
+    git add "$VERSION_INFO_FILE"
+}
diff --git a/src/plone/staticresources/static/.gitignore b/src/plone/staticresources/static/.gitignore
new file mode 100644
index 000000000..a9cca2e0f
--- /dev/null
+++ b/src/plone/staticresources/static/.gitignore
@@ -0,0 +1,4 @@
+# Ignore the following resources which were copied from the original project,
+# but we don't want to include.
+icons-country-flags/.gitignore
+icons-country-flags/scripts/
diff --git a/src/plone/staticresources/static/icons-country-flags/.gitignore b/src/plone/staticresources/static/icons-country-flags/.gitignore
deleted file mode 100644
index 4445c23f8..000000000
--- a/src/plone/staticresources/static/icons-country-flags/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.DS_Store
-npm-debug.log
-.vscode