name: 'Install Nodarx Modules' description: 'Installs custom PHP/Vue modules from git repositories with dependency resolution' author: 'Nodarx' branding: icon: 'package' color: 'purple' inputs: modules: description: 'JSON array of modules to install. Example: [{"name":"mail","repo":"https://git.ktrix.dev/Nodarx/mail","branch":"main","dependencies":["mail_manager"]}]' required: true install-path: description: 'Base directory where modules will be installed' required: false default: './modules' skip-dependencies-check: description: 'Skip dependency validation (not recommended)' required: false default: 'false' outputs: installed-modules: description: 'Comma-separated list of installed module names in installation order' value: ${{ steps.install.outputs.installed_modules }} install-path: description: 'Path where modules were installed' value: ${{ steps.install.outputs.install_path }} status: description: 'Installation status (success/failure)' value: ${{ steps.install.outputs.status }} runs: using: 'composite' steps: - name: Check prerequisites shell: bash run: | echo "::group::Checking prerequisites" # Check for required commands MISSING_CMDS=() for cmd in git jq; do if ! command -v $cmd &> /dev/null; then MISSING_CMDS+=($cmd) fi done # Install missing commands if [ ${#MISSING_CMDS[@]} -gt 0 ]; then echo "Installing missing commands: ${MISSING_CMDS[*]}" sudo apt-get update -qq for cmd in "${MISSING_CMDS[@]}"; do sudo apt-get install -y $cmd done fi # Verify all commands are now available for cmd in git jq; do if ! command -v $cmd &> /dev/null; then echo "::error::Required command '$cmd' not found after installation attempt." exit 1 fi done echo "✓ All prerequisites met" echo "::endgroup::" - name: Validate and parse modules JSON id: parse shell: bash env: MODULES_JSON: ${{ inputs.modules }} SKIP_DEPS_CHECK: ${{ inputs.skip-dependencies-check }} run: | echo "::group::Validating modules configuration" # Validate JSON syntax if ! echo "$MODULES_JSON" | jq empty 2>/dev/null; then echo "::error::Invalid JSON in modules input" exit 1 fi # Validate JSON is an array if [ "$(echo "$MODULES_JSON" | jq 'type')" != '"array"' ]; then echo "::error::Modules input must be a JSON array" exit 1 fi # Check if array is empty MODULE_COUNT=$(echo "$MODULES_JSON" | jq 'length') if [ "$MODULE_COUNT" -eq 0 ]; then echo "::error::Modules array is empty. Please specify at least one module." exit 1 fi echo "Found $MODULE_COUNT module(s) to install" # Validate each module has required fields INVALID_MODULES=$(echo "$MODULES_JSON" | jq -r ' to_entries[] | select(.value.name == null or .value.name == "" or .value.repo == null or .value.repo == "") | .key ') if [ -n "$INVALID_MODULES" ]; then echo "::error::Modules at indices $INVALID_MODULES are missing required fields 'name' or 'repo'" exit 1 fi # Extract all module names ALL_MODULES=$(echo "$MODULES_JSON" | jq -r '.[].name' | tr '\n' ' ') echo "Modules to install: $ALL_MODULES" # Validate dependencies exist (if not skipped) if [ "$SKIP_DEPS_CHECK" != "true" ]; then MISSING_DEPS=$(echo "$MODULES_JSON" | jq -r --arg all_modules "$ALL_MODULES" ' .[] | select(.dependencies != null) | .dependencies[] as $dep | select(($all_modules | contains($dep)) | not) | $dep ' | sort -u) if [ -n "$MISSING_DEPS" ]; then echo "::error::The following dependencies are referenced but not defined: $MISSING_DEPS" exit 1 fi fi echo "✓ Modules configuration is valid" echo "::endgroup::" - name: Order modules by dependencies id: order shell: bash env: MODULES_JSON: ${{ inputs.modules }} SKIP_DEPS_CHECK: ${{ inputs.skip-dependencies-check }} run: | echo "::group::Calculating installation order" # Create temporary files for topological sort TEMP_DIR=$(mktemp -d) EDGES_FILE="$TEMP_DIR/edges" NODES_FILE="$TEMP_DIR/nodes" # Extract all module names echo "$MODULES_JSON" | jq -r '.[].name' > "$NODES_FILE" # Create dependency edges (dependent -> dependency) # For each module with dependencies, create "module dep" pairs echo "$MODULES_JSON" | jq -r ' .[] | select(.dependencies != null and (.dependencies | length) > 0) | . as $item | .dependencies[] | ($item.name + " " + .) ' > "$EDGES_FILE" # Perform topological sort if [ "$SKIP_DEPS_CHECK" != "true" ] && [ -s "$EDGES_FILE" ]; then # Check for circular dependencies if ! tsort "$EDGES_FILE" &>/dev/null; then echo "::error::Circular dependency detected. Cannot determine installation order." cat "$EDGES_FILE" rm -rf "$TEMP_DIR" exit 1 fi # Get sorted order (reverse tsort output since it gives reverse topological order) INSTALL_ORDER=$(cat "$NODES_FILE" "$EDGES_FILE" | tsort | tac | tr '\n' ' ') else # No dependencies or skip check - install in order given INSTALL_ORDER=$(cat "$NODES_FILE" | tr '\n' ' ') fi # Clean up rm -rf "$TEMP_DIR" echo "Installation order: $INSTALL_ORDER" echo "install_order=$INSTALL_ORDER" >> $GITHUB_OUTPUT echo "✓ Installation order determined" echo "::endgroup::" - name: Clone module repositories id: clone shell: bash env: MODULES_JSON: ${{ inputs.modules }} INSTALL_PATH: ${{ inputs.install-path }} INSTALL_ORDER: ${{ steps.order.outputs.install_order }} run: | echo "::group::Cloning module repositories" # Create base install directory mkdir -p "$INSTALL_PATH" # Clone each module in order for MODULE_NAME in $INSTALL_ORDER; do echo "" echo "=== Cloning module: $MODULE_NAME ===" # Extract module info from JSON MODULE_INFO=$(echo "$MODULES_JSON" | jq -r --arg name "$MODULE_NAME" ' .[] | select(.name == $name) ') REPO_URL=$(echo "$MODULE_INFO" | jq -r '.repo') BRANCH=$(echo "$MODULE_INFO" | jq -r '.branch // empty') MODULE_PATH="$INSTALL_PATH/$MODULE_NAME" echo "Repository: $REPO_URL" if [ -n "$BRANCH" ]; then echo "Branch: $BRANCH" else echo "Branch: (repository default)" fi echo "Path: $MODULE_PATH" # Clone the repository if [ -d "$MODULE_PATH" ]; then echo "::warning::Directory $MODULE_PATH already exists, pulling latest changes..." cd "$MODULE_PATH" if [ -n "$BRANCH" ]; then git pull origin "$BRANCH" || echo "::warning::Pull failed, continuing with existing code" else git pull || echo "::warning::Pull failed, continuing with existing code" fi cd - > /dev/null else if [ -n "$BRANCH" ]; then if git clone --branch "$BRANCH" --depth 1 "$REPO_URL" "$MODULE_PATH"; then echo "✓ Cloned $MODULE_NAME successfully" else echo "::error::Failed to clone $MODULE_NAME from $REPO_URL (branch: $BRANCH)" exit 1 fi else if git clone --depth 1 "$REPO_URL" "$MODULE_PATH"; then echo "✓ Cloned $MODULE_NAME successfully" else echo "::error::Failed to clone $MODULE_NAME from $REPO_URL" exit 1 fi fi fi done echo "" echo "✓ All modules cloned successfully" echo "::endgroup::" - name: Install module dependencies id: install shell: bash env: INSTALL_PATH: ${{ inputs.install-path }} INSTALL_ORDER: ${{ steps.order.outputs.install_order }} run: | echo "::group::Installing module dependencies" INSTALLED_MODULES=() for MODULE_NAME in $INSTALL_ORDER; do MODULE_PATH="$INSTALL_PATH/$MODULE_NAME" echo "" echo "=== Installing dependencies for: $MODULE_NAME ===" echo "Path: $MODULE_PATH" if [ ! -d "$MODULE_PATH" ]; then echo "::error::Module directory not found: $MODULE_PATH" exit 1 fi cd "$MODULE_PATH" # Detect and install PHP dependencies if [ -f "composer.json" ]; then echo "Found composer.json - installing PHP dependencies..." # Check if composer is available if ! command -v composer &> /dev/null; then echo "::error::composer not found. Please ensure PHP and Composer are installed (use action-server-install with install-php: true)" exit 1 fi # Install with composer if composer install --no-dev --optimize-autoloader --no-interaction; then echo "✓ Composer dependencies installed" else echo "::error::Failed to install composer dependencies for $MODULE_NAME" exit 1 fi else echo "No composer.json found, skipping PHP dependencies" fi # Detect and install Node dependencies if [ -f "package.json" ]; then echo "Found package.json - installing Node dependencies..." # Check if npm is available if ! command -v npm &> /dev/null; then echo "::error::npm not found. Please ensure Node.js is installed (use action-server-install with install-node: true)" exit 1 fi # Use npm ci if lockfile exists, otherwise npm install if [ -f "package-lock.json" ]; then echo "Using npm ci (lockfile found)..." if npm ci --production; then echo "✓ npm dependencies installed (with lockfile)" else echo "::warning::npm ci failed, trying npm install..." if npm install --production; then echo "✓ npm dependencies installed" else echo "::error::Failed to install npm dependencies for $MODULE_NAME" exit 1 fi fi else echo "Using npm install (no lockfile)..." if npm install --production; then echo "✓ npm dependencies installed" else echo "::error::Failed to install npm dependencies for $MODULE_NAME" exit 1 fi fi else echo "No package.json found, skipping Node dependencies" fi # Return to previous directory cd - > /dev/null INSTALLED_MODULES+=("$MODULE_NAME") echo "✓ $MODULE_NAME installation complete" done # Set outputs INSTALLED_LIST=$(IFS=,; echo "${INSTALLED_MODULES[*]}") echo "installed_modules=$INSTALLED_LIST" >> $GITHUB_OUTPUT echo "install_path=$INSTALL_PATH" >> $GITHUB_OUTPUT echo "status=success" >> $GITHUB_OUTPUT echo "" echo "::notice::✓ Successfully installed ${#INSTALLED_MODULES[@]} module(s): $INSTALLED_LIST" echo "::endgroup::" - name: Verify installations shell: bash env: INSTALL_PATH: ${{ inputs.install-path }} INSTALL_ORDER: ${{ steps.order.outputs.install_order }} run: | echo "::group::Verifying module installations" for MODULE_NAME in $INSTALL_ORDER; do MODULE_PATH="$INSTALL_PATH/$MODULE_NAME" echo "" echo "Verifying: $MODULE_NAME" # Check module directory exists if [ ! -d "$MODULE_PATH" ]; then echo "::error::Module directory not found: $MODULE_PATH" exit 1 fi # Check .git directory exists if [ ! -d "$MODULE_PATH/.git" ]; then echo "::warning::No .git directory found in $MODULE_NAME" fi # Verify composer installation if [ -f "$MODULE_PATH/composer.json" ]; then if [ -d "$MODULE_PATH/vendor" ]; then echo " ✓ PHP dependencies installed (vendor/ exists)" else echo "::error::composer.json exists but vendor/ directory not found in $MODULE_NAME" exit 1 fi fi # Verify npm installation if [ -f "$MODULE_PATH/package.json" ]; then NODE_DEP_COUNT=$(jq -r '((.dependencies // {}) | length) + ((.optionalDependencies // {}) | length)' "$MODULE_PATH/package.json" 2>/dev/null || echo "0") if [ "$NODE_DEP_COUNT" -gt 0 ]; then if [ -d "$MODULE_PATH/node_modules" ]; then echo " ✓ Node dependencies installed (node_modules/ exists)" else echo "::error::package.json has production dependencies but node_modules/ directory not found in $MODULE_NAME" exit 1 fi else echo " ✓ Node module has no production dependencies (node_modules/ not required)" fi fi echo " ✓ $MODULE_NAME verified" done echo "" echo "✓ All modules verified successfully" echo "::endgroup::"