Files
wayscriber/tools/check-version-consistency.sh
T

396 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: tools/check-version-consistency.sh [--release-version X.Y.Z[.N]]
Checks that release/version metadata agrees across:
* Cargo.toml
* configurator/Cargo.toml
* Cargo.lock
* packaging/PKGBUILD
* packaging/.SRCINFO
* flake.nix
* global.json
Also keeps the configurator's supported libadwaita floor at 1.4 across its
Cargo feature, package metadata, release assertions, and AUR generation.
Raise that floor only as a coordinated release-platform change.
Also rejects hardcoded release tags in README.md install examples, which go
stale on the next release. Use a placeholder such as RELEASE_TAG, or link to
the latest release, instead.
Repo packaging metadata is a release template and must use sha256sums=('SKIP').
Release/AUR automation writes the real source archive checksum after the tag
exists.
When --release-version is provided, it must either match Cargo.toml exactly
or be a packaging hotfix version of the Cargo version (for example, Cargo
0.9.19 with release 0.9.19.1). Hotfix releases require packaging/PKGBUILD
and packaging/.SRCINFO to use the hotfix version.
EOF
}
release_version=""
while [[ $# -gt 0 ]]; do
case "$1" in
--release-version)
if [[ $# -lt 2 ]]; then
echo "error: --release-version requires a value" >&2
usage
exit 1
fi
release_version="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown arg: $1" >&2
usage
exit 1
;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PYTHON=""
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1 \
&& "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info[0] == 3 else 1)' >/dev/null 2>&1; then
PYTHON="$candidate"
break
fi
done
if [[ -z "$PYTHON" ]]; then
echo "error: python3 or python pointing to Python 3 is required" >&2
exit 1
fi
"$PYTHON" - "$REPO_ROOT" "$release_version" <<'PY'
import json
import pathlib
import re
import sys
try:
import tomllib
except ImportError:
tomllib = None
root = pathlib.Path(sys.argv[1])
release_version = sys.argv[2] or None
errors = []
version_re = re.compile(r"^\d+\.\d+\.\d+(?:\.\d+)?$")
checksum_re = re.compile(r"^[0-9a-fA-F]{64}$")
supported_libadwaita_floor = "1.4"
repository_tool_sdk = "11.0.100-rc.1.26425.128"
# A floor raise must add a reviewed runner contract instead of inheriting the
# previous Ubuntu base image by accident.
release_runner_libadwaita_floors = {
"ubuntu-24.04": "1.4",
}
def read_text(path):
return (root / path).read_text(encoding="utf-8")
def global_json_sdk(path):
try:
document = json.loads(read_text(path))
sdk = document["sdk"]
version = sdk["version"]
except (OSError, json.JSONDecodeError, KeyError, TypeError) as error:
errors.append(f"{path} SDK metadata is invalid: {error}")
return None
if not isinstance(version, str) or not version.strip():
errors.append(f"{path} SDK metadata is invalid: sdk.version must be a non-empty string")
return None
if sdk.get("rollForward") != "disable":
errors.append(f"{path} SDK rollForward must be disable")
if sdk.get("allowPrerelease") is not True:
errors.append(f"{path} SDK allowPrerelease must be true")
return version.strip()
def cargo_version(path):
text = read_text(path)
if tomllib is not None:
return tomllib.loads(text)["package"]["version"]
package_match = re.search(r"(?ms)^\[package\]\s*(.*?)(?:^\[|\Z)", text)
if not package_match:
errors.append(f"{path}: missing [package] section")
return ""
version_match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', package_match.group(1))
if not version_match:
errors.append(f"{path}: missing package version")
return ""
return version_match.group(1)
def cargo_libadwaita_features(path):
text = read_text(path)
if tomllib is not None:
dependency = tomllib.loads(text).get("dependencies", {}).get("libadwaita")
if not isinstance(dependency, dict):
errors.append(f"{path}: missing structured libadwaita dependency")
return []
features = dependency.get("features", [])
if not isinstance(features, list) or not all(
isinstance(feature, str) for feature in features
):
errors.append(f"{path}: libadwaita features must be a string list")
return []
return features
dependency_match = re.search(r"(?m)^libadwaita\s*=\s*\{([^\n]+)\}$", text)
if not dependency_match:
errors.append(f"{path}: missing structured libadwaita dependency")
return []
features_match = re.search(r"features\s*=\s*\[([^\]]*)\]", dependency_match.group(1))
if not features_match:
return []
return re.findall(r'"([^"]+)"', features_match.group(1))
def single_metadata_floor(label, path, pattern):
matches = re.findall(pattern, read_text(path), re.MULTILINE)
if len(matches) != 1:
errors.append(f"{label}: expected one libadwaita floor, found {len(matches)}")
return None
return matches[0]
def workflow_job_runner(path, job_name):
text = read_text(path)
job_match = re.search(
rf"(?m)^ {re.escape(job_name)}:[ \t]*$",
text,
)
if not job_match:
errors.append(f"{path}: missing {job_name} job")
return None
remaining = text[job_match.end():]
next_job_match = re.search(r"(?m)^ [A-Za-z0-9_-]+:[ \t]*$", remaining)
job_text = remaining[:next_job_match.start()] if next_job_match else remaining
runners = re.findall(r"(?m)^ runs-on:[ \t]*([^#\n]+?)[ \t]*$", job_text)
if len(runners) != 1:
errors.append(
f"{path} {job_name} job: expected one literal runs-on value, "
f"found {len(runners)}"
)
return None
return runners[0]
def lock_package_version(path, package):
pattern = re.compile(
r'\[\[package\]\]\s+name\s*=\s*"' + re.escape(package) + r'"\s+version\s*=\s*"([^"]+)"',
re.MULTILINE,
)
match = pattern.search(read_text(path))
return match.group(1) if match else None
def pkgbuild_version(path):
match = re.search(r"^pkgver=(.+)$", read_text(path), re.MULTILINE)
return match.group(1).strip() if match else None
def srcinfo_version(path):
match = re.search(r"^\s*pkgver = (.+)$", read_text(path), re.MULTILINE)
return match.group(1).strip() if match else None
def pkgbuild_sha256sums(path):
match = re.search(r"(?ms)^sha256sums=\((.*?)\)", read_text(path))
if not match:
return []
values = []
for value_match in re.finditer(r"'([^']*)'|\"([^\"]*)\"|(\S+)", match.group(1)):
value = next(group for group in value_match.groups() if group is not None)
values.append(value.strip())
return values
def srcinfo_sha256sums(path):
return [
match.group(1).strip()
for match in re.finditer(r"^\s*sha256sums = (.+)$", read_text(path), re.MULTILINE)
]
def is_hotfix_of(version, base):
return re.fullmatch(re.escape(base) + r"\.\d+", version) is not None
def require_equal(label, actual, expected):
if actual != expected:
errors.append(f"{label}: expected {expected}, got {actual or 'missing'}")
def require_template_sha256sums(label, values):
if values == ["SKIP"]:
return
if not values:
errors.append(f"{label}: expected SKIP template checksum, got missing")
return
actual = ", ".join(values)
if any(checksum_re.fullmatch(value) for value in values):
errors.append(
f"{label}: expected SKIP template checksum, got fixed SHA {actual}; "
"release/AUR automation writes the real checksum after the tag exists"
)
else:
errors.append(f"{label}: expected SKIP template checksum, got {actual}")
root_version = cargo_version("Cargo.toml")
config_version = cargo_version("configurator/Cargo.toml")
require_equal("configurator/Cargo.toml", config_version, root_version)
require_equal("global.json SDK", global_json_sdk("global.json"), repository_tool_sdk)
expected_libadwaita_feature = "v" + supported_libadwaita_floor.replace(".", "_")
libadwaita_features = cargo_libadwaita_features("configurator/Cargo.toml")
if libadwaita_features != [expected_libadwaita_feature]:
errors.append(
"configurator/Cargo.toml libadwaita features: "
f"expected [{expected_libadwaita_feature!r}], got {libadwaita_features!r}"
)
libadwaita_floors = {
"configurator deb libadwaita floor": single_metadata_floor(
"configurator deb libadwaita floor",
"packaging/package.configurator.yaml",
r"^\s*-\s*libadwaita-1-0 \(>= ([0-9]+\.[0-9]+)\)\s*$",
),
"configurator rpm libadwaita floor": single_metadata_floor(
"configurator rpm libadwaita floor",
"packaging/package.configurator.yaml",
r"^\s*-\s*libadwaita >= ([0-9]+\.[0-9]+)\s*$",
),
"packaging/PKGBUILD libadwaita floor": single_metadata_floor(
"packaging/PKGBUILD libadwaita floor",
"packaging/PKGBUILD",
r"^\s*'libadwaita>=([0-9]+\.[0-9]+)'\s*$",
),
"packaging/.SRCINFO libadwaita floor": single_metadata_floor(
"packaging/.SRCINFO libadwaita floor",
"packaging/.SRCINFO",
r"^\s*depends = libadwaita>=([0-9]+\.[0-9]+)\s*$",
),
"AUR updater generated libadwaita floor": single_metadata_floor(
"AUR updater generated libadwaita floor",
"tools/update-aur-from-manifest.sh",
r"^\s*ensure_runtime_dependency 'libadwaita>=([0-9]+\.[0-9]+)' gcc-libs\s*$",
),
"AUR updater PKGBUILD validation floor": single_metadata_floor(
"AUR updater PKGBUILD validation floor",
"tools/update-aur-from-manifest.sh",
r'''^\s*&& grep -Eq "[^"\n]*libadwaita>=([0-9]+\.[0-9]+)[^"\n]*" PKGBUILD''',
),
"AUR updater .SRCINFO validation floor": single_metadata_floor(
"AUR updater .SRCINFO validation floor",
"tools/update-aur-from-manifest.sh",
r"^\s*&& grep -Fxq .*depends = libadwaita>=([0-9]+\.[0-9]+).*\.SRCINFO",
),
}
for label, floor in libadwaita_floors.items():
if floor is not None:
require_equal(label, floor, supported_libadwaita_floor)
release_package_runner = workflow_job_runner(
".github/workflows/build-packages.yml",
"package",
)
if release_package_runner is not None:
release_runner_floor = release_runner_libadwaita_floors.get(release_package_runner)
if release_runner_floor is None:
errors.append(
"release package runner libadwaita floor: "
f"no reviewed contract for {release_package_runner}"
)
else:
require_equal(
f"release package runner {release_package_runner} libadwaita floor",
supported_libadwaita_floor,
release_runner_floor,
)
require_equal(
"Cargo.lock wayscriber",
lock_package_version("Cargo.lock", "wayscriber"),
root_version,
)
require_equal(
"Cargo.lock wayscriber-configurator",
lock_package_version("Cargo.lock", "wayscriber-configurator"),
root_version,
)
if not version_re.fullmatch(root_version):
errors.append(f"Cargo.toml version has unsupported format: {root_version}")
packaging_version = pkgbuild_version("packaging/PKGBUILD")
srcinfo_pkgver = srcinfo_version("packaging/.SRCINFO")
if release_version:
if not version_re.fullmatch(release_version):
errors.append(f"release version has unsupported format: {release_version}")
elif release_version != root_version and not is_hotfix_of(release_version, root_version):
errors.append(
f"release version {release_version} must equal Cargo version {root_version} "
f"or be a hotfix of it, such as {root_version}.1"
)
expected_packaging_version = release_version
else:
expected_packaging_version = root_version
if packaging_version and is_hotfix_of(packaging_version, root_version):
expected_packaging_version = packaging_version
require_equal("packaging/PKGBUILD pkgver", packaging_version, expected_packaging_version)
require_equal("packaging/.SRCINFO pkgver", srcinfo_pkgver, expected_packaging_version)
require_template_sha256sums(
"packaging/PKGBUILD sha256sums", pkgbuild_sha256sums("packaging/PKGBUILD")
)
require_template_sha256sums(
"packaging/.SRCINFO sha256sums", srcinfo_sha256sums("packaging/.SRCINFO")
)
flake_text = read_text("flake.nix")
if "builtins.fromTOML (builtins.readFile ./Cargo.toml)" not in flake_text:
errors.append("flake.nix package version should be derived from Cargo.toml")
if not all(
token in flake_text
for token in ("package.rust-version", "rustToolchain.version", "versionAtLeast")
):
errors.append(
"flake.nix should compare the selected rustc against Cargo.toml rust-version"
)
# Install examples that pin a concrete tag are stale one release later.
readme_pin_patterns = (
(r"wayscriber\?ref=v?\d+\.\d+\.\d+(?:\.\d+)?", "pinned flake ref"),
(r"/releases/(?:tag|download)/v?\d+\.\d+\.\d+(?:\.\d+)?", "pinned release URL"),
)
readme_text = read_text("README.md")
for pattern, label in readme_pin_patterns:
for match in sorted(set(re.findall(pattern, readme_text))):
errors.append(
f"README.md: {label} '{match}' goes stale on the next release; "
"use a RELEASE_TAG placeholder or link to /releases/latest"
)
if errors:
print("Version consistency check failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
sys.exit(1)
print(
f"Version consistency OK: Cargo={root_version}, "
f"packaging={expected_packaging_version}, checksum=SKIP, "
f"libadwaita={supported_libadwaita_floor}"
)
PY