Update validate_manifest.sh

This commit is contained in:
2025-12-23 23:06:00 -06:00
parent b8b142dcb2
commit 46be7f966e

View File

@@ -49,288 +49,288 @@ warn() { echo "WARN: $*" 1>&2; }
err() { echo "ERROR: $*" 1>&2; } err() { echo "ERROR: $*" 1>&2; }
die() { die() {
err "$*" err "$*"
exit 1 exit 1
} }
have() { have() {
command -v "$1" >/dev/null 2>&1 command -v "$1" >/dev/null 2>&1
} }
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
Usage: Usage:
./scripts/validate_manifest.sh [MANIFEST_XML] ./scripts/validate_manifest.sh [MANIFEST_XML]
./scripts/validate_manifest.sh --auto ./scripts/validate_manifest.sh --auto
Notes: Notes:
- If MANIFEST_XML is omitted, --auto is recommended. - If MANIFEST_XML is omitted, --auto is recommended.
- Exits nonzero on validation failure. - Exits nonzero on validation failure.
USAGE USAGE
} }
parse_args() { parse_args() {
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
AUTO=true AUTO=true
return 0 return 0
fi fi
if [[ $# -eq 1 && "$1" == "--auto" ]]; then if [[ $# -eq 1 && "$1" == "--auto" ]]; then
AUTO=true AUTO=true
return 0 return 0
fi fi
if [[ $# -eq 1 && "$1" == "-h" || $# -eq 1 && "$1" == "--help" ]]; then if [[ $# -eq 1 && "$1" == "-h" || $# -eq 1 && "$1" == "--help" ]]; then
usage usage
exit 0 exit 0
fi fi
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
MANIFEST="$1" MANIFEST="$1"
return 0 return 0
fi fi
usage usage
die "Invalid arguments." die "Invalid arguments."
} }
find_manifest_auto() { find_manifest_auto() {
local root="./src" local root="./src"
[[ -d "$root" ]] || die "Auto detect requires ./src directory." [[ -d "$root" ]] || die "Auto detect requires ./src directory."
# First pass: known canonical names # First pass: known canonical names
local candidates local candidates
candidates=$(find "$root" -type f \( -name 'templateDetails.xml' -o -name '*.xml' \) -not -path '*/.git/*' -print 2>/dev/null || true) candidates=$(find "$root" -type f \( -name 'templateDetails.xml' -o -name '*.xml' \) -not -path '*/.git/*' -print 2>/dev/null || true)
# Filter to those that look like Joomla manifests by checking for a root <extension> element. # Filter to those that look like Joomla manifests by checking for a root <extension> element.
local matches=() local matches=()
while IFS= read -r f; do while IFS= read -r f; do
[[ -f "$f" ]] || continue [[ -f "$f" ]] || continue
if grep -qE '<extension(\s|>)' "$f"; then if grep -qE '<extension(\s|>)' "$f"; then
matches+=("$f") matches+=("$f")
fi fi
done <<< "$candidates" done <<< "$candidates"
if [[ ${#matches[@]} -eq 0 ]]; then if [[ ${#matches[@]} -eq 0 ]]; then
die "No manifest XML detected under ./src. Provide a manifest path explicitly." die "No manifest XML detected under ./src. Provide a manifest path explicitly."
fi fi
if [[ ${#matches[@]} -gt 1 ]]; then if [[ ${#matches[@]} -gt 1 ]]; then
err "Multiple candidate manifest XML files found. Provide the intended file explicitly:" err "Multiple candidate manifest XML files found. Provide the intended file explicitly:"
for m in "${matches[@]}"; do for m in "${matches[@]}"; do
err "- $m" err "- $m"
done done
exit 2 exit 2
fi fi
MANIFEST="${matches[0]}" MANIFEST="${matches[0]}"
} }
xmllint_check() { xmllint_check() {
local f="$1" local f="$1"
if ! have xmllint; then if ! have xmllint; then
die "xmllint is required for XML validation. Install libxml2 utils in the runner environment." die "xmllint is required for XML validation. Install libxml2 utils in the runner environment."
fi fi
# Syntax validation # Syntax validation
xmllint --noout "$f" >/dev/null xmllint --noout "$f" >/dev/null
} }
xpath() { xpath() {
local f="$1" local f="$1"
local expr="$2" local expr="$2"
xmllint --xpath "$expr" "$f" 2>/dev/null || true xmllint --xpath "$expr" "$f" 2>/dev/null || true
} }
trim() { trim() {
local s="$1" local s="$1"
# shellcheck disable=SC2001 # shellcheck disable=SC2001
echo "$s" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//' echo "$s" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//'
} }
required_text() { required_text() {
local f="$1" local f="$1"
local label="$2" local label="$2"
local expr="$3" local expr="$3"
local out local out
out="$(xpath "$f" "$expr")" out="$(xpath "$f" "$expr")"
out="$(trim "$out")" out="$(trim "$out")"
[[ -n "$out" ]] || die "Missing or empty required element: $label" [[ -n "$out" ]] || die "Missing or empty required element: $label"
echo "$out" echo "$out"
} }
required_attr() { required_attr() {
local f="$1" local f="$1"
local label="$2" local label="$2"
local expr="$3" local expr="$3"
local out local out
out="$(xpath "$f" "$expr")" out="$(xpath "$f" "$expr")"
out="$(trim "$out")" out="$(trim "$out")"
[[ -n "$out" ]] || die "Missing required attribute: $label" [[ -n "$out" ]] || die "Missing required attribute: $label"
echo "$out" echo "$out"
} }
validate_root_and_type() { validate_root_and_type() {
local f="$1" local f="$1"
local root local root
root="$(xpath "$f" 'name(/*)')" root="$(xpath "$f" 'name(/*)')"
root="$(trim "$root")" root="$(trim "$root")"
[[ "$root" == "extension" ]] || die "Invalid root element '$root'. Expected: extension" [[ "$root" == "extension" ]] || die "Invalid root element '$root'. Expected: extension"
local type local type
type="$(required_attr "$f" 'extension@type' 'string(/extension/@type)')" type="$(required_attr "$f" 'extension@type' 'string(/extension/@type)')"
case "$type" in case "$type" in
template|component|module|plugin|package) template|component|module|plugin|package)
info "Detected manifest type: $type" info "Detected manifest type: $type"
;; ;;
*) *)
die "Unsupported or invalid Joomla manifest type '$type'. Expected one of: template, component, module, plugin, package" die "Unsupported or invalid Joomla manifest type '$type'. Expected one of: template, component, module, plugin, package"
;; ;;
esac esac
echo "$type" echo "$type"
} }
validate_required_fields() { validate_required_fields() {
local f="$1" local f="$1"
# Joomla manifests typically include these fields. # Joomla manifests typically include these fields.
required_text "$f" 'name' 'string(/extension/name)' required_text "$f" 'name' 'string(/extension/name)'
required_text "$f" 'version' 'string(/extension/version)' required_text "$f" 'version' 'string(/extension/version)'
# Author is not always mandatory, but it is governance relevant. # Author is not always mandatory, but it is governance relevant.
local author local author
author="$(xpath "$f" 'string(/extension/author)')" author="$(xpath "$f" 'string(/extension/author)')"
author="$(trim "$author")" author="$(trim "$author")"
if [[ -z "$author" ]]; then if [[ -z "$author" ]]; then
warn "author is missing. Governance recommended: include <author>" warn "author is missing. Governance recommended: include <author>"
fi fi
# Creation date is common and helps auditability. # Creation date is common and helps auditability.
local cdate local cdate
cdate="$(xpath "$f" 'string(/extension/creationDate)')" cdate="$(xpath "$f" 'string(/extension/creationDate)')"
cdate="$(trim "$cdate")" cdate="$(trim "$cdate")"
if [[ -z "$cdate" ]]; then if [[ -z "$cdate" ]]; then
warn "creationDate is missing. Governance recommended: include <creationDate>" warn "creationDate is missing. Governance recommended: include <creationDate>"
fi fi
# Basic packaging elements: at least one of files, folders, fileset, or languages. # Basic packaging elements: at least one of files, folders, fileset, or languages.
local has_files local has_files
has_files="$(xpath "$f" 'count(/extension/files)')" has_files="$(xpath "$f" 'count(/extension/files)')"
local has_folders local has_folders
has_folders="$(xpath "$f" 'count(/extension/folders)')" has_folders="$(xpath "$f" 'count(/extension/folders)')"
local has_filesets local has_filesets
has_filesets="$(xpath "$f" 'count(/extension/fileset | /extension/filesets | /extension/file)')" has_filesets="$(xpath "$f" 'count(/extension/fileset | /extension/filesets | /extension/file)')"
local has_lang local has_lang
has_lang="$(xpath "$f" 'count(/extension/languages | /extension/administration/languages)')" has_lang="$(xpath "$f" 'count(/extension/languages | /extension/administration/languages)')"
# xmllint returns numbers as strings # xmllint returns numbers as strings
has_files="$(trim "$has_files")" has_files="$(trim "$has_files")"
has_folders="$(trim "$has_folders")" has_folders="$(trim "$has_folders")"
has_filesets="$(trim "$has_filesets")" has_filesets="$(trim "$has_filesets")"
has_lang="$(trim "$has_lang")" has_lang="$(trim "$has_lang")"
if [[ "${has_files:-0}" == "0" && "${has_folders:-0}" == "0" && "${has_filesets:-0}" == "0" && "${has_lang:-0}" == "0" ]]; then if [[ "${has_files:-0}" == "0" && "${has_folders:-0}" == "0" && "${has_filesets:-0}" == "0" && "${has_lang:-0}" == "0" ]]; then
die "Manifest appears to lack payload declarations. Expected one of: <files>, <folders>, <fileset>, or <languages>." die "Manifest appears to lack payload declarations. Expected one of: <files>, <folders>, <fileset>, or <languages>."
fi fi
} }
validate_version_format() { validate_version_format() {
local v="$1" local v="$1"
# Governance check: allow semantic style versions, warn if non standard. # Governance check: allow semantic style versions, warn if non standard.
if [[ ! "$v" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?([-.][A-Za-z0-9\.]+)?$ ]]; then if [[ ! "$v" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?([-.][A-Za-z0-9\.]+)?$ ]]; then
warn "Version '$v' does not look like a conventional semantic version." warn "Version '$v' does not look like a conventional semantic version."
fi fi
} }
validate_type_specific() { validate_type_specific() {
local f="$1" local f="$1"
local type="$2" local type="$2"
case "$type" in case "$type" in
plugin) plugin)
local group local group
group="$(required_attr "$f" 'extension@group' 'string(/extension/@group)')" group="$(required_attr "$f" 'extension@group' 'string(/extension/@group)')"
info "Plugin group: $group" info "Plugin group: $group"
;; ;;
module) module)
# client is optional for some older manifests, but recommended. # client is optional for some older manifests, but recommended.
local client local client
client="$(xpath "$f" 'string(/extension/@client)')" client="$(xpath "$f" 'string(/extension/@client)')"
client="$(trim "$client")" client="$(trim "$client")"
if [[ -z "$client" ]]; then if [[ -z "$client" ]]; then
warn "Module client attribute is missing. Governance recommended: set client=site or client=administrator." warn "Module client attribute is missing. Governance recommended: set client=site or client=administrator."
else else
info "Module client: $client" info "Module client: $client"
fi fi
;; ;;
template) template)
local client local client
client="$(xpath "$f" 'string(/extension/@client)')" client="$(xpath "$f" 'string(/extension/@client)')"
client="$(trim "$client")" client="$(trim "$client")"
if [[ -z "$client" ]]; then if [[ -z "$client" ]]; then
warn "Template client attribute is missing. Governance recommended: set client=site." warn "Template client attribute is missing. Governance recommended: set client=site."
else else
info "Template client: $client" info "Template client: $client"
fi fi
;; ;;
component) component)
# method=upgrade is a common governance default. # method=upgrade is a common governance default.
local method local method
method="$(xpath "$f" 'string(/extension/@method)')" method="$(xpath "$f" 'string(/extension/@method)')"
method="$(trim "$method")" method="$(trim "$method")"
if [[ -z "$method" ]]; then if [[ -z "$method" ]]; then
warn "Component method attribute is missing. Governance recommended: set method=upgrade." warn "Component method attribute is missing. Governance recommended: set method=upgrade."
else else
info "Component method: $method" info "Component method: $method"
fi fi
;; ;;
package) package)
# Packages should declare contained extensions. # Packages should declare contained extensions.
local count local count
count="$(xpath "$f" 'count(/extension/files/file)')" count="$(xpath "$f" 'count(/extension/files/file)')"
count="$(trim "$count")" count="$(trim "$count")"
if [[ "${count:-0}" == "0" ]]; then if [[ "${count:-0}" == "0" ]]; then
warn "Package manifest contains no /extension/files/file entries. Validate that it declares packaged extensions." warn "Package manifest contains no /extension/files/file entries. Validate that it declares packaged extensions."
else else
info "Package files entries: $count" info "Package files entries: $count"
fi fi
;; ;;
esac esac
} }
main() { main() {
parse_args "$@" parse_args "$@"
if [[ "$AUTO" == "true" ]]; then if [[ "$AUTO" == "true" ]]; then
find_manifest_auto find_manifest_auto
fi fi
[[ -n "$MANIFEST" ]] || die "Manifest path not resolved." [[ -n "$MANIFEST" ]] || die "Manifest path not resolved."
[[ -f "$MANIFEST" ]] || die "Manifest file not found: $MANIFEST" [[ -f "$MANIFEST" ]] || die "Manifest file not found: $MANIFEST"
info "Validating Joomla manifest: $MANIFEST" info "Validating Joomla manifest: $MANIFEST"
xmllint_check "$MANIFEST" xmllint_check "$MANIFEST"
local type local type
type="$(validate_root_and_type "$MANIFEST")" type="$(validate_root_and_type "$MANIFEST")"
validate_required_fields "$MANIFEST" validate_required_fields "$MANIFEST"
local version local version
version="$(required_text "$MANIFEST" 'version' 'string(/extension/version)')" version="$(required_text "$MANIFEST" 'version' 'string(/extension/version)')"
validate_version_format "$version" validate_version_format "$version"
validate_type_specific "$MANIFEST" "$type" validate_type_specific "$MANIFEST" "$type"
info "Manifest validation PASSED." info "Manifest validation PASSED."
} }
main "$@" main "$@"