solarboat 0.8.2

A CLI tool for intelligent Terraform operations management with automatic dependency detection
Documentation
name: 'Solarboat CLI Action'
description: 'Run Solarboat CLI commands in your GitHub Actions workflow'
branding:
  icon: 'anchor'
  color: 'blue'

inputs:
  command:
    description: 'Command to run (scan, plan, or apply)'
    required: true
  output-dir:
    description: 'Directory to save Terraform plan files'
    default: 'terraform-plans'
    required: false
  apply-dryrun:
    description: 'Run apply in dry-run mode (enabled by default for safety)'
    default: 'true'
    required: false
  ignore-workspaces:
    description: 'Comma-separated list of workspaces to ignore'
    required: false
    default: ''
  path:
    description: 'Directory to scan for modules (default: .)'
    required: false
    default: '.'
  all:
    description: 'Process all stateful modules regardless of changes'
    required: false
    default: 'false'
  watch:
    description: 'Show real-time output (forces parallel=1)'
    required: false
    default: 'false'
  parallel:
    description: 'Number of parallel module processes (max 4)'
    required: false
    default: '1'
  default-branch:
    description: 'Default git branch to compare against for changes'
    required: false
    default: 'main'
  var-files:
    description: 'Comma-separated list of var files to use'
    required: false
    default: ''
  config:
    description: 'Path to Solarboat configuration file'
    required: false
    default: ''
  solarboat-version:
    description: 'Version of Solarboat CLI to use (default: latest)'
    required: false
    default: 'latest'
  terraform-version:
    description: 'Version of Terraform to use (default: latest)'
    required: false
    default: 'latest'
  continue-on-error:
    description: 'Continue workflow even if Solarboat fails'
    required: false
    default: 'false'
  github_token:
    description: 'GitHub token for PR comments and API access'
    required: false

outputs:
  result:
    description: 'Result of the Solarboat command (success, failure)'
    value: ${{ steps.solarboat.outputs.result }}
  plans-path:
    description: 'Path to generated Terraform plans'
    value: ${{ steps.solarboat.outputs.plans-path }}
  changed-modules:
    description: 'Number of changed modules found'
    value: ${{ steps.solarboat.outputs.changed-modules }}

runs:
  using: 'composite'
  steps:
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ inputs.terraform-version }}
        terraform_wrapper: false

    - name: Install Solarboat CLI
      shell: bash
      run: |
        echo "🚀 Installing Solarboat CLI"
        
        # Determine version to install
        VERSION="${{ inputs.solarboat-version }}"
        if [ "$VERSION" = "latest" ]; then
          VERSION=$(curl -s https://api.github.com/repos/devqik/solarboat/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
          echo "Latest version detected: $VERSION"
        fi
        
        # Detect architecture
        ARCH=$(uname -m)
        OS=$(uname -s | tr '[:upper:]' '[:lower:]')
        
        case "$ARCH" in
          x86_64) ARCH="x86_64" ;;
          aarch64|arm64) ARCH="aarch64" ;;
          *) echo "❌ Unsupported architecture: $ARCH" && exit 1 ;;
        esac
        
        case "$OS" in
          linux) OS="unknown-linux-gnu" ;;
          darwin) OS="apple-darwin" ;;
          *) echo "❌ Unsupported OS: $OS" && exit 1 ;;
        esac
        
        BINARY_NAME="solarboat-${ARCH}-${OS}"
        DOWNLOAD_URL="https://github.com/devqik/solarboat/releases/download/${VERSION}/${BINARY_NAME}.tar.gz"
        
        echo "Downloading from: $DOWNLOAD_URL"
        
        # Download and install with error handling
        if ! curl -L -f -o solarboat.tar.gz "$DOWNLOAD_URL"; then
          echo "❌ Failed to download Solarboat binary"
          echo "Falling back to cargo install..."
          cargo install solarboat --version "${VERSION#v}" || cargo install solarboat
        else
          tar -xzf solarboat.tar.gz
          chmod +x solarboat
          sudo mv solarboat /usr/local/bin/
          rm -f solarboat.tar.gz
        fi
        
        # Verify installation
        if solarboat --version; then
          echo "✅ Solarboat CLI installed successfully"
        else
          echo "❌ Solarboat installation verification failed"
          exit 1
        fi

    - name: Validate inputs
      shell: bash
      run: |
        echo "🔍 Validating inputs"
        
        # Validate command
        case "${{ inputs.command }}" in
          scan|plan|apply) 
            echo "✅ Valid command: ${{ inputs.command }}"
            ;;
          *)
            echo "❌ Invalid command: ${{ inputs.command }}"
            echo "Valid commands: scan, plan, apply"
            exit 1
            ;;
        esac
        
        # Validate parallel value
        if [ "${{ inputs.parallel }}" -gt 4 ]; then
          echo "⚠️ Parallel value capped at 4 (requested: ${{ inputs.parallel }})"
        fi
        
        # Validate paths exist
        if [ ! -d "${{ inputs.path }}" ]; then
          echo "❌ Path does not exist: ${{ inputs.path }}"
          exit 1
        fi
        
        # Check for git repository
        if ! git rev-parse --git-dir > /dev/null 2>&1; then
          echo "❌ Not a git repository. Solarboat requires git for change detection."
          exit 1
        fi
        
        echo "✅ Input validation passed"

    - name: Run Solarboat CLI
      id: solarboat
      shell: bash
      continue-on-error: ${{ inputs.continue-on-error == 'true' }}
      run: |
        echo "🚀 Running Solarboat CLI"
        
        # Set error handling
        set -euo pipefail
        
        # Build CLI args array
        ARGS=()
        
        # Add path argument
        if [ -n "${{ inputs.path }}" ] && [ "${{ inputs.path }}" != "." ]; then
          ARGS+=("--path" "${{ inputs.path }}")
        fi
        
        # Add configuration arguments
        if [ -n "${{ inputs.config }}" ]; then
          ARGS+=("--config" "${{ inputs.config }}")
        fi
        
        # Add common arguments
        if [ -n "${{ inputs.ignore-workspaces }}" ]; then
          ARGS+=("--ignore-workspaces" "${{ inputs.ignore-workspaces }}")
        fi
        
        if [ -n "${{ inputs.var-files }}" ]; then
          ARGS+=("--var-files" "${{ inputs.var-files }}")
        fi
        
        if [ "${{ inputs.all }}" = "true" ]; then
          ARGS+=("--all")
        fi
        
        if [ "${{ inputs.watch }}" = "true" ]; then
          ARGS+=("--watch")
        fi
        
        if [ -n "${{ inputs.parallel }}" ] && [ "${{ inputs.parallel }}" != "1" ]; then
          PARALLEL_VALUE=$(( ${{ inputs.parallel }} > 4 ? 4 : ${{ inputs.parallel }} ))
          ARGS+=("--parallel" "$PARALLEL_VALUE")
        fi
        
        if [ -n "${{ inputs.default-branch }}" ] && [ "${{ inputs.default-branch }}" != "main" ]; then
          ARGS+=("--default-branch" "${{ inputs.default-branch }}")
        fi
        
        # Command-specific arguments
        case "${{ inputs.command }}" in
          plan)
            if [ -n "${{ inputs.output-dir }}" ]; then
              ARGS+=("--output-dir" "${{ inputs.output-dir }}")
            fi
            ;;
          apply)
            if [ -n "${{ inputs.apply-dryrun }}" ]; then
              ARGS+=("--dry-run=${{ inputs.apply-dryrun }}")
            fi
            ;;
        esac
        
        # Execute command with error handling
        echo "Executing: solarboat ${{ inputs.command }} ${ARGS[*]}"
        
        # Capture output and exit code
        EXIT_CODE=0
        OUTPUT=$(solarboat "${{ inputs.command }}" "${ARGS[@]}" 2>&1) || EXIT_CODE=$?
        
        echo "$OUTPUT"
        
        # Set outputs
        if [ $EXIT_CODE -eq 0 ]; then
          echo "result=success" >> $GITHUB_OUTPUT
          echo "✅ Solarboat command completed successfully"
        else
          echo "result=failure" >> $GITHUB_OUTPUT
          echo "❌ Solarboat command failed with exit code: $EXIT_CODE"
        fi
        
        # Extract changed modules count from output
        CHANGED_MODULES=$(echo "$OUTPUT" | grep -o "Found [0-9]* changed modules" | grep -o "[0-9]*" || echo "0")
        echo "changed-modules=$CHANGED_MODULES" >> $GITHUB_OUTPUT
        
        # Set plans path for plan command
        if [ "${{ inputs.command }}" = "plan" ]; then
          echo "plans-path=${{ inputs.output-dir }}" >> $GITHUB_OUTPUT
        fi
        
        # Exit with original code if continue-on-error is false
        if [ "${{ inputs.continue-on-error }}" != "true" ] && [ $EXIT_CODE -ne 0 ]; then
          exit $EXIT_CODE
        fi

    - name: Upload Terraform Plans
      if: inputs.command == 'plan' && steps.solarboat.outputs.result == 'success'
      uses: actions/upload-artifact@v4
      with:
        name: terraform-plans-${{ github.run_number }}
        path: ${{ inputs.output-dir }}/
        retention-days: 30
        compression-level: 6
        if-no-files-found: warn

    - name: Generate Summary
      shell: bash
      run: |
        echo "## 🚀 Solarboat CLI Results" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY
        echo "### 📊 Execution Summary" >> $GITHUB_STEP_SUMMARY
        echo "- **Command**: \`${{ inputs.command }}\`" >> $GITHUB_STEP_SUMMARY
        echo "- **Result**: ${{ steps.solarboat.outputs.result == 'success' && '✅ Success' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY
        echo "- **Path**: \`${{ inputs.path }}\`" >> $GITHUB_STEP_SUMMARY
        echo "- **Changed Modules**: ${{ steps.solarboat.outputs.changed-modules }}" >> $GITHUB_STEP_SUMMARY
        
        if [ "${{ inputs.command }}" = "plan" ] && [ "${{ steps.solarboat.outputs.result }}" = "success" ]; then
          echo "- **Plans Location**: \`${{ steps.solarboat.outputs.plans-path }}\`" >> $GITHUB_STEP_SUMMARY
        fi
        
        echo "" >> $GITHUB_STEP_SUMMARY
        echo "### 🔧 Configuration" >> $GITHUB_STEP_SUMMARY
        echo "- **Parallel**: ${{ inputs.parallel }}" >> $GITHUB_STEP_SUMMARY
        echo "- **Default Branch**: ${{ inputs.default-branch }}" >> $GITHUB_STEP_SUMMARY
        
        if [ -n "${{ inputs.ignore-workspaces }}" ]; then
          echo "- **Ignored Workspaces**: \`${{ inputs.ignore-workspaces }}\`" >> $GITHUB_STEP_SUMMARY
        fi
        
        if [ -n "${{ inputs.var-files }}" ]; then
          echo "- **Variable Files**: \`${{ inputs.var-files }}\`" >> $GITHUB_STEP_SUMMARY
        fi

    - name: Comment on PR
      if: github.event_name == 'pull_request' && inputs.command == 'plan' && steps.solarboat.outputs.result == 'success' && inputs.github_token != ''
      uses: actions/github-script@v7
      with:
        github-token: ${{ inputs.github_token }}
        script: |
          const artifactUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts`;
          const changedModules = '${{ steps.solarboat.outputs.changed-modules }}';
          
          const comment = `## 🚀 Solarboat CLI Plan Results
          
          ### 📊 Summary
          - **Changed Modules**: ${changedModules}
          - **Status**: ✅ Plans generated successfully
          - **Artifact**: [terraform-plans-${{ github.run_number }}](${artifactUrl})
          
          ### 📋 Next Steps
          ${changedModules === '0' ? 
            '🎉 No modules were changed - no infrastructure changes detected.' :
            `1. 📥 Download and review the plan artifacts
          2. 🔍 Verify the planned changes are expected
          3. ✅ Approve and merge when ready`}
          
          ### 🔗 Links
          - [View Action Run](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})
          - [Download Plans](${artifactUrl})
          
          > 💡 Plans are retained for 30 days and include detailed change information for each module.`;
          
          // Check if we already commented
          const comments = await github.rest.issues.listComments({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: context.issue.number,
          });
          
          const existingComment = comments.data.find(comment => 
            comment.body.includes('🚀 Solarboat CLI Plan Results')
          );
          
          if (existingComment) {
            // Update existing comment
            await github.rest.issues.updateComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: existingComment.id,
              body: comment
            });
          } else {
            // Create new comment
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: comment
            });
          }