Public/Select-EnvkEnvironment.ps1
|
function Select-EnvkEnvironment { <# .SYNOPSIS Determines which configured environment is active based on the current day, time, and schedule rules. .DESCRIPTION Evaluates the environments defined in the Envoke config object and returns the name of the environment that should be active right now. Supports schedule-based automatic detection, priority-based overlap resolution, optional fallback environments, and a manual override parameter. The function does NOT call Get-EnvkConfig -- it receives the already-loaded config object. This separation keeps Select-EnvkEnvironment testable without file I/O. The function does NOT call Test-BootGuard -- the boot guard is a separate concern managed by the orchestrator (Phase 7). Select-EnvkEnvironment is purely schedule evaluation. Logic flow: 1. Environment override: if -Environment is provided, validate it exists in config and return it immediately (schedule detection skipped). 2. Collect schedule matches: iterate environments; record the no-schedule environment as the fallback; for each scheduled environment, test day and hour match; collect all matches with their priorities. 3. No matches + fallback: log and return the fallback environment name. 4. No matches + no fallback: call Get-NextScheduledEnv, log the next activation time, and return $null. 5. Single top-priority match: log and return the matched environment name. 6. Priority conflict (tie at top): log warning; in interactive+debug mode prompt user; otherwise use first match by config order and log a warning. Hour boundary: endHour is exclusive. An environment with endHour=15 is NOT active at hour 15 (deactivates at the start of hour 15, i.e., 3:00 PM). Day matching: uses DayOfWeek.ToString() to produce the full English day name ("Monday", "Tuesday", etc.) matching the config schema enum values. This avoids implicit enum coercion issues between PS 5.1 and PS 7. .PARAMETER Config The parsed config object returned by Get-EnvkConfig. Mandatory. .PARAMETER Environment Optional environment name override. When provided, schedule detection is skipped and this environment is returned directly after validation. Validated dynamically against the config environment names at runtime -- no ValidateSet is used, which allows arbitrary custom environment profiles without code changes (ENV-02). .PARAMETER EnableDebug When set, enables the interactive PromptForChoice conflict resolution path (in combination with [System.Environment]::UserInteractive). In non-interactive sessions such as Task Scheduler, the prompt path is never reached regardless of this flag. .PARAMETER Now The reference datetime for schedule evaluation. Defaults to the current system time (Get-Date). Injectable for deterministic testing. .OUTPUTS System.String The name of the selected environment, or $null when no environment matches and no fallback is configured. .NOTES Author: Aaron AlAnsari Created: 2026-02-25 Priority field: optional integer on each environment. Higher integer wins when multiple environments match the current schedule. Default is 0 when not specified. Overlap conflict: when two or more environments share the same top priority and both match the schedule, behavior depends on the session type: - Interactive + EnableDebug: PromptForChoice (user picks) - Non-interactive or not EnableDebug: first match in config order (with warning) #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [PSCustomObject]$Config, [Parameter()] [string]$Environment, [Parameter()] [switch]$EnableDebug, [Parameter()] [datetime]$Now = (Get-Date) ) # --------------------------------------------------------------------------- # 1. Environment override (-Environment parameter) # --------------------------------------------------------------------------- if (-not [string]::IsNullOrEmpty($Environment)) { $envNames = @($Config.environments.PSObject.Properties.Name) if ($envNames -notcontains $Environment) { $validList = $envNames -join ', ' throw "Environment '$Environment' not found in config. Valid environments: $validList" } Write-EnvkLog -Level 'INFO' -Message "Environment override: $Environment (schedule detection skipped)" return $Environment } # --------------------------------------------------------------------------- # 2. Collect schedule matches # --------------------------------------------------------------------------- $matchedEnvs = [System.Collections.Generic.List[PSCustomObject]]::new() $fallbackName = $null foreach ($prop in $Config.environments.PSObject.Properties) { $envName = $prop.Name $envValue = $prop.Value if ($null -eq $envValue.schedule) { # No schedule = fallback environment. $fallbackName = $envName continue } $sched = $envValue.schedule # Day match: use .ToString() to produce the full English day name to avoid # implicit enum coercion differences between PS 5.1 and PS 7. $dayMatch = $sched.days -contains $Now.DayOfWeek.ToString() # Hour match: end-exclusive. Hour 15 does NOT match an environment with endHour=15. $hourMatch = $Now.Hour -ge $sched.startHour -and $Now.Hour -lt $sched.endHour if ($dayMatch -and $hourMatch) { $priority = if ($null -ne $envValue.priority) { $envValue.priority } else { 0 } $matchedEnvs.Add([PSCustomObject]@{ Name = $envName Priority = $priority }) } } # --------------------------------------------------------------------------- # 3. No schedule matches # --------------------------------------------------------------------------- if ($matchedEnvs.Count -eq 0) { if ($null -ne $fallbackName) { Write-EnvkLog -Level 'INFO' -Message "No schedule match. Using fallback environment: $fallbackName" return $fallbackName } # No fallback either -- log next activation and return $null. $next = Get-NextScheduledEnv -Config $Config -Now $Now Write-EnvkLog -Level 'INFO' -Message "No environment matches current schedule. Next: $next" return $null } # --------------------------------------------------------------------------- # 4. Priority resolution # --------------------------------------------------------------------------- $topPriority = ($matchedEnvs | Sort-Object -Property Priority -Descending)[0].Priority $topMatches = @($matchedEnvs | Where-Object { $_.Priority -eq $topPriority }) # --------------------------------------------------------------------------- # 5. Single match at top priority # --------------------------------------------------------------------------- if ($topMatches.Count -eq 1) { $selected = $topMatches[0].Name Write-EnvkLog -Level 'INFO' -Message "Schedule matched: $selected" return $selected } # --------------------------------------------------------------------------- # 6. Priority conflict (multiple environments at same top priority) # --------------------------------------------------------------------------- $conflictNames = ($topMatches | ForEach-Object { $_.Name }) -join ', ' Write-EnvkLog -Level 'WARNING' -Message "Schedule overlap: $conflictNames all match at priority $topPriority" if ([System.Environment]::UserInteractive -and $EnableDebug) { # Interactive + debug mode: prompt the user to choose. $choices = $topMatches | ForEach-Object { [System.Management.Automation.Host.ChoiceDescription]::new("&$($_.Name)", "Select the $($_.Name) environment") } $idx = $Host.UI.PromptForChoice( 'Schedule Conflict', 'Multiple environments match. Choose one:', $choices, 0 ) $selected = $topMatches[$idx].Name Write-EnvkLog -Level 'INFO' -Message "User selected: $selected" return $selected } # Non-interactive or not debug mode: use first match by config order. $selected = $topMatches[0].Name Write-EnvkLog -Level 'WARNING' -Message "Non-interactive mode: using first match by config order: $selected" return $selected } |