From 174a823be4e9ca4bf5fcece40e863cb81b704861 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:09:59 -0600 Subject: [PATCH] Script repair --- scripts/fix_paths.sh | 37 ++-- scripts/fix_tabs.sh | 85 +++++++++ scripts/update_changelog.sh | 100 +++++----- scripts/validate_manifest.sh | 360 +++++++++++++++++++++++++++++++---- 4 files changed, 477 insertions(+), 105 deletions(-) create mode 100644 scripts/fix_tabs.sh diff --git a/scripts/fix_paths.sh b/scripts/fix_paths.sh index 090fad0..9e291e9 100644 --- a/scripts/fix_paths.sh +++ b/scripts/fix_paths.sh @@ -17,18 +17,13 @@ # # You should have received a copy of the GNU General Public License (./LICENSE.md). # ----------------------------------------------------------------------------- - # FILE INFORMATION # DEFGROUP: MokoStandards # INGROUP: Generic.Script # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /scripts/fix_paths.sh # VERSION: 01.00.00 -# BRIEF: Replace Windows-style path separators with POSIX separators in text files. - -# ============================================================================= -# fix_paths.sh -# +# BRIEF: Replace Windows-style path separators with POSIX separators in text files.# # Purpose: # - Normalize path separators in text files to forward slashes (/). # - Intended for CI validation and optional remediation workflows. @@ -44,16 +39,16 @@ set -euo pipefail ROOT_DIR="${1:-.}" info() { - echo "INFO: $*" + echo "INFO: $*" } warn() { - echo "WARN: $*" 1>&2 + echo "WARN: $*" 1>&2 } die() { - echo "ERROR: $*" 1>&2 - exit 1 + echo "ERROR: $*" 1>&2 + exit 1 } command -v find >/dev/null 2>&1 || die "find not available" @@ -63,18 +58,18 @@ command -v file >/dev/null 2>&1 || die "file not available" info "Scanning for text files under: $ROOT_DIR" while IFS= read -r -d '' file; do - if file "$file" | grep -qi "text"; then - if grep -q '\\\\' "$file"; then - sed -i.bak 's#\\\\#/#g' "$file" && rm -f "$file.bak" - info "Normalized paths in $file" - fi - fi + if file "$file" | grep -qi "text"; then + if grep -q '\\\\' "$file"; then + sed -i.bak 's#\\\\#/#g' "$file" && rm -f "$file.bak" + info "Normalized paths in $file" + fi + fi done < <( - find "$ROOT_DIR" \ - -type f \ - -not -path "*/.git/*" \ - -not -path "*/node_modules/*" \ - -print0 + find "$ROOT_DIR" \ + -type f \ + -not -path "*/.git/*" \ + -not -path "*/node_modules/*" \ + -print0 ) info "Path normalization complete." diff --git a/scripts/fix_tabs.sh b/scripts/fix_tabs.sh new file mode 100644 index 0000000..b949572 --- /dev/null +++ b/scripts/fix_tabs.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License (./LICENSE.md). +# ----------------------------------------------------------------------------- +# FILE INFORMATION +# DEFGROUP: MokoStandards +# INGROUP: Generic.Script +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /scripts/fix_tabs.sh +# VERSION: 01.00.00 +# BRIEF: Replace tab characters with two spaces in text files. +# Purpose: +# - Replace tab characters with two spaces in text files. +# - Designed for optional remediation workflows. +# - Skips binary files and version control metadata. +# - Preserves file contents aside from tab replacement. +# +# Usage: +# ./scripts/fix_tabs.sh +# ./scripts/fix_tabs.sh ./src +# ============================================================================= + +set -euo pipefail + +ROOT_DIR="${1:-.}" + +info() { + echo "INFO: $*" +} + +warn() { + echo "WARN: $*" 1>&2 +} + +die() { + echo "ERROR: $*" 1>&2 + exit 1 +} + +command -v find >/dev/null 2>&1 || die "find not available" +command -v sed >/dev/null 2>&1 || die "sed not available" +command -v file >/dev/null 2>&1 || die "file not available" +command -v grep >/dev/null 2>&1 || die "grep not available" + +info "Scanning for tab characters under: $ROOT_DIR" + +changed=0 +scanned=0 + +while IFS= read -r -d '' f; do + scanned=$((scanned + 1)) + + if ! file "$f" | grep -qi "text"; then + continue + fi + + if grep -q $'\t' "$f"; then + sed -i.bak $'s/\t/ /g' "$f" && rm -f "$f.bak" + info "Replaced tabs in $f" + changed=$((changed + 1)) + fi +done < <( + find "$ROOT_DIR" \ + -type f \ + -not -path "*/.git/*" \ + -not -path "*/node_modules/*" \ + -print0 +) + +info "Scan complete. Files scanned: $scanned. Files changed: $changed." diff --git a/scripts/update_changelog.sh b/scripts/update_changelog.sh index a9a2998..9d460f5 100644 --- a/scripts/update_changelog.sh +++ b/scripts/update_changelog.sh @@ -18,16 +18,14 @@ # # You should have received a copy of the GNU General Public License (./LICENSE.md). # ----------------------------------------------------------------------------- - # FILE INFORMATION # DEFGROUP: MokoStandards # INGROUP: Generic.Script # REPO: https://github.com/mokoconsulting-tech/MokoDefaults # PATH: /scripts/update_changelog.sh # VERSION: 01.00.00 -# BRIEF: Insert a versioned CHANGELOG.md entry immediately after the main Changelog heading. -# NOTES -# # Purpose: +# BRIEF: Insert a versioned CHANGELOG.md entry immediately after the main Changelog heading +# Purpose: # - Apply the MokoWaaS-Brand CHANGELOG template entry for a given version. # - Insert a new header at the top of CHANGELOG.md, immediately after "# Changelog". # - Avoid duplicates if an entry for the version already exists. @@ -45,80 +43,80 @@ set -euo pipefail CHANGELOG_FILE="CHANGELOG.md" die() { - echo "ERROR: $*" 1>&2 - exit 1 + echo "ERROR: $*" 1>&2 + exit 1 } info() { - echo "INFO: $*" + echo "INFO: $*" } require_cmd() { - command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } validate_version() { - local v="$1" - [[ "$v" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]] || die "Invalid version '$v'. Expected NN.NN.NN (example 03.01.00)." + local v="$1" + [[ "$v" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]] || die "Invalid version '$v'. Expected NN.NN.NN (example 03.01.00)." } main() { - require_cmd awk - require_cmd grep - require_cmd mktemp - require_cmd date + require_cmd awk + require_cmd grep + require_cmd mktemp + require_cmd date - [[ $# -eq 1 ]] || die "Usage: $0 " - local version="$1" - validate_version "$version" + [[ $# -eq 1 ]] || die "Usage: $0 " + local version="$1" + validate_version "$version" - [[ -f "$CHANGELOG_FILE" ]] || die "Missing $CHANGELOG_FILE in repo root." + [[ -f "$CHANGELOG_FILE" ]] || die "Missing $CHANGELOG_FILE in repo root." - if ! grep -qE '^# Changelog[[:space:]]*$' "$CHANGELOG_FILE"; then - die "$CHANGELOG_FILE must contain a top level heading exactly: # Changelog" - fi + if ! grep -qE '^# Changelog[[:space:]]*$' "$CHANGELOG_FILE"; then + die "$CHANGELOG_FILE must contain a top level heading exactly: # Changelog" + fi - if grep -qE "^## \[$version\][[:space:]]" "$CHANGELOG_FILE"; then - info "CHANGELOG.md already contains an entry for version $version. No action taken." - exit 0 - fi + if grep -qE "^## \[$version\][[:space:]]" "$CHANGELOG_FILE"; then + info "CHANGELOG.md already contains an entry for version $version. No action taken." + exit 0 + fi - local stamp - stamp="$(date '+%Y-%m-%d')" + local stamp + stamp="$(date '+%Y-%m-%d')" - local tmp - tmp="$(mktemp)" - trap 'rm -f "$tmp"' EXIT + local tmp + tmp="$(mktemp)" + trap 'rm -f "$tmp"' EXIT - awk -v v="$version" -v d="$stamp" ' - BEGIN { inserted=0 } - { + awk -v v="$version" -v d="$stamp" ' + BEGIN { inserted=0 } + { print $0 if (inserted==0 && $0 ~ /^# Changelog[[:space:]]*$/) { - print "" - print "## [" v "] " d - print "- Version bump." - print "" - inserted=1 + print "" + print "## [" v "] " d + print "- Version bump." + print "" + inserted=1 } - } - END { + } + END { if (inserted==0) { - exit 3 + exit 3 } - } - ' "$CHANGELOG_FILE" > "$tmp" || { - rc=$? - if [[ $rc -eq 3 ]]; then + } + ' "$CHANGELOG_FILE" > "$tmp" || { + rc=$? + if [[ $rc -eq 3 ]]; then die "Insertion point not found. Expected: # Changelog" - fi - die "Failed to update $CHANGELOG_FILE (awk exit code $rc)." - } + fi + die "Failed to update $CHANGELOG_FILE (awk exit code $rc)." + } - mv "$tmp" "$CHANGELOG_FILE" - trap - EXIT + mv "$tmp" "$CHANGELOG_FILE" + trap - EXIT - info "Inserted CHANGELOG.md entry for version $version on $stamp." + info "Inserted CHANGELOG.md entry for version $version on $stamp." } main "$@" diff --git a/scripts/validate_manifest.sh b/scripts/validate_manifest.sh index 703f768..c78115b 100644 --- a/scripts/validate_manifest.sh +++ b/scripts/validate_manifest.sh @@ -1,42 +1,336 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License (./LICENSE.md). +# ----------------------------------------------------------------------------- +# FILE INFORMATION +# DEFGROUP: MokoStandards +# INGROUP: Joomla.Validation +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /scripts/validate_manifest.sh +# VERSION: 01.00.00 +# BRIEF: Validate a Joomla project manifest XML for structural and governance compliance +# Purpose: +# - Validate the XML manifest for a Joomla project. +# - Supports common Joomla extension types: template, component, module, plugin, package. +# - Performs syntax validation, required field checks, and type specific attribute checks. +# - Designed for CI enforcement and local pre commit validation. +# +# Usage: +# ./scripts/validate_manifest.sh [MANIFEST_XML] +# ./scripts/validate_manifest.sh --auto +# +# Examples: +# ./scripts/validate_manifest.sh ./src/templateDetails.xml +# ./scripts/validate_manifest.sh --auto +# ============================================================================= -MANIFEST="src/mokowaasbrand.xml" +set -euo pipefail -echo "Validating Joomla manifest: $MANIFEST" +AUTO=false +MANIFEST="" -if [ ! -f "$MANIFEST" ]; then - echo "ERROR: Manifest not found: $MANIFEST" - exit 1 -fi +info() { echo "INFO: $*"; } +warn() { echo "WARN: $*" 1>&2; } +err() { echo "ERROR: $*" 1>&2; } -# Check XML syntax -if ! xmllint --noout "$MANIFEST"; then - echo "ERROR: Manifest XML is not valid." - exit 1 -fi +die() { + err "$*" + exit 1 +} -# Required fields -REQUIRED_NODES=( - "//extension" - "//name" - "//version" - "//author" - "//creationDate" -) +have() { + command -v "$1" >/dev/null 2>&1 +} -for NODE in "${REQUIRED_NODES[@]}"; do - if ! xmllint --xpath "$NODE" "$MANIFEST" > /dev/null 2>&1; then - echo "ERROR: Required manifest node missing: $NODE" - exit 1 - fi -done +usage() { + cat <<'USAGE' +Usage: + ./scripts/validate_manifest.sh [MANIFEST_XML] + ./scripts/validate_manifest.sh --auto -VERSION=$(xmllint --xpath "string(//version)" "$MANIFEST") +Notes: + - If MANIFEST_XML is omitted, --auto is recommended. + - Exits nonzero on validation failure. +USAGE +} -if [ -z "$VERSION" ]; then - echo "ERROR: Version node is empty in manifest." - exit 1 -fi +parse_args() { + if [[ $# -eq 0 ]]; then + AUTO=true + return 0 + fi -echo "Manifest OK. Version: $VERSION" + if [[ $# -eq 1 && "$1" == "--auto" ]]; then + AUTO=true + return 0 + fi + + if [[ $# -eq 1 && "$1" == "-h" || $# -eq 1 && "$1" == "--help" ]]; then + usage + exit 0 + fi + + if [[ $# -eq 1 ]]; then + MANIFEST="$1" + return 0 + fi + + usage + die "Invalid arguments." +} + +find_manifest_auto() { + local root="./src" + [[ -d "$root" ]] || die "Auto detect requires ./src directory." + + # First pass: known canonical names + local candidates + 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 element. + local matches=() + while IFS= read -r f; do + [[ -f "$f" ]] || continue + if grep -qE ')' "$f"; then + matches+=("$f") + fi + done <<< "$candidates" + + if [[ ${#matches[@]} -eq 0 ]]; then + die "No manifest XML detected under ./src. Provide a manifest path explicitly." + fi + + if [[ ${#matches[@]} -gt 1 ]]; then + err "Multiple candidate manifest XML files found. Provide the intended file explicitly:" + for m in "${matches[@]}"; do + err "- $m" + done + exit 2 + fi + + MANIFEST="${matches[0]}" +} + +xmllint_check() { + local f="$1" + if ! have xmllint; then + die "xmllint is required for XML validation. Install libxml2 utils in the runner environment." + fi + + # Syntax validation + xmllint --noout "$f" >/dev/null +} + +xpath() { + local f="$1" + local expr="$2" + xmllint --xpath "$expr" "$f" 2>/dev/null || true +} + +trim() { + local s="$1" + # shellcheck disable=SC2001 + echo "$s" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//' +} + +required_text() { + local f="$1" + local label="$2" + local expr="$3" + + local out + out="$(xpath "$f" "$expr")" + out="$(trim "$out")" + [[ -n "$out" ]] || die "Missing or empty required element: $label" + echo "$out" +} + +required_attr() { + local f="$1" + local label="$2" + local expr="$3" + + local out + out="$(xpath "$f" "$expr")" + out="$(trim "$out")" + [[ -n "$out" ]] || die "Missing required attribute: $label" + echo "$out" +} + +validate_root_and_type() { + local f="$1" + + local root + root="$(xpath "$f" 'name(/*)')" + root="$(trim "$root")" + + [[ "$root" == "extension" ]] || die "Invalid root element '$root'. Expected: extension" + + local type + type="$(required_attr "$f" 'extension@type' 'string(/extension/@type)')" + + case "$type" in + template|component|module|plugin|package) + info "Detected manifest type: $type" + ;; + *) + die "Unsupported or invalid Joomla manifest type '$type'. Expected one of: template, component, module, plugin, package" + ;; + esac + + echo "$type" +} + +validate_required_fields() { + local f="$1" + + # Joomla manifests typically include these fields. + required_text "$f" 'name' 'string(/extension/name)' + required_text "$f" 'version' 'string(/extension/version)' + + # Author is not always mandatory, but it is governance relevant. + local author + author="$(xpath "$f" 'string(/extension/author)')" + author="$(trim "$author")" + if [[ -z "$author" ]]; then + warn "author is missing. Governance recommended: include " + fi + + # Creation date is common and helps auditability. + local cdate + cdate="$(xpath "$f" 'string(/extension/creationDate)')" + cdate="$(trim "$cdate")" + if [[ -z "$cdate" ]]; then + warn "creationDate is missing. Governance recommended: include " + fi + + # Basic packaging elements: at least one of files, folders, fileset, or languages. + local has_files + has_files="$(xpath "$f" 'count(/extension/files)')" + local has_folders + has_folders="$(xpath "$f" 'count(/extension/folders)')" + local has_filesets + has_filesets="$(xpath "$f" 'count(/extension/fileset | /extension/filesets | /extension/file)')" + local has_lang + has_lang="$(xpath "$f" 'count(/extension/languages | /extension/administration/languages)')" + + # xmllint returns numbers as strings + has_files="$(trim "$has_files")" + has_folders="$(trim "$has_folders")" + has_filesets="$(trim "$has_filesets")" + has_lang="$(trim "$has_lang")" + + 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: , , , or ." + fi +} + +validate_version_format() { + local v="$1" + + # Governance check: allow semantic style versions, warn if non standard. + if [[ ! "$v" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?([-.][A-Za-z0-9\.]+)?$ ]]; then + warn "Version '$v' does not look like a conventional semantic version." + fi +} + +validate_type_specific() { + local f="$1" + local type="$2" + + case "$type" in + plugin) + local group + group="$(required_attr "$f" 'extension@group' 'string(/extension/@group)')" + info "Plugin group: $group" + ;; + module) + # client is optional for some older manifests, but recommended. + local client + client="$(xpath "$f" 'string(/extension/@client)')" + client="$(trim "$client")" + if [[ -z "$client" ]]; then + warn "Module client attribute is missing. Governance recommended: set client=site or client=administrator." + else + info "Module client: $client" + fi + ;; + template) + local client + client="$(xpath "$f" 'string(/extension/@client)')" + client="$(trim "$client")" + if [[ -z "$client" ]]; then + warn "Template client attribute is missing. Governance recommended: set client=site." + else + info "Template client: $client" + fi + ;; + component) + # method=upgrade is a common governance default. + local method + method="$(xpath "$f" 'string(/extension/@method)')" + method="$(trim "$method")" + if [[ -z "$method" ]]; then + warn "Component method attribute is missing. Governance recommended: set method=upgrade." + else + info "Component method: $method" + fi + ;; + package) + # Packages should declare contained extensions. + local count + count="$(xpath "$f" 'count(/extension/files/file)')" + count="$(trim "$count")" + if [[ "${count:-0}" == "0" ]]; then + warn "Package manifest contains no /extension/files/file entries. Validate that it declares packaged extensions." + else + info "Package files entries: $count" + fi + ;; + esac +} + +main() { + parse_args "$@" + + if [[ "$AUTO" == "true" ]]; then + find_manifest_auto + fi + + [[ -n "$MANIFEST" ]] || die "Manifest path not resolved." + [[ -f "$MANIFEST" ]] || die "Manifest file not found: $MANIFEST" + + info "Validating Joomla manifest: $MANIFEST" + + xmllint_check "$MANIFEST" + + local type + type="$(validate_root_and_type "$MANIFEST")" + + validate_required_fields "$MANIFEST" + + local version + version="$(required_text "$MANIFEST" 'version' 'string(/extension/version)')" + validate_version_format "$version" + + validate_type_specific "$MANIFEST" "$type" + + info "Manifest validation PASSED." +} + +main "$@"