Assets/JobRunner.ps1

<#
.SYNOPSIS
    Master Wrapper for executing Ops jobs.
.DESCRIPTION
    Runs PowerShell or Node.js scripts, handles logging, error catching, and alerting.
.PARAMETER ScriptPath
    Full path to the script to execute.
.PARAMETER JobName
    Name of the job for logging and alerting.
.PARAMETER LogLevel
    Logging level (DEBUG, INFO, WARNING, ERROR).
.PARAMETER ScriptArguments
    Arguments to pass to the script.
.PARAMETER EmailRecipients
    Comma-separated list of email addresses for failure alerts.
.PARAMETER AlertWebhookUrl
    Webhook URL for failure alerts (e.g., Slack, Teams).
.PARAMETER RequiredSecrets
    List of secret names to inject as environment variables.
#>

param (
  [Parameter(Mandatory = $true)]
  [string]$ScriptPath,

  [Parameter(Mandatory = $true)]
  [string]$JobName,

  [string]$LogLevel = "INFO",

  [string[]]$ScriptArguments = @(),

  [string[]]$EmailRecipients = @(),

  [string]$AlertWebhookUrl,

  [string[]]$RequiredSecrets = @()
)

$ErrorActionPreference = "Stop"
$env:OPS_LOG_LEVEL = $LogLevel

# Event Log Source
$EventSource = "WinBatchOrchestrator"
if (-not ([System.Diagnostics.EventLog]::SourceExists($EventSource))) {
  # Fallback if source doesn't exist (requires admin to create)
  $EventSource = "Application"
}

# Import Utils
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Import-Module (Join-Path $ScriptDir "OpsUtils.psm1") -Force

$LogDir = "C:\Ops\Logs"
if (-not (Test-Path $LogDir)) {
  New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}

$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$LogFile = Join-Path $LogDir "$JobName-$Timestamp.log"
$JsonLogFile = Join-Path $LogDir "$JobName.history.json" # Appending log for history

$env:OPS_LOG_FILE = $JsonLogFile

Start-Transcript -Path $LogFile -Append | Out-Null

# Concurrency Locking
$MutexName = "Global\OpsJob-$JobName"
$Mutex = $null
try {
  $Mutex = New-Object System.Threading.Mutex($false, $MutexName)
}
catch {
  Write-Warning "Could not create Mutex. Proceeding without concurrency check."
}

if ($null -ne $Mutex) {
  if (-not $Mutex.WaitOne(0, $false)) {
    Write-OpsLog -Message "Job $JobName is already running. Skipping execution." -Level "WARNING"
    Stop-Transcript | Out-Null
    exit 0
  }
}

Write-OpsLog -Message "Starting Job: $JobName" -Level "INFO"
Write-OpsLog -Message "Script: $ScriptPath" -Level "INFO"
Write-EventLog -LogName Application -Source $EventSource -EntryType Information -EventId 100 -Message "Starting OpsJob: $JobName"

$ExitCode = 0

try {
  # Secret Injection
  foreach ($SecretName in $RequiredSecrets) {
    $SecretPath = Join-Path "C:\Ops\Secrets" "$SecretName.xml"
    if (Test-Path $SecretPath) {
      try {
        $Cred = Import-Clixml -Path $SecretPath
        $EnvVarName = "SECRET_$($SecretName.ToUpper())"
        Set-Item -Path "env:$EnvVarName" -Value $Cred.GetNetworkCredential().Password
        Write-OpsLog -Message "Injected secret: $SecretName as $EnvVarName" -Level "DEBUG"
      }
      catch {
        Write-OpsLog -Message "Failed to load secret: $SecretName" -Level "WARNING"
      }
    }
    else {
      Write-OpsLog -Message "Secret not found: $SecretName" -Level "WARNING"
    }
  }

  if (-not (Test-Path $ScriptPath)) {
    throw "Script not found: $ScriptPath"
  }

  $Extension = [System.IO.Path]::GetExtension($ScriptPath).ToLower()

  if ($Extension -eq ".ps1") {
    Write-OpsLog -Message "Executing PowerShell script..." -Level "INFO"
    & $ScriptPath @ScriptArguments
  }
  elseif ($Extension -eq ".js") {
    Write-OpsLog -Message "Executing Node.js script..." -Level "INFO"
        
    # Node.js Dependency Check
    $ScriptDir = Split-Path -Parent $ScriptPath
    if (Test-Path (Join-Path $ScriptDir "package.json")) {
      if (-not (Test-Path (Join-Path $ScriptDir "node_modules"))) {
        Write-OpsLog -Message "Installing Node.js dependencies..." -Level "INFO"
        $InstallProcess = Start-Process -FilePath "npm" -ArgumentList "install --production" -WorkingDirectory $ScriptDir -PassThru -Wait -NoNewWindow
        if ($InstallProcess.ExitCode -ne 0) {
          Write-OpsLog -Message "npm install failed with code $($InstallProcess.ExitCode)" -Level "WARNING"
        }
      }
    }

        $NodeArgs = @($ScriptPath) + $ScriptArguments
        $Process = Start-Process -FilePath "node" -ArgumentList $NodeArgs -PassThru -Wait -NoNewWindow
        if ($Process.ExitCode -ne 0) {
            throw "Node.js script exited with code $($Process.ExitCode)"
        }
    }
    else {
        throw "Unsupported script extension: $Extension"
    }

    Write-OpsLog -Message "Job completed successfully." -Level "INFO"
    Write-EventLog -LogName Application -Source $EventSource -EntryType Information -EventId 101 -Message "OpsJob Success: $JobName"
}
catch {
    $ExitCode = 1
    $ErrorMessage = $_.Exception.Message
    Write-OpsLog -Message "Job Failed: $ErrorMessage" -Level "ERROR"
    Write-EventLog -LogName Application -Source $EventSource -EntryType Error -EventId 102 -Message "OpsJob Failed: $JobName`nError: $ErrorMessage"
    
    # Email Alert
    if ($EmailRecipients.Count -gt 0) {
        Write-OpsLog -Message "Sending alert email..." -Level "INFO"
        $SmtpServer = "smtp.example.com" 
        $From = "ops-alerts@example.com"
        $Subject = "FAILURE: Job $JobName"
        $Body = "Job $JobName failed.`n`nError: $ErrorMessage`n`nSee attached log."
        
        try {
            Send-MailMessage -To $EmailRecipients -From $From -Subject $Subject -Body $Body -SmtpServer $SmtpServer -Attachments $LogFile -ErrorAction Stop
        }
        catch {
            Write-OpsLog -Message "Failed to send email: $_" -Level "ERROR"
        }
    }

    # Webhook Alert
    if (-not [string]::IsNullOrWhiteSpace($AlertWebhookUrl)) {
        Write-OpsLog -Message "Sending webhook alert..." -Level "INFO"
        $Payload = @{
            text = "FAILURE: Job $JobName"
            attachments = @(@{
                color = "danger"
                title = "Job Failed: $JobName"
                text = "Error: $ErrorMessage"
                fields = @(
                    @{ title = "Script"; value = $ScriptPath; short = $false }
                    @{ title = "Log"; value = $LogFile; short = $false }
                )
            })
        } | ConvertTo-Json -Depth 5

        try {
            Invoke-RestMethod -Uri $AlertWebhookUrl -Method Post -Body $Payload -ContentType "application/json" -ErrorAction Stop
        } catch {
            Write-OpsLog -Message "Failed to send webhook: $_" -Level "ERROR"
        }
    }
}
finally {
    if ($null -ne $Mutex) {
        $Mutex.ReleaseMutex()
        $Mutex.Dispose()
    }
    Stop-Transcript | Out-Null
    if ($ExitCode -ne 0) {
        exit $ExitCode
    }
}