core/private/Invoke-JaxContainerRunner.ps1

function Invoke-JaxContainerRunner {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary] $Entity,
        [hashtable] $Context = @{},
        [hashtable] $CommonParameters = @{}
    )

    $vaultTainted = $Context.ContainsKey('VaultSecretsResolved') -and [bool]$Context['VaultSecretsResolved']
    $debugImage = if ($vaultTainted) { '<redacted>' } else { $Entity['Container']['image'] }
    Write-Debug "FUNC: $($MyInvocation.MyCommand.Name) Args: $($Entity['Key']) Image=$debugImage"

    $containerConfig = $Entity['Container']
    $image = $null
    if ($containerConfig -is [System.Collections.IDictionary] -and $containerConfig.Contains('image')) {
        $image = $containerConfig['image']
    }
    if ([string]::IsNullOrWhiteSpace($image) -and $Context.ContainsKey('ModuleDockerImage')) {
        $image = $Context['ModuleDockerImage']
    }
    if ([string]::IsNullOrWhiteSpace($image) -and $Context.ContainsKey('DefaultContainerImage')) {
        $image = $Context['DefaultContainerImage']
    }
    if ([string]::IsNullOrWhiteSpace($image)) {
        throw "Container runner requires an image (container.image or module.docker.image)."
    }

    $repoRoot = $Context['RepoRoot']
    if ([string]::IsNullOrWhiteSpace($repoRoot)) {
        $repoRoot = Get-JaxRepoRoot @CommonParameters
    }

    $mounts = @()
    $mounts += "${repoRoot}:/jax/src"
    if ($containerConfig -is [System.Collections.IDictionary] -and $containerConfig.Contains('mounts')) {
        $mounts += @($containerConfig['mounts'])
    }

    $envVars = @()
    if ($containerConfig -is [System.Collections.IDictionary] -and $containerConfig.Contains('env')) {
        $envVars += @($containerConfig['env'])
    }
    if (Test-Path Env:VAULT_ADDR) {
        $envVars += @("VAULT_ADDR=$((Get-ChildItem -Path Env:VAULT_ADDR).Value)")
    }

    $innerRunner = $Entity['InnerRunner']
    if ([string]::IsNullOrWhiteSpace($innerRunner)) {
        $innerRunner = $Entity['Runner']
    }
    $innerRunner = Resolve-JaxRunnerName -Name $innerRunner @CommonParameters

    $containerCommand = @()
    if ($innerRunner -in @('script', 'pwshscript')) {
        $scriptPath = Resolve-JaxContainerPath -HostPath $Entity['Script'] -RepoRoot $repoRoot @CommonParameters
        $containerCommand = @('pwsh', '-NoProfile', '-File', $scriptPath)
        $entityArgs = $Entity['Args']
        if ($entityArgs -is [System.Collections.IDictionary]) {
            foreach ($key in $entityArgs.Keys) {
                $value = $entityArgs[$key]
                if ($value -is [bool]) {
                    if ($value) { $containerCommand += "-$key" }
                    continue
                }
                $containerCommand += "-$key"
                $containerCommand += [string]$value
            }
        } elseif ($entityArgs -is [System.Collections.IEnumerable] -and $entityArgs -isnot [string]) {
            $containerCommand += @($entityArgs | ForEach-Object { [string]$_ })
        }
    } elseif ($innerRunner -eq 'psake') {
        $buildFile = $Entity['PsakeFile']
        if ([string]::IsNullOrWhiteSpace($buildFile) -and $Context.ContainsKey('PsakeFile')) {
            $buildFile = $Context['PsakeFile']
        }
        if ([string]::IsNullOrWhiteSpace($buildFile)) {
            throw "Container psake runner requires 'PsakeFile' (entity or context)."
        }
        $buildFile = Resolve-JaxContainerPath -HostPath $buildFile -RepoRoot $repoRoot @CommonParameters
        $taskList = @()
        if ($Entity.Contains('Tasks') -and $null -ne $Entity['Tasks']) {
            $taskList = @($Entity['Tasks'])
        }
        $escapedBuildFile = ([string]$buildFile).Replace("'", "''")
        $escapedTasks = @($taskList | ForEach-Object { ([string]$_).Replace("'", "''") })
        $taskListLiteral = if ($escapedTasks.Count -gt 0) { "@('" + ($escapedTasks -join "','") + "')" } else { '@()' }
        $psakeCommand = "Import-Module psake; Invoke-psake -buildFile '$escapedBuildFile' -taskList $taskListLiteral -nologo"
        $containerCommand = @('pwsh', '-NoProfile', '-Command', $psakeCommand)
    } else {
        throw "Unsupported inner runner for container execution: '$innerRunner'."
    }

    $dockerArgs = @('run', '--rm')
    foreach ($mount in $mounts) {
        $dockerArgs += @('-v', [string]$mount)
    }
    foreach ($envVar in $envVars) {
        $dockerArgs += @('-e', [string]$envVar)
    }
    $dockerArgs += [string]$image
    $dockerArgs += $containerCommand

    function Format-JaxContainerDisplayArgument {
        param([AllowEmptyString()] [string] $Value)

        if ($Value -notmatch '[\s''"]') { return $Value }
        return "'" + $Value.Replace("'", "''") + "'"
    }

    $displayArgs = @('run', '--rm')
    foreach ($mount in $mounts) {
        $displayMount = if ($vaultTainted) { '<redacted>' } else { Format-JaxContainerDisplayArgument ([string]$mount) }
        $displayArgs += @('-v', $displayMount)
    }
    foreach ($envVar in $envVars) {
        $envName = ([string]$envVar -split '=', 2)[0]
        $displayArgs += @('-e', "${envName}=<redacted>")
    }
    if ($vaultTainted) {
        $displayArgs += '<image redacted>'
        $displayArgs += '<arguments redacted>'
    } else {
        $displayArgs += Format-JaxContainerDisplayArgument ([string]$image)
        $displayArgs += @($containerCommand | ForEach-Object { Format-JaxContainerDisplayArgument ([string]$_) })
    }
    $dockerCommandDisplay = 'docker ' + ($displayArgs -join ' ')

    if ($Context['DryRun']) {
        return $dockerCommandDisplay
    }

    $imageDisplay = if ($vaultTainted) { '<redacted>' } else { $image }
    Write-Host "🐳 Running in container: $imageDisplay"
    Write-Host "🧷 docker run: $dockerCommandDisplay"
    & docker @dockerArgs
}