diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml index 3a0342fa..e8324605 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml @@ -87,7 +87,7 @@ parameters: default: false variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' # v0.8.7 sideload advisor defaults. Override at the pipeline / variable-group diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml index ad408b72..27312c37 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -120,7 +120,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.75' + value: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml index 1d552c56..05d7b4e8 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml @@ -70,7 +70,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml index 8a7c0636..6465e6d3 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml @@ -60,7 +60,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.75' + value: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml index 88adfa22..be1c7c1f 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml @@ -110,7 +110,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml index 0df3fcba..c32897c4 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml @@ -105,7 +105,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml index 4bb79a08..34eb7ec0 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml @@ -93,7 +93,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml index edb9bd09..bd06e2c6 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml @@ -43,7 +43,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.75' + value: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml index 1be87f04..c71a049b 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml @@ -46,7 +46,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.75' + value: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml index 0d1694fd..7ca5358b 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml @@ -84,7 +84,7 @@ parameters: default: '' variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml index 4d680f6a..dcf7f48b 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml @@ -79,7 +79,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). @@ -102,10 +102,161 @@ variables: # SIDELOAD_KV_AUTH: 'oidc' # oidc | managedidentity | serviceprincipal stages: +# --------------------------------------------------------------------------- +# PREFLIGHT (always runs on a Microsoft-hosted Windows agent) +# --------------------------------------------------------------------------- +# Purpose: explain - in the stage step summary - WHY the `Sideload` stage did +# or did not run, so an operator who triggers Step.6 without first completing +# the opt-in setup sees a clear "what to do next" panel instead of a silent +# "Skipped" status (the symptom that prompted v0.8.76). +# +# Cost: ~10s on windows-latest. No Azure / Key Vault access. Always runs. +# +# Outcomes: +# - Gate OFF (SIDELOAD_UPDATES unset / not in 'true'|'True'|'TRUE'|'1'): +# Preflight succeeds with an informational panel showing how to enable +# Step.6. The `Sideload` stage is then SKIPPED by its own condition. +# - Gate ON + SIDELOAD_STATE_ROOT missing: Preflight FAILS with a panel +# listing the missing variable(s). The `Sideload` stage is skipped +# because its `dependsOn: Preflight` cannot be satisfied. +# - All checks pass: Preflight succeeds, `Sideload` proceeds onto your +# self-hosted pool. +# +# NOTE: Unlike the GitHub Actions equivalent, Azure DevOps does NOT enumerate +# agent availability here. The ADO pool/agent API requires `Agent Pools (read)` +# scope which most pipeline identities do not hold. Verify your `azlocal-sideload` +# capability is present in your self-hosted pool MANUALLY (Project Settings -> +# Agent pools -> -> Capabilities). +- stage: Preflight + displayName: 'Step.06 Preflight - Validate Opt-in' + jobs: + - job: PreflightCheck + displayName: 'Validate Sideload Opt-in + Required Configuration' + pool: + vmImage: windows-latest + steps: + - task: PowerShell@2 + displayName: 'Run Step.6 preflight' + env: + SIDELOAD_UPDATES: $(SIDELOAD_UPDATES) + SIDELOAD_STATE_ROOT: $(SIDELOAD_STATE_ROOT) + SIDELOAD_AUTH_MAP_PATH: $(SIDELOAD_AUTH_MAP_PATH) + SIDELOAD_CATALOG_PATH: $(SIDELOAD_CATALOG_PATH) + APPLY_UPDATES_SCHEDULE_PATH: $(APPLY_UPDATES_SCHEDULE_PATH) + AGENT_TEMPDIRECTORY: $(Agent.TempDirectory) + inputs: + targetType: 'inline' + pwsh: true + script: | + $ErrorActionPreference = 'Stop' + $summaryPath = Join-Path $env:AGENT_TEMPDIRECTORY 'sideload-preflight.md' + $lines = [System.Collections.Generic.List[string]]::new() + function Append-Summary([string]$line) { [void]$lines.Add($line) } + + # Master gate: case-insensitive 'true'/'True'/'TRUE' OR literal '1'. + $gateRaw = "$($env:SIDELOAD_UPDATES)".Trim() + $gateOn = ($gateRaw -in @('true','True','TRUE','1')) + + Append-Summary '## Step.6 - Sideload Updates - Preflight' + Append-Summary '' + + if (-not $gateOn) { + Append-Summary '> [!NOTE]' + Append-Summary '> **Step.6 (Sideload Updates) is OPT-IN and currently OFF.**' + Append-Summary '>' + if ([string]::IsNullOrWhiteSpace($gateRaw)) { + Append-Summary '> The pipeline variable `SIDELOAD_UPDATES` is **not set**. The `Sideload` stage for this run was skipped.' + } else { + Append-Summary "> The pipeline variable ``SIDELOAD_UPDATES`` is set to ``'$gateRaw'`` which is not one of the accepted opt-in values (``'true'`` / ``'True'`` / ``'TRUE'`` / ``'1'``). The ``Sideload`` stage for this run was skipped." + } + Append-Summary '' + Append-Summary '### How to enable Step.6' + Append-Summary '' + Append-Summary '1. **Provision an on-prem self-hosted agent** on the same network as your Azure Local clusters. Add it to a self-hosted POOL and give the agent a custom capability named `azlocal-sideload` (Project Settings -> Agent pools -> -> -> Capabilities -> User-defined). See [Self-hosted Windows agents](https://learn.microsoft.com/azure/devops/pipelines/agents/windows-agent).' + Append-Summary '2. **Replace `` in this YAML** with your pool name (under the `Sideload` stage `pool.name`).' + Append-Summary '3. **Stage the config files** in your repository under `./config/`:' + Append-Summary ' - `apply-updates-schedule.yml` (the ring-aware apply schedule - generated by Step.3 Schedule Audit)' + Append-Summary ' - `sideload-auth-map.csv` (per-cluster Key Vault + secret-name map for WinRM credentials)' + Append-Summary ' - `sideload-catalog.yml` (committed catalog of approved `CombinedSolutionBundle` versions, `Version` -> `DownloadUri` + `Sha256`)' + Append-Summary '4. **Set the pipeline Variables** (Pipeline -> Edit -> Variables, or a Variable Group):' + Append-Summary ' - `SIDELOAD_STATE_ROOT` = shared UNC path the agent can read/write (e.g. `\\fileserver\azlocal-sideload`)' + Append-Summary ' - Optional: `SIDELOAD_CACHE_ROOT`, `SIDELOAD_LEAD_DAYS`, `SIDELOAD_ROBOCOPY_SWITCHES`, `SIDELOAD_HEARTBEAT_STALE_MINUTES`, `SIDELOAD_REMOTING_FQDN_SUFFIX`, `SIDELOAD_KV_AUTH`' + Append-Summary '5. **Configure the service connection** named `AzureLocal-ServiceConnection` (Workload Identity Federation) with read access to your Resource Graph + Key Vault.' + Append-Summary '6. **Flip the master gate**: set pipeline variable `SIDELOAD_UPDATES` = `true` (also accepted: `True`, `TRUE`, `1`).' + Append-Summary '' + Append-Summary '> [!IMPORTANT]' + Append-Summary '> Step.6 cannot run on a Microsoft-hosted agent. The job copies multi-GB update media to the cluster `import` SMB share and PowerShell-remotes into a cluster node - both require line-of-sight to the on-prem fabric VLAN and (typically) AD domain membership.' + Append-Summary '' + Append-Summary 'See `Automation-Pipeline-Examples/README.md` section 8.6 (Sideload Updates) for the full setup walkthrough.' + + [System.IO.File]::WriteAllLines($summaryPath, $lines) + Write-Host "##vso[task.uploadsummary]$summaryPath" + Write-Host "##[warning]Step.6 sideload is OPT-IN - SIDELOAD_UPDATES is not set to an opt-in value ('true'/'True'/'TRUE'/'1'). See the stage summary for enablement instructions." + exit 0 + } + + # Gate is ON - validate required configuration. + Append-Summary '### Master gate' + Append-Summary '' + Append-Summary "- ``SIDELOAD_UPDATES`` = ``'$gateRaw'`` (opt-in)" + Append-Summary '' + Append-Summary '### Required configuration' + Append-Summary '' + $missing = [System.Collections.Generic.List[string]]::new() + $stateRoot = "$($env:SIDELOAD_STATE_ROOT)".Trim() + if ([string]::IsNullOrWhiteSpace($stateRoot)) { + [void]$missing.Add('SIDELOAD_STATE_ROOT') + Append-Summary '- `SIDELOAD_STATE_ROOT` - **MISSING** (required: shared UNC root reachable from the self-hosted agent, e.g. `\\fileserver\azlocal-sideload`)' + } else { + Append-Summary "- ``SIDELOAD_STATE_ROOT`` = ``$stateRoot``" + } + Append-Summary "- ``SIDELOAD_AUTH_MAP_PATH`` = ``$($env:SIDELOAD_AUTH_MAP_PATH)``" + Append-Summary "- ``SIDELOAD_CATALOG_PATH`` = ``$($env:SIDELOAD_CATALOG_PATH)``" + Append-Summary "- ``APPLY_UPDATES_SCHEDULE_PATH`` = ``$($env:APPLY_UPDATES_SCHEDULE_PATH)``" + Append-Summary '' + + if ($missing.Count -gt 0) { + Append-Summary '> [!CAUTION]' + Append-Summary "> **Setup incomplete.** Missing required variable(s): ``$($missing -join '`, `')``. The ``Sideload`` stage will not run." + Append-Summary '' + Append-Summary 'See `Automation-Pipeline-Examples/README.md` section 8.6 (Sideload Updates) for the full setup walkthrough.' + + [System.IO.File]::WriteAllLines($summaryPath, $lines) + Write-Host "##vso[task.uploadsummary]$summaryPath" + Write-Host "##vso[task.logissue type=error]Step.6 preflight failed - required variable(s) not set: $($missing -join ', ')" + exit 1 + } + + Append-Summary '### Self-hosted agent' + Append-Summary '' + Append-Summary '- Verify your self-hosted pool advertises the `azlocal-sideload` capability and has at least one **Online** agent (Project Settings -> Agent pools -> -> Agents).' + Append-Summary '- The ADO agent pool API requires `Agent Pools (read)` scope which the pipeline identity does not normally hold, so this preflight cannot enumerate agents automatically.' + Append-Summary '' + Append-Summary '> [!TIP]' + Append-Summary '> **Preflight passed.** The `Sideload` stage will run on your self-hosted pool. If no `azlocal-sideload`-capable agent is online the job will queue for up to the pipeline default (no-time-limit by default; cancel the run if it stays Queued for more than a few minutes).' + Append-Summary '' + + [System.IO.File]::WriteAllLines($summaryPath, $lines) + Write-Host "##vso[task.uploadsummary]$summaryPath" + - stage: Sideload displayName: 'Step.06 - Sideload Updates (Opt-in)' - # Master opt-in gate: do nothing unless SIDELOAD_UPDATES is explicitly 'true'. - condition: eq(variables['SIDELOAD_UPDATES'], 'true') + # Wait for preflight to pass before queuing onto the self-hosted pool. + # If preflight FAILS (missing config), this stage is skipped (`dependsOn` unmet). + # If preflight succeeds with the gate OFF, this stage is skipped by its own `condition`. + dependsOn: Preflight + # Master opt-in gate: do nothing unless SIDELOAD_UPDATES is one of the + # accepted truthy values (case-insensitive 'true' or literal '1'). + condition: | + and( + succeeded(), + or( + eq(variables['SIDELOAD_UPDATES'], 'true'), + eq(variables['SIDELOAD_UPDATES'], 'True'), + eq(variables['SIDELOAD_UPDATES'], 'TRUE'), + eq(variables['SIDELOAD_UPDATES'], '1') + ) + ) jobs: - job: AdvanceSideload displayName: 'Advance Sideload State Machine' diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md index 7cd7a6e4..e16067dc 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md @@ -1,7 +1,15 @@ # Sideload Updates (on-prem, opt-in) - Step 6 -> **Introduced in v0.8.7.** Opt-in, off by default. This pipeline is inert unless the -> repository variable `SIDELOAD_UPDATES` is the literal string `'true'`. +> **Introduced in v0.8.7. Preflight job added in v0.8.76.** Opt-in, off by default. +> The job is inert unless the repository (GH) / pipeline (ADO) variable +> `SIDELOAD_UPDATES` is one of `'true'`, `'True'`, `'TRUE'`, or `'1'`. +> +> Every run starts with a `preflight` job/stage (~10s on a Microsoft-hosted Windows +> runner) that writes a clear panel to the run step summary explaining what is set, +> what is missing, and how to enable Step.6. When the gate is OFF the preflight +> succeeds with an enablement walkthrough; when the gate is ON but required +> configuration is missing it fails fast and the `sideload` job/stage is skipped. +> See section 9 below for the preflight behaviour matrix. The **Sideload Updates** pipeline (`sideload-updates.yml`, logical pipeline id `sideload-updates`, displayed as **Step.6**) pre-stages Azure Local solution-update @@ -170,8 +178,8 @@ packages: | Variable | Default | Purpose | |---|---|---| -| `SIDELOAD_UPDATES` | `false` | **Master gate.** The job is skipped unless this is the literal `'true'`. | -| `SIDELOAD_STATE_ROOT` | (none) | Shared UNC root holding `state\`, `logs\`, `cache\`. **Required** when enabled. | +| `SIDELOAD_UPDATES` | (unset) | **Master gate.** The job is skipped unless this is one of `'true'`, `'True'`, `'TRUE'`, or `'1'`. Any other value (including blanks, `'false'`, `'yes'`) keeps the pipeline inert. | +| `SIDELOAD_STATE_ROOT` | (none) | Shared UNC root holding `state\`, `logs\`, `cache\`. **Required** when enabled. Validated by the preflight (section 9). | | `SIDELOAD_CACHE_ROOT` | `\cache` | Shared verified media cache. | | `SIDELOAD_AUTH_MAP_PATH` | `./config/sideload-auth-map.csv` | Auth-map CSV (see 4.1). `Copy-AzLocalPipelineExample` drops a header-only starter here (same `config/` folder on GitHub and Azure DevOps). | | `SIDELOAD_CATALOG_PATH` | `./config/sideload-catalog.yml` | Catalog YAML (see 5). `Copy-AzLocalPipelineExample` drops an empty skeleton starter here. | @@ -218,3 +226,58 @@ packages: 9. The state machine advances each cluster to `Imported` / `SideloadFlagged`; the downstream **Step.7 - Apply Updates** wave then applies the staged update during the cluster's `UpdateStartWindow`. + +--- + +## 9. Preflight (v0.8.76+) + +Step.6 is **opt-in and off by default** and requires an on-prem self-hosted runner / +agent that most repos and projects do not have. Before v0.8.76 a triggered run with +no setup completed simply showed `Status: Skipped` (no logs, no annotation), which +gave operators no actionable feedback. v0.8.76 prepends a `preflight` job +(GitHub Actions) / `Preflight` stage (Azure DevOps) that always runs on +`windows-latest` (Microsoft-hosted, no Azure/Key Vault access, ~10s). + +### Behaviour matrix + +| `SIDELOAD_UPDATES` | `SIDELOAD_STATE_ROOT` | Self-hosted runner (GH only) | Preflight outcome | `sideload` job | +|---|---|---|---|---| +| unset / `'false'` / other | n/a | n/a | **Succeeds** with enablement walkthrough in step summary + `::notice` annotation | Skipped by its own `if:` / `condition:` | +| `'true'` / `'True'` / `'TRUE'` / `'1'` | unset | n/a | **Fails** (exit 1) with "missing variable" panel + `::error` | Skipped (`needs:` / `dependsOn:` unmet) | +| accepted | set | no online `azlocal-sideload` runner AND runners API returned a list | **Fails** (exit 1) with "no online runner" panel + `::error` | Skipped | +| accepted | set | runners API returned 403/404 (most repos) | **Succeeds** with warning ("could not enumerate runners - verify manually") | Runs | +| accepted | set | online `azlocal-sideload` runner found | **Succeeds** with "Preflight passed" panel | Runs | + +### Why a Microsoft-hosted Windows runner is fine for preflight + +The preflight does NOT touch the cluster fabric, Key Vault, or any Azure resource. +It only reads workflow / pipeline variables, optionally queries the GH runners API, +and writes markdown to the step summary. There is no domain-membership or +VLAN-reachability requirement, so `windows-latest` is the correct minimal-cost host. + +The actual `sideload` job still **must** run on the self-hosted runner with the +`azlocal-sideload` label (GH) or pool capability (ADO), because the on-prem copy + +WinRM steps cannot work from a Microsoft-hosted runner (no line-of-sight to the +fabric VLAN, no AD trust). + +### Runner-enumeration permission (GH Actions only) + +The preflight tries to call `GET /repos/{owner}/{repo}/actions/runners` with +`GITHUB_TOKEN` and the workflow's `actions: read` permission. GitHub's repo +runners API actually requires **repo-admin** privilege, which `GITHUB_TOKEN` does +not hold by default - so for most callers the API will return 403 and the +preflight degrades to a warning ("verify manually under Settings -> Actions -> +Runners"). When the API does return a list (e.g. a fine-grained PAT or a GitHub +App is wired into the workflow with `administration: read`) the preflight reports +the exact set of matching online runners. + +### Azure DevOps does not enumerate agents + +The ADO preflight stage validates the gate + `SIDELOAD_STATE_ROOT` only. The +`Agent Pools (read)` scope required to call the ADO agent enumeration API is +not normally granted to the pipeline identity, so the preflight relies on the +operator having verified the `azlocal-sideload` capability is present in their +self-hosted pool (Project Settings -> Agent pools -> <pool> -> Capabilities). +If no matching agent is online the `Sideload` stage will sit `Queued` until +manually cancelled. + diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml index bc88783e..1e4b08d8 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml @@ -96,7 +96,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' REQUIRED_MODULE_VERSION: ${{ github.event.inputs.module_version || vars.REQUIRED_MODULE_VERSION || '' }} # v0.8.4 - opt this workflow into Node.js 24 for all JavaScript actions # (actions/checkout, actions/download-artifact, actions/upload-artifact, diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml index 91dbe5f3..ed7c5cf4 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -135,7 +135,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml index 4437656b..083eb953 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml @@ -72,7 +72,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml index 5bbeac9d..aaf6c785 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml @@ -79,7 +79,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml index 142dfcce..ee96aea3 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml @@ -116,7 +116,7 @@ env: # PSGallery, and emits a ::notice annotation if the YAML appears stale - # prompting you to refresh via Copy-AzLocalPipelineExample -Update. See # Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install # the latest, the default "fix-forward" behaviour): manual workflow_dispatch # input > repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml index b5be44a6..6cad12aa 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml @@ -116,7 +116,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml index 9d744d4e..7f3f4c59 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml @@ -105,7 +105,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml index 484a1317..3de89674 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml @@ -48,7 +48,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml index 1d2323b7..1416e113 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml @@ -65,7 +65,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml index 342ff668..bb220fe1 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml @@ -86,7 +86,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml index dad18bd3..efbfda40 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml @@ -83,7 +83,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.75' + GENERATED_AGAINST_MODULE_VERSION: '0.8.76' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). @@ -137,14 +137,193 @@ concurrency: cancel-in-progress: false jobs: + # --------------------------------------------------------------------------- + # PREFLIGHT (always runs on a Microsoft-hosted Windows runner) + # --------------------------------------------------------------------------- + # Purpose: explain - in the run step summary - WHY the `sideload` job did or + # did not run, so an operator who triggers Step.6 without first completing + # the opt-in setup sees a clear "what to do next" panel instead of a silent + # "Skipped" status (the symptom that prompted v0.8.76). + # + # Cost: ~10s on windows-latest. No Azure/Key Vault access. Always runs. + # + # Outcomes: + # - Gate OFF (SIDELOAD_UPDATES unset / not in 'true'|'True'|'TRUE'|'1'): + # preflight succeeds (exit 0) with an informational panel showing how + # to enable Step.6. The `sideload` job is then SKIPPED by its own `if:`. + # - Gate ON + SIDELOAD_STATE_ROOT missing: preflight FAILS (exit 1) with + # a panel listing the missing variable(s). The `sideload` job is + # skipped because its `needs: preflight` is unmet. + # - Gate ON + best-effort runner check shows no online `azlocal-sideload` + # runner: preflight FAILS (exit 1). NOTE: the runner enumeration API + # requires repo-admin permission which GITHUB_TOKEN does not normally + # grant; if the API call fails with 403/404 the preflight degrades + # gracefully and emits a strong warning instead of a hard failure. + # - All checks pass: preflight succeeds, `sideload` job proceeds. + preflight: + name: Step.6 Preflight - Validate Opt-in + Self-Hosted Runner + runs-on: windows-latest + permissions: + actions: read # Best-effort runner enumeration (may 403 - graceful fallback) + contents: read + steps: + - name: Validate opt-in, configuration, and self-hosted runner + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $ErrorActionPreference = 'Stop' + $summaryPath = $env:GITHUB_STEP_SUMMARY + function Append-Summary([string]$line) { + Add-Content -LiteralPath $summaryPath -Value $line -Encoding utf8 + } + + # Master gate: case-insensitive 'true'/'True'/'TRUE' OR literal '1'. + $gateRaw = "$($env:SIDELOAD_UPDATES)".Trim() + $gateOn = ($gateRaw -in @('true','True','TRUE','1')) + + Append-Summary '## Step.6 - Sideload Updates - Preflight' + Append-Summary '' + + if (-not $gateOn) { + Append-Summary '> [!NOTE]' + Append-Summary '> **Step.6 (Sideload Updates) is OPT-IN and currently OFF.**' + Append-Summary '>' + if ([string]::IsNullOrWhiteSpace($gateRaw)) { + Append-Summary '> The repository variable `SIDELOAD_UPDATES` is **not set**. The `sideload` job for this run was skipped.' + } else { + Append-Summary "> The repository variable ``SIDELOAD_UPDATES`` is set to ``'$gateRaw'`` which is not one of the accepted opt-in values (``'true'`` / ``'True'`` / ``'TRUE'`` / ``'1'``). The ``sideload`` job for this run was skipped." + } + Append-Summary '' + Append-Summary '### How to enable Step.6' + Append-Summary '' + Append-Summary '1. **Provision an on-prem self-hosted runner** on the same network as your Azure Local clusters. Register it with **both** labels `self-hosted` (default) and `azlocal-sideload` (custom). Use a dedicated runner group if you want to restrict which repos can target it. See [Adding self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners).' + Append-Summary '2. **Stage the config files** in your repository under `./config/`:' + Append-Summary ' - `apply-updates-schedule.yml` (the ring-aware apply schedule - generated by Step.3 Schedule Audit)' + Append-Summary ' - `sideload-auth-map.csv` (per-cluster Key Vault + secret-name map for WinRM credentials)' + Append-Summary ' - `sideload-catalog.yml` (committed catalog of approved `CombinedSolutionBundle` versions, `Version` -> `DownloadUri` + `Sha256`)' + Append-Summary '3. **Set the repository Variables** (Settings -> Secrets and variables -> Actions -> Variables):' + Append-Summary ' - `SIDELOAD_STATE_ROOT` = shared UNC path the runner can read/write (e.g. `\\fileserver\azlocal-sideload`)' + Append-Summary ' - `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID` (Azure login)' + Append-Summary ' - Optional: `SIDELOAD_CACHE_ROOT`, `SIDELOAD_LEAD_DAYS`, `SIDELOAD_ROBOCOPY_SWITCHES`, `SIDELOAD_HEARTBEAT_STALE_MINUTES`, `SIDELOAD_REMOTING_FQDN_SUFFIX`, `SIDELOAD_KV_AUTH`' + Append-Summary '4. **Set the repository Secret** `AZURE_CLIENT_ID` (the OIDC app registration used by `azure/login`).' + Append-Summary '5. **Flip the master gate**: set repository variable `SIDELOAD_UPDATES` = `true` (also accepted: `True`, `TRUE`, `1`).' + Append-Summary '' + Append-Summary '> [!IMPORTANT]' + Append-Summary '> Step.6 cannot run on a Microsoft-hosted runner. The job copies multi-GB update media to the cluster `import` SMB share and PowerShell-remotes into a cluster node - both require line-of-sight to the on-prem fabric VLAN and (typically) AD domain membership.' + Append-Summary '' + Append-Summary 'See `Automation-Pipeline-Examples/README.md` section 8.6 (Sideload Updates) for the full setup walkthrough.' + Write-Output "::notice title=Step.6 sideload is OPT-IN::SIDELOAD_UPDATES is not set to an opt-in value ('true'/'True'/'TRUE'/'1'). See the run summary for enablement instructions." + exit 0 + } + + # Gate is ON - validate required configuration. + Append-Summary '### Master gate' + Append-Summary '' + Append-Summary "- ``SIDELOAD_UPDATES`` = ``'$gateRaw'`` (opt-in)" + Append-Summary '' + Append-Summary '### Required configuration' + Append-Summary '' + $missing = [System.Collections.Generic.List[string]]::new() + $stateRoot = "$($env:SIDELOAD_STATE_ROOT)".Trim() + if ([string]::IsNullOrWhiteSpace($stateRoot)) { + [void]$missing.Add('SIDELOAD_STATE_ROOT') + Append-Summary '- `SIDELOAD_STATE_ROOT` - **MISSING** (required: shared UNC root reachable from the self-hosted runner, e.g. `\\fileserver\azlocal-sideload`)' + } else { + Append-Summary "- ``SIDELOAD_STATE_ROOT`` = ``$stateRoot``" + } + Append-Summary "- ``SIDELOAD_AUTH_MAP_PATH`` = ``$($env:SIDELOAD_AUTH_MAP_PATH)``" + Append-Summary "- ``SIDELOAD_CATALOG_PATH`` = ``$($env:SIDELOAD_CATALOG_PATH)``" + Append-Summary "- ``APPLY_UPDATES_SCHEDULE_PATH`` = ``$($env:APPLY_UPDATES_SCHEDULE_PATH)``" + Append-Summary '' + + if ($missing.Count -gt 0) { + Append-Summary '> [!CAUTION]' + Append-Summary "> **Setup incomplete.** Missing required variable(s): ``$($missing -join '`, `')``. The ``sideload`` job will not run." + Append-Summary '' + Append-Summary 'See `Automation-Pipeline-Examples/README.md` section 8.6 (Sideload Updates) for the full setup walkthrough.' + Write-Output "::error title=Step.6 preflight failed::Required variable(s) not set: $($missing -join ', ')" + exit 1 + } + + # Best-effort: query the runners API to confirm an online self-hosted runner + # with the 'azlocal-sideload' label is registered. The repo runners API + # requires admin permission; GITHUB_TOKEN typically does NOT have it, so + # 403/404 is expected for most callers and downgrades the check to a warning. + Append-Summary '### Self-hosted runner' + Append-Summary '' + $runnerCheckPassed = $false + $runnerCheckSkipped = $false + try { + $headers = @{ + Authorization = "Bearer $env:GH_TOKEN" + Accept = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + 'User-Agent' = 'AzLocal.UpdateManagement-preflight' + } + $uri = "https://api.github.com/repos/$env:GITHUB_REPOSITORY/actions/runners?per_page=100" + $response = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction Stop + $matching = @($response.runners | Where-Object { + $labelNames = @($_.labels | ForEach-Object { $_.name }) + ($labelNames -contains 'self-hosted') -and ($labelNames -contains 'azlocal-sideload') + }) + $online = @($matching | Where-Object { $_.status -eq 'online' }) + if ($online.Count -gt 0) { + Append-Summary "- **Found $($online.Count) online runner(s)** with the ``azlocal-sideload`` label:" + foreach ($r in $online) { + Append-Summary " - ``$($r.name)`` (os: $($r.os), status: $($r.status), busy: $($r.busy))" + } + $runnerCheckPassed = $true + } elseif ($matching.Count -gt 0) { + Append-Summary "- **Found $($matching.Count) runner(s)** with the ``azlocal-sideload`` label, but **none are online**:" + foreach ($r in $matching) { + Append-Summary " - ``$($r.name)`` - status: $($r.status)" + } + } else { + Append-Summary '- **No self-hosted runner registered with the `azlocal-sideload` label.**' + } + } + catch { + $runnerCheckSkipped = $true + $msg = $_.Exception.Message + Append-Summary "- Unable to enumerate runners via the GitHub API ($msg)." + Append-Summary ' - The repo runners API requires admin permission; `GITHUB_TOKEN` usually cannot read it. This is expected and not necessarily a problem.' + Append-Summary ' - **Manual verification:** Settings -> Actions -> Runners. Confirm a self-hosted runner exists with the `azlocal-sideload` label and is **Idle** (green).' + } + Append-Summary '' + + if (-not $runnerCheckPassed -and -not $runnerCheckSkipped) { + Append-Summary '> [!CAUTION]' + Append-Summary '> **No online `azlocal-sideload` runner.** The `sideload` job would queue indefinitely (default GH Actions job queue timeout: 24h) until a matching runner comes online. Failing preflight now to prevent a stuck run.' + Append-Summary '' + Append-Summary 'Register a self-hosted runner on a Windows machine on the cluster network, label it with `azlocal-sideload` (the `self-hosted` label is added automatically), then re-run this workflow. See `Automation-Pipeline-Examples/README.md` section 8.6.' + Write-Output "::error title=Step.6 preflight failed::No online self-hosted runner with the 'azlocal-sideload' label was found in this repository." + exit 1 + } + + if ($runnerCheckSkipped) { + Append-Summary '> [!WARNING]' + Append-Summary '> Preflight could not verify runner availability. If no `azlocal-sideload` runner is registered the `sideload` job will queue silently for up to 24h.' + Append-Summary '' + } else { + Append-Summary '> [!TIP]' + Append-Summary '> **Preflight passed.** The `Advance Sideload State Machine` job will run on your self-hosted runner.' + Append-Summary '' + } + sideload: name: Advance Sideload State Machine + # Wait for preflight to pass before queuing onto the self-hosted runner. + # If preflight FAILS (missing config / no runner), this job is skipped. + # If preflight succeeds with the gate OFF, this job is skipped by its own `if:`. + needs: preflight # Requires an on-prem self-hosted RUNNER (GitHub Actions terminology) on the # same network as the clusters, registered with the `azlocal-sideload` label # (use a dedicated runner group to restrict which repos can target it). runs-on: [self-hosted, azlocal-sideload] - # Master opt-in gate: do nothing unless SIDELOAD_UPDATES is explicitly 'true'. - if: ${{ vars.SIDELOAD_UPDATES == 'true' }} + # Master opt-in gate: do nothing unless SIDELOAD_UPDATES is one of the + # accepted truthy values (case-insensitive 'true' or literal '1'). + if: ${{ contains(fromJSON('["true","True","TRUE","1"]'), vars.SIDELOAD_UPDATES) }} permissions: id-token: write contents: read diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 2de0c3a0..b13e0359 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 @@ -3,7 +3,7 @@ RootModule = 'AzLocal.UpdateManagement.psm1' # Version number of this module. - ModuleVersion = '0.8.75' + ModuleVersion = '0.8.76' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -309,12 +309,15 @@ # ReleaseNotes of this module ReleaseNotes = @' -## Version 0.8.75 - Patch: removes the duplicate Cycle calendar from the Step.3 apply-updates schedule audit step summary on runs with Recommend-view findings (uncovered/partial cron windows, missing or orphan rings, unparseable crons, or NoWindowTag when a cluster CSV was supplied). The Recommend snippet's plain 5-column calendar was inlined into the wrapper's action-required block ABOVE the wrapper's own enriched 7-column calendar. Clean-fleet runs (no findings) were unaffected because the snippet is never written when its items list is empty. New additive `-OmitCycleCalendar` switch on `Test-AzLocalApplyUpdatesScheduleCoverage`; `Export-AzLocalApplyUpdatesScheduleAudit` opts in so only the enriched calendar surfaces regardless of findings. Direct callers of the Recommend view see no behaviour change. Drift-notice banner now recommends `Update-AzLocalPipelineExample` (marker-aware merge) with the right `-Platform` and a `-Destination` hint inferred from the detected pipeline host. Section 8.3 of `Automation-Pipeline-Examples/README.md` gains three Step.3 step-summary screenshots. No public API or export-count change (still 60). +## Version 0.8.76 - Patch: adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline. Before v0.8.76, triggering Step.6 without first completing the opt-in setup (master gate `SIDELOAD_UPDATES` not set, or set + no self-hosted `azlocal-sideload` runner registered) produced `Status: Skipped` with no logs, no annotation, and no actionable feedback - operators had to read the YAML to understand why. v0.8.76 prepends a `preflight` job (`runs-on: windows-latest`, ~10s, no Azure access) that ALWAYS runs and writes a clear panel to the run step summary explaining what is set, what is missing, and how to enable Step.6. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). No public API change or new exports (still 60). -- **FIX (Step.3 step summary)**: the cycle-calendar table was rendered twice on any run with Recommend-view findings - once by `Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend` (plain 5-col, inlined into the wrapper as the action-required block when `$hasIssues` is true), and once by the wrapper itself (enriched 7-col with per-day cron firings + `Tag Start Window Match (>=95%)`). Clean fleets were unaffected because the Recommend snippet is only written when its items list is non-empty. Additive `[switch]$OmitCycleCalendar` parameter on `Test-AzLocalApplyUpdatesScheduleCoverage` (default off) suppresses the inner emission; `Export-AzLocalApplyUpdatesScheduleAudit` opts in so a single enriched calendar renders in every scenario. New Pester regression asserts the switch is bound + exactly one `## Cycle calendar - next` heading rendered. -- **CHANGED (drift-notice banner)**: `Add-AzLocalPipelineVersionBanner` now recommends `Update-AzLocalPipelineExample` (marker-aware merge, preserves `AZLOCAL-CUSTOMIZE` blocks) instead of `Copy-AzLocalPipelineExample -Update` (clean overwrite). Inlines the right `-Platform` and `-Destination` hint per detected host: `'\.github\workflows' -Platform GitHub` on GH, `'\pipelines' -Platform AzureDevOps` on ADO. Symmetric YAML-newer warning gets the same treatment. PassThru verdict is `'YAML older than module - run Update-AzLocalPipelineExample to refresh'`. -- **DOCS**: 3 Step.3 screenshots added to `Automation-Pipeline-Examples/README.md` section 8.3 (cron coverage, NoWindowTag, enriched cycle calendar). `docs/images/README.md` referenced-by table updated. -- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.74'` to `'0.8.75'`. +- **NEW (GitHub Actions `sideload-updates.yml`)**: `preflight` job on `windows-latest` with `permissions: actions: read, contents: read`. Outcomes: gate OFF -> succeeds (`::notice`) with full enablement walkthrough in step summary; gate ON + `SIDELOAD_STATE_ROOT` missing -> fails (`::error`, exit 1) with missing-variable panel; gate ON + best-effort runner enumeration finds no online `azlocal-sideload` runner -> fails with no-online-runner panel; gate ON + runner API returns 403/404 (typical, `GITHUB_TOKEN` lacks admin) -> succeeds with manual-verification warning; all OK -> succeeds with "Preflight passed" panel. The `sideload` job gains `needs: preflight` so it cannot queue indefinitely when prerequisites are missing. +- **NEW (Azure DevOps `sideload-updates.yml`)**: `Preflight` stage on `pool: vmImage: windows-latest`. Validates the gate + `SIDELOAD_STATE_ROOT`; uses `##vso[task.uploadsummary]` to render the markdown panel. Does NOT enumerate ADO agents (the `Agent Pools (read)` scope is not normally granted to the pipeline identity). The `Sideload` stage gains `dependsOn: Preflight`. +- **CHANGE (master gate)**: both `sideload-updates.yml` files now accept `SIDELOAD_UPDATES` in `'true'` / `'True'` / `'TRUE'` / `'1'`. GH uses `contains(fromJSON(...), vars.SIDELOAD_UPDATES)`; ADO uses an `or(eq..., eq...)` condition. Operators copying the README still get `'true'` as the canonical value; the additional accepted values are forgiving of case habits. +- **DOCS**: `Automation-Pipeline-Examples/docs/sideload.md` updated - section 6 gate row reflects the four accepted values, header note documents the preflight, new section 9 "Preflight (v0.8.76+)" with full behaviour matrix + rationale for why a Microsoft-hosted Windows runner is the right host for preflight (no fabric / Key Vault access needed). +- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.75'` to `'0.8.76'`. + +## Version 0.8.75 - Patch: removes the duplicate Cycle calendar from the Step.3 apply-updates schedule audit step summary on runs with Recommend-view findings (uncovered/partial cron windows, missing or orphan rings, unparseable crons, or NoWindowTag when a cluster CSV was supplied). Clean fleets were unaffected. New additive `-OmitCycleCalendar` switch on `Test-AzLocalApplyUpdatesScheduleCoverage`; `Export-AzLocalApplyUpdatesScheduleAudit` opts in so a single enriched calendar renders in every scenario. Drift-notice banner now recommends `Update-AzLocalPipelineExample` (marker-aware merge) with the right `-Platform` and a `-Destination` hint per detected host. Three Step.3 step-summary screenshots added. No public API or export-count change (still 60). See CHANGELOG.md for the full per-bullet detail. ## Version 0.8.74 - Consistent "Up to Date" classification across Step.5 / Step.7 / Step.9 readiness reports, deep-tree fix for the `Progress` column in the Step.7 monitor and the standalone HTML "Recent Update Run History" report, and removal of dead `target="_blank"` portal-link attrs (GH / ADO step-summary sanitisers strip them - replaced with an explicit Ctrl/Cmd/middle-click tip). New shared `Get-AzLocalClusterReadinessStatus` private helper is the single source of truth for the readiness priority cascade. Step.7 / Step.5 swap their binary `Ready?` column for a readable `Status` column, Step.7 adds a `UP_TO_DATE_COUNT` step output, and the "No Clusters Ready" summary now renders an Up-to-Date vs Not-Ready breakdown so an idle apply-updates run is shown as healthy steady state (notice, not warning). New `Get-AzLocalUpdateRunStepStats` private helper walks the nested step tree so leaf-step progress climbs as the install proceeds. All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.73'` to `'0.8.74'`. No public API or export-count change (still 60). See CHANGELOG.md / docs/release-history.md for the full per-bullet detail. diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index ea15d2ba..8a71b879 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 @@ -151,7 +151,7 @@ Set-StrictMode -Version 1.0 # bumps to one but not the other are caught before release. Two consumers: # - Start-AzLocalClusterUpdate emits this in the run log header. # - Get-AzLocalFleetStatusData stamps it into exported fleet-state JSON. -$script:ModuleVersion = '0.8.75' +$script:ModuleVersion = '0.8.76' $script:DefaultApiVersion = '2025-10-01' $script:DefaultLogFolder = Join-Path -Path $env:ProgramData -ChildPath 'AzLocal.UpdateManagement' diff --git a/AzLocal.UpdateManagement/CHANGELOG.md b/AzLocal.UpdateManagement/CHANGELOG.md index abc3815f..c694dd66 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to the AzLocal.UpdateManagement module (renamed from AzStack The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.76] - 2026-06-12 + +Patch release. Adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline so an operator who triggers Step.6 without first completing the opt-in setup sees a clear "what to do next" panel in the run step summary instead of a silent `Status: Skipped`. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). + +### Added +- **GitHub Actions `sideload-updates.yml`**: new `preflight` job on `runs-on: windows-latest` with `permissions: actions: read, contents: read`. The job ALWAYS runs (no `if:` gate). It writes a markdown panel to `$GITHUB_STEP_SUMMARY` describing the gate state, required-variable check (currently `SIDELOAD_STATE_ROOT` only - other paths have sensible defaults), and a best-effort self-hosted-runner enumeration via the `/repos/{owner}/{repo}/actions/runners` API. The `sideload` job gains `needs: preflight`, so it cannot queue indefinitely when prerequisites are missing. +- **Azure DevOps `sideload-updates.yml`**: new `Preflight` stage on `pool: vmImage: windows-latest` that performs the gate + `SIDELOAD_STATE_ROOT` check and uploads the panel via `##vso[task.uploadsummary]`. The `Sideload` stage gains `dependsOn: Preflight`. +- **`Automation-Pipeline-Examples/docs/sideload.md` section 9** "Preflight (v0.8.76+)" - documents the full preflight behaviour matrix and explains why a Microsoft-hosted Windows runner is the right host for preflight (no fabric or Key Vault access required). + +### Changed +- **Master gate broadened**: both `sideload-updates.yml` files now accept `SIDELOAD_UPDATES` in `'true'` / `'True'` / `'TRUE'` / `'1'`. GH Actions uses `contains(fromJSON('["true","True","TRUE","1"]'), vars.SIDELOAD_UPDATES)`; ADO uses an `or(eq..., eq...)` condition. The README continues to recommend `'true'` as the canonical value. +- **`docs/sideload.md` section 6** gate row updated to reflect the four accepted values; header note documents the preflight. +- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.75'` to `'0.8.76'`. + +### Why preflight runs on a Microsoft-hosted Windows runner +The preflight does NOT touch the cluster fabric, Key Vault, or any Azure resource. It only reads workflow / pipeline variables, optionally queries the GH runners API with `GITHUB_TOKEN`, and writes markdown to the step summary. There is no domain-membership or VLAN-reachability requirement, so `windows-latest` is the correct minimal-cost host (~10s) and aligns with every other pipeline in the suite. The `sideload` job itself still requires the on-prem self-hosted runner with the `azlocal-sideload` label - that part has not changed. + +### Notes on runner enumeration +The repo runners API requires repo-admin permission, which `GITHUB_TOKEN` does not normally grant. If the API call returns 403/404 the preflight degrades to a warning panel ("could not enumerate runners - verify manually under Settings -> Actions -> Runners"); it does NOT fail the run. If the API does return a list (e.g. a fine-grained PAT or GitHub App is wired in with `administration: read`), the preflight reports the exact set of matching online runners and fails fast when none are online. Azure DevOps does not enumerate agents at all (the `Agent Pools (read)` scope is not normally granted to the pipeline identity). + ## [0.8.75] - 2026-06-12 Patch release. Removes the duplicate **Cycle calendar** table from the Step.3 apply-updates schedule audit step summary - before this fix, on any run where the Recommend view had at least one action-required finding (uncovered/partial cron windows, `RingMissingFromSchedule`, `RingOrphanedInSchedule`, `UnparseableCron`, or `NoWindowTag` when a cluster CSV was supplied), the Recommend snippet's plain 5-column calendar was inlined into the wrapper output **above** the wrapper's own enriched 7-column calendar, so operators saw two calendars back-to-back. Clean-fleet runs (no findings) were unaffected because the Recommend snippet is never written when its items list is empty. Also lands three Step.3 step-summary screenshots in `Automation-Pipeline-Examples/README.md` section 8.3. No public API or export-count change (still 60). diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 5aa59789..8ee64875 100644 --- a/AzLocal.UpdateManagement/README.md +++ b/AzLocal.UpdateManagement/README.md @@ -2,7 +2,7 @@ > ⚠️ **Disclaimer**: This module is **NOT** a Microsoft supported service offering or product. It is provided as example code only, with no warranty or official support. Refer to the [MIT license](https://github.com/NeilBird/Azure-Local/blob/main/LICENSE) for further information. -**Latest Version:** v0.8.75 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.75) +**Latest Version:** v0.8.76 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.76) > 📢 **Renamed in v0.7.3**: this module was previously published as `AzStackHci.ManageUpdates`. The new module name aligns with the Azure Local product name (_Microsoft retired the *Azure Stack HCI* brand in late 2024_). The module GUID is preserved across the rename. If you have the old name installed, run: > @@ -23,7 +23,7 @@ Azure Local REST API specification (includes update management endpoints): https **This README (overview + most-recent release notes):** - [Where to Start](#where-to-start) -- [What's New in v0.8.75](#whats-new-in-v0875) +- [What's New in v0.8.76](#whats-new-in-v0876) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,19 +86,18 @@ If you are new to this module, work through these in order from a regular PowerS > Most CI/CD pipelines in [Automation-Pipeline-Examples/](Automation-Pipeline-Examples/) are direct implementations of one of these workflows. Start there if you want a copy-pasteable end-to-end pipeline. -## What's New in v0.8.75 +## What's New in v0.8.76 -**Patch release. Removes the duplicate Cycle calendar table from the Step.3 apply-updates schedule audit step summary** - before v0.8.75, on any run with at least one Recommend-view action-required finding (uncovered/partial cron windows, `RingMissingFromSchedule`, `RingOrphanedInSchedule`, `UnparseableCron`, or `NoWindowTag` when a cluster CSV was supplied), the Recommend snippet's plain 5-column calendar was inlined into the wrapper output (as part of the action-required block) **above** the wrapper's own enriched 7-column calendar, so operators saw two calendars back-to-back. Clean-fleet runs (no findings) were unaffected because the Recommend snippet is never written when its items list is empty. Also lands three Step.3 step-summary screenshots in `Automation-Pipeline-Examples/README.md` section 8.3. No public API or export-count change (still 60). +**Patch release. Adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline.** Before v0.8.76, triggering Step.6 without first completing the opt-in setup (master gate `SIDELOAD_UPDATES` not set, or set without registering a self-hosted `azlocal-sideload` runner) produced `Status: Skipped` with no logs, no annotation, and no actionable feedback - operators had to read the YAML to figure out why. v0.8.76 prepends a `preflight` job (`runs-on: windows-latest`, ~10s, no Azure access) that ALWAYS runs and writes a clear panel to the run step summary explaining what is set, what is missing, and how to enable Step.6. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). No public API change or new exports (still 60). -1. **Fixed**: the Step.3 step summary rendered the Cycle calendar twice on runs with findings. `Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend` writes a self-contained snippet to its `schedule-coverage-recommend.md` output - the snippet carries its action-required H2s, the proposed cron YAML block, AND a plain (5-column) cycle-calendar block (`Date | Day | CycleWeek | Eligible rings | AllowedUpdateVersions`). That snippet is only written when the Recommend view emits at least one row (clean fleets skip it). `Export-AzLocalApplyUpdatesScheduleAudit` (the Step.3 wrapper) inlined that file as the action-required block when `$hasIssues` was true AND then rendered its OWN enriched (7-column) calendar lower in the summary - with two extra columns (`Ring CRON Start Time (apply-updates pipeline)` and `Tag Start Window Match (>=95%)`) computed from the Step.6 cron triggers + cluster CSV. -2. **Added**: new additive `[switch]$OmitCycleCalendar` parameter on `Test-AzLocalApplyUpdatesScheduleCoverage` (default off, so direct callers see the calendar unchanged). `Export-AzLocalApplyUpdatesScheduleAudit` passes `-OmitCycleCalendar=$true` so only the enriched calendar surfaces in the Step.3 summary regardless of whether the run has findings. -3. **Added**: Pester regression spies on the Recommend mock to assert the switch is bound, and asserts the rendered step summary contains exactly one `## Cycle calendar - next` heading. -4. **Changed (drift-notice banner)**: `Add-AzLocalPipelineVersionBanner` now recommends `Update-AzLocalPipelineExample` (the marker-aware merge tool that preserves `AZLOCAL-CUSTOMIZE` blocks) instead of `Copy-AzLocalPipelineExample -Update` (the clean-overwrite tool) in the YAML-older-than-module annotation, and inlines the right `-Platform` value plus a sensible `-Destination` hint based on the detected pipeline host: `Update-AzLocalPipelineExample -Destination '\.github\workflows' -Platform GitHub` on GitHub Actions, `Update-AzLocalPipelineExample -Destination '\pipelines' -Platform AzureDevOps` on Azure DevOps. Symmetric warning when the YAML is newer than the installed module gets the same treatment. Three new Pester cases pin the GH / ADO / verdict text. -5. **Docs**: section 8.3 of `Automation-Pipeline-Examples/README.md` (Apply-Updates Schedule Coverage Audit runbook) gains three Step.3 step-summary screenshots - cron coverage remediation, NoWindowTag remediation, and the enriched cycle calendar. +1. **Added (GitHub Actions)**: new `preflight` job in `sideload-updates.yml` on `runs-on: windows-latest` with `permissions: actions: read, contents: read`. The job ALWAYS runs (no `if:` gate). Outcomes: gate OFF -> succeeds with a `::notice` annotation and a full enablement walkthrough in the step summary; gate ON + `SIDELOAD_STATE_ROOT` missing -> fails (`::error`, `exit 1`) with a missing-variable panel; gate ON + best-effort `/repos/{owner}/{repo}/actions/runners` enumeration finds no online `azlocal-sideload` runner -> fails with a no-online-runner panel; gate ON + the API returns 403/404 (the typical case - `GITHUB_TOKEN` lacks admin) -> succeeds with a manual-verification warning panel; all OK -> succeeds with a "Preflight passed" panel. The `sideload` job gains `needs: preflight` so it cannot queue indefinitely when prerequisites are missing. +2. **Added (Azure DevOps)**: new `Preflight` stage in `sideload-updates.yml` on `pool: vmImage: windows-latest`. Validates the gate + `SIDELOAD_STATE_ROOT`; uploads the markdown panel via `##vso[task.uploadsummary]`. Does NOT enumerate ADO agents (the `Agent Pools (read)` scope is not normally granted to the pipeline identity, so the preflight degrades to documentation in that case). The `Sideload` stage gains `dependsOn: Preflight`. +3. **Changed (master gate broadened)**: both `sideload-updates.yml` files now accept `SIDELOAD_UPDATES` in `'true'` / `'True'` / `'TRUE'` / `'1'`. GH uses `contains(fromJSON('["true","True","TRUE","1"]'), vars.SIDELOAD_UPDATES)`; ADO uses an `or(eq..., eq...)` condition. The README still recommends `'true'` as the canonical value; the additional accepted values are forgiving of case-insensitive habits. +4. **Docs**: `Automation-Pipeline-Examples/docs/sideload.md` updated - section 6 gate row reflects the four accepted values, header note documents the preflight, new section 9 "Preflight (v0.8.76+)" with the full behaviour matrix and a short rationale for why a Microsoft-hosted Windows runner is the correct host for preflight (no fabric / Key Vault access required). -`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.74` to `0.8.75` across all bundled pipeline templates. +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.75` to `0.8.76` across all bundled pipeline templates. -See [CHANGELOG.md](CHANGELOG.md#0875---2026-06-12) for the full v0.8.75 entry. See [`What's New in v0.8.74`](docs/release-history.md#whats-new-in-v0874) in the Release History for the previous release. +See [CHANGELOG.md](CHANGELOG.md#0876---2026-06-12) for the full v0.8.76 entry. See [`What's New in v0.8.75`](docs/release-history.md#whats-new-in-v0875) in the Release History for the previous release. ## Files @@ -577,7 +576,7 @@ This code is provided as-is for educational and reference purposes. The full What's-New history (v0.7.81 and earlier) has moved to [docs/release-history.md](docs/release-history.md). -The most recent release notes for **v0.8.75** stay above under [`What's New in v0.8.75`](#whats-new-in-v0875). +The most recent release notes for **v0.8.76** stay above under [`What's New in v0.8.76`](#whats-new-in-v0876). ### What's New in v0.8.73 diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 22d0df23..8492f2d3 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -34,8 +34,8 @@ Describe 'Module: AzLocal.UpdateManagement' { $script:ModuleInfo | Should -Not -BeNullOrEmpty } - It 'Should have version 0.8.75' { - $script:ModuleInfo.Version | Should -Be '0.8.75' + It 'Should have version 0.8.76' { + $script:ModuleInfo.Version | Should -Be '0.8.76' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -218,6 +218,35 @@ Describe 'Module: AzLocal.UpdateManagement' { $content | Should -Not -Match 'function\s+Convert-ScheduleRow' -Because "Step.3 $Platform must not contain inline schedule-row helpers (removed in v0.8.5)" } + It 'v0.8.76: sideload-updates.yml (GitHub Actions) has a preflight job on windows-latest' { + $yamlPath = Join-Path -Path $PSScriptRoot -ChildPath '..\Automation-Pipeline-Examples\github-actions\sideload-updates.yml' + Test-Path $yamlPath | Should -BeTrue + $content = Get-Content -Path $yamlPath -Raw + + # New preflight job runs on a Microsoft-hosted Windows runner so it + # ALWAYS executes regardless of self-hosted runner availability, + # and writes setup guidance to the step summary when the gate or + # required state-root variable is missing. + $content | Should -Match '(?m)^\s*preflight:\s*$' -Because "GH sideload pipeline must define a preflight job (v0.8.76)" + $content | Should -Match '(?ms)preflight:.*?runs-on:\s*windows-latest' -Because "GH preflight job must run on windows-latest (no fabric / Key Vault access required)" + $content | Should -Match '(?ms)preflight:.*?permissions:.*?actions:\s*read' -Because "GH preflight needs actions:read to call the runners API" + $content | Should -Match '(?m)^\s*needs:\s*preflight\s*$' -Because "the sideload job must gate on preflight" + $content | Should -Match 'contains\(fromJSON\(.*true.*True.*TRUE.*1.*\),\s*vars\.SIDELOAD_UPDATES\)' -Because "master gate must accept true/True/TRUE/1 (v0.8.76)" + } + + It 'v0.8.76: sideload-updates.yml (Azure DevOps) has a Preflight stage on windows-latest' { + $yamlPath = Join-Path -Path $PSScriptRoot -ChildPath '..\Automation-Pipeline-Examples\azure-devops\sideload-updates.yml' + Test-Path $yamlPath | Should -BeTrue + $content = Get-Content -Path $yamlPath -Raw + + $content | Should -Match '(?m)^-\s*stage:\s*Preflight\s*$' -Because "ADO sideload pipeline must define a Preflight stage (v0.8.76)" + $content | Should -Match '(?ms)stage:\s*Preflight.*?vmImage:\s*windows-latest' -Because "ADO Preflight stage must run on windows-latest" + $content | Should -Match '(?m)^\s*dependsOn:\s*Preflight\s*$' -Because "the Sideload stage must depend on Preflight" + $content | Should -Match '##vso\[task\.uploadsummary\]' -Because "ADO preflight must write the markdown panel via uploadsummary" + $content | Should -Match "eq\(variables\['SIDELOAD_UPDATES'\],\s*'true'\)" -Because "ADO gate must accept 'true'" + $content | Should -Match "eq\(variables\['SIDELOAD_UPDATES'\],\s*'1'\)" -Because "ADO gate must accept '1' (v0.8.76)" + } + It 'Should export exactly 60 functions' { $script:ModuleInfo.ExportedFunctions.Count | Should -Be 60 } diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 7692b2c2..d6569778 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -8,3 +8,5 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-12T14:22:24Z","0.8.75","1182","1140","3","1","151.94","148.79","11.11","Invoke-Tests.ps1","5.1.26100.8655" "2026-06-12T14:26:27Z","0.8.75","1182","1142","1","1","60.06","57.7","4","Invoke-Tests.ps1","5.1.26100.8655" "2026-06-12T14:30:16Z","0.8.75","1182","1143","0","1","82.37","79.88","4.06","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-12T15:26:59Z","0.8.76","1184","1141","4","1","112.14","109.68","10.52","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-12T15:30:21Z","0.8.76","1184","1145","0","1","100.71","98.24","3.89","Invoke-Tests.ps1","5.1.26100.8655" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index 5a7ef3eb..81b8b8f0 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,17 @@ > > **For older releases**, this is the canonical reference; the main README intentionally stays slim so the most recent block is easy to find. > -> **For v0.8.75 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0875) `What's New in v0.8.75` section. +> **For v0.8.76 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0876) `What's New in v0.8.76` section. + +--- + +### What's New in v0.8.75 + +v0.8.75 removes the duplicate Cycle calendar table from the Step.3 apply-updates schedule audit step summary on runs with Recommend-view findings (uncovered/partial cron windows, `RingMissingFromSchedule`, `RingOrphanedInSchedule`, `UnparseableCron`, or `NoWindowTag` when a cluster CSV was supplied). The Recommend snippet's plain 5-column calendar was inlined into the wrapper's action-required block **above** the wrapper's own enriched 7-column calendar, so operators saw two calendars back-to-back. Clean-fleet runs were unaffected. New additive `[switch]$OmitCycleCalendar` parameter on `Test-AzLocalApplyUpdatesScheduleCoverage` (default off, so direct callers see no behaviour change); `Export-AzLocalApplyUpdatesScheduleAudit` opts in so only the enriched calendar surfaces in the Step.3 summary regardless of findings. The drift-notice banner from `Add-AzLocalPipelineVersionBanner` now recommends `Update-AzLocalPipelineExample` (marker-aware merge that preserves `AZLOCAL-CUSTOMIZE` blocks) with the right `-Platform` and a `-Destination` hint per detected pipeline host. Three Step.3 step-summary screenshots added to `Automation-Pipeline-Examples/README.md` section 8.3. No public API or export-count change (still 60). + +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.74` to `0.8.75` across all bundled pipeline templates. + +See [CHANGELOG.md](../CHANGELOG.md#0875---2026-06-12) for the full v0.8.75 entry. ---