diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..93a921b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,635 @@ +name: Test Module Installation Action + +on: + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + # Test basic single module installation + test-single-module: + name: Single Module (No Dependencies) + runs-on: ubuntu-latest + + steps: + - name: Checkout action-module-install + uses: actions/checkout@v4 + with: + path: action-module-install + + - name: Clone action-server-install + run: | + git clone https://git.ktrix.dev/Nodarx/action-server-install action-server-install + cd action-server-install + git checkout main + + - name: Setup server environment + uses: ./action-server-install + with: + install-php: 'true' + install-node: 'true' + php-version: '8.5' + node-version: '24' + server-path: './server' + + - name: Create test module repository + run: | + mkdir -p /tmp/test-repos/simple-module + cd /tmp/test-repos/simple-module + git init + git config user.email "test@example.com" + git config user.name "Test User" + + # Create a simple Node.js module + cat > package.json << 'EOF' + { + "name": "simple-module", + "version": "1.0.0", + "description": "Test module", + "main": "index.js" + } + EOF + + echo "console.log('Simple module loaded');" > index.js + + git add . + git commit -m "Initial commit" + + - name: Install single module + id: install + uses: ./action-module-install + with: + modules: | + [ + { + "name": "simple-module", + "repo": "file:///tmp/test-repos/simple-module" + } + ] + + - name: Verify installation + run: | + echo "Checking installation..." + + # Check module directory exists + if [ ! -d "./modules/simple-module" ]; then + echo "::error::Module directory not found" + exit 1 + fi + + # Check files were cloned + if [ ! -f "./modules/simple-module/package.json" ]; then + echo "::error::package.json not found" + exit 1 + fi + + # Check npm install ran + if [ ! -d "./modules/simple-module/node_modules" ]; then + echo "::error::node_modules not found - npm install didn't run" + exit 1 + fi + + # Check outputs + if [ "${{ steps.install.outputs.status }}" != "success" ]; then + echo "::error::Installation status is not success" + exit 1 + fi + + if [ "${{ steps.install.outputs.installed-modules }}" != "simple-module" ]; then + echo "::error::Installed modules output is incorrect" + exit 1 + fi + + echo "✓ Single module installation verified" + + # Test modules with linear dependencies + test-linear-dependencies: + name: Linear Dependencies (A → B → C) + runs-on: ubuntu-latest + + steps: + - name: Checkout action-module-install + uses: actions/checkout@v4 + with: + path: action-module-install + + - name: Clone action-server-install + run: | + git clone https://git.ktrix.dev/Nodarx/action-server-install action-server-install + cd action-server-install + git checkout main + + - name: Setup server environment + uses: ./action-server-install + with: + install-php: 'true' + php-version: '8.5' + server-path: './server' + + - name: Create test repositories + run: | + # Create module-a (no dependencies) + mkdir -p /tmp/test-repos/module-a + cd /tmp/test-repos/module-a + git init + git config user.email "test@example.com" + git config user.name "Test User" + cat > composer.json << 'EOF' + { + "name": "test/module-a", + "description": "Module A", + "require": {} + } + EOF + git add . && git commit -m "Initial commit" + + # Create module-b (depends on module-a) + mkdir -p /tmp/test-repos/module-b + cd /tmp/test-repos/module-b + git init + git config user.email "test@example.com" + git config user.name "Test User" + cat > composer.json << 'EOF' + { + "name": "test/module-b", + "description": "Module B", + "require": {} + } + EOF + git add . && git commit -m "Initial commit" + + # Create module-c (depends on module-b) + mkdir -p /tmp/test-repos/module-c + cd /tmp/test-repos/module-c + git init + git config user.email "test@example.com" + git config user.name "Test User" + cat > composer.json << 'EOF' + { + "name": "test/module-c", + "description": "Module C", + "require": {} + } + EOF + git add . && git commit -m "Initial commit" + + - name: Install modules with dependencies + id: install + uses: ./action-module-install + with: + modules: | + [ + { + "name": "module-c", + "repo": "file:///tmp/test-repos/module-c", + "dependencies": ["module-b"] + }, + { + "name": "module-a", + "repo": "file:///tmp/test-repos/module-a" + }, + { + "name": "module-b", + "repo": "file:///tmp/test-repos/module-b", + "dependencies": ["module-a"] + } + ] + + - name: Verify dependency order + run: | + INSTALLED="${{ steps.install.outputs.installed-modules }}" + echo "Installation order: $INSTALLED" + + # Verify all modules installed + for module in module-a module-b module-c; do + if [ ! -d "./modules/$module" ]; then + echo "::error::Module $module not found" + exit 1 + fi + + if [ ! -d "./modules/$module/vendor" ]; then + echo "::error::Composer dependencies not installed for $module" + exit 1 + fi + done + + # Verify order: module-a must come before module-b, module-b before module-c + echo "$INSTALLED" | grep -E "(module-a.*module-b.*module-c|module-a.*module-c)" || { + echo "::error::Installation order is incorrect: $INSTALLED" + echo "Expected: module-a installed before module-b and module-c" + exit 1 + } + + echo "✓ Linear dependency installation verified" + + # Test PHP module + test-php-module: + name: PHP Module with Composer + runs-on: ubuntu-latest + + steps: + - name: Checkout action-module-install + uses: actions/checkout@v4 + with: + path: action-module-install + + - name: Clone action-server-install + run: | + git clone https://git.ktrix.dev/Nodarx/action-server-install action-server-install + cd action-server-install + git checkout main + + - name: Setup PHP environment + uses: ./action-server-install + with: + install-php: 'true' + php-version: '8.5' + server-path: './server' + + - name: Create PHP test module + run: | + mkdir -p /tmp/test-repos/php-module + cd /tmp/test-repos/php-module + git init + git config user.email "test@example.com" + git config user.name "Test User" + + cat > composer.json << 'EOF' + { + "name": "test/php-module", + "description": "PHP test module", + "require": { + "php": ">=8.0" + }, + "autoload": { + "psr-4": { + "Test\\PhpModule\\": "src/" + } + } + } + EOF + + mkdir -p src + cat > src/TestClass.php << 'EOF' + package.json << 'EOF' + { + "name": "vue-module", + "version": "1.0.0", + "description": "Vue test module", + "main": "index.js", + "dependencies": {} + } + EOF + + cat > index.js << 'EOF' + export default { + name: 'VueModule', + getMessage() { + return 'Vue module loaded'; + } + }; + EOF + + git add . && git commit -m "Initial commit" + + - name: Install Node.js module + uses: ./action-module-install + with: + modules: | + [ + { + "name": "vue-module", + "repo": "file:///tmp/test-repos/vue-module" + } + ] + + - name: Verify Node.js installation + run: | + # Check package.json exists + if [ ! -f "./modules/vue-module/package.json" ]; then + echo "::error::package.json not found" + exit 1 + fi + + # Check node_modules directory created + if [ ! -d "./modules/vue-module/node_modules" ]; then + echo "::error::node_modules directory not found - npm install failed" + exit 1 + fi + + echo "✓ Node.js module installation verified" + + # Test mixed PHP and Node.js modules + test-mixed-modules: + name: Mixed PHP and Node Modules + runs-on: ubuntu-latest + + steps: + - name: Checkout action-module-install + uses: actions/checkout@v4 + with: + path: action-module-install + + - name: Clone action-server-install + run: | + git clone https://git.ktrix.dev/Nodarx/action-server-install action-server-install + cd action-server-install + git checkout main + + - name: Setup both PHP and Node.js + uses: ./action-server-install + with: + install-php: 'true' + install-node: 'true' + php-version: '8.5' + node-version: '24' + server-path: './server' + + - name: Create mixed test modules + run: | + # PHP backend module + mkdir -p /tmp/test-repos/backend + cd /tmp/test-repos/backend + git init + git config user.email "test@example.com" + git config user.name "Test User" + cat > composer.json << 'EOF' + { + "name": "test/backend", + "require": {} + } + EOF + git add . && git commit -m "Initial commit" + + # Node.js frontend module + mkdir -p /tmp/test-repos/frontend + cd /tmp/test-repos/frontend + git init + git config user.email "test@example.com" + git config user.name "Test User" + cat > package.json << 'EOF' + { + "name": "frontend", + "version": "1.0.0" + } + EOF + git add . && git commit -m "Initial commit" + + - name: Install mixed modules + uses: ./action-module-install + with: + modules: | + [ + { + "name": "backend", + "repo": "file:///tmp/test-repos/backend" + }, + { + "name": "frontend", + "repo": "file:///tmp/test-repos/frontend" + } + ] + + - name: Verify mixed installation + run: | + # Verify PHP module + if [ ! -d "./modules/backend/vendor" ]; then + echo "::error::Backend composer dependencies not installed" + exit 1 + fi + + # Verify Node module + if [ ! -d "./modules/frontend/node_modules" ]; then + echo "::error::Frontend npm dependencies not installed" + exit 1 + fi + + echo "✓ Mixed PHP and Node.js modules verified" + + # Test error handling for invalid inputs + test-error-handling: + name: Error Handling + runs-on: ubuntu-latest + + steps: + - name: Checkout action-module-install + uses: actions/checkout@v4 + with: + path: action-module-install + + - name: Clone action-server-install + run: | + git clone https://git.ktrix.dev/Nodarx/action-server-install action-server-install + cd action-server-install + git checkout main + + - name: Setup environment + uses: ./action-server-install + with: + install-php: 'true' + install-node: 'true' + server-path: './server' + + - name: Test invalid JSON + id: test-invalid-json + continue-on-error: true + uses: ./action-module-install + with: + modules: 'invalid json string' + + - name: Verify invalid JSON was caught + run: | + if [ "${{ steps.test-invalid-json.outcome }}" = "success" ]; then + echo "::error::Action should have failed with invalid JSON" + exit 1 + fi + echo "✓ Invalid JSON properly rejected" + + - name: Test missing dependency + id: test-missing-dep + continue-on-error: true + uses: ./action-module-install + with: + modules: | + [ + { + "name": "module-a", + "repo": "file:///tmp/fake", + "dependencies": ["non-existent-module"] + } + ] + + - name: Verify missing dependency was caught + run: | + if [ "${{ steps.test-missing-dep.outcome }}" = "success" ]; then + echo "::error::Action should have failed with missing dependency" + exit 1 + fi + echo "✓ Missing dependency properly detected" + + - name: Test circular dependency + id: test-circular + continue-on-error: true + run: | + # Create repos for circular dependency test + mkdir -p /tmp/test-repos/circle-a /tmp/test-repos/circle-b + + cd /tmp/test-repos/circle-a + git init + git config user.email "test@example.com" + git config user.name "Test User" + echo '{"name": "circle-a"}' > package.json + git add . && git commit -m "Initial" + + cd /tmp/test-repos/circle-b + git init + git config user.email "test@example.com" + git config user.name "Test User" + echo '{"name": "circle-b"}' > package.json + git add . && git commit -m "Initial" + + - name: Run circular dependency test + id: test-circular-run + continue-on-error: true + uses: ./action-module-install + with: + modules: | + [ + { + "name": "circle-a", + "repo": "file:///tmp/test-repos/circle-a", + "dependencies": ["circle-b"] + }, + { + "name": "circle-b", + "repo": "file:///tmp/test-repos/circle-b", + "dependencies": ["circle-a"] + } + ] + + - name: Verify circular dependency was caught + run: | + if [ "${{ steps.test-circular-run.outcome }}" = "success" ]; then + echo "::error::Action should have failed with circular dependency" + exit 1 + fi + echo "✓ Circular dependency properly detected" + + # Test summary + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: + - test-single-module + - test-linear-dependencies + - test-php-module + - test-node-module + - test-mixed-modules + - test-error-handling + if: always() + + steps: + - name: Check test results + run: | + echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Test | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Single Module | ${{ needs.test-single-module.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Linear Dependencies | ${{ needs.test-linear-dependencies.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| PHP Module | ${{ needs.test-php-module.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Node Module | ${{ needs.test-node-module.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Mixed Modules | ${{ needs.test-mixed-modules.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Error Handling | ${{ needs.test-error-handling.result }} |" >> $GITHUB_STEP_SUMMARY + + # Check if any test failed + if [ "${{ needs.test-single-module.result }}" != "success" ] || \ + [ "${{ needs.test-linear-dependencies.result }}" != "success" ] || \ + [ "${{ needs.test-php-module.result }}" != "success" ] || \ + [ "${{ needs.test-node-module.result }}" != "success" ] || \ + [ "${{ needs.test-mixed-modules.result }}" != "success" ] || \ + [ "${{ needs.test-error-handling.result }}" != "success" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ One or more tests failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ All tests passed!" >> $GITHUB_STEP_SUMMARY diff --git a/README.md b/README.md index eea47b8..d4c3a47 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,407 @@ -# action-module-install +# Nodarx Module Install Action + +Install custom PHP/Vue modules from git repositories with automatic dependency resolution. + +This GitHub Action clones module repositories, resolves dependencies, and installs both PHP (Composer) and Node.js (npm) dependencies in the correct order. + +## Features + +- 🔗 **Dependency Resolution**: Automatically orders modules based on their dependencies +- 📦 **Multi-Language Support**: Handles both PHP (Composer) and Node.js (npm) projects +- 🔄 **Flexible**: Support for custom branches, tags, or commits +- ✅ **Validation**: Detects circular dependencies and validates module configuration +- 🎯 **Simple**: Just provide a JSON array of modules and their dependencies + +## Quick Start + +```yaml +name: Deploy with Modules + +on: [push] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Install server environment (PHP + Node.js) + - uses: Nodarx/action-server-install@v1 + with: + install-php: 'true' + install-node: 'true' + php-version: '8.5' + node-version: '24' + server-path: './server' + + # Install modules with dependencies + - uses: Nodarx/action-module-install@v1 + with: + modules: | + [ + {"name": "mail_manager", "repo": "https://git.ktrix.dev/Nodarx/mail_manager"}, + {"name": "provider_jmapc", "repo": "https://git.ktrix.dev/Nodarx/provider_jmapc"}, + {"name": "mail", "repo": "https://git.ktrix.dev/Nodarx/mail", "dependencies": ["mail_manager", "provider_jmapc"]} + ] +``` + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `modules` | JSON array of modules to install (see [Module Schema](#module-schema)) | Yes | - | +| `install-path` | Base directory where modules will be installed | No | `./modules` | +| `skip-dependencies-check` | Skip dependency validation (not recommended) | No | `false` | + +## Outputs + +| Output | Description | Example | +|--------|-------------|---------| +| `installed-modules` | Comma-separated list of installed module names in installation order | `mail_manager,provider_jmapc,mail` | +| `install-path` | Path where modules were installed | `./modules` | +| `status` | Installation status | `success` | + +## Module Schema + +Each module in the `modules` array should follow this structure: + +```json +{ + "name": "module-name", // Required: Unique module identifier + "repo": "https://...", // Required: Git repository URL + "branch": "main", // Optional: Branch, tag, or commit (default: main) + "dependencies": ["module1"] // Optional: Array of module names this module depends on +} +``` + +### Module Schema Examples + +**Minimal Module (No Dependencies)** +```json +{ + "name": "standalone-module", + "repo": "https://git.ktrix.dev/Nodarx/standalone" +} +``` + +**Module with Branch Specification** +```json +{ + "name": "beta-module", + "repo": "https://git.ktrix.dev/Nodarx/beta", + "branch": "develop" +} +``` + +**Module with Dependencies** +```json +{ + "name": "mail", + "repo": "https://git.ktrix.dev/Nodarx/mail", + "branch": "main", + "dependencies": ["mail_manager", "provider_jmapc"] +} +``` + +## Usage Examples + +### Single Module + +```yaml +- uses: Nodarx/action-module-install@v1 + with: + modules: | + [ + {"name": "auth", "repo": "https://git.ktrix.dev/Nodarx/auth"} + ] +``` + +### Linear Dependencies (A → B → C) + +```yaml +- uses: Nodarx/action-module-install@v1 + with: + modules: | + [ + {"name": "core", "repo": "https://git.ktrix.dev/Nodarx/core"}, + {"name": "api", "repo": "https://git.ktrix.dev/Nodarx/api", "dependencies": ["core"]}, + {"name": "web", "repo": "https://git.ktrix.dev/Nodarx/web", "dependencies": ["api"]} + ] +``` +Installation order: `core` → `api` → `web` + +### Diamond Dependencies (A,B → C; C → D) + +```yaml +- uses: Nodarx/action-module-install@v1 + with: + modules: | + [ + {"name": "utils", "repo": "https://git.ktrix.dev/Nodarx/utils"}, + {"name": "storage", "repo": "https://git.ktrix.dev/Nodarx/storage"}, + {"name": "database", "repo": "https://git.ktrix.dev/Nodarx/database", "dependencies": ["utils", "storage"]}, + {"name": "api", "repo": "https://git.ktrix.dev/Nodarx/api", "dependencies": ["database"]} + ] +``` +Installation order: `utils,storage` → `database` → `api` + +### Mail System Example (Real-World) + +```yaml +- uses: Nodarx/action-server-install@v1 + with: + install-php: 'true' + install-node: 'true' + php-version: '8.5' + node-version: '24' + +- uses: Nodarx/action-module-install@v1 + id: modules + with: + modules: | + [ + { + "name": "mail_manager", + "repo": "https://git.ktrix.dev/Nodarx/mail_manager", + "branch": "main" + }, + { + "name": "provider_jmapc", + "repo": "https://git.ktrix.dev/Nodarx/provider_jmapc", + "branch": "stable1" + }, + { + "name": "mail", + "repo": "https://git.ktrix.dev/Nodarx/mail", + "branch": "main", + "dependencies": ["mail_manager", "provider_jmapc"] + } + ] + install-path: './modules' + +- name: Verify installation + run: | + echo "Installed: ${{ steps.modules.outputs.installed-modules }}" + ls -la ./modules/ +``` + +### Different Branches per Module + +```yaml +- uses: Nodarx/action-module-install@v1 + with: + modules: | + [ + {"name": "core", "repo": "https://git.ktrix.dev/Nodarx/core", "branch": "v2.0"}, + {"name": "plugin", "repo": "https://git.ktrix.dev/Nodarx/plugin", "branch": "develop"}, + {"name": "theme", "repo": "https://git.ktrix.dev/Nodarx/theme", "branch": "stable1"} + ] +``` + +### Custom Install Path + +```yaml +- uses: Nodarx/action-module-install@v1 + with: + install-path: './custom/modules/directory' + modules: | + [ + {"name": "module1", "repo": "https://git.ktrix.dev/Nodarx/module1"} + ] +``` + +## How It Works + +1. **Validation**: Validates JSON structure and checks that all dependencies are defined +2. **Dependency Resolution**: Uses topological sort to determine correct installation order +3. **Cloning**: Clones each module repository in dependency order +4. **Installation**: Auto-detects and runs: + - `composer install` for modules with `composer.json` + - `npm install` or `npm ci` for modules with `package.json` +5. **Verification**: Verifies all modules installed successfully + +## Dependency Resolution + +The action automatically determines the correct installation order: + +``` +Input modules: +- mail (depends on: mail_manager, provider_jmapc) +- mail_manager (no dependencies) +- provider_jmapc (no dependencies) + +Installation order: +1. mail_manager +2. provider_jmapc +3. mail +``` + +Dependencies are installed **before** their dependents, ensuring all required modules are available when needed. + +## Error Handling + +The action will fail with descriptive errors for: + +- ❌ **Invalid JSON**: Syntax errors in modules input +- ❌ **Missing Fields**: Modules without `name` or `repo` +- ❌ **Circular Dependencies**: A → B → A +- ❌ **Missing Dependencies**: Module depends on undefined module +- ❌ **Clone Failures**: Repository not accessible or branch doesn't exist +- ❌ **Install Failures**: Composer or npm installation errors +- ❌ **Missing Prerequisites**: PHP/Composer or Node/npm not installed + +## Prerequisites + +Before using this action, ensure PHP and/or Node.js are installed: + +```yaml +- uses: Nodarx/action-server-install@v1 + with: + install-php: 'true' # If modules use Composer + install-node: 'true' # If modules use npm +``` + +Or use standard setup actions: + +```yaml +- uses: actions/setup-node@v4 + with: + node-version: '24' + +- uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + tools: composer:v2 +``` + +## Troubleshooting + +### Circular Dependency Error + +``` +Error: Circular dependency detected. Cannot determine installation order. +``` + +**Solution**: Review your dependencies to remove circular references. Example: +``` +❌ moduleA depends on moduleB, moduleB depends on moduleA +✅ moduleA depends on moduleB, moduleB has no dependencies +``` + +### Missing Dependency Error + +``` +Error: The following dependencies are referenced but not defined: module_x +``` + +**Solution**: Add the missing module to your modules array: +```json +[ + {"name": "module_x", "repo": "https://..."}, + {"name": "dependent", "repo": "https://...", "dependencies": ["module_x"]} +] +``` + +### Composer/npm Not Found + +``` +Error: composer not found. Please ensure PHP and Composer are installed +``` + +**Solution**: Install PHP/Node.js before this action: +```yaml +- uses: Nodarx/action-server-install@v1 + with: + install-php: 'true' + install-node: 'true' +``` + +### Clone Failed (Branch Not Found) + +``` +Error: Failed to clone module_name from https://... (branch: xyz) +``` + +**Solution**: Verify the branch exists in the repository. Use `main`, `master`, or a valid branch/tag name. + +## Complete Workflow Example + +```yaml +name: Deploy Application with Modules + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup server environment + uses: Nodarx/action-server-install@v1 + with: + install-php: 'true' + install-node: 'true' + php-version: '8.5' + node-version: '24' + server-path: './server' + + - name: Install application modules + id: install-modules + uses: Nodarx/action-module-install@v1 + with: + modules: | + [ + { + "name": "mail_manager", + "repo": "https://git.ktrix.dev/Nodarx/mail_manager", + "branch": "main" + }, + { + "name": "provider_jmapc", + "repo": "https://git.ktrix.dev/Nodarx/provider_jmapc", + "branch": "stable1" + }, + { + "name": "mail", + "repo": "https://git.ktrix.dev/Nodarx/mail", + "branch": "main", + "dependencies": ["mail_manager", "provider_jmapc"] + }, + { + "name": "calendar", + "repo": "https://git.ktrix.dev/Nodarx/calendar", + "branch": "main", + "dependencies": ["mail_manager"] + } + ] + install-path: './server/modules' + + - name: Display installation results + run: | + echo "Installation Status: ${{ steps.install-modules.outputs.status }}" + echo "Installed Modules: ${{ steps.install-modules.outputs.installed-modules }}" + echo "Install Path: ${{ steps.install-modules.outputs.install-path }}" + + echo "" + echo "Module directories:" + ls -la ${{ steps.install-modules.outputs.install-path }} + + - name: Run application tests + run: | + cd ./server + npm test +``` + +## License + +MIT License - see [LICENSE](LICENSE) file for details. + +## Author + +Created by [Nodarx](https://github.com/Nodarx) diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..984ab91 --- /dev/null +++ b/action.yml @@ -0,0 +1,385 @@ +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) + echo "$MODULES_JSON" | jq -r '.[] | select(.dependencies != null and (.dependencies | length) > 0) | .name as $$module | .dependencies[] | "\($$module) \(.)"' > "$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 // "main"') + MODULE_PATH="$INSTALL_PATH/$MODULE_NAME" + + echo "Repository: $REPO_URL" + echo "Branch: $BRANCH" + 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" + git pull origin "$BRANCH" || echo "::warning::Pull failed, continuing with existing code" + cd - > /dev/null + else + 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 + 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 + if [ -d "$MODULE_PATH/node_modules" ]; then + echo " ✓ Node dependencies installed (node_modules/ exists)" + else + echo "::error::package.json exists but node_modules/ directory not found in $MODULE_NAME" + exit 1 + fi + fi + + echo " ✓ $MODULE_NAME verified" + done + + echo "" + echo "✓ All modules verified successfully" + echo "::endgroup::"