Automation-Pipeline-Examples/apply-module-development-channel.ps1

#Requires -Version 5.1
# AZLOCAL-DEVELOPMENT-CHANNEL-VERSION: 1.3.0
<#
.SYNOPSIS
    Pins AzLocal pipelines to an exact module version.
.DESCRIPTION
    This script is managed under the repository's DevChannel folder. It
    detects GitHub Actions when a .github/workflows folder exists at the parent
    repository root; otherwise it selects Azure DevOps. Use -Platform to
    override detection in an unusual mixed-layout repository.
 
    For GitHub, the script creates or updates the non-secret repository
    variable REQUIRED_MODULE_VERSION through the authenticated GitHub CLI. For
    Azure DevOps, it prints the exact variable-group create/update command for
    the supplied version. Every bundled pipeline then installs that exact
    PowerShell Gallery candidate through Install-Module -RequiredVersion.
 
    Before applying the pin, the script invokes the sibling
    Update-Module-And-Pipelines.ps1 with -RequiredVersion so the exact module
    is installed/imported and the pipeline YAMLs are refreshed from its
    templates. With -Disable, it invokes the updater with -LatestListed before
    removing or describing removal of the pin.
 
    By default, the script verifies that the exact candidate resolves from
    PowerShell Gallery before changing the repository. Use -Disable after the
    candidate is listed to delete the variable and restore latest-listed
    module resolution.
.PARAMETER RequiredVersion
    Exact AzLocal.UpdateManagement version to assign to the repository variable.
.PARAMETER Disable
    Deletes the repository variable and restores latest-listed resolution.
.PARAMETER Repository
    GitHub repository in owner/name form. Defaults to the repository associated
    with the current directory. Ignored for Azure DevOps.
.PARAMETER Platform
    Pipeline platform. Auto selects GitHub when .github/workflows exists at the
    repository root and AzureDevOps otherwise.
.PARAMETER VariableName
    Repository variable to manage. Defaults to REQUIRED_MODULE_VERSION.
.PARAMETER SkipGalleryValidation
    Skips the exact-version PowerShell Gallery lookup. Intended only for
    diagnosing Gallery propagation delays.
.PARAMETER Scope
    Install scope passed to Update-Module-And-Pipelines.ps1.
.PARAMETER NoPush
    Passes -NoPush to Update-Module-And-Pipelines.ps1 so refreshed files remain
    uncommitted for local review.
.PARAMETER SkipPipelineRefresh
    Skips local module installation/import and pipeline refresh. Use only when
    the repository content has already been prepared or when removing a stale
    pin during recovery.
.PARAMETER AllowRollbackMarkerRemoval
    Passes narrow rollback approval to Update-Module-And-Pipelines.ps1. Use
    only after reviewing customization sections that exist in the current
    YAMLs but not in the older target templates.
.PARAMETER KeepNewerVersions
    With -Disable, keeps locally installed module versions above the latest
    listed Gallery version. Without it, the updater confirms removal so a new
    PowerShell session cannot auto-load the former development candidate.
.EXAMPLE
    .\DevChannel\Apply-ModuleDevelopmentChannel.ps1 -RequiredVersion 0.9.29 -Repository contoso/azure-local-operations
 
    Validate v0.9.29 and pin every bundled GitHub pipeline in the target repo.
.EXAMPLE
    .\DevChannel\Apply-ModuleDevelopmentChannel.ps1 -Disable -Repository contoso/azure-local-operations
 
    Remove the development-channel pin after the candidate is listed.
.EXAMPLE
    .\DevChannel\Apply-ModuleDevelopmentChannel.ps1 -RequiredVersion 0.9.29 -Platform AzureDevOps
 
    Print the Azure DevOps variable-group commands for candidate v0.9.29.
.NOTES
    Requires GitHub CLI authentication with permission to manage Actions
    repository variables. REQUIRED_MODULE_VERSION is configuration, not a
    secret; never use this helper to store credentials.
#>

[CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Enable')]
param(
    [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Enable')]
    [ValidatePattern('^\d+\.\d+\.\d+(?:\.\d+)?$')]
    [version]$RequiredVersion,

    [Parameter(Mandatory = $true, ParameterSetName = 'Disable')]
    [switch]$Disable,

    [Parameter()]
    [ValidatePattern('^[^/\s]+/[^/\s]+$')]
    [string]$Repository,

    [Parameter()]
    [ValidateSet('Auto', 'GitHub', 'AzureDevOps')]
    [string]$Platform = 'Auto',

    [Parameter()]
    [ValidatePattern('^[A-Z][A-Z0-9_]{0,99}$')]
    [string]$VariableName = 'REQUIRED_MODULE_VERSION',

    [Parameter(ParameterSetName = 'Enable')]
    [switch]$SkipGalleryValidation,

    [Parameter()]
    [ValidateSet('CurrentUser', 'AllUsers')]
    [string]$Scope = 'CurrentUser',

    [Parameter()]
    [switch]$NoPush,

    [Parameter()]
    [switch]$SkipPipelineRefresh,

    [Parameter()]
    [switch]$AllowRollbackMarkerRemoval,

    [Parameter(ParameterSetName = 'Disable')]
    [switch]$KeepNewerVersions
)

$ErrorActionPreference = 'Stop'
$moduleName = 'AzLocal.UpdateManagement'

$effectivePlatform = $Platform
if ($effectivePlatform -eq 'Auto') {
    $repoRoot = Split-Path -Parent $PSScriptRoot
    $githubWorkflowsPath = Join-Path -Path $repoRoot -ChildPath '.github\workflows'
    $effectivePlatform = if (Test-Path -LiteralPath $githubWorkflowsPath -PathType Container) { 'GitHub' } else { 'AzureDevOps' }
    Write-Host "Detected pipeline platform: $effectivePlatform" -ForegroundColor Cyan
}

$versionText = $null
if (-not $Disable) {
    $versionText = $RequiredVersion.ToString()
    if (-not $SkipGalleryValidation) {
        try {
            $candidate = Find-Module -Name $moduleName -RequiredVersion $versionText -Repository PSGallery -ErrorAction Stop
        }
        catch {
            throw "PowerShell Gallery cannot resolve $moduleName $versionText by exact version. Wait for Gallery propagation or verify the version before retrying. $($_.Exception.Message)"
        }
        if ([version]$candidate.Version -ne $RequiredVersion) {
            throw "PowerShell Gallery returned $($candidate.Version), not requested version $versionText."
        }
    }
}

if (-not $SkipPipelineRefresh) {
    $repoRoot = Split-Path -Parent $PSScriptRoot
    $updaterPath = Join-Path -Path $repoRoot -ChildPath 'Update-Module-And-Pipelines.ps1'
    if (-not (Test-Path -LiteralPath $updaterPath -PathType Leaf)) {
        throw "Required sibling updater not found at '$updaterPath'. Re-run Copy-AzLocalPipelineExample or Update-AzLocalPipelineExample, or use -SkipPipelineRefresh only if the repository is already prepared."
    }

    $updaterParameters = @{
        Scope = $Scope
    }
    if ($Disable) {
        $updaterParameters.LatestListed = $true
    }
    else {
        $updaterParameters.RequiredVersion = $RequiredVersion
    }
    if ($NoPush) {
        $updaterParameters.NoPush = $true
    }
    if ($AllowRollbackMarkerRemoval) {
        $updaterParameters.AllowRollbackMarkerRemoval = $true
    }
    if ($KeepNewerVersions) {
        $updaterParameters.KeepNewerVersions = $true
    }

    $refreshDescription = if ($Disable) {
        'restore the latest listed module and pipeline templates'
    }
    else {
        "install module $versionText and refresh its pipeline templates"
    }
    if ($PSCmdlet.ShouldProcess($updaterPath, $refreshDescription)) {
        & $updaterPath @updaterParameters
    }
}

if ($effectivePlatform -eq 'AzureDevOps') {
    Write-Host 'Azure DevOps variable-group action required.' -ForegroundColor Yellow
    Write-Host 'Replace <group-id> with the ID of the variable group linked to the AzLocal pipelines.'
    if ($Disable) {
        Write-Host 'Remove the candidate pin to restore latest-listed module resolution:'
        Write-Host " az pipelines variable-group variable delete --group-id <group-id> --name $VariableName" -ForegroundColor Cyan
    }
    else {
        Write-Host "Create the candidate pin if $VariableName is absent:"
        Write-Host " az pipelines variable-group variable create --group-id <group-id> --name $VariableName --value $versionText" -ForegroundColor Cyan
        Write-Host 'Or update the existing variable:'
        Write-Host " az pipelines variable-group variable update --group-id <group-id> --name $VariableName --value $versionText" -ForegroundColor Cyan
    }
    return
}

if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
    throw 'GitHub CLI (gh) is required. Install it and run gh auth login.'
}

& gh auth status *> $null
if ($LASTEXITCODE -ne 0) {
    throw 'GitHub CLI is not authenticated. Run gh auth login and retry.'
}

if (-not $Repository) {
    $repoOutput = @(& gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>&1)
    if ($LASTEXITCODE -ne 0 -or -not $repoOutput) {
        throw "Could not infer the GitHub repository from the current directory. Pass -Repository owner/name. $($repoOutput -join ' ')"
    }
    $Repository = ([string]$repoOutput[-1]).Trim()
}

if ($Disable) {
    $existing = @(& gh variable list --repo $Repository --json name --jq ".[] | select(.name == `"$VariableName`") | .name" 2>&1)
    if ($LASTEXITCODE -ne 0) {
        throw "Could not inspect repository variables for '$Repository'. $($existing -join ' ')"
    }

    if (-not (@($existing | Where-Object { ([string]$_).Trim() -eq $VariableName }).Count)) {
        Write-Host "$VariableName is already absent from $Repository. Latest-listed module resolution is active." -ForegroundColor Green
        return
    }

    if ($PSCmdlet.ShouldProcess($Repository, "delete GitHub Actions variable $VariableName")) {
        $deleteOutput = @(& gh variable delete $VariableName --repo $Repository 2>&1)
        if ($LASTEXITCODE -ne 0) {
            throw "Could not delete $VariableName from '$Repository'. $($deleteOutput -join ' ')"
        }
        Write-Host "Development channel disabled for $Repository. Pipelines will install the latest listed $moduleName version." -ForegroundColor Green
    }
    return
}

if ($PSCmdlet.ShouldProcess($Repository, "set GitHub Actions variable $VariableName=$versionText")) {
    $setOutput = @(& gh variable set $VariableName --repo $Repository --body $versionText 2>&1)
    if ($LASTEXITCODE -ne 0) {
        throw "Could not set $VariableName in '$Repository'. $($setOutput -join ' ')"
    }
    Write-Host "Development channel enabled for ${Repository}: $VariableName=$versionText" -ForegroundColor Green
    Write-Host "All bundled pipelines now install the exact candidate with Install-Module -RequiredVersion."
}