Private/Read-AzLocalApplyUpdatesYamlCrons.ps1
|
function Read-AzLocalApplyUpdatesYamlCrons { <# .SYNOPSIS Extracts cron schedule entries from apply-updates pipeline YAML files (GitHub Actions and Azure DevOps). .DESCRIPTION Pure regex pre-scan; deliberately does NOT take a dependency on powershell-yaml. Used by Test-AzLocalApplyUpdatesScheduleCoverage. Discovery rules: - If Path is a file, scan that file. - If Path is a directory, recursively find files matching any of 'Step.6_apply-updates*.yml', 'Step.6_apply-updates*.yaml', 'apply-updates*.yml', or 'apply-updates*.yaml'. (The 'Step.5_' prefix is the v0.7.68+ shipped name; the un-prefixed form is the legacy name still supported for backwards compatibility.) Platform is inferred from the parent directory name when the YAML is under .../github-actions/ or .../azure-devops/. Falls back to the Platform parameter when path-based inference is inconclusive. Parsing rules: - GitHub Actions: lines matching '- cron: "<expr>"' or "- cron: '<expr>'" under a `schedule:` map. - Azure DevOps: `cron:` keys inside a `schedules:` list. Same regex works because cron lines look identical. Cron expressions wrapped in single or double quotes are both accepted. .PARAMETER Path File or directory to scan. .PARAMETER Platform Default platform tag when path inference is inconclusive. One of 'GitHubActions', 'AzureDevOps', or 'Unknown'. .OUTPUTS PSCustomObject[] - one per discovered cron, with: File - full path RelativePath - path relative to Path (or basename if Path is a file) Platform - 'GitHubActions' | 'AzureDevOps' | 'Unknown' CronExpression - the cron string (quotes stripped) LineNumber - 1-based line in the source file .EXAMPLE Read-AzLocalApplyUpdatesYamlCrons -Path .\Automation-Pipeline-Examples #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $false)] [ValidateSet('GitHubActions', 'AzureDevOps', 'Unknown')] [string]$Platform = 'Unknown' ) if (-not (Test-Path -LiteralPath $Path)) { throw "Path not found: $Path" } $item = Get-Item -LiteralPath $Path $files = if ($item.PSIsContainer) { # NOTE: Get-ChildItem -LiteralPath -Recurse -Include silently ignores the # -Include filter and returns every recursed file (confirmed in PS 5.1). # That caused v0.7.68 to pick up every Step.N_*.yml sibling (Step.1, Step.3, # Step.6, Step.7 all carry their own schedule crons) and treat their crons # as apply-updates crons - garbage in the audit, and on PS 7 the binder # surfaced it as 'Cannot bind argument to parameter Expression because it # is an empty string' once any unparseable capture was reached. # Use -Filter (which is honoured under -Recurse) one pattern at a time, # then dedupe by FullName. $patterns = @( 'Step.6_apply-updates*.yml', 'Step.6_apply-updates*.yaml', 'apply-updates*.yml', 'apply-updates*.yaml' ) $hits = @() foreach ($pattern in $patterns) { $hits += @(Get-ChildItem -Path $item.FullName -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue) } @($hits | Sort-Object FullName -Unique) } else { @($item) } $output = New-Object System.Collections.Generic.List[PSCustomObject] foreach ($f in $files) { $inferred = $Platform if ($f.FullName -match '[\\/]github-actions[\\/]') { $inferred = 'GitHubActions' } elseif ($f.FullName -match '[\\/]azure-devops[\\/]') { $inferred = 'AzureDevOps' } $relative = if ($item.PSIsContainer) { $f.FullName.Substring($item.FullName.Length).TrimStart('\','/') } else { $f.Name } $lineNum = 0 $lines = Get-Content -LiteralPath $f.FullName -ErrorAction Stop foreach ($line in $lines) { $lineNum++ # Match cron lines in both quote styles. Allow leading dash + space for # list-style entries (- cron: '...') and bare key style (cron: '...'). if ($line -match "^\s*-?\s*cron\s*:\s*['""]([^'""]+)['""]") { $expr = $matches[1].Trim() # The character class [^'""]+ also matches whitespace runs, so # `cron: ' '` would survive with an empty capture after Trim(). # Skip those instead of feeding an empty string to a downstream # [Parameter(Mandatory)][string] binder. if ([string]::IsNullOrWhiteSpace($expr)) { continue } $output.Add([PSCustomObject]@{ File = $f.FullName RelativePath = $relative Platform = $inferred CronExpression = $expr LineNumber = $lineNum }) } } } # WARNING: Callers MUST use direct assignment ($x = func ...) and NEVER # wrap with @(func ...). The unary-comma return below preserves Object[N] # shape for any N including 0 and 1, but @() at the call site collapses # to Object[1] containing the inner array, silently producing one-row # output instead of N rows. See `docs/MODULE-REVIEW-AND-RECOMMENDATIONS.md` # Finding 1 for the v0.7.75 incident. return , $output.ToArray() } |