diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4cfa73..b424006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,7 @@ jobs: scripts/validate/manifest.sh scripts/validate/xml_wellformed.sh + scripts/validate/workflows.sh - name: Optional validations run: | @@ -59,6 +60,7 @@ jobs: scripts/validate/php_syntax.sh scripts/validate/tabs.sh scripts/validate/version_alignment.sh + scripts/validate/version_hierarchy.sh - name: CI summary if: always() diff --git a/scripts/README.md b/scripts/README.md index 5323fb6..d90c4a4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -121,6 +121,28 @@ Validates Joomla language directory structure and INI files. ### `changelog.sh` Validates CHANGELOG.md structure and version entries. +### `version_hierarchy.sh` +Validates version hierarchy across branch prefixes: +- Checks for version conflicts across dev/, rc/, and version/ branches +- Ensures no version exists in multiple priority levels simultaneously +- Reports any violations of the hierarchy rules + +Usage: +```bash +./scripts/validate/version_hierarchy.sh +``` + +### `workflows.sh` +Validates GitHub Actions workflow files: +- Checks YAML syntax using Python's yaml module +- Ensures no tab characters are present +- Validates required workflow structure (name, on, jobs) + +Usage: +```bash +./scripts/validate/workflows.sh +``` + ## Fix Scripts (`fix/`) These scripts automatically fix common issues detected by validation scripts. @@ -279,6 +301,65 @@ Example output: [SUCCESS] SUCCESS: All scripts follow enterprise standards ``` +### `list_versions.sh` +Lists all version branches organized by prefix: +- Displays dev/, rc/, and version/ branches +- Shows versions sorted in ascending order +- Provides a summary count of each branch type + +Usage: +```bash +./scripts/run/list_versions.sh +``` + +Example output: +``` +======================================== +Version Branches Summary +======================================== + +📦 Stable Versions (version/) +---------------------------------------- + ✓ version/03.00.00 + ✓ version/03.01.00 + +🔧 Release Candidates (rc/) +---------------------------------------- + ➜ rc/03.02.00 + +🚧 Development Versions (dev/) +---------------------------------------- + ⚡ dev/03.05.00 + ⚡ dev/04.00.00 + +======================================== +Total: 2 stable, 1 RC, 2 dev +======================================== +``` + +### `check_version.sh` +Checks if a version can be created in a specific branch prefix: +- Validates against version hierarchy rules +- Checks for existing branches +- Reports conflicts with higher priority branches + +Usage: +```bash +./scripts/run/check_version.sh +``` + +Examples: +```bash +./scripts/run/check_version.sh dev/ 03.05.00 +./scripts/run/check_version.sh rc/ 03.01.00 +./scripts/run/check_version.sh version/ 02.00.00 +``` + +Exit codes: +- 0: Version can be created (no conflicts) +- 1: Version cannot be created (conflicts found) +- 2: Invalid arguments + ## Best Practices ### Enterprise Standards diff --git a/scripts/run/check_version.sh b/scripts/run/check_version.sh new file mode 100755 index 0000000..910fae6 --- /dev/null +++ b/scripts/run/check_version.sh @@ -0,0 +1,191 @@ +#!/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 +# along with this program (./LICENSE.md). +# ============================================================================ + +# ============================================================================ +# FILE INFORMATION +# ============================================================================ +# DEFGROUP: Script.Run +# INGROUP: Version.Management +# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +# PATH: /scripts/run/check_version.sh +# VERSION: 01.00.00 +# BRIEF: Check if a version can be created in a specific branch prefix +# NOTE: Validates against version hierarchy rules before creation +# ============================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "${SCRIPT_DIR}/lib/common.sh" + +# ---------------------------------------------------------------------------- +# Usage and validation +# ---------------------------------------------------------------------------- + +usage() { +cat <<-USAGE +Usage: $0 + +Check if a version can be created in the specified branch prefix. + +Arguments: + BRANCH_PREFIX One of: dev/, rc/, version/ + VERSION Version in format NN.NN.NN (e.g., 03.01.00) + +Examples: + $0 dev/ 03.05.00 + $0 rc/ 03.01.00 + $0 version/ 02.00.00 + +Exit codes: + 0 - Version can be created (no conflicts) + 1 - Version cannot be created (conflicts found) + 2 - Invalid arguments or usage + +Version Hierarchy Rules: + - version/X.Y.Z (stable) - always allowed, highest priority + - rc/X.Y.Z (RC) - blocked if version/X.Y.Z exists + - dev/X.Y.Z (dev) - blocked if version/X.Y.Z or rc/X.Y.Z exists + +USAGE +exit 2 +} + +validate_prefix() { + local prefix="$1" + case "$prefix" in + "dev/"|"rc/"|"version/") + return 0 + ;; + *) + die "Invalid branch prefix: $prefix (must be dev/, rc/, or version/)" + ;; + esac +} + +validate_version() { + local v="$1" + if ! printf '%s' "$v" | grep -Eq '^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$'; then + die "Invalid version format: $v (expected NN.NN.NN)" + fi +} + +# ---------------------------------------------------------------------------- +# Main logic +# ---------------------------------------------------------------------------- + +check_version_can_be_created() { + local prefix="$1" + local version="$2" + + log_info "Checking if ${prefix}${version} can be created..." + log_info "" + + # Check if the exact branch already exists + if git ls-remote --exit-code --heads origin "${prefix}${version}" >/dev/null 2>&1; then + log_error "✗ Branch already exists: ${prefix}${version}" + return 1 + fi + + local conflicts=0 + + case "$prefix" in + "version/") + log_info "Creating stable version - no hierarchy checks needed" + log_info "✓ version/${version} can be created" + ;; + "rc/") + log_info "Checking if version exists in stable..." + if git ls-remote --exit-code --heads origin "version/${version}" >/dev/null 2>&1; then + log_error "✗ CONFLICT: Version ${version} already exists in stable (version/${version})" + log_error " Cannot create RC for a version that exists in stable" + conflicts=$((conflicts + 1)) + else + log_info "✓ No conflict with stable versions" + log_info "✓ rc/${version} can be created" + fi + ;; + "dev/") + log_info "Checking if version exists in stable..." + if git ls-remote --exit-code --heads origin "version/${version}" >/dev/null 2>&1; then + log_error "✗ CONFLICT: Version ${version} already exists in stable (version/${version})" + log_error " Cannot create dev for a version that exists in stable" + conflicts=$((conflicts + 1)) + else + log_info "✓ No conflict with stable versions" + fi + + log_info "Checking if version exists in RC..." + if git ls-remote --exit-code --heads origin "rc/${version}" >/dev/null 2>&1; then + log_error "✗ CONFLICT: Version ${version} already exists in RC (rc/${version})" + log_error " Cannot create dev for a version that exists in RC" + conflicts=$((conflicts + 1)) + else + log_info "✓ No conflict with RC versions" + fi + + if [ $conflicts -eq 0 ]; then + log_info "✓ dev/${version} can be created" + fi + ;; + esac + + log_info "" + + if [ $conflicts -gt 0 ]; then + log_error "Version ${prefix}${version} cannot be created ($conflicts conflict(s) found)" + return 1 + fi + + log_info "Version ${prefix}${version} can be created safely" + return 0 +} + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +# Parse arguments +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage +fi + +[ $# -eq 2 ] || usage + +BRANCH_PREFIX="$1" +VERSION="$2" + +validate_prefix "$BRANCH_PREFIX" +validate_version "$VERSION" + +log_info "Version Creation Check" +log_info "======================" +log_info "" + +check_version_can_be_created "$BRANCH_PREFIX" "$VERSION" +exit_code=$? + +log_info "" +log_info "======================" + +exit $exit_code diff --git a/scripts/run/list_versions.sh b/scripts/run/list_versions.sh new file mode 100755 index 0000000..ebf22d6 --- /dev/null +++ b/scripts/run/list_versions.sh @@ -0,0 +1,127 @@ +#!/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 +# along with this program (./LICENSE.md). +# ============================================================================ + +# ============================================================================ +# FILE INFORMATION +# ============================================================================ +# DEFGROUP: Script.Run +# INGROUP: Version.Management +# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +# PATH: /scripts/run/list_versions.sh +# VERSION: 01.00.00 +# BRIEF: List all version branches organized by prefix +# NOTE: Displays dev/, rc/, and version/ branches in a structured format +# ============================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "${SCRIPT_DIR}/lib/common.sh" + +# ---------------------------------------------------------------------------- +# Functions +# ---------------------------------------------------------------------------- + +list_version_branches() { + log_info "Fetching version branches from remote..." + + # Get all branches with version-like names + local branches + branches=$(git ls-remote --heads origin 2>/dev/null | awk '{print $2}' | sed 's|refs/heads/||' || echo "") + + if [ -z "$branches" ]; then + log_warn "No remote branches found or unable to fetch branches" + return 0 + fi + + # Categorize versions + local dev_versions=() + local rc_versions=() + local stable_versions=() + + while IFS= read -r branch; do + if [[ "$branch" =~ ^dev/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then + dev_versions+=("${BASH_REMATCH[1]}") + elif [[ "$branch" =~ ^rc/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then + rc_versions+=("${BASH_REMATCH[1]}") + elif [[ "$branch" =~ ^version/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then + stable_versions+=("${BASH_REMATCH[1]}") + fi + done <<< "$branches" + + # Sort versions + IFS=$'\n' dev_versions=($(sort -V <<< "${dev_versions[*]}" 2>/dev/null || echo "${dev_versions[@]}")) + IFS=$'\n' rc_versions=($(sort -V <<< "${rc_versions[*]}" 2>/dev/null || echo "${rc_versions[@]}")) + IFS=$'\n' stable_versions=($(sort -V <<< "${stable_versions[*]}" 2>/dev/null || echo "${stable_versions[@]}")) + unset IFS + + # Display results + echo "" + echo "========================================" + echo "Version Branches Summary" + echo "========================================" + echo "" + + echo "📦 Stable Versions (version/)" + echo "----------------------------------------" + if [ ${#stable_versions[@]} -eq 0 ]; then + echo " (none)" + else + for version in "${stable_versions[@]}"; do + echo " ✓ version/$version" + done + fi + echo "" + + echo "🔧 Release Candidates (rc/)" + echo "----------------------------------------" + if [ ${#rc_versions[@]} -eq 0 ]; then + echo " (none)" + else + for version in "${rc_versions[@]}"; do + echo " ➜ rc/$version" + done + fi + echo "" + + echo "🚧 Development Versions (dev/)" + echo "----------------------------------------" + if [ ${#dev_versions[@]} -eq 0 ]; then + echo " (none)" + else + for version in "${dev_versions[@]}"; do + echo " ⚡ dev/$version" + done + fi + echo "" + + echo "========================================" + echo "Total: ${#stable_versions[@]} stable, ${#rc_versions[@]} RC, ${#dev_versions[@]} dev" + echo "========================================" +} + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +list_version_branches diff --git a/scripts/validate/version_hierarchy.sh b/scripts/validate/version_hierarchy.sh new file mode 100755 index 0000000..e692735 --- /dev/null +++ b/scripts/validate/version_hierarchy.sh @@ -0,0 +1,134 @@ +#!/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 +# along with this program (./LICENSE.md). +# ============================================================================ + +# ============================================================================ +# FILE INFORMATION +# ============================================================================ +# DEFGROUP: Script.Validate +# INGROUP: Version.Management +# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +# PATH: /scripts/validate/version_hierarchy.sh +# VERSION: 01.00.00 +# BRIEF: Validate version hierarchy across branch prefixes +# NOTE: Checks for version conflicts across dev/, rc/, and version/ branches +# ============================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "${SCRIPT_DIR}/lib/common.sh" + +# ---------------------------------------------------------------------------- +# Functions +# ---------------------------------------------------------------------------- + +check_version_hierarchy() { + log_info "Checking version hierarchy across branch prefixes..." + + local violations=0 + + # Get all branches with version-like names + local branches + branches=$(git ls-remote --heads origin 2>/dev/null | awk '{print $2}' | sed 's|refs/heads/||' || echo "") + + if [ -z "$branches" ]; then + log_warn "No remote branches found or unable to fetch branches" + return 0 + fi + + # Extract versions from branches + local dev_versions=() + local rc_versions=() + local stable_versions=() + + while IFS= read -r branch; do + if [[ "$branch" =~ ^dev/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then + dev_versions+=("${BASH_REMATCH[1]}") + elif [[ "$branch" =~ ^rc/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then + rc_versions+=("${BASH_REMATCH[1]}") + elif [[ "$branch" =~ ^version/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then + stable_versions+=("${BASH_REMATCH[1]}") + fi + done <<< "$branches" + + log_info "Found ${#dev_versions[@]} dev versions, ${#rc_versions[@]} RC versions, ${#stable_versions[@]} stable versions" + + # Check for violations: + # 1. dev/ version that exists in rc/ or version/ + for version in "${dev_versions[@]}"; do + # Check if exists in stable + for stable in "${stable_versions[@]}"; do + if [ "$version" = "$stable" ]; then + log_error "VIOLATION: Version $version exists in both dev/ and version/ branches" + violations=$((violations + 1)) + fi + done + + # Check if exists in RC + for rc in "${rc_versions[@]}"; do + if [ "$version" = "$rc" ]; then + log_error "VIOLATION: Version $version exists in both dev/ and rc/ branches" + violations=$((violations + 1)) + fi + done + done + + # 2. rc/ version that exists in version/ + for version in "${rc_versions[@]}"; do + for stable in "${stable_versions[@]}"; do + if [ "$version" = "$stable" ]; then + log_error "VIOLATION: Version $version exists in both rc/ and version/ branches" + violations=$((violations + 1)) + fi + done + done + + if [ $violations -eq 0 ]; then + log_info "✓ No version hierarchy violations found" + return 0 + else + log_error "✗ Found $violations version hierarchy violation(s)" + return 1 + fi +} + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +log_info "Version Hierarchy Validation" +log_info "=============================" +log_info "" + +check_version_hierarchy +exit_code=$? + +log_info "" +log_info "=============================" +if [ $exit_code -eq 0 ]; then + log_info "Version hierarchy validation passed" +else + log_error "Version hierarchy validation failed" +fi + +exit $exit_code diff --git a/scripts/validate/workflows.sh b/scripts/validate/workflows.sh new file mode 100755 index 0000000..a8a147d --- /dev/null +++ b/scripts/validate/workflows.sh @@ -0,0 +1,186 @@ +#!/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 +# along with this program (./LICENSE.md). +# ============================================================================ + +# ============================================================================ +# FILE INFORMATION +# ============================================================================ +# DEFGROUP: Script.Validate +# INGROUP: CI.Validation +# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +# PATH: /scripts/validate/workflows.sh +# VERSION: 01.00.00 +# BRIEF: Validate GitHub Actions workflow files +# NOTE: Checks YAML syntax, structure, and best practices +# ============================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +. "${SCRIPT_DIR}/lib/common.sh" + +# ---------------------------------------------------------------------------- +# Functions +# ---------------------------------------------------------------------------- + +validate_yaml_syntax() { + local file="$1" + + if ! command -v python3 >/dev/null 2>&1; then + log_warn "python3 not found, skipping YAML syntax validation" + return 0 + fi + + python3 - "$file" <<'PYEOF' +import sys +import yaml + +file_path = sys.argv[1] + +try: + with open(file_path, 'r') as f: + yaml.safe_load(f) + print(f"✓ Valid YAML: {file_path}") + sys.exit(0) +except yaml.YAMLError as e: + print(f"✗ YAML Error in {file_path}: {e}", file=sys.stderr) + sys.exit(1) +except Exception as e: + print(f"✗ Error reading {file_path}: {e}", file=sys.stderr) + sys.exit(1) +PYEOF +} + +check_no_tabs() { + local file="$1" + + if grep -q $'\t' "$file"; then + log_error "✗ File contains tab characters: $file" + return 1 + fi + + return 0 +} + +check_workflow_structure() { + local file="$1" + local filename=$(basename "$file") + + # Check for required top-level keys + if ! grep -q "^name:" "$file"; then + log_warn "Missing 'name:' in $filename" + fi + + if ! grep -q "^on:" "$file"; then + log_error "✗ Missing 'on:' trigger in $filename" + return 1 + fi + + if ! grep -q "^jobs:" "$file"; then + log_error "✗ Missing 'jobs:' in $filename" + return 1 + fi + + return 0 +} + +validate_workflow_file() { + local file="$1" + local filename=$(basename "$file") + + log_info "Validating: $filename" + + local errors=0 + + # Check YAML syntax + if ! validate_yaml_syntax "$file"; then + errors=$((errors + 1)) + fi + + # Check for tabs + if ! check_no_tabs "$file"; then + errors=$((errors + 1)) + fi + + # Check structure + if ! check_workflow_structure "$file"; then + errors=$((errors + 1)) + fi + + if [ $errors -eq 0 ]; then + log_info "✓ $filename passed all checks" + return 0 + else + log_error "✗ $filename failed $errors check(s)" + return 1 + fi +} + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +log_info "GitHub Actions Workflow Validation" +log_info "===================================" +log_info "" + +WORKFLOWS_DIR=".github/workflows" + +if [ ! -d "$WORKFLOWS_DIR" ]; then + log_error "Workflows directory not found: $WORKFLOWS_DIR" + exit 1 +fi + +total=0 +passed=0 +failed=0 + +for workflow in "$WORKFLOWS_DIR"/*.yml "$WORKFLOWS_DIR"/*.yaml; do + if [ ! -f "$workflow" ]; then + continue + fi + + total=$((total + 1)) + + if validate_workflow_file "$workflow"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + echo "" +done + +log_info "===================================" +log_info "Summary:" +log_info " Total workflows: $total" +log_info " Passed: $passed" +log_info " Failed: $failed" +log_info "===================================" + +if [ $failed -gt 0 ]; then + log_error "Workflow validation failed" + exit 1 +fi + +log_info "All workflows validated successfully" +exit 0