mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
396 lines
14 KiB
Bash
Executable File
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
|