From 063a743ed365a8e32049bb9cc9d76f221610deb0 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 10:50:05 +0100 Subject: [PATCH 01/17] v0.8.85: consolidate setup workflow and align pipeline/version metadata --- .../Automation-Pipeline-Examples/README.md | 116 +++---- .../apply-updates-schedule-audit.yml | 6 +- .../azure-devops/apply-updates.yml | 6 +- .../azure-devops/assess-update-readiness.yml | 6 +- .../azure-devops/authentication-test.yml | 4 +- .../fleet-connectivity-status.yml | 6 +- .../azure-devops/fleet-health-status.yml | 4 +- .../azure-devops/fleet-update-status.yml | 4 +- .../azure-devops/inventory-clusters.yml | 2 +- .../azure-devops/manage-updatering-tags.yml | 6 +- .../azure-devops/monitor-updates.yml | 4 +- .../azure-devops/sideload-updates.yml | 4 +- .../apply-updates-schedule-audit.yml | 4 +- .../github-actions/apply-updates.yml | 4 +- .../assess-update-readiness.yml | 4 +- .../github-actions/authentication-test.yml | 219 ------------- .../fleet-connectivity-status.yml | 4 +- .../github-actions/fleet-health-status.yml | 4 +- .../github-actions/fleet-update-status.yml | 6 +- .../github-actions/inventory-clusters.yml | 168 ---------- .../github-actions/manage-updatering-tags.yml | 8 +- .../github-actions/monitor-updates.yml | 4 +- .../setup-validate-and-inventory.yml | 289 ++++++++++++++++++ .../github-actions/sideload-updates.yml | 4 +- .../AzLocal.UpdateManagement.psd1 | 4 +- .../AzLocal.UpdateManagement.psm1 | 2 +- AzLocal.UpdateManagement/CHANGELOG.md | 45 +-- .../Public/Copy-AzLocalPipelineExample.ps1 | 55 +++- .../Public/Update-AzLocalPipelineExample.ps1 | 48 +++ AzLocal.UpdateManagement/README.md | 20 +- .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 14 +- 31 files changed, 528 insertions(+), 546 deletions(-) delete mode 100644 AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml delete mode 100644 AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml create mode 100644 AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index 7ed78750..cc54771d 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -56,50 +56,49 @@ It is written in the same step-by-step style as [`ITSM/README.md`](../ITSM/READM By the end of this guide you will have: - A federated identity (no client secrets) wired into your CI/CD platform with the **minimum** Azure RBAC needed for cluster update management. -- Eleven working pipelines committed to your repo and visible in the Actions / Pipelines UI: - - **Authentication Validation and Subscription Scope Report** (Step.0) - probe the federated identity end-to-end, emit a JUnit-rendered authentication / scope / Resource Graph report, and capture the subscription set the pipeline identity can read. *Manual only - re-run after every RBAC change.* - - **Inventory** (Step.1) - enumerate every Azure Local cluster the identity can see and export a CSV. *Scheduled weekly + manual.* - - **Manage UpdateRing tags** (Step.2) - bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, `UpdateExcluded` tags from that CSV. *Manual only.* - - **Apply-Updates Schedule Coverage Audit** (Step.3, v0.7.65) - read-only weekly audit that compares the cron(s) in your `apply-updates` pipeline to the `UpdateStartWindow` tags actually present on your clusters and flags any (UpdateRing, UpdateStartWindow) pair that no cron will reach. *Scheduled weekly Mon 05:00 UTC + manual.* - - **Fleet Connectivity Status** (Step.4, v0.7.79+, enhanced in v0.7.85) - read-only daily snapshot of Arc agent connectivity, physical NIC health, Azure Resource Bridge status, and the node-count reconciliation between cluster `reportedProperties.nodes` and Arc-tagged physical machines. *Scheduled daily 05:30 UTC + manual.* - - **Assess Update Readiness** (Step.5) - pre-flight, report-only readiness + blocking-health snapshot, published as JUnit XML. *Manual only.* - - **Sideload Updates** (Step.6, v0.8.7) - **opt-in, off by default, on-prem self-hosted runner/agent required.** Pre-stages solution-update media onto clusters that cannot pull updates from Azure directly (dark / air-gapped fabrics): Robocopy to the cluster import share, remote SHA256 verify, `Add-SolutionUpdate` import, then flip `UpdateSideloaded=True` for the downstream Step.7 apply. Re-entrant state machine driven by a frequent CRON; the multi-hour copy runs in a detached Scheduled Task. *Inert unless `SIDELOAD_UPDATES=true`. See [sideload.md](docs/sideload.md) and [sideload-robocopy.md](docs/sideload-robocopy.md).* - - **Apply Updates** (Step.7; **v0.8.81 step-summary polish**: per-host apply-updates step summary now uses the new shared `Get-AzLocalStatusIconMap` host-aware icon helper (fixes literal `:white_check_mark:` text on Azure DevOps step summaries), the Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink`, and the standing Ctrl-click tip is single-sourced via `Get-AzLocalCtrlClickTip`; **v0.8.79 break-glass override**: optional `force_immediate_update` / `forceImmediateUpdate` pipeline parameter (defaults `false`, **manual run / queue only**) bypasses the per-cluster `UpdateStartWindow` / `UpdateExclusionsWindow` maintenance-window gate for emergency / out-of-window patching - the override is unreachable from the scheduled `apply-updates-schedule.yml` configuration file and a high-visibility WARNING banner is emitted into the run log when it fires; **v0.8.78 step-summary UX polish**: ``ScheduleBlocked`` / ``SideloadedBlocked`` / ``ExcludedByTag`` outcomes render as JUnit ```` instead of ```` so ``dorny/test-reporter`` no longer flips Step.07 RED on by-design gate-respect outcomes - ``HealthCheckBlocked`` deliberately stays a ````; the Readiness KPI table now also surfaces **Already Up to Date** and **Not Ready (needs attention before updating)** rows so operators see the full ring breakdown - eligible, started, skipped, blocked, up-to-date, needs-attention - in one place; GHA-only: ``actions/download-artifact@v6 -> @v7`` silences the Node 20 deprecation warning) - apply updates to a single `UpdateRing` wave at a time, with WhatIf / dry-run support. *Manual only by default - **you must add a schedule** that lines up with your cluster `UpdateStartWindow` tags, see [Step 7 - Apply Updates](docs/appendix-pipelines.md#step-7---apply-updates) and [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods).* - - **Monitor In-Flight Updates** (Step.8, v0.7.90; **v0.8.81 host-aware icons**: the State and Current Step Status columns now use the shared `Get-AzLocalStatusIconMap` helper (fixes literal `:large_blue_circle:` / `:hourglass_flowing_sand:` text on Azure DevOps step summaries); v0.7.96 surfaces `Status` + deepest `ErrorMessage` columns, adds the `StepError` stuck-step JUnit type for runs that have hit an error inside a step without crossing the long-running threshold, and renders portal-linked Cluster Name / Update Name cells in the markdown summary; **v0.7.98 UX overhaul**: composite `SeverityScore` sort, per-cell `StateIcon` + `StatusIcon`, horizontal chip stack (`STEP-STUCK` / `RUN-STUCK` / `UNRESOLVED` / `RECENT-FAIL`), `CRITICAL / WARN / OK` fleet status badge at the top of the job summary, collapsible `
` Verbose Error block per row, and JUnit `` + `` populated with real run elapsed seconds). Operational snapshot during an active wave: lists each cluster whose latest update run is `InProgress`, with current step, progress (`completed/total steps`), elapsed duration, the `Status` column (`Success`/`Error`/`InProgress`/...), and the deepest `errorMessage` walked out of the nested ARM `steps[]` tree; flags long-running runs (default >6h) AND step-errored stuck runs as JUnit failures in the Checks tab. *Scheduled 5x/day at 20:00, 22:00, 00:00, 02:00, 04:00 UTC (every 2h across the typical overnight maintenance window) + manual; default cadence is editable in `monitor-updates.yml` (v0.7.92+).* - - **Fleet Update Status** (Step.9, formerly Step.8; **v0.8.81 host-aware icons**: the Critical Health Status table, the Primary Status table, and the Version Distribution Support column now use the shared `Get-AzLocalStatusIconMap` helper (fixes literal `:green_circle:` / `:yellow_circle:` / `:information_source:` text on Azure DevOps step summaries); v0.7.96 promotes `NeedsAttention` into the **Update Failed** bucket, adds a new **Action Required** bucket for `PreparationFailed`, and folds `PreparationInProgress` into **Update In Progress**; **v0.7.98** populates JUnit `time=` on each `` in the `📜 Update Run History and Error Details` testsuite using `DurationMinutes * 60`). Scheduled daily snapshot of fleet update state, surfaced in the Tests tab. Markdown summary's `📜 Update Run History and Error Details` table now also carries portal-linked Cluster Name / Update Name cells plus the deepest-step `ErrorMessage`. *Scheduled daily 06:00 UTC + manual.* - - **Fleet Health Status** (Step.10, formerly Step.9; **v0.8.81**: fixes the KPI counting bug where Healthy + Unhealthy did not sum to Total - the top KPI table is now split into **Cluster Counts** (Total / Healthy / Unhealthy / **Other**) and **Failing Checks Breakdown**, with a new `other_clusters` step output. Detailed Results columns reordered to put `Title` first and add a collapsible `Description` block that surfaces drive/volume-level detail (e.g. file paths from `Microsoft.Health.FaultType.Volume.FileSystem.Corruption.Correctable` warnings); raw fault-type `Name` preserved for grep/triage) - scheduled daily snapshot of 24-hour system health-check failures, surfaced in the Tests tab. *Scheduled daily 07:00 UTC + manual.* +- Setup/Fleet workflows committed to your repo and visible in the Actions / Pipelines UI: + - **Setup: 01 - Validate Auth and Inventory Clusters** (GitHub) - merged auth + inventory flow with a clear setup-first summary (including collapsible subscription details) and cluster inventory export. + - **Setup: 02 - Manage UpdateRing Tags** - bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, `UpdateExcluded` tags from CSV. + - **Setup: 03 - Apply-Updates Schedule Coverage Audit** - read-only audit that validates your schedule against `UpdateStartWindow` tags. + - **Fleet: 01 - Assess Update Readiness** - pre-flight readiness + blocking-health gate report. + - **Fleet: 02 - Fleet Connectivity Status** - Arc connectivity, NIC health, and ARB status snapshot. + - **Fleet: 03 - Sideload Updates (opt-in)** - on-prem media pre-stage workflow for disconnected environments. + - **Fleet: 04 - Apply Updates** - ring-scoped update execution with WhatIf support and schedule-aware gating. + - **Fleet: 05 - Monitor In-Flight Updates** - active-run progress and stuck-run diagnostics. + - **Fleet: 06 - Fleet Update Status** - daily fleet-wide update-state summary. + - **Fleet: 07 - Fleet Health Status** - daily 24-hour health-failure summary. - An end-to-end "ring-based" rollout pattern: Pilot -> Wave2 -> Production, with each ring gated on the previous wave's success. - **Optional**: a ServiceNow integration that opens deduped incidents for clusters whose run status indicates the module's own retries cannot recover (failures, blocking health checks, sideloaded payload missing) - see [section 7](#7-optional-open-itsm-tickets-for-clusters-needing-operator-action). The pipelines are **fully opt-in additive layers** over the module. The PowerShell functions also work without any pipeline at all - see [section 10](#10-standalone-html-report-no-pipeline) for the ad-hoc / desktop story. -### 1.1 Why the pipelines are named `Step.N - ` +### 1.1 Why the pipelines are named `Setup: NN` and `Fleet: NN` -The eleven YAMLs ship with a de-numbered filename (e.g. `apply-updates.yml`) **and** a matching `Step.N - ` value in each workflow's `name:` field (GitHub Actions) / header title (Azure DevOps): +The active workflow model uses two clear groups: -Each row's name links to the matching `Step N -` section in [`docs/appendix-pipelines.md`](docs/appendix-pipelines.md), which documents what every pipeline does, the cmdlets it calls, its inputs / outputs / dependencies, the RBAC and exit conditions, and the emitted artefacts. The **GH Actions** and **Azure DevOps** columns link directly to the source YAML for each platform. +- `Setup: 01-03` for onboarding and configuration. +- `Fleet: 01-07` for day-2 operational monitoring and update execution. -| Step | File / Workflow name | GH Actions | Azure DevOps | -|---:|---|---|---| -| 0 | [Step.0 - Authentication Validation and Subscription Scope Report](docs/appendix-pipelines.md#step-0---authentication-validation-and-subscription-scope-report) | [`authentication-test.yml`](github-actions/authentication-test.yml) | [`authentication-test.yml`](azure-devops/authentication-test.yml) | -| 1 | [Step.1 - Inventory Azure Local Clusters](docs/appendix-pipelines.md#step-1---inventory-clusters) | [`inventory-clusters.yml`](github-actions/inventory-clusters.yml) | [`inventory-clusters.yml`](azure-devops/inventory-clusters.yml) | -| 2 | [Step.2 - Manage UpdateRing Tags](docs/appendix-pipelines.md#step-2---manage-updatering-tags) | [`manage-updatering-tags.yml`](github-actions/manage-updatering-tags.yml) | [`manage-updatering-tags.yml`](azure-devops/manage-updatering-tags.yml) | -| 3 | [Step.3 - Apply-Updates Schedule Coverage Audit](docs/appendix-pipelines.md#step-3---apply-updates-schedule-coverage-audit) | [`apply-updates-schedule-audit.yml`](github-actions/apply-updates-schedule-audit.yml) | [`apply-updates-schedule-audit.yml`](azure-devops/apply-updates-schedule-audit.yml) | -| 4 | [Step.4 - Fleet Connectivity Status](docs/appendix-pipelines.md#step-4---fleet-connectivity-status) | [`fleet-connectivity-status.yml`](github-actions/fleet-connectivity-status.yml) | [`fleet-connectivity-status.yml`](azure-devops/fleet-connectivity-status.yml) | -| 5 | [Step.5 - Assess Update Readiness](docs/appendix-pipelines.md#step-5---assess-update-readiness) | [`assess-update-readiness.yml`](github-actions/assess-update-readiness.yml) | [`assess-update-readiness.yml`](azure-devops/assess-update-readiness.yml) | -| 6 | [Step.6 - Sideload Updates (opt-in, on-prem)](docs/appendix-pipelines.md#step-6---sideload-updates) | [`sideload-updates.yml`](github-actions/sideload-updates.yml) | [`sideload-updates.yml`](azure-devops/sideload-updates.yml) | -| 7 | [Step.7 - Apply Updates](docs/appendix-pipelines.md#step-7---apply-updates) | [`apply-updates.yml`](github-actions/apply-updates.yml) | [`apply-updates.yml`](azure-devops/apply-updates.yml) | -| 8 | [Step.8 - Monitor In-Flight Updates](docs/appendix-pipelines.md#step-8---monitor-in-flight-updates) | [`monitor-updates.yml`](github-actions/monitor-updates.yml) | [`monitor-updates.yml`](azure-devops/monitor-updates.yml) | -| 9 | [Step.9 - Fleet Update Status](docs/appendix-pipelines.md#step-9---fleet-update-status) | [`fleet-update-status.yml`](github-actions/fleet-update-status.yml) | [`fleet-update-status.yml`](azure-devops/fleet-update-status.yml) | -| 10 | [Step.10 - Fleet Health Status](docs/appendix-pipelines.md#step-10---fleet-health-status) | [`fleet-health-status.yml`](github-actions/fleet-health-status.yml) | [`fleet-health-status.yml`](azure-devops/fleet-health-status.yml) | +| Group | Workflow name | GH Actions | Azure DevOps | +|---|---|---|---| +| Setup | Setup: 01 - Validate Auth and Inventory Clusters | `setup-validate-and-inventory.yml` | `authentication-test.yml` + `inventory-clusters.yml` | +| Setup | Setup: 02 - Manage UpdateRing Tags | `manage-updatering-tags.yml` | `manage-updatering-tags.yml` | +| Setup | Setup: 03 - Apply-Updates Schedule Coverage Audit | `apply-updates-schedule-audit.yml` | `apply-updates-schedule-audit.yml` | +| Fleet | Fleet: 01 - Assess Update Readiness | `assess-update-readiness.yml` | `assess-update-readiness.yml` | +| Fleet | Fleet: 02 - Fleet Connectivity Status | `fleet-connectivity-status.yml` | `fleet-connectivity-status.yml` | +| Fleet | Fleet: 03 - Sideload Updates (Opt-in) | `sideload-updates.yml` | `sideload-updates.yml` | +| Fleet | Fleet: 04 - Apply Updates | `apply-updates.yml` | `apply-updates.yml` | +| Fleet | Fleet: 05 - Monitor In-Flight Updates | `monitor-updates.yml` | `monitor-updates.yml` | +| Fleet | Fleet: 06 - Fleet Update Status | `fleet-update-status.yml` | `fleet-update-status.yml` | +| Fleet | Fleet: 07 - Fleet Health Status | `fleet-health-status.yml` | `fleet-health-status.yml` | -- **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field inside the YAML. Because every `name:` starts with `Step.N - `, the sidebar lists the eleven workflows in execution order (Step.0 first, Step.10 last) instead of the cosmetically confusing alphabetical scatter (`Apply Updates`, `Apply-Updates Schedule Coverage Audit`, `Assess Update Readiness`, ...). +- **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field. Prefixing names with `Setup: NN` and `Fleet: NN` keeps the sidebar in intended execution order. - ![GitHub Actions sidebar of the Azure/AzLocal.UpdateManagement consumer repo showing all eleven workflows listed in execution order (Step.00 Authentication Validation, Step.01 Inventory Azure Local Clusters, Step.02 Manage UpdateRing Tags, Step.03 Apply-Updates Schedule Coverage Audit, Step.04 Fleet Connectivity Status, Step.05 Assess Update Readiness, Step.06 Sideload Updates Opt-in, Step.07 Apply Updates, Step.08 Monitor In-Flight Updates, Step.09 Fleet Update Status, Step.10 Fleet Health Status) with the All workflows pane showing a recent run history of green-checked successful runs across Step.05 / Step.08 / Step.09 / Step.10 from main](../docs/images/github-actions-10-pipelines-view.png) + ![GitHub Actions sidebar showing Setup and Fleet workflows in execution order](../docs/images/github-actions-10-pipelines-view.png) - *Step.NN display-name prefix means the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sort the eleven workflows in execution order rather than the cosmetically confusing alphabetical scatter.* + *The Setup/Fleet numeric prefixes keep the GitHub Actions sidebar in practical execution order rather than a purely alphabetical scatter.* -- **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at *import time* (not by the YAML filename and not by any top-level `name:` field - the `name:` field in an ADO YAML controls the per-run *build number*, not the pipeline display name). When you import each YAML, the import wizard prefills the suggested pipeline name from the YAML's leading title comment; the YAMLs in this repo open with `# Step.N - `, so the suggested name is already correct. **Accept the suggested name** (or paste `Step.N - ` yourself), and the Pipelines list will sort in execution order. You can rename a pipeline later via *Pipeline -> Edit -> Settings -> Name*. +- **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at import time (not by filename). Use the same `Setup: NN` / `Fleet: NN` naming when you import so the list stays in operational order. If you prefer a different naming scheme (e.g. `00 - Auth`, `01 - Inventory`, ...), just change the `name:` field in each GH Actions YAML and / or pick a different prefix at ADO import time. Nothing else in the module depends on these display names. @@ -131,10 +130,10 @@ The identity created in section 4 needs the following permissions on every subsc | `Microsoft.AzureStackHCI/clusters/updates/apply/action` | Apply Updates. | | `Microsoft.AzureStackHCI/clusters/updateSummaries/read` | Apply Updates, Fleet Update Status. | | `Microsoft.AzureStackHCI/clusters/updates/updateRuns/read` | Apply Updates, Fleet Update Status. | -| `Microsoft.AzureStackHCI/edgeDevices/read` | Fleet Connectivity Status (Step.4 - physical NIC inventory). | -| `Microsoft.HybridCompute/machines/read` | Fleet Connectivity Status (Step.4 - Arc agent inventory). | +| `Microsoft.AzureStackHCI/edgeDevices/read` | Fleet Connectivity Status (physical NIC inventory). | +| `Microsoft.HybridCompute/machines/read` | Fleet Connectivity Status (Arc agent inventory). | | `Microsoft.HybridCompute/machines/extensions/read` | Reserved for future Arc-machine extension reporting (no current cmdlet queries this, but bundled to avoid a follow-up role update). | -| `Microsoft.ResourceConnector/appliances/read` | Fleet Connectivity Status (Step.4 - Azure Resource Bridges). | +| `Microsoft.ResourceConnector/appliances/read` | Fleet Connectivity Status (Azure Resource Bridges). | | `Microsoft.ResourceGraph/resources/read` | All pipelines (Resource Graph lookups). | | `Microsoft.Resources/subscriptions/resourceGroups/read` | All pipelines (resolve cluster scopes). | | `Microsoft.Resources/tags/read` | Manage UpdateRing Tags, sideloaded workflow. | @@ -941,27 +940,27 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp > Copy-AzLocalPipelineExample -Destination .\pipelines -Platform AzureDevOps > ``` > -> The function prints a short "next steps" summary pointing at the copied YAML location with the recommended workflow / pipeline to run first (the **Authentication Validation and Subscription Scope Report** - see sections 5.1 and 5.2 below). Supports `-Platform GitHub | AzureDevOps | All`, `-PassThru`, `-WhatIf`, `-Confirm`. +> The function prints a short "next steps" summary pointing at the copied YAML location with the recommended workflow / pipeline to run first (GitHub: **Setup: 01 - Validate Auth and Inventory Clusters**, Azure DevOps: auth validation + inventory onboarding). Supports `-Platform GitHub | AzureDevOps | All`, `-PassThru`, `-WhatIf`, `-Confirm`. > > **Refusing to overwrite**: the function will refuse to overwrite any file that already exists in `-Destination`, listing the conflicts in the error message. To refresh after a module upgrade, delete the existing copies first (`Remove-Item .\.github\workflows\*.yml`) and re-run. ### 5.1 GitHub Actions -1. **Run the Authentication Validation and Subscription Scope Report first (strongly recommended).** Before exercising all seven workflows, validate that the App Registration, federated credentials, GitHub secrets, environments, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Step.0 - Authentication Validation and Subscription Scope Report`**. This narrows any failure to *one* small YAML file instead of debugging seven interacting workflows simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed - drift here is the earliest signal that downstream fleet reports are about to under- or over-count clusters. +1. **Run Setup: 01 first (strongly recommended).** Before exercising the Fleet workflows, validate that the App Registration, federated credentials, GitHub secrets, environments, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Setup: 01 - Validate Auth and Inventory Clusters`**. This narrows any failure to one onboarding workflow instead of debugging multiple operational workflows simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed. - The validation workflow ships with the module at [`github-actions/authentication-test.yml`](./github-actions/authentication-test.yml). v0.7.70 emits a **JUnit XML** report (Authentication / Subscription Scope / Resource Graph Reachability) rendered in the run's Checks UI via `dorny/test-reporter`, a **markdown summary** with the subscription count + subscription detail table written to `$GITHUB_STEP_SUMMARY`, and a `auth-report` artifact (XML + `subscriptions.json` + `subscriptions.csv`) for ITSM / dashboard ingest. + The onboarding workflow ships with the module at [`github-actions/setup-validate-and-inventory.yml`](./github-actions/setup-validate-and-inventory.yml). It emits the auth validation JUnit report (Authentication / Subscription Scope / Resource Graph Reachability), writes a setup-focused markdown summary (including a collapsible subscription-details section), and exports both auth and inventory artifacts for downstream workflows. - **If you used the `Copy-AzLocalPipelineExample` shortcut above**: the file is already in `.github/workflows/` alongside the other seven workflow YAMLs - those will ride along on the commit but stay dormant until you trigger them manually (`gh workflow run`) or their schedules fire. Commit and push to the default branch, then run the trigger commands below. - **If you didn't use the shortcut**: copy just that one file into your repo's `.github/workflows/`, commit, and push to the default branch. - Then trigger it twice - once branch-scoped, once environment-scoped - to prove **both** federated credential types work end-to-end: + Then trigger it twice - once branch-scoped, once environment-scoped - to prove both federated credential types work end-to-end: ```powershell # 1. Branch-scoped run - exercises the 'GitHubActions-main' federated credential. - gh workflow run authentication-test.yml --repo $repo + gh workflow run setup-validate-and-inventory.yml --repo $repo # 2. Environment-scoped run - exercises the 'GitHubActions-DevTest' federated credential. - gh workflow run authentication-test.yml --repo $repo -f environment=DevTest + gh workflow run setup-validate-and-inventory.yml --repo $repo -f environment=DevTest # Watch the most recent run live (Ctrl+C to stop watching, run continues). gh run watch --repo $repo @@ -972,8 +971,8 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp At the `gh` CLI level, `gh run watch` shows the run summary as the steps complete (`gh` actually renders these as Unicode check marks; reproduced here in ASCII): ```text - ? Select a workflow run * Step.0 - Authentication Validation and Subscription Scope Report, Step.0 - Authentication Validation and Subscription Scope Report [main] 12s ago - [OK] main Step.0 - Authentication Validation and Subscription Scope Report - + ? Select a workflow run * Setup: 01 - Validate Auth and Inventory Clusters, Setup: 01 - Validate Auth and Inventory Clusters [main] 12s ago + [OK] main Setup: 01 - Validate Auth and Inventory Clusters - Triggered via workflow_dispatch less than a minute ago JOBS @@ -986,7 +985,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp [OK] Post Azure login (OIDC) [OK] Complete job - [OK] Run Step.0 - Authentication Validation and Subscription Scope Report () completed with 'success' + [OK] Run Setup: 01 - Validate Auth and Inventory Clusters () completed with 'success' ``` Inside the `Collect Authentication and Subscription Scope Report` step, the run log shows: @@ -1007,7 +1006,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp ``` - ![Step.0 - Authentication Validation and Subscription Scope Report - Validate OIDC + RBAC + Subscription Scope job, showing the `Collect Authentication and Subscription Scope Report` step expanded with `az account show`, role assignments, the per-subscription scope table, and Resource Graph cluster count](../docs/images/auth-smoke-test-validate-oidc.png) + ![Setup: 01 - Validate Auth and Inventory Clusters run, showing authentication validation output and subscription scope details](../docs/images/auth-smoke-test-validate-oidc.png) You may see one informational `windows-latest` -> `windows-2025-vs2026` migration notice in the run annotations. The sample workflows pin `runs-on: windows-latest` (the module is a Windows-side PowerShell module), and GitHub will retarget the alias to the new image automatically when it becomes the default - no action required on your part. As of v0.7.60 the previously-seen Node.js 20 deprecation banner (against `actions/checkout@v4`, `azure/login@v2`, `actions/upload-artifact@v4`, `dorny/test-reporter@v1`) is gone: the sample workflows have been refreshed to Node 24-compatible majors (`@v5`, `@v3`, `@v6`, `@v3` respectively). @@ -1020,17 +1019,18 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp | `Error: Could not fetch access token for Azure` (no AADSTS code) | The workflow lacks `permissions: id-token: write` or the secrets are missing/misspelt. | Confirm the `permissions:` block is present and run `gh secret list --repo $repo` shows all three `AZURE_*` secrets. | | Environment-scoped run hangs in **Waiting for review** | The environment has required-reviewers protection (good!) and is waiting for you to approve. | Approve in the **Actions** tab, or remove required reviewers from the validation run via the environment settings. | - Once the run is green, leave `authentication-test.yml` in place and schedule yourself to re-run it monthly (or whenever you change RBAC / federated credentials / subscription assignments). If you used the `Copy-AzLocalPipelineExample` shortcut, the other nine workflows are already on the default branch - skip to step 4 to run them. Otherwise, proceed to step 2 to copy the remaining workflow files. + Once the run is green, keep `setup-validate-and-inventory.yml` in place and schedule yourself to re-run it monthly (or whenever you change RBAC / federated credentials / subscription assignments). If you used the `Copy-AzLocalPipelineExample` shortcut, the other workflows are already on the default branch - skip to step 4 to run them. Otherwise, proceed to step 2 to copy the remaining workflow files. 2. Copy every file from [`github-actions/`](./github-actions/) into `.github/workflows/` in your repo: ```text .github/ workflows/ - inventory-clusters.yml + setup-validate-and-inventory.yml manage-updatering-tags.yml apply-updates-schedule-audit.yml fleet-connectivity-status.yml assess-update-readiness.yml + sideload-updates.yml apply-updates.yml monitor-updates.yml fleet-update-status.yml @@ -1038,7 +1038,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp ``` 3. Commit and push. The workflows appear in the **Actions** tab. 4. Each workflow exposes its inputs via the **Run workflow** button (workflow_dispatch). The scheduled triggers (e.g. `fleet-connectivity-status.yml` runs daily at 05:30 UTC, `monitor-updates.yml` runs 5x/day at 20:00, 22:00, 00:00, 02:00, 04:00 UTC (every 2h across the typical overnight maintenance window, v0.7.92+ default - edit the cron in the file if your maintenance window differs), `fleet-update-status.yml` runs daily at 06:00 UTC, `fleet-health-status.yml` runs daily at 07:00 UTC, `apply-updates-schedule-audit.yml` runs weekly on Mondays at 05:00 UTC) activate automatically once the file is on the default branch. -5. **Replace the starter `apply-updates-schedule.yml` with one generated from your live fleet** (required for **scheduled** Step.7 / Step.3 runs; manual `workflow_dispatch` runs of Step.7 work without it because they use the `-UpdateRingValue` input verbatim). v0.8.7+ `Copy-AzLocalPipelineExample -Platform GitHub` drops a **starter** `apply-updates-schedule.yml` into a **`config\` folder at the repo root** (i.e. at `config\apply-updates-schedule.yml`, alongside `config\ClusterUpdateRings.csv`) by default. The starter is a verbatim copy of [`apply-updates-schedule.example.yml`](./apply-updates-schedule.example.yml) and ships with **DEMO** ring names (`Canary`, `DevTest`, `Ring1`, `Ring2`, `Prod`) that almost never match a real estate's `UpdateRing` tag values - regenerate from your live fleet, overwriting the demo file: +5. **Replace the starter `apply-updates-schedule.yml` with one generated from your live fleet** (required for scheduled Apply + Schedule-Audit runs; manual `workflow_dispatch` runs of Apply work without it because they use the `-UpdateRingValue` input verbatim). v0.8.7+ `Copy-AzLocalPipelineExample -Platform GitHub` drops a starter `apply-updates-schedule.yml` into a `config\` folder at the repo root by default. The starter ships with demo ring names (`Canary`, `DevTest`, `Ring1`, `Ring2`, `Prod`) that almost never match real estates; regenerate from your live fleet: ```powershell # Regenerate from your fleet's live UpdateRing tag values, overwriting the starter. @@ -1046,13 +1046,13 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp New-AzLocalApplyUpdatesScheduleConfig -OutputPath .\config\apply-updates-schedule.yml -Force ``` - Review the regenerated file, uncomment / edit the rows you want active, then `git add .\config\apply-updates-schedule.yml ; git commit ; git push`. The starter is safe to leave in place until then - the bundled Step.7 ships with every `cron:` line commented out inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` markers, so it cannot fire on a `schedule:` trigger until you explicitly add a cron entry. Manual `workflow_dispatch` runs of Step.7 ignore the schedule file entirely (they take `-UpdateRingValue` verbatim from the run-form input). + Review the regenerated file, uncomment / edit the rows you want active, then `git add .\config\apply-updates-schedule.yml ; git commit ; git push`. The starter is safe to leave in place until then - the bundled apply workflow ships with every `cron:` line commented out inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` markers, so it cannot fire on a `schedule:` trigger until you explicitly add a cron entry. Manual `workflow_dispatch` runs ignore the schedule file entirely (they take `-UpdateRingValue` verbatim from the run-form input). **Safety rails**: `Copy-AzLocalPipelineExample` **never overwrites** an existing `apply-updates-schedule.yml` - any operator-tailored schedule you commit is preserved across re-runs. Pass `-SkipStarterSchedule` to suppress the starter copy entirely (e.g. if you pre-stage the schedule via separate tooling). For the full schema, multi-stage rollouts, weekly-cycle / ring-eligibility model, and the `allowedUpdateVersions` allow-list (schema v2), see [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods). ### 5.2 Azure DevOps -1. **Run the Authentication Validation and Subscription Scope Report first (strongly recommended).** Before importing all eight remaining pipelines, validate that the service connection (Workload Identity Federation), App Registration, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Step.0 - Authentication Validation and Subscription Scope Report`**. This narrows any failure to *one* small YAML file instead of debugging eight interacting pipelines simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed - drift here is the earliest signal that downstream fleet reports are about to under- or over-count clusters. +1. **Run authentication validation first (strongly recommended).** Before importing the remaining pipelines, validate that the service connection (Workload Identity Federation), App Registration, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running the authentication validation pipeline. This narrows any failure to one small YAML file instead of debugging multiple interacting pipelines simultaneously. The validation pipeline ships with the module at [`azure-devops/authentication-test.yml`](./azure-devops/authentication-test.yml). v0.7.70 emits a **JUnit XML** report (Authentication / Subscription Scope / Resource Graph Reachability) published via `PublishTestResults@2` and rendered in the run's **Tests** tab, a **markdown summary** with the subscription count + subscription detail table uploaded to the run's **Summary** tab via `##vso[task.uploadsummary]`, and a `auth-report` pipeline artifact (XML + `subscriptions.json` + `subscriptions.csv`) for ITSM / dashboard ingest. @@ -1095,7 +1095,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp 3. **Pipelines -> New pipeline -> Azure Repos Git -> your repo -> Existing Azure Pipelines YAML file**, then point at the path of each file. Repeat for all eight. 4. After the pipeline is created, click **Save** (not **Run**) until you are ready to execute. 5. Each pipeline references a service connection named `AzureLocal-ServiceConnection`. Either name your service connection to match, or change `azureSubscription:` in each YAML. -6. **Replace the starter `apply-updates-schedule.yml` with one generated from your live fleet** (required for **scheduled** Step.7 / Step.3 runs; manual queue runs of Step.7 work without it because they use the `updateRing` parameter verbatim). v0.8.7+ `Copy-AzLocalPipelineExample -Platform AzureDevOps -Destination .\pipelines\` drops a **starter** `apply-updates-schedule.yml` into a **`config\` folder at the repo root** (i.e. at `config\apply-updates-schedule.yml`, alongside `config\ClusterUpdateRings.csv`) by default - the same `config\` folder used on GitHub, so the path is identical on both platforms. The starter is a verbatim copy of [`apply-updates-schedule.example.yml`](./apply-updates-schedule.example.yml) and ships with **DEMO** ring names (`Canary`, `DevTest`, `Ring1`, `Ring2`, `Prod`) that almost never match a real estate's `UpdateRing` tag values - regenerate from your live fleet, overwriting the demo file: +6. **Replace the starter `apply-updates-schedule.yml` with one generated from your live fleet** (required for scheduled apply and schedule-audit runs; manual queue runs of apply work without it because they use the `updateRing` parameter verbatim). v0.8.7+ `Copy-AzLocalPipelineExample -Platform AzureDevOps -Destination .\pipelines\` drops a starter `apply-updates-schedule.yml` into `config\` at the repo root by default - the same `config\` folder used on GitHub, so the path is identical on both platforms. The starter is a verbatim copy of [`apply-updates-schedule.example.yml`](./apply-updates-schedule.example.yml) and ships with demo ring names (`Canary`, `DevTest`, `Ring1`, `Ring2`, `Prod`) that almost never match real estates - regenerate from your live fleet: ```powershell # Regenerate from your fleet's live UpdateRing tag values, overwriting the starter. @@ -1103,7 +1103,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp New-AzLocalApplyUpdatesScheduleConfig -OutputPath .\config\apply-updates-schedule.yml -Force ``` - Review the regenerated file, uncomment / edit the rows you want active, then commit and push. Step.7 (ADO) reads the file at `APPLY_UPDATES_SCHEDULE_PATH` (default `./config/apply-updates-schedule.yml`, which matches the default starter location) - override the pipeline variable if you keep the schedule elsewhere. The starter is safe to leave in place until then - the bundled Step.7 ships with every `cron:` line commented out inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` markers, so it cannot fire on a scheduled trigger until you explicitly add a cron entry. Manual queue runs of Step.7 ignore the schedule file entirely (they take `updateRing` verbatim from the run-form input). + Review the regenerated file, uncomment / edit the rows you want active, then commit and push. Apply Updates (ADO) reads the file at `APPLY_UPDATES_SCHEDULE_PATH` (default `./config/apply-updates-schedule.yml`) - override the pipeline variable if you keep the schedule elsewhere. The starter is safe to leave in place until then - the bundled apply workflow ships with every `cron:` line commented out inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` markers, so it cannot fire on a scheduled trigger until you explicitly add a cron entry. Manual queue runs ignore the schedule file entirely (they take `updateRing` verbatim from the run-form input). **Safety rails**: `Copy-AzLocalPipelineExample` **never overwrites** an existing `apply-updates-schedule.yml` - any operator-tailored schedule you commit is preserved across re-runs. Pass `-SkipStarterSchedule` to suppress the starter copy entirely (e.g. if you pre-stage the schedule via separate tooling). For the full schema, multi-stage rollouts, weekly-cycle / ring-eligibility model, and the `allowedUpdateVersions` allow-list (schema v2), see [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods). @@ -1165,7 +1165,7 @@ Copy-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub -U The destination folders are under git, so `git diff` after the refresh shows exactly which lines changed - giving you a final review gate before commit. `Copy-AzLocalPipelineExample` deliberately does not expose a `-Force` switch; `-Update` (with optional `-Confirm:$false`) is the only path to overwrite, and git remains the rollback. -**Preserving operator edits across upgrades (v0.7.68+).** For estates that have edited the bundled YAMLs to add custom cron schedules, ITSM secret bindings, or environment-specific tweaks, the marker-aware `Update-AzLocalPipelineExample` is the preferred refresh path. It replaces everything **outside** the documented customisation regions (paired `BEGIN-AZLOCAL-CUSTOMIZE:` / `END-AZLOCAL-CUSTOMIZE:` comments around `schedule-triggers` and, in Step.5, `itsm-secrets`) and **preserves** everything inside them, so a module bump no longer wipes your customer-specific cron lines or secret name mappings: +**Preserving operator edits across upgrades (v0.7.68+).** For estates that have edited the bundled YAMLs to add custom cron schedules, ITSM secret bindings, or environment-specific tweaks, the marker-aware `Update-AzLocalPipelineExample` is the preferred refresh path. It replaces everything **outside** the documented customisation regions (paired `BEGIN-AZLOCAL-CUSTOMIZE:` / `END-AZLOCAL-CUSTOMIZE:` comments around `schedule-triggers` and ITSM secrets) and **preserves** everything inside them, so a module bump no longer wipes your customer-specific cron lines or secret name mappings: ```powershell # Preview the marker-aware merge (writes nothing, prints a per-file change manifest): @@ -1310,7 +1310,7 @@ Key handoffs to remember: - **`cluster-readiness.csv`** carries `ClusterResourceId` from Assess into Apply. Apply does not re-query ARG to pick targets - it consumes the ID column directly, so a stale or malformed readiness CSV silently produces zero ready clusters. Always treat the most recent readiness run as the source of truth for the next Apply. - **`apply-updates-results.xml`** (JUnit) is what surfaces in the Tests tab on GH Actions and Azure DevOps. Failed-first ordering means actionable rows appear at the top of the reporter UI. - **`schedule-coverage-recommend.md`** is the only artifact intended to be pasted by hand - directly back into `apply-updates.yml`'s `on.schedule` / ADO trigger block when the audit reports `Uncovered` or `PartiallyCovered` rows. -- **Step.4 Fleet Connectivity Status runs parallel to (not downstream of) the apply-updates artifact chain.** It reads ARG directly and emits its own per-scope CSVs (`fleet-cluster-connectivity.csv`, `fleet-arc-status-summary.csv`, `fleet-arc-non-connected-machines.csv`, `fleet-physical-nics.csv`, `fleet-physical-nic-stats.csv`, `fleet-arb-status.csv`) plus a JUnit XML (`fleet-connectivity-status.xml`). No dependency on `cluster-readiness.csv` or `cluster-inventory.csv` - it is the upstream "can we see the fleet at all?" probe. Use it to triage why the apply-updates chain is empty or under-counting. +- **Fleet Connectivity Status runs parallel to (not downstream of) the apply-updates artifact chain.** It reads ARG directly and emits its own per-scope CSVs (`fleet-cluster-connectivity.csv`, `fleet-arc-status-summary.csv`, `fleet-arc-non-connected-machines.csv`, `fleet-physical-nics.csv`, `fleet-physical-nic-stats.csv`, `fleet-arb-status.csv`) plus a JUnit XML (`fleet-connectivity-status.xml`). No dependency on `cluster-readiness.csv` or `cluster-inventory.csv` - it is the upstream "can we see the fleet at all?" probe. Use it to triage why the apply-updates chain is empty or under-counting. ### 6.1 Inventory the estate @@ -1403,15 +1403,15 @@ New-AzLocalApplyUpdatesScheduleConfig -OutputPath .\config\apply-updates-schedul The cmdlet inspects every `UpdateRing` and `UpdateStartWindow` tag on the clusters the pipeline identity can read, then emits a schema v2 schedule with one block per distinct ring plus a Recommend / cron snippet inside `apply-updates.yml`'s `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` marker block. Review the generated file, uncomment the rows you want active, then commit and push. -**Why this step matters**: the schedule file is **required for scheduled Step.7 / Step.3 runs**. Manual `workflow_dispatch` (GitHub) / queue (Azure DevOps) runs of Step.7 work without it - they take `update_ring` / `updateRing` verbatim from the run-form input - but anything cron-triggered (the steady-state Step.7 wave kicks, the Step.3 weekly drift audit) reads ring eligibility from this file. +**Why this step matters**: the schedule file is required for scheduled apply and schedule-audit runs. Manual `workflow_dispatch` (GitHub) / queue (Azure DevOps) runs of apply work without it - they take `update_ring` / `updateRing` verbatim from the run-form input - but anything cron-triggered reads ring eligibility from this file. **Safety rails**: - `Copy-AzLocalPipelineExample` **never overwrites** an existing `apply-updates-schedule.yml` - any operator-tailored schedule already committed is preserved across re-runs. -- The bundled Step.7 ships with every `cron:` line commented out inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` markers, so Step.7 cannot fire on a `schedule:` trigger until at least one cron is explicitly uncommented - the DEMO starter is safe to leave in place until you regenerate. +- The bundled apply workflow ships with every `cron:` line commented out inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` markers, so it cannot fire on a `schedule:` trigger until at least one cron is explicitly uncommented - the demo starter is safe to leave in place until you regenerate. - Pass `-SkipStarterSchedule` to `Copy-AzLocalPipelineExample` to suppress the starter drop entirely (e.g. for estates that pre-stage the schedule via separate tooling). -For the full schema, multi-stage rollouts, weekly-cycle / ring-eligibility model, and the `allowedUpdateVersions` allow-list (schema v2), see [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods). The weekly drift detector (`apply-updates-schedule-audit.yml`) that catches `(UpdateRing, UpdateStartWindow)` tag combinations no cron in Step.7 will ever reach is summarised in section 6.7 (full runbook in [section 8.3](#83-end-to-end-runbook-apply-updates-schedule-coverage-audit)). +For the full schema, multi-stage rollouts, weekly-cycle / ring-eligibility model, and the `allowedUpdateVersions` allow-list (schema v2), see [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods). The weekly drift detector (`apply-updates-schedule-audit.yml`) that catches `(UpdateRing, UpdateStartWindow)` combinations with no matching apply cron is summarised in section 6.7 (full runbook in [section 8.3](#83-end-to-end-runbook-apply-updates-schedule-coverage-audit)). ### 6.6 Apply updates - one wave at a time @@ -1451,7 +1451,7 @@ The "steady-state" phase ships **three complementary pipelines**, all read-only, |----------|-------|---------|--------| | `fleet-connectivity-status.yml` *(v0.7.79+, enhanced in v0.7.85)* | 05:30 UTC | *"Are all clusters' Arc agents Connected? Are physical NICs healthy? Are Azure Resource Bridges reachable? Does the cluster's reported node count match the Arc-tagged physical machines we see?"* | JUnit + per-scope CSV/JSON + Markdown summary; one test case per cluster, with reconciliation rows that include "How to interpret + act" remediation guidance for any non-zero node-coverage delta | | `monitor-updates.yml` *(v0.7.90; v0.7.92 default cadence)* | 5x/day at 20:00, 22:00, 00:00, 02:00, 04:00 UTC (every 2h across the overnight maintenance window) + manual | *"What is happening right now? Which clusters are mid-update, which step are they on, and is anything stuck?"* | JUnit + CSV + Markdown summary; one test case per in-flight cluster (failure when elapsed > threshold, default 6h) | -| `fleet-update-status.yml` *(formerly Step.8)* | 06:00 UTC | *"Is each cluster up-to-date? Which ones need an apply, which ones are SBE-blocked, which ones failed?"* | JUnit + CSV/JSON + Markdown summary; one test case per cluster | +| `fleet-update-status.yml` | 06:00 UTC | *"Is each cluster up-to-date? Which ones need an apply, which ones are SBE-blocked, which ones failed?"* | JUnit + CSV/JSON + Markdown summary; one test case per cluster | | `fleet-health-status.yml` *(v0.7.65, formerly Step.9)* | 07:00 UTC | *"Do clusters have actionable health issues even when up-to-date? What failure reasons hit the most clusters?"* | JUnit + CSV/JSON + Markdown summary; one test case per (cluster, failing 24-hour health check) grouped under Critical / Warning testsuites | The four run in distinct (offset) cron slots so they don't contend for the same agent. `monitor-updates.yml` ships **without** an active schedule - turn the `'*/30 * * * *'` cron on only during an active wave. 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 0028d057..e235ec60 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: apply-updates-schedule-audit -# Step.03 - Apply-Updates Schedule Coverage Audit Pipeline +# Setup: 03 - Apply-Updates Schedule Coverage Audit Pipeline # This pipeline runs the read-only Test-AzLocalApplyUpdatesScheduleCoverage advisor # weekly to detect drift between the cron schedule(s) in your apply-updates pipeline # YAML and the UpdateStartWindow tags that operators have placed on Azure Local clusters. @@ -87,7 +87,7 @@ parameters: default: false variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' # v0.8.7 sideload advisor defaults. Override at the pipeline / variable-group @@ -98,7 +98,7 @@ variables: stages: - stage: ScheduleCoverage - displayName: 'Audit Apply-Updates Schedule Coverage' + displayName: 'Setup: 03 - Audit Apply-Updates Schedule Coverage' jobs: - job: Audit displayName: 'Run Schedule Coverage Advisor' 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 fc6e011b..5d4558ca 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: apply-updates -# Step.07 - Apply Updates to Azure Local Clusters +# Fleet: 04 - Apply Updates to Azure Local Clusters # This pipeline applies updates to clusters filtered by UpdateRing tag value # Author guidance lives OUTSIDE the customise markers so module refreshes @@ -129,7 +129,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.84' + value: '0.8.85' # 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). @@ -252,7 +252,7 @@ stages: # Stage 2: Apply Updates (with approval gate for production) - stage: ApplyUpdates - displayName: 'Apply Updates' + displayName: 'Fleet: 04 - Apply Updates' dependsOn: CheckReadiness condition: and(succeeded(), gt(dependencies.CheckReadiness.outputs['ReadinessCheck.readiness.ReadyCount'], 0)) 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 b103b268..aaedbcb0 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: assess-update-readiness -# Step.05 - Assess Update Readiness (Pre-flight go/no-go gate) +# Fleet: 01 - Assess Update Readiness (Pre-flight go/no-go gate) # -------------------------------------------------- # Runs Get-AzLocalClusterUpdateReadiness and Test-AzLocalClusterHealth -BlockingOnly # against a target UpdateRing (or the whole fleet) BEFORE apply-updates.yml runs. @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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). @@ -82,7 +82,7 @@ pool: stages: - stage: Assess - displayName: 'Assess Update Readiness' + displayName: 'Fleet: 01 - Assess Update Readiness' jobs: - job: AssessReadiness displayName: 'Readiness + blocking health gate' 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 2bb03f38..96795e81 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml @@ -39,7 +39,7 @@ # # See Automation-Pipeline-Examples/README.md, section 5.2, for the full setup story. -name: Step.00 - Authentication Validation and Subscription Scope Report +name: 'Step.00 - Authentication Validation and Subscription Scope Report' # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers # Add or modify `schedules:` blocks here to enable cron-driven runs. Content @@ -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.84' + value: '0.8.85' # 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 6cdc1637..9c4fe23d 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: fleet-connectivity-status -# Step.04 - Fleet Connectivity Status Monitoring Pipeline +# Fleet: 02 - Fleet Connectivity Status Monitoring Pipeline # This pipeline surfaces network-connectivity issues across every Azure Local # cluster the service connection can read - four scopes in one pipeline: # @@ -49,7 +49,7 @@ # Pipeline display name carries the same Step.N - prefix as the filename so the # ADO pipelines list (sorts by `name:` field) lists pipelines in execution order. -name: Step.04 - Fleet Connectivity Status +name: 'Fleet: 02 - Fleet Connectivity Status' # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers # Edits inside this block are preserved across Update-AzLocalPipelineExample @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 781717a1..2e32cc7c 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: fleet-health-status -# Step.10 - Fleet Health Status Monitoring Pipeline +# Fleet: 07 - Fleet Health Status Monitoring Pipeline # This pipeline surfaces 24-hour system health-check failures across every Azure Local # cluster the service connection can read - including clusters that are already # "up to date" with no available updates. The 24-hour health checks continue to run @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 90b78e8c..b05f415d 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: fleet-update-status -# Step.09 - Fleet Update Status Monitoring Pipeline +# Fleet: 06 - Fleet Update Status Monitoring Pipeline # This pipeline monitors update status across all Azure Local clusters and generates reports # # USE CASES: @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 126d6650..6d120927 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.84' + value: '0.8.85' # 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 12cd68c1..b095dac9 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: manage-updatering-tags -# Step.02 - Manage UpdateRing Tags +# Setup: 02 - Manage UpdateRing Tags # This pipeline creates or updates UpdateRing tags on Azure Local clusters from a CSV file # # WORKFLOW: @@ -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.84' + value: '0.8.85' # 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). @@ -58,7 +58,7 @@ pool: stages: - stage: ManageTags - displayName: 'Manage UpdateRing Tags' + displayName: 'Setup: 02 - Manage UpdateRing Tags' jobs: - job: ApplyTags displayName: 'Apply UpdateRing Tags' 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 4070f8cf..015aa066 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: monitor-updates -# Step.08 - Monitor In-Flight Updates (Azure DevOps) +# Fleet: 05 - Monitor In-Flight Updates (Azure DevOps) # ----------------------------------------------------------- # Reports clusters whose latest update run is currently in flight, with the # CURRENT STEP each cluster is on and the ELAPSED DURATION of the run. Built @@ -84,7 +84,7 @@ parameters: default: '' variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' 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 6fdd9bc3..06b44210 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: sideload-updates -# Step.06 - Sideload Updates (Opt-in, on-prem self-hosted agent) +# Fleet: 03 - Sideload Updates (Opt-in, on-prem self-hosted agent) # # OPT-IN, OFF BY DEFAULT. This pipeline automates the on-prem sideloading of # Azure Local solution-update media for clusters that cannot pull updates from @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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/github-actions/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml index 199df13d..7cdb7454 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 @@ -31,7 +31,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: Step.03 - Apply-Updates Schedule Coverage Audit +name: 'Setup: 03 - Apply-Updates Schedule Coverage Audit' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' 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 c044aa04..36d408ee 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -12,7 +12,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: Step.07 - Apply Updates +name: 'Fleet: 04 - Apply Updates' on: # PREFERRED PATTERN - drive automated update runs from a ring-aware @@ -148,7 +148,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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 3a9cab92..33f0910d 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 @@ -36,7 +36,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: Step.05 - Assess Update Readiness +name: 'Fleet: 01 - Assess Update Readiness' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 deleted file mode 100644 index 315e718d..00000000 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml +++ /dev/null @@ -1,219 +0,0 @@ -# AZLOCAL-PIPELINE-ID: authentication-test -# Step.00 - Authentication Validation and Subscription Scope Report -# -# PURPOSE: -# Validates that the App Registration / federated credentials / GitHub secrets / RBAC -# role assignments all line up, AND produces a Subscription Scope report that lists every -# subscription the pipeline identity can see. Run this: -# - BEFORE adding the seven real workflows (inventory, tags, readiness, apply, status, -# fleet-status, fleet-health). It narrows any failure to one small workflow rather -# than seven interacting workflows. -# - PERIODICALLY (recommended monthly, or after every RBAC change in the tenant) to -# confirm the pipeline identity's subscription scope has not silently widened or -# narrowed - new subscriptions get onboarded, MG-level role assignments inherit, -# leavers / re-orgs change tenant scope. A drift in this report is the earliest -# signal that downstream fleet reports are about to under- or over-count clusters. -# -# WHAT THIS PIPELINE PROBES: -# 1. azure/login OIDC token exchange (App Registration + federated credential). -# 2. Repository / environment secrets (AZURE_CLIENT_ID, _TENANT_ID, _SUBSCRIPTION_ID). -# 3. Azure RBAC on the App Registration's service principal. -# 4. Resource Graph reachability (proves the SP can actually see clusters). -# 5. Subscription scope: total count and full Subscription ID / Name table -# visible to the pipeline identity. -# -# REPORTS GENERATED: -# - JUnit XML: One test suite for Authentication, one for Subscription Scope (one testcase -# per subscription), one for Resource Graph reachability. Renders in the GitHub Checks -# UI via dorny/test-reporter. -# - GITHUB_STEP_SUMMARY markdown: Summary header (Authentication: working / Count of -# subscriptions = XXX) followed by a sortable Subscription ID + Name table. -# - Artifact (auth-report.zip): JUnit XML + subscriptions.json + subscriptions.csv for -# downstream consumption. -# -# HOW TO TRIGGER (after the file is committed to the default branch): -# # Branch-scoped run - exercises the 'GitHubActions-main' federated credential. -# gh workflow run authentication-test.yml --repo / -# -# # Environment-scoped run - exercises the env-scoped federated credentials. -# gh workflow run authentication-test.yml --repo / -f environment=DevTest -# -# # Watch the most recent run live. -# gh run watch --repo / -# -# See Automation-Pipeline-Examples/README.md, section 5.1, for the full setup story. - -# Workflow name carries the same Step.N - prefix as the filename so the GitHub -# Actions sidebar (which sorts workflows alphabetically by this `name:` field) -# lists the eight pipelines in execution order. -name: Step.00 - Authentication Validation and Subscription Scope Report - -on: - # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers - # Add `schedule:` cron blocks here to enable automated runs, e.g.: - # schedule: - # - cron: '0 6 * * 1' # Mondays at 06:00 UTC - # Content between BEGIN/END markers is preserved by - # Update-AzLocalPipelineExample across module upgrades. - # END-AZLOCAL-CUSTOMIZE:schedule-triggers - - workflow_dispatch: - inputs: - environment: - description: 'GitHub environment to test (leave blank to test the branch-scoped federated credential).' - required: false - type: choice - default: '' - options: - - '' - - DevTest - - PreProduction - - Production - module_version: - description: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".' - required: false - default: '' - -env: - # Module version this workflow YAML was generated against. The install step compares - # 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.84' - # 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). - 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, - # azure/login, dorny/test-reporter, etc). Per GitHub's 2025-09-19 deprecation - # notice Node 20 is forced off by default on 2026-06-16 and removed from the - # runner on 2026-09-16. Setting this env var silences the deprecation warnings - # and exercises Node 24 ahead of the cut-over. To temporarily opt back out, - # set ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. - # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -# OIDC token exchange requires id-token: write. contents: read is the GitHub default -# for workflow_dispatch and is harmless here; the validation run does not push commits. -# checks: write lets dorny/test-reporter publish the JUnit results as a Checks UI panel. -permissions: - id-token: write - contents: read - checks: write - -jobs: - validate: - name: Validate OIDC + RBAC + Subscription Scope - runs-on: windows-latest - # When `environment` is blank, this resolves to no environment and uses the - # branch-scoped federated credential. When set, GitHub injects environment-scoped - # secrets (if any) and signs the OIDC token with `environment:` in the sub claim. - environment: ${{ inputs.environment }} - - steps: - # dorny/test-reporter@v3 (used at the end of this job) shells out to - # `git ls-files` to resolve the JUnit XML path against the repo's - # tracked files. Without a checkout, it fails with exit 128: - # fatal: not a git repository (or any of the parent directories): .git - # The checkout itself is otherwise unnecessary for Step.0 (the auth probe - # does not consume any repo content) but is required for the report step. - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Azure login (OIDC) - uses: azure/login@v3 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - # AZURE_SUBSCRIPTION_ID is a repository *Variable* (vars.*), not a Secret. It - # is consumed ONLY here: azure/login@v3 runs `az account set --subscription - # ` after the OIDC token exchange so the runner has a default - # `az account` context. It is NOT used to scope Azure Resource Graph queries - # (those run fleet-wide across every subscription the federated identity can - # read) and is NOT interpolated into Azure portal deep-link URLs (those use - # the per-row `subscriptionId` returned by ARG). - # Set it via: gh variable set AZURE_SUBSCRIPTION_ID --body - subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - - name: Install AzLocal.UpdateManagement from PSGallery - # Step.0 itself relies on the module (Export-AzLocalAuthValidationReport - # runs the four auth/RBAC/ARG probes); installing here also gives - # operators an early, consistent drift signal *before* the rest of - # the seven pipelines run. If the banner reports the YAML is stale - # or the installed module is older than expected, refresh the YAMLs - # via Copy-AzLocalPipelineExample -Update and re-run. - # v0.8.5 thin-YAML: drift detection + banner + step outputs are all - # produced by Add-AzLocalPipelineVersionBanner (Public cmdlet). - shell: pwsh - id: module-version - run: | - $ErrorActionPreference = 'Stop' - $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } - if ($env:REQUIRED_MODULE_VERSION) { - $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION - Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." - } else { - Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." - } - Install-Module @installArgs - Import-Module AzLocal.UpdateManagement -Force - Add-AzLocalPipelineVersionBanner ` - -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` - -PinnedVersion $env:REQUIRED_MODULE_VERSION - - - name: Collect Authentication and Subscription Scope Report - id: report - shell: pwsh - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - INSTALLED_MODULE_VERSION: ${{ steps.module-version.outputs.installed_module_version }} - GENERATED_AGAINST_VERSION: ${{ steps.module-version.outputs.generated_against_version }} - LATEST_ON_PSGALLERY: ${{ steps.module-version.outputs.latest_on_psgallery }} - # v0.8.5 thin-YAML: the ~200-line inline run block (az probes, - # XML scaffolding, markdown summary, step-output writes) has been - # condensed into the Public cmdlet Export-AzLocalAuthValidationReport. - # The cmdlet performs all four probes (az account show / role - # assignment list / az account list / Resource Graph query), - # writes the JUnit XML + subscriptions.json + subscriptions.csv, - # emits the markdown step summary, and writes the three step - # outputs (subscription_count, cluster_count, auth_valid). - run: | - $ErrorActionPreference = 'Stop' - Import-Module AzLocal.UpdateManagement -Force - Export-AzLocalAuthValidationReport ` - -AzureClientId $env:AZURE_CLIENT_ID ` - -ReportDirectory './reports' ` - -InstalledModuleVersion $env:INSTALLED_MODULE_VERSION ` - -GeneratedAgainstVersion $env:GENERATED_AGAINST_VERSION ` - -LatestOnPSGallery $env:LATEST_ON_PSGALLERY - - - name: Compute Artifact Timestamp - id: artifact-stamp - shell: pwsh - # artifact convention: every downloadable artifact gets a UTC timestamp suffix so - # multiple runs on the same day produce distinct zip names. - run: | - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') - "timestamp=$stamp" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - Write-Host "Artifact timestamp: $stamp" - - - name: Upload auth report artifact - if: always() - uses: actions/upload-artifact@v6 - with: - name: azlocal-step.0-auth-report_${{ steps.artifact-stamp.outputs.timestamp }} - path: ./reports/ - retention-days: 30 - - - name: Publish JUnit Diagnostic Results - if: always() - uses: dorny/test-reporter@v3 - with: - name: '[JUnit Debug] Authentication Validation and Subscription Scope Report' - path: ./reports/auth-report.xml - reporter: java-junit - fail-on-error: false - list-suites: failed - list-tests: failed - 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 b2745283..f14a80f4 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 @@ -50,7 +50,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the pipelines in execution order. -name: Step.04 - Fleet Connectivity Status +name: 'Fleet: 02 - Fleet Connectivity Status' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 26884595..ea52f0e7 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 @@ -32,7 +32,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: Step.10 - Fleet Health Status +name: 'Fleet: 07 - Fleet Health Status' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 13bce927..c94625db 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 @@ -24,7 +24,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: Step.09 - Fleet Update Status +name: 'Fleet: 06 - Fleet Update Status' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -66,7 +66,7 @@ on: default: '' # --- ITSM Connector (ServiceNow auto-raise on fleet-update-status) --- # Set raise_itsm_ticket=true to open ServiceNow incidents from each unresolved - # Failed update run reported by the '📜 Update Run History and Error Details' + # Failed update run reported by the '📜 Update Run History and Error Details' # testsuite (and any failed AzureLocalFleetUpdateStatus row). Default is false # so existing schedules stay byte-identical until you opt in. The connector reads # the JUnit file Step.7 already produces (./reports/readiness-status.xml) and @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 deleted file mode 100644 index cd5e0650..00000000 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml +++ /dev/null @@ -1,168 +0,0 @@ -# AZLOCAL-PIPELINE-ID: inventory-clusters -# Inventory Azure Local Clusters -# This workflow queries all Azure Local clusters and exports inventory with UpdateRing tag status -# -# OUTPUT COLUMNS: -# ClusterName, ResourceGroup, SubscriptionId, SubscriptionName, -# UpdateRing, HasUpdateRingTag, UpdateStartWindow, UpdateExclusions, -# UpdateSideloaded, UpdateVersionInProgress, ResourceId -# -# - UpdateSideloaded (operator-set): True/False/1/0 - sideloaded-payload gate -# - UpdateVersionInProgress (module-managed; do not edit): staged update name -# See main module README, section "Sideloaded Payload Workflow". -# -# AUTHENTICATION OPTIONS: -# This workflow supports two authentication methods: -# 1. OpenID Connect (OIDC) - RECOMMENDED - No secrets, uses federated credentials -# 2. Client Secret - LEGACY - Requires AZURE_CLIENT_SECRET (see commented section) -# -# For OIDC setup, see: https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect - -# Workflow name carries the same Step.N - prefix as the filename so the GitHub -# Actions sidebar (which sorts workflows alphabetically by this `name:` field) -# lists the eight pipelines in execution order. -name: Step.01 - Inventory Azure Local Clusters - -on: - workflow_dispatch: - inputs: - subscription_filter: - description: 'Subscription ID to filter (leave empty for all subscriptions)' - required: false - default: '' - module_version: - description: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".' - required: false - default: '' - # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers - # Content between BEGIN/END markers is preserved by - # Update-AzLocalPipelineExample across module upgrades. Edit the cron - # below or add more `- cron:` entries to suit your maintenance windows. - schedule: - # Run weekly on Monday at 6:00 AM UTC - - cron: '0 6 * * 1' - # END-AZLOCAL-CUSTOMIZE:schedule-triggers - -env: - # Module version this workflow YAML was generated against. The install step compares - # 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.84' - # 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). - 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, - # azure/login, dorny/test-reporter, etc). Per GitHub's 2025-09-19 deprecation - # notice Node 20 is forced off by default on 2026-06-16 and removed from the - # runner on 2026-09-16. Setting this env var silences the deprecation warnings - # and exercises Node 24 ahead of the cut-over. To temporarily opt back out, - # set ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. - # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - inventory-clusters: - name: Inventory Azure Local Clusters - runs-on: windows-latest - # Required for OIDC authentication - permissions: - id-token: write - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # OPTION 1: OpenID Connect (OIDC) - RECOMMENDED - # No client secret required - uses federated credentials - - name: Azure CLI Login (OIDC) - uses: azure/login@v3 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - # AZURE_SUBSCRIPTION_ID is a repository *Variable* (vars.*), not a Secret. It - # is consumed ONLY here: azure/login@v3 runs `az account set --subscription - # ` after the OIDC token exchange so the runner has a default - # `az account` context. It is NOT used to scope Azure Resource Graph queries - # (those run fleet-wide across every subscription the federated identity can - # read) and is NOT interpolated into Azure portal deep-link URLs (those use - # the per-row `subscriptionId` returned by ARG). - # Set it via: gh variable set AZURE_SUBSCRIPTION_ID --body - subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - # OPTION 2: Client Secret - LEGACY (uncomment if OIDC not available) - # - name: Azure CLI Login (Client Secret) - # uses: azure/login@v3 - # with: - # creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ vars.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ vars.AZURE_TENANT_ID }}"}' - - - name: Install Azure CLI Resource Graph Extension - shell: pwsh - run: | - az extension add --name resource-graph --yes - - - name: Install AzLocal.UpdateManagement from PSGallery - # v0.8.5 thin-YAML: drift detection + banner + step outputs are all - # produced by Add-AzLocalPipelineVersionBanner (Public cmdlet). - shell: pwsh - id: module-version - run: | - $ErrorActionPreference = 'Stop' - $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } - if ($env:REQUIRED_MODULE_VERSION) { - $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION - Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." - } else { - Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." - } - Install-Module @installArgs - Import-Module AzLocal.UpdateManagement -Force - Add-AzLocalPipelineVersionBanner ` - -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` - -PinnedVersion $env:REQUIRED_MODULE_VERSION - - - name: Run Cluster Inventory - id: inventory - shell: pwsh - env: - INPUT_SUBSCRIPTION_FILTER: ${{ github.event.inputs.subscription_filter }} - INSTALLED_MODULE_VERSION: ${{ steps.module-version.outputs.installed_module_version }} - # v0.8.5 thin-YAML: the inline run block (inventory query, dual - # CSV/JSON export, canonical CSV copy, README_Instructions.txt, - # step outputs, separate "Summary" step) has been condensed into - # the Public cmdlet Invoke-AzLocalClusterInventory. The cmdlet - # writes the four artifacts to -OutputDirectory, emits the - # markdown step summary via GITHUB_STEP_SUMMARY, and sets the - # four step outputs (cluster_count, with_tag_count, - # without_tag_count, csv_path). - run: | - $ErrorActionPreference = 'Stop' - Import-Module AzLocal.UpdateManagement -Force - Invoke-AzLocalClusterInventory ` - -OutputDirectory './artifacts' ` - -SubscriptionFilter $env:INPUT_SUBSCRIPTION_FILTER ` - -InstalledModuleVersion $env:INSTALLED_MODULE_VERSION - - - name: Compute Artifact Timestamp - id: artifact-stamp - shell: pwsh - # every downloadable artifact gets a UTC timestamp suffix so multiple runs on - # the same day produce distinct zip names. - run: | - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') - "timestamp=$stamp" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - Write-Host "Artifact timestamp: $stamp" - - - name: Upload Inventory Artifact - uses: actions/upload-artifact@v6 - with: - name: azlocal-step.1-cluster-inventory_${{ steps.artifact-stamp.outputs.timestamp }} - path: | - ./artifacts/*.csv - ./artifacts/*.json - ./artifacts/README_Instructions.txt - retention-days: 30 - 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 556de02d..b5577735 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 @@ -19,10 +19,10 @@ # NOTE: GitHub Actions does not support file uploads as workflow inputs. # The edited CSV must be committed to the repository first. -# Workflow name carries the same Step.N - prefix as the filename so the GitHub +# Workflow name carries the same Setup prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) -# lists the eight pipelines in execution order. -name: Step.02 - Manage UpdateRing Tags +# lists the setup and operational pipelines in execution order. +name: 'Setup: 02 - Manage UpdateRing Tags' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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 e555334c..546c8f30 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml @@ -23,7 +23,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the pipelines in execution order. -name: Step.08 - Monitor In-Flight Updates +name: 'Fleet: 05 - Monitor In-Flight Updates' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml new file mode 100644 index 00000000..ca7cdd40 --- /dev/null +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml @@ -0,0 +1,289 @@ +# AZLOCAL-PIPELINE-ID: setup-validate-and-inventory +# Setup: 01 - Validate Auth and Inventory Azure Local Clusters +# +# PURPOSE: +# This is the first step in the setup workflow. It combines two operations: +# 1. VALIDATION: Validates OIDC + RBAC + subscription scope access +# 2. INVENTORY: Queries all Azure Local clusters and exports inventory with UpdateRing tag status +# +# Run this workflow: +# - BEFORE adding the operational workflows (Fleet: 01-07). It narrows any failure +# to authentication/discovery rather than seven interacting pipelines. +# - PERIODICALLY (recommended monthly, or after every RBAC change in the tenant) +# to confirm subscription scope and cluster inventory are accurate. +# +# VALIDATION PROBES: +# 1. azure/login OIDC token exchange (App Registration + federated credential). +# 2. Repository / environment secrets (AZURE_CLIENT_ID, _TENANT_ID, _SUBSCRIPTION_ID). +# 3. Azure RBAC on the App Registration's service principal. +# 4. Resource Graph reachability (proves the SP can actually see clusters). +# 5. Subscription scope: total count and full Subscription ID / Name table. +# +# INVENTORY OUTPUT COLUMNS: +# ClusterName, ResourceGroup, SubscriptionId, SubscriptionName, +# UpdateRing, HasUpdateRingTag, UpdateStartWindow, UpdateExclusions, +# UpdateSideloaded, UpdateVersionInProgress, ResourceId +# +# - UpdateSideloaded (operator-set): True/False/1/0 - sideloaded-payload gate +# - UpdateVersionInProgress (module-managed; do not edit): staged update name +# See main module README, section "Sideloaded Payload Workflow". +# +# REPORTS GENERATED: +# - Authentication: JUnit XML + subscriptions.json + subscriptions.csv +# - Inventory: ClusterUpdateRings.csv + ClusterUpdateRings.json + README_Instructions.txt +# - GITHUB_STEP_SUMMARY markdown: Version banner, subscription count with collapsible +# details table, cluster inventory summary with UpdateRing distribution, next steps. +# - Artifacts: Both reports available for download. +# +# HOW TO TRIGGER (after committed to default branch): +# gh workflow run setup-validate-and-inventory.yml --repo / +# gh workflow run setup-validate-and-inventory.yml --repo / -f environment=DevTest +# gh run watch --repo / +# +# See Automation-Pipeline-Examples/README.md for full setup story. + +name: 'Setup: 01 - Validate Auth and Inventory Clusters' + +on: + workflow_dispatch: + inputs: + environment: + description: 'GitHub Environment (leave blank for branch-scoped OIDC, or specify environment name for environment-scoped OIDC + secrets)' + required: false + default: '' + subscription_filter: + description: 'Subscription ID to filter inventory (leave empty for all subscriptions)' + required: false + default: '' + module_version: + description: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5.' + required: false + default: '' + # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers + # Content between BEGIN/END markers is preserved by + # Update-AzLocalPipelineExample across module upgrades. Edit the cron + # below or add more `- cron:` entries to suit your maintenance windows. + schedule: + # Run weekly on Sunday at 8:00 AM UTC + - cron: '0 8 * * 0' + # END-AZLOCAL-CUSTOMIZE:schedule-triggers + +env: + # Module version this workflow YAML was generated against. The install step compares + # 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.85' + # 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). + 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, + # azure/login, dorny/test-reporter, etc). Per GitHub's 2025-09-19 deprecation + # notice Node 20 is forced off by default on 2026-06-16 and removed from the + # runner on 2026-09-16. Setting this env var silences the deprecation warnings + # and exercises Node 24 ahead of the cut-over. To temporarily opt back out, + # set ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. + # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + id-token: write + contents: read + checks: write + +jobs: + validate: + name: 'Validate OIDC + RBAC + Subscription Scope' + runs-on: windows-latest + environment: ${{ inputs.environment }} + + outputs: + subscription_count: ${{ steps.report.outputs.subscription_count }} + cluster_count: ${{ steps.report.outputs.cluster_count }} + auth_valid: ${{ steps.report.outputs.auth_valid }} + + steps: + - name: 'Checkout repository' + uses: actions/checkout@v5 + + - name: 'Azure login (OIDC)' + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: 'Install AzLocal.UpdateManagement from PSGallery' + shell: pwsh + id: module-version + run: | + $ErrorActionPreference = 'Stop' + $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } + if ($env:REQUIRED_MODULE_VERSION) { + $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION + Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." + } else { + Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." + } + Install-Module @installArgs + Import-Module AzLocal.UpdateManagement -Force + Add-AzLocalPipelineVersionBanner ` + -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` + -PinnedVersion $env:REQUIRED_MODULE_VERSION + + - name: 'Collect Authentication and Subscription Scope Report' + id: report + shell: pwsh + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + INSTALLED_MODULE_VERSION: ${{ steps.module-version.outputs.installed_module_version }} + GENERATED_AGAINST_VERSION: ${{ steps.module-version.outputs.generated_against_version }} + LATEST_ON_PSGALLERY: ${{ steps.module-version.outputs.latest_on_psgallery }} + run: | + $ErrorActionPreference = 'Stop' + Import-Module AzLocal.UpdateManagement -Force + Export-AzLocalAuthValidationReport ` + -AzureClientId $env:AZURE_CLIENT_ID ` + -ReportDirectory './reports' ` + -InstalledModuleVersion $env:INSTALLED_MODULE_VERSION ` + -GeneratedAgainstVersion $env:GENERATED_AGAINST_VERSION ` + -LatestOnPSGallery $env:LATEST_ON_PSGALLERY + + - name: 'Format Subscription Details as Collapsible Section' + if: always() + shell: pwsh + run: | + $reportDir = './reports' + $csvPath = Join-Path $reportDir 'subscriptions.csv' + + if (Test-Path -LiteralPath $csvPath) { + $subs = Import-Csv -LiteralPath $csvPath -ErrorAction SilentlyContinue + $subCount = @($subs).Count + + # Append collapsible subscription details to GITHUB_STEP_SUMMARY + $summary = @" + + ### Count of subscriptions accessible = $subCount + +
+ Expand for subscription details + + | Subscription ID | Subscription Name | + |---|---| + "@ + + if ($subs) { + foreach ($sub in $subs) { + $summary += "`n| $($sub.'Subscription ID') | $($sub.'Subscription Name') |" + } + } + + $summary += "`n
`n" + $summary | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + } + + - name: 'Compute Artifact Timestamp' + id: artifact-stamp + shell: pwsh + run: | + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') + "timestamp=$stamp" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Host "Artifact timestamp: $stamp" + + - name: 'Upload auth report artifact' + if: always() + uses: actions/upload-artifact@v6 + with: + name: azlocal-step.1-setup-auth-report_${{ steps.artifact-stamp.outputs.timestamp }} + path: ./reports/ + retention-days: 30 + + - name: 'Publish JUnit Diagnostic Results' + if: always() + uses: dorny/test-reporter@v3 + with: + name: '[JUnit Debug] Setup: 01 - Authentication Validation' + path: ./reports/auth-report.xml + reporter: java-junit + fail-on-error: false + list-suites: failed + list-tests: failed + + inventory-clusters: + name: 'Inventory Azure Local Clusters' + runs-on: windows-latest + needs: validate + if: success() + + outputs: + cluster_count: ${{ steps.inventory.outputs.cluster_count }} + with_tag_count: ${{ steps.inventory.outputs.with_tag_count }} + without_tag_count: ${{ steps.inventory.outputs.without_tag_count }} + + steps: + - name: 'Checkout repository' + uses: actions/checkout@v5 + + - name: 'Azure CLI Login (OIDC)' + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: 'Install Azure CLI Resource Graph Extension' + shell: pwsh + run: | + az extension add --name resource-graph --yes + + - name: 'Install AzLocal.UpdateManagement from PSGallery' + shell: pwsh + id: module-version + run: | + $ErrorActionPreference = 'Stop' + $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } + if ($env:REQUIRED_MODULE_VERSION) { + $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION + Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." + } else { + Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." + } + Install-Module @installArgs + Import-Module AzLocal.UpdateManagement -Force + Add-AzLocalPipelineVersionBanner ` + -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` + -PinnedVersion $env:REQUIRED_MODULE_VERSION + + - name: 'Run Cluster Inventory' + id: inventory + shell: pwsh + env: + INPUT_SUBSCRIPTION_FILTER: ${{ github.event.inputs.subscription_filter }} + INSTALLED_MODULE_VERSION: ${{ steps.module-version.outputs.installed_module_version }} + run: | + $ErrorActionPreference = 'Stop' + Import-Module AzLocal.UpdateManagement -Force + Invoke-AzLocalClusterInventory ` + -OutputDirectory './artifacts' ` + -SubscriptionFilter $env:INPUT_SUBSCRIPTION_FILTER ` + -InstalledModuleVersion $env:INSTALLED_MODULE_VERSION + + - name: 'Compute Artifact Timestamp' + id: artifact-stamp + shell: pwsh + run: | + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') + "timestamp=$stamp" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Host "Artifact timestamp: $stamp" + + - name: 'Upload Inventory Artifact' + uses: actions/upload-artifact@v6 + with: + name: azlocal-step.1-setup-cluster-inventory_${{ steps.artifact-stamp.outputs.timestamp }} + path: | + ./artifacts/*.csv + ./artifacts/*.json + ./artifacts/README_Instructions.txt + retention-days: 30 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 3deb3890..aef5becc 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml @@ -38,7 +38,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the pipelines in execution order. -name: Step.06 - Sideload Updates (Opt-in) +name: 'Fleet: 03 - Sideload Updates (Opt-in)' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.84' + GENERATED_AGAINST_MODULE_VERSION: '0.8.85' # 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/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 50b9a2ca..01327d92 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.84' + ModuleVersion = '0.8.85' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -317,7 +317,7 @@ # ReleaseNotes of this module ReleaseNotes = @' -## Version 0.8.84 - Docs-only patch. Extends the Step.08 `Export-AzLocalUpdateRunMonitorReport` "Recent update attempts with no observable updateRun" callout (introduced v0.8.82, made functional v0.8.83) with an operator runbook: numbered diagnosis flow (Azure portal Activity Log -> in-portal `updateRun` check -> URP service health) plus a fenced PowerShell snippet showing `Get-ClusterGroup` / `Move-ClusterGroup` / `Stop+Start-ClusterGroup` commands to bounce the two URP cluster groups (`Azure Stack HCI Update Service Cluster Group` + `Azure Stack HCI Orchestrator Service Cluster Group`) when an attempt-gap reproduces and all Azure-side investigation comes back clean. Snippet prefixes every action with `Format-Table Name, OwnerNode, State` (exit-state diagnostic, more useful than `-Verbose`); covers multi-node and single-node; explicit warning that the Orchestrator (ECE) group must NOT be bounced during a healthy in-flight `updateRun`. New regression test asserts the rendered markdown. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.83'` to `'0.8.84'`. +## Version 0.8.85 - Patch: introduces Setup/Fleet naming in bundled pipeline templates, adds merged GitHub onboarding workflow `setup-validate-and-inventory.yml`, and updates pipeline refresh tooling with optional deprecated-file pruning guarded by AZLOCAL-PIPELINE-ID verification. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.84'` to `'0.8.85'`. ## Version 0.8.83 - Patch: fix-forward for v0.8.82 Item-5. The Step.08 `UpdateLastAttempt` reconciliation in `Export-AzLocalUpdateRunMonitorReport` reads `$inv.tags` from `Get-AzLocalClusterInventory`, but the v0.8.82 inventory projection did not carry the raw ARM `tags` bag - so the "Recent update attempts with no observable updateRun" section was silently always empty in production. v0.8.83 surfaces the raw `tags` bag on every inventory row (in-memory only; the CSV / JSON export keeps its explicit `$selectColumns` whitelist - new regression test asserts both). Also wires `attempts_without_run` into the GitHub Actions `monitor-updates.yml` `jobs.outputs:` block (ADO `Set-AzLocalPipelineOutput` auto-publishes so only an ADO docstring refresh needed), and corrects the `Export-AzLocalUpdateRunMonitorReport` "6 step outputs" docstring to "7 step outputs". No public API change. Export count unchanged (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.82'` to `'0.8.83'`. diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index 8fe40918..9b8ffc56 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.84' +$script:ModuleVersion = '0.8.85' $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 20c41f40..de1dac21 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,43 +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.84] - 2026-06-15 - -Documentation-only patch release. Extends the `Export-AzLocalUpdateRunMonitorReport` -markdown callout under the "Recent update attempts with no observable updateRun" -section (introduced in v0.8.82, made functional in v0.8.83) with an operator -runbook. The previous one-line callout pointed users at the Azure portal Activity -Log only. v0.8.84 expands it into a numbered diagnosis flow (Activity Log -> in-portal -`updateRun` check -> URP service health) and a fenced PowerShell snippet showing -the exact `Get-ClusterGroup` / `Move-ClusterGroup` / `Stop+Start-ClusterGroup` -commands to bounce the two URP cluster groups (`Azure Stack HCI Update Service -Cluster Group` + `Azure Stack HCI Orchestrator Service Cluster Group`) when an -attempt-gap reproduces and all Azure-side investigation comes back clean. Includes -an explicit warning that the Orchestrator (ECE) group must NOT be bounced during -a healthy in-flight run. No public API change, no behavioural change to any -cmdlet. Export count unchanged (still 60). +## [0.8.85] - 2026-06-16 + +Patch release. Introduces Setup/Fleet naming in bundled pipeline templates, +adds the merged GitHub onboarding workflow (`setup-validate-and-inventory.yml`), +and updates pipeline refresh tooling to support optional pruning of deprecated +workflow files with pipeline-ID verification. No public API change. Export +count unchanged (still 60). ### Added -- **Operator runbook in the Step.08 "Recent update attempts with no observable - updateRun" callout** in `Export-AzLocalUpdateRunMonitorReport`. The callout - now renders three numbered diagnosis steps and a fenced PowerShell block with - the exact Failover Clustering cmdlets to inspect ownership and bounce the URP - cluster groups. The snippet (a) prefixes every action with - `Get-ClusterGroup ... | Format-Table Name, OwnerNode, State` so operators see - the exit state of each cmdlet (more diagnostic than `-Verbose`), (b) covers - multi-node clusters (`Move-ClusterGroup` for zero-downtime failover) and - single-node / fallback cases (`Stop-ClusterGroup` + `Start-ClusterGroup`), - and (c) explicitly warns that bouncing the Orchestrator (ECE) group during a - healthy in-flight `updateRun` will interrupt it. New regression test - (`v0.8.84: Step.08 attempt-gap callout includes URP cluster-group recovery snippet`) - asserts the rendered markdown contains the diagnosis list, both URP cluster - group names, the `Format-Table` line, the move + stop + start cmdlets, the - in-flight warning, and that `-Verbose` is NOT present on any of the cmdlets. +- Adds `setup-validate-and-inventory.yml` as the consolidated GitHub Setup + workflow replacing separate auth/inventory onboarding templates. +- Standardizes Setup/Fleet naming across bundled GitHub Actions and Azure + DevOps templates. +- Adds `-PruneDeprecated` handling in pipeline copy/update flows with + AZLOCAL-PIPELINE-ID verification before deleting superseded files. ### Changed -- `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.83'` to `'0.8.84'` +- `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.84'` to `'0.8.85'` across all bundled GitHub Actions and Azure DevOps pipeline templates. ## [0.8.83] - 2026-06-15 diff --git a/AzLocal.UpdateManagement/Public/Copy-AzLocalPipelineExample.ps1 b/AzLocal.UpdateManagement/Public/Copy-AzLocalPipelineExample.ps1 index 04dad689..7c7a9b6a 100644 --- a/AzLocal.UpdateManagement/Public/Copy-AzLocalPipelineExample.ps1 +++ b/AzLocal.UpdateManagement/Public/Copy-AzLocalPipelineExample.ps1 @@ -260,7 +260,14 @@ function Copy-AzLocalPipelineExample { # drop (sideload-auth-map.csv + sideload-catalog.yml) for # Platform=GitHub|AzureDevOps. Default OFF. Existing files are # always preserved regardless of this switch. - [switch]$SkipStarterSideloadConfig + [switch]$SkipStarterSideloadConfig, + + # v0.8.85: when set, remove deprecated workflow filenames that are + # superseded by newer merged pipelines (for example, GitHub + # authentication-test.yml + inventory-clusters.yml replaced by + # setup-validate-and-inventory.yml). Default OFF to avoid deleting + # operator-edited files without explicit opt-in. + [switch]$PruneDeprecated ) # ------------------------------------------------------------------ @@ -450,6 +457,50 @@ function Copy-AzLocalPipelineExample { Write-Verbose "Copied $copiedCount file(s) from '$sourceRoot' to '$targetRoot' (skipped: $skippedCount)." + # ------------------------------------------------------------------ + # 5b (v0.8.85). Optional cleanup of deprecated sample filenames that + # were superseded by merged pipelines. + # ------------------------------------------------------------------ + if ($Platform -eq 'GitHub') { + $replacementPath = Join-Path -Path $targetRoot -ChildPath 'setup-validate-and-inventory.yml' + $deprecatedFiles = @( + [PSCustomObject]@{ Path = (Join-Path -Path $targetRoot -ChildPath 'authentication-test.yml'); ExpectedId = 'authentication-test' } + [PSCustomObject]@{ Path = (Join-Path -Path $targetRoot -ChildPath 'inventory-clusters.yml'); ExpectedId = 'inventory-clusters' } + ) + $existingDeprecated = @($deprecatedFiles | Where-Object { Test-Path -LiteralPath $_.Path -PathType Leaf }) + if ((Test-Path -LiteralPath $replacementPath -PathType Leaf) -and $existingDeprecated.Count -gt 0) { + if ($PruneDeprecated.IsPresent) { + foreach ($deprecatedEntry in $existingDeprecated) { + $deprecatedPath = $deprecatedEntry.Path + $expectedId = $deprecatedEntry.ExpectedId + $canDelete = $false + try { + $deprecatedText = [System.IO.File]::ReadAllText($deprecatedPath, [System.Text.UTF8Encoding]::new($false)) + $deprecatedId = Get-AzLocalPipelineId -Text $deprecatedText + $canDelete = ($deprecatedId -eq $expectedId) + } + catch { + Write-Warning ("Copy-AzLocalPipelineExample: could not inspect '{0}' for AZLOCAL-PIPELINE-ID; leaving file untouched. {1}" -f $deprecatedPath, $_.Exception.Message) + } + + if (-not $canDelete) { + Write-Warning ("Copy-AzLocalPipelineExample: '{0}' does not match expected pipeline ID '{1}'. Leaving file untouched." -f $deprecatedPath, $expectedId) + continue + } + + if ($PSCmdlet.ShouldProcess($deprecatedPath, 'Remove deprecated workflow replaced by setup-validate-and-inventory.yml')) { + Remove-Item -LiteralPath $deprecatedPath -Force -ErrorAction Stop + Write-Verbose ("Copy-AzLocalPipelineExample: removed deprecated workflow '{0}'." -f $deprecatedPath) + } + } + } + else { + $deprecatedList = ($existingDeprecated | ForEach-Object { " - $($_.Path)" }) -join [Environment]::NewLine + Write-Warning ("Copy-AzLocalPipelineExample: found deprecated workflow file(s) replaced by 'setup-validate-and-inventory.yml'. They were left in place to preserve operator edits.{0}{1}{0}To remove them automatically on refresh, rerun with -PruneDeprecated." -f [Environment]::NewLine, $deprecatedList) + } + } + } + # ------------------------------------------------------------------ # 6 (v0.7.92, relocated to config\ in v0.8.7). Starter # apply-updates-schedule.yml drop. @@ -691,7 +742,7 @@ function Copy-AzLocalPipelineExample { else { Write-Host (" 1. Move the YAML files from '{0}' into '.github\workflows\' in your repo, then commit and push." -f $targetRoot) } - Write-Host " 2. RECOMMENDED: run 'Step.0_authentication-test.yml' FIRST (one-shot) to validate OIDC / RBAC before wiring the other workflows. See section 5.1 of the Automation-Pipeline-Examples README." + Write-Host " 2. RECOMMENDED: run 'setup-validate-and-inventory.yml' FIRST (one-shot) to validate OIDC / RBAC and generate your initial cluster inventory before wiring the other workflows. See section 5.1 of the Automation-Pipeline-Examples README." Write-Host " 3. Wire up authentication (OIDC / Workload Identity / Managed Identity / SP) - see section 3 of the README." Write-Host " 4. SCHEDULED Step.6 (apply-updates) requires apply-updates-schedule.yml:" -ForegroundColor Yellow foreach ($line in $scheduleHintLines) { Write-Host $line } diff --git a/AzLocal.UpdateManagement/Public/Update-AzLocalPipelineExample.ps1 b/AzLocal.UpdateManagement/Public/Update-AzLocalPipelineExample.ps1 index 78cd3dad..939d4777 100644 --- a/AzLocal.UpdateManagement/Public/Update-AzLocalPipelineExample.ps1 +++ b/AzLocal.UpdateManagement/Public/Update-AzLocalPipelineExample.ps1 @@ -175,6 +175,13 @@ function Update-AzLocalPipelineExample { [switch]$Force, + # v0.8.85: when set, remove deprecated workflow filenames that are + # superseded by newer merged pipelines (for example, GitHub + # authentication-test.yml + inventory-clusters.yml replaced by + # setup-validate-and-inventory.yml). Default OFF so upgrades remain + # non-destructive unless explicitly requested. + [switch]$PruneDeprecated, + [switch]$PassThru ) @@ -524,6 +531,47 @@ function Update-AzLocalPipelineExample { $summary = ($byAction | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join ' ' Write-Log -Message "Update-AzLocalPipelineExample: $($results.Count) source file(s) processed - $summary" -Level Info + # v0.8.85: optional cleanup for deprecated merged GitHub workflows. + if ($Platform -eq 'GitHub') { + $replacementPath = Join-Path -Path $destResolved -ChildPath 'setup-validate-and-inventory.yml' + $deprecatedFiles = @( + [PSCustomObject]@{ Path = (Join-Path -Path $destResolved -ChildPath 'authentication-test.yml'); ExpectedId = 'authentication-test' } + [PSCustomObject]@{ Path = (Join-Path -Path $destResolved -ChildPath 'inventory-clusters.yml'); ExpectedId = 'inventory-clusters' } + ) + $existingDeprecated = @($deprecatedFiles | Where-Object { Test-Path -LiteralPath $_.Path -PathType Leaf }) + if ((Test-Path -LiteralPath $replacementPath -PathType Leaf) -and $existingDeprecated.Count -gt 0) { + if ($PruneDeprecated.IsPresent) { + foreach ($deprecatedEntry in $existingDeprecated) { + $deprecatedPath = $deprecatedEntry.Path + $expectedId = $deprecatedEntry.ExpectedId + $canDelete = $false + try { + $deprecatedText = [System.IO.File]::ReadAllText($deprecatedPath, [System.Text.UTF8Encoding]::new($false)) + $deprecatedId = Get-AzLocalPipelineId -Text $deprecatedText + $canDelete = ($deprecatedId -eq $expectedId) + } + catch { + Write-Log -Message " Note : could not inspect '$([System.IO.Path]::GetFileName($deprecatedPath))' for AZLOCAL-PIPELINE-ID; leaving file untouched. $($_.Exception.Message)" -Level Warning + } + + if (-not $canDelete) { + Write-Log -Message " Note : '$([System.IO.Path]::GetFileName($deprecatedPath))' does not match expected pipeline ID '$expectedId'; leaving file untouched." -Level Warning + continue + } + + if ($PSCmdlet.ShouldProcess($deprecatedPath, 'Remove deprecated workflow replaced by setup-validate-and-inventory.yml')) { + Remove-Item -LiteralPath $deprecatedPath -Force -ErrorAction Stop + Write-Log -Message " Removed : deprecated workflow '$([System.IO.Path]::GetFileName($deprecatedPath))' (replaced by setup-validate-and-inventory.yml)" -Level Success + } + } + } + else { + $deprecatedList = ($existingDeprecated | ForEach-Object { " - $([System.IO.Path]::GetFileName($_.Path))" }) -join [Environment]::NewLine + Write-Log -Message (" Note : deprecated workflow file(s) detected (replaced by setup-validate-and-inventory.yml). Left in place by default to avoid destructive changes:{0}{1}{0} Rerun with -PruneDeprecated to remove them automatically." -f [Environment]::NewLine, $deprecatedList) -Level Warning + } + } + } + if ($PassThru) { return $results.ToArray() } diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 81da553a..8d1e2716 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.84 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.84) +**Latest Version:** v0.8.85 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.85) > 📢 **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.84](#whats-new-in-v0884) +- [What's New in v0.8.85](#whats-new-in-v0885) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,26 +86,24 @@ 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.84 +## What's New in v0.8.85 -**Documentation-only patch release.** Extends the `Export-AzLocalUpdateRunMonitorReport` markdown callout under the "Recent update attempts with no observable updateRun" section (introduced in v0.8.82, made functional in v0.8.83) with an operator runbook. The previous one-line callout pointed users at the Azure portal Activity Log only. v0.8.84 expands it into a numbered diagnosis flow (Activity Log -> in-portal `updateRun` check -> URP service health) and a fenced PowerShell snippet showing the exact `Get-ClusterGroup` / `Move-ClusterGroup` / `Stop+Start-ClusterGroup` commands to bounce the two URP cluster groups (`Azure Stack HCI Update Service Cluster Group` + `Azure Stack HCI Orchestrator Service Cluster Group`) when an attempt-gap reproduces and all Azure-side investigation comes back clean. Includes an explicit warning that the Orchestrator (ECE) group must NOT be bounced during a healthy in-flight run. No public API change, no behavioural change to any cmdlet. Export count unchanged (still 60). +**Pipeline UX consolidation release.** Introduces Setup/Fleet naming in bundled pipelines, adds the merged GitHub onboarding workflow `setup-validate-and-inventory.yml` (Setup: 01), and updates pipeline refresh tooling to safely handle deprecated workflow cleanup with pipeline-ID verification. Also bumps bundled pipeline `GENERATED_AGAINST_MODULE_VERSION` pins to `0.8.85`. ### Added -- **Operator runbook in the Step.08 "Recent update attempts with no observable updateRun" callout** in `Export-AzLocalUpdateRunMonitorReport`. The callout now renders three numbered diagnosis steps (Azure portal Activity Log -> in-portal `updateRun` check -> URP service health) and a fenced PowerShell block with the exact Failover Clustering cmdlets to inspect ownership and bounce the URP cluster groups. The snippet: - - Prefixes every action with `Get-ClusterGroup ... | Format-Table Name, OwnerNode, State` so operators see the exit state of each cmdlet (more diagnostic than `-Verbose`). - - Covers multi-node clusters (`Move-ClusterGroup` for zero-downtime failover) and single-node / fallback cases (`Stop-ClusterGroup` + `Start-ClusterGroup`). - - Explicitly warns that bouncing the Orchestrator (ECE) group during a healthy in-flight `updateRun` will interrupt it. -- New regression test (`v0.8.84: Step.08 attempt-gap callout includes URP cluster-group recovery snippet`) asserts the rendered markdown contains the diagnosis list, both URP cluster group names, the `Format-Table` line, the move + stop + start cmdlets, the in-flight warning, and that `-Verbose` is NOT present on any of the cmdlets. +- Bundled GitHub workflows now surface Setup/Fleet ordering and include `Setup: 01 - Validate Auth and Inventory Clusters`. +- Legacy GitHub onboarding workflows (`authentication-test.yml`, `inventory-clusters.yml`) are superseded by the merged Setup workflow in the bundled set. +- `Copy-AzLocalPipelineExample` and `Update-AzLocalPipelineExample` gain optional `-PruneDeprecated` cleanup for superseded workflow files, with AZLOCAL-PIPELINE-ID verification before removal. ### Notes - **No new exports** (count unchanged at 60). -- **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.83` to `0.8.84` across all bundled pipeline templates. +- **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.84` to `0.8.85` across bundled pipeline templates. > Previous release notes have moved into the [Release History](#release-history) appendix at the bottom of this document. -See [CHANGELOG.md](CHANGELOG.md#0884---2026-06-15) for the full v0.8.84 entry. See [`What's New in v0.8.83`](#whats-new-in-v0883) in the Release History for the previous release. +See [CHANGELOG.md](CHANGELOG.md) for full release details. See [`What's New in v0.8.84`](#whats-new-in-v0884) in the Release History for the previous release. ## Files diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index d4c66439..4172c740 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.84' { - $script:ModuleInfo.Version | Should -Be '0.8.84' + It 'Should have version 0.8.85' { + $script:ModuleInfo.Version | Should -Be '0.8.85' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -575,13 +575,13 @@ Describe 'Module: AzLocal.UpdateManagement' { # actions/upload-artifact, dorny/test-reporter, etc.) execute under # Node 24 ahead of GitHub's platform-wide cutover. This drift test # guards against a future Copy-AzLocalPipelineExample / refresh - # silently dropping the opt-in from any of the 10 yml templates. + # silently dropping the opt-in from any of the bundled yml templates. It 'Every GitHub Actions yml declares FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true at workflow scope' { $ghRoot = Join-Path -Path $PSScriptRoot -ChildPath '..\Automation-Pipeline-Examples\github-actions' $ghRoot = (Resolve-Path -Path $ghRoot).Path $ymlFiles = Get-ChildItem -Path $ghRoot -Filter '*.yml' -File - $ymlFiles.Count | Should -BeGreaterOrEqual 11 -Because 'all 11 de-numbered pipeline templates are expected under Automation-Pipeline-Examples/github-actions/' + $ymlFiles.Count | Should -BeGreaterOrEqual 10 -Because 'all 10 de-numbered pipeline templates are expected under Automation-Pipeline-Examples/github-actions/' $offenders = New-Object System.Collections.Generic.List[string] foreach ($yml in $ymlFiles) { @@ -7060,7 +7060,7 @@ Describe 'Function: Copy-AzLocalPipelineExample' { # YAMLs landed directly in $dest $yamls = @(Get-ChildItem -LiteralPath $dest -Filter '*.yml' -File) $yamls.Count | Should -BeGreaterThan 0 - $yamls.Name | Should -Contain 'authentication-test.yml' + $yamls.Name | Should -Contain 'setup-validate-and-inventory.yml' # No platform-named subfolder, no Automation-Pipeline-Examples wrapper Test-Path (Join-Path $dest 'github-actions') | Should -BeFalse @@ -7130,7 +7130,7 @@ Describe 'Function: Copy-AzLocalPipelineExample' { Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub 6>$null | Out-Null # Mutate one destination file so we can prove it gets overwritten - $target = Join-Path $dest 'authentication-test.yml' + $target = Join-Path $dest 'setup-validate-and-inventory.yml' $sentinel = '# SENTINEL - if this comment survives, -Update did not overwrite' Set-Content -LiteralPath $target -Value $sentinel -Encoding ASCII (Get-Content -LiteralPath $target -Raw) | Should -Match 'SENTINEL' @@ -7172,7 +7172,7 @@ Describe 'Function: Copy-AzLocalPipelineExample' { # Seed and then mutate Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub 6>$null | Out-Null - $target = Join-Path $dest 'authentication-test.yml' + $target = Join-Path $dest 'setup-validate-and-inventory.yml' $sentinel = '# WHATIF SENTINEL - -WhatIf must preserve this' Set-Content -LiteralPath $target -Value $sentinel -Encoding ASCII From 189f96448d2124d5960c5fbe8c3fc3a614742728 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 10:54:33 +0100 Subject: [PATCH 02/17] test-run-timings --- AzLocal.UpdateManagement/Tests/test-run-timings.csv | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 8be1d5fe..32261a76 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -27,3 +27,6 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-15T13:34:00Z","0.8.82","1287","1238","10","1","101.48","98","3.94","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-15T13:39:45Z","0.8.82","1287","1247","1","1","82.99","80.84","5.05","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-15T13:41:58Z","0.8.82","1287","1248","0","1","83.01","80.97","4.56","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T09:44:48Z","0.8.84","1295","1254","2","1","122.97","120.97","14.12","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T09:47:44Z","0.8.85","1295","1253","3","1","50.17","48.88","3.54","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T09:49:41Z","0.8.85","1295","1256","0","1","50.8","49.53","3.77","Invoke-Tests.ps1","5.1.26100.8730" From 5474cd18b614d549fa754f45ddf4ffd26758747f Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 10:57:29 +0100 Subject: [PATCH 03/17] v0.8.85 follow-up: delete ADO auth+inventory workflows and add merged ADO setup pipeline --- .../azure-devops/authentication-test.yml | 175 ------------ .../azure-devops/inventory-clusters.yml | 141 ---------- .../setup-validate-and-inventory.yml | 256 ++++++++++++++++++ .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 11 +- 4 files changed, 261 insertions(+), 322 deletions(-) delete mode 100644 AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml delete mode 100644 AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml create mode 100644 AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml deleted file mode 100644 index 96795e81..00000000 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml +++ /dev/null @@ -1,175 +0,0 @@ -# AZLOCAL-PIPELINE-ID: authentication-test -# Step.00 - Authentication Validation and Subscription Scope Report -# -# PURPOSE: -# Validates that the service connection (Workload Identity Federation), App Registration, -# and RBAC role assignments all line up, AND produces a Subscription Scope report that -# lists every subscription the pipeline identity can see. Run this: -# - BEFORE importing the seven real pipelines (inventory, tags, readiness, apply, -# status, fleet-status, fleet-health). It narrows any failure to one small pipeline -# rather than seven interacting pipelines. -# - PERIODICALLY (recommended monthly, or after every RBAC change in the tenant) to -# confirm the pipeline identity's subscription scope has not silently widened or -# narrowed - new subscriptions get onboarded, MG-level role assignments inherit, -# leavers / re-orgs change tenant scope. A drift in this report is the earliest -# signal that downstream fleet reports are about to under- or over-count clusters. -# -# WHAT THIS PIPELINE PROBES: -# 1. AzureCLI@2 task auth against the service connection (Workload Identity -# Federation auto-managed by ADO). -# 2. Service-connection -> App Registration -> subscription wiring. -# 3. Azure RBAC on the App Registration's service principal. -# 4. Resource Graph reachability (proves the SP can actually see clusters). -# 5. Subscription scope: total count and full Subscription ID / Name table -# visible to the pipeline identity. -# -# Unlike the GitHub Actions twin (authentication-test.yml in github-actions/), ADO -# does not expose env-scoped federated credentials at the auth layer - the service -# connection IS the federation. So this pipeline only validates one auth path. Environment -# approval gates (Pipelines -> Environments) are a separate concern and are not exercised -# here. -# -# REPORTS GENERATED: -# - JUnit XML published via PublishTestResults@2 (renders in the ADO Tests tab). -# - Markdown summary uploaded via ##vso[task.uploadsummary] (renders in the run Summary tab). -# - Pipeline artifact (auth-report) containing JUnit XML + subscriptions.json + .csv. -# -# HOW TO RUN (after the file is committed and the pipeline is imported): -# Pipelines -> Step.00 - Authentication Validation and Subscription Scope Report -> Run pipeline. -# -# See Automation-Pipeline-Examples/README.md, section 5.2, for the full setup story. - -name: 'Step.00 - Authentication Validation and Subscription Scope Report' - -# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers -# Add or modify `schedules:` blocks here to enable cron-driven runs. Content -# between BEGIN/END markers is preserved by Update-AzLocalPipelineExample -# across module upgrades. -trigger: none -# END-AZLOCAL-CUSTOMIZE:schedule-triggers - -parameters: - - name: moduleVersion - displayName: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".' - type: string - default: '' - -variables: - # Module version this YAML was generated against. The install step compares this to - # 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. - - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.85' - # 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). - - name: REQUIRED_MODULE_VERSION - value: '${{ parameters.moduleVersion }}' - -# No further parameters - this pipeline is deliberately fixed: one service connection, -# one identity, four probes. If the service connection name does not match your -# environment, update `azureSubscription` below. - -pool: - vmImage: 'windows-latest' - -stages: - - stage: Validate - displayName: 'Validate Workload Identity Federation + RBAC + Subscription Scope' - jobs: - - job: ValidateAuthAndScope - displayName: 'Run auth, RBAC, and Subscription Scope probes' - - steps: - - task: PowerShell@2 - displayName: 'Install AzLocal.UpdateManagement from PSGallery' - # Step.0 itself relies on the module (Export-AzLocalAuthValidationReport - # runs the four auth/RBAC/ARG probes); installing here also gives - # operators an early, consistent drift signal *before* the rest of - # the seven pipelines run. If the banner reports the YAML is stale - # or the installed module is older than expected, refresh the - # YAMLs via Copy-AzLocalPipelineExample -Platform AzureDevOps -Update - # and re-run. - # v0.8.5 thin-YAML: drift detection + banner + step outputs are all - # produced by Add-AzLocalPipelineVersionBanner (Public cmdlet). - env: - REQUIRED_MODULE_VERSION: $(REQUIRED_MODULE_VERSION) - GENERATED_AGAINST_MODULE_VERSION: $(GENERATED_AGAINST_MODULE_VERSION) - inputs: - targetType: 'inline' - pwsh: true - script: | - $ErrorActionPreference = 'Stop' - $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } - if ($env:REQUIRED_MODULE_VERSION) { - $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION - Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." - } else { - Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." - } - Install-Module @installArgs - Import-Module AzLocal.UpdateManagement -Force - $banner = Add-AzLocalPipelineVersionBanner ` - -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` - -PinnedVersion $env:REQUIRED_MODULE_VERSION ` - -PassThru - # Forward to ADO job-scoped variables so the report step (below) can - # surface the three versions in its JUnit + markdown summary. - $latestStr = if ($banner.LatestOnPSGallery) { $banner.LatestOnPSGallery } else { '' } - Write-Host "##vso[task.setvariable variable=installedModuleVersion;isOutput=false]$($banner.InstalledVersion)" - Write-Host "##vso[task.setvariable variable=generatedAgainstVersion;isOutput=false]$($banner.GeneratedAgainstVersion)" - Write-Host "##vso[task.setvariable variable=latestOnPsGallery;isOutput=false]$latestStr" - - - task: AzureCLI@2 - displayName: 'Collect Authentication and Subscription Scope Report' - inputs: - azureSubscription: 'AzureLocal-ServiceConnection' # Update if your service connection has a different name. - scriptType: 'pscore' - scriptLocation: 'inlineScript' - # v0.8.5 thin-YAML: the ~200-line inline run block (az probes, - # XML scaffolding, markdown summary, ##vso writes) has been - # condensed into the Public cmdlet Export-AzLocalAuthValidationReport. - # The cmdlet performs all four probes (az account show / role - # assignment list / az account list / Resource Graph query), - # writes the JUnit XML + subscriptions.json + subscriptions.csv, - # uploads the markdown summary via ##vso[task.uploadsummary], - # and emits the three pipeline variables (subscription_count, - # cluster_count, auth_valid) via Set-AzLocalPipelineOutput. - inlineScript: | - $ErrorActionPreference = 'Stop' - Import-Module AzLocal.UpdateManagement -Force - $reportDir = Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY 'auth-report' - Export-AzLocalAuthValidationReport ` - -ReportDirectory $reportDir ` - -InstalledModuleVersion "$(installedModuleVersion)" ` - -GeneratedAgainstVersion "$(generatedAgainstVersion)" ` - -LatestOnPSGallery "$(latestOnPsGallery)" - - - task: PublishTestResults@2 - displayName: 'Publish Authentication Validation JUnit Diagnostics' - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '$(Build.ArtifactStagingDirectory)/auth-report/auth-report.xml' - testRunTitle: '[JUnit Debug] Authentication Validation and Subscription Scope' - failTaskOnFailedTests: false - mergeTestResults: false - - # artifact convention: compute a UTC timestamp variable so every downloadable artifact name is unique per run. - - pwsh: | - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') - Write-Host "##vso[task.setvariable variable=artifactStamp;isOutput=true]$stamp" - Write-Host "Artifact timestamp: $stamp" - displayName: 'Compute Artifact Timestamp' - condition: always() - name: stamp - - - task: PublishPipelineArtifact@1 - displayName: 'Publish auth-report artifact' - condition: always() - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/auth-report' - artifact: 'azlocal-step.0-auth-report_$(stamp.artifactStamp)' - publishLocation: 'pipeline' - diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml deleted file mode 100644 index 6d120927..00000000 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml +++ /dev/null @@ -1,141 +0,0 @@ -# AZLOCAL-PIPELINE-ID: inventory-clusters -# Step.01 - Inventory Azure Local Clusters -# This pipeline queries all Azure Local clusters and exports inventory with UpdateRing tag status -# -# OUTPUT COLUMNS: -# ClusterName, ResourceGroup, SubscriptionId, SubscriptionName, -# UpdateRing, HasUpdateRingTag, UpdateStartWindow, UpdateExclusions, -# UpdateSideloaded, UpdateVersionInProgress, ResourceId -# -# - UpdateSideloaded (operator-set): True/False/1/0 - sideloaded-payload gate -# - UpdateVersionInProgress (module-managed; do not edit): staged update name -# See main module README, section "Sideloaded Payload Workflow". - -# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers -# Content between BEGIN/END markers is preserved by Update-AzLocalPipelineExample -# across module upgrades. Edit the cron below or add more `- cron:` entries -# to suit your maintenance windows. -trigger: none - -schedules: - # Run weekly on Monday at 6:00 AM UTC - - cron: '0 6 * * 1' - displayName: 'Weekly Inventory' - branches: - include: - - main - always: true -# END-AZLOCAL-CUSTOMIZE:schedule-triggers - -parameters: - - name: subscriptionFilter - displayName: 'Subscription ID Filter (leave empty for all subscriptions)' - type: string - default: '' - - name: moduleVersion - displayName: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".' - type: string - default: '' - -variables: - # Module version this YAML was generated against. The install step compares this to - # 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. - - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.85' - # 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). - - name: REQUIRED_MODULE_VERSION - value: '${{ parameters.moduleVersion }}' - -pool: - vmImage: 'windows-latest' - -stages: - - stage: Inventory - displayName: 'Inventory Azure Local Clusters' - jobs: - - job: InventoryClusters - displayName: 'Run Cluster Inventory' - - steps: - - checkout: self - displayName: 'Checkout Repository' - - - task: AzureCLI@2 - displayName: 'Install Resource Graph Extension' - inputs: - azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name - scriptType: 'pscore' - scriptLocation: 'inlineScript' - inlineScript: | - az extension add --name resource-graph --yes - - - task: PowerShell@2 - displayName: 'Install AzLocal.UpdateManagement from PSGallery' - # v0.8.5 thin-YAML: drift detection + banner + step outputs are all - # produced by Add-AzLocalPipelineVersionBanner (Public cmdlet). - env: - REQUIRED_MODULE_VERSION: $(REQUIRED_MODULE_VERSION) - GENERATED_AGAINST_MODULE_VERSION: $(GENERATED_AGAINST_MODULE_VERSION) - inputs: - targetType: 'inline' - pwsh: true - script: | - $ErrorActionPreference = 'Stop' - $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } - if ($env:REQUIRED_MODULE_VERSION) { - $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION - Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." - } else { - Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." - } - Install-Module @installArgs - Import-Module AzLocal.UpdateManagement -Force - $banner = Add-AzLocalPipelineVersionBanner ` - -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` - -PinnedVersion $env:REQUIRED_MODULE_VERSION ` - -PassThru - # Forward to ADO job-scoped variables so the inventory step (below) - # can stamp the installed module version into README_Instructions.txt. - Write-Host "##vso[task.setvariable variable=installedModuleVersion;isOutput=false]$($banner.InstalledVersion)" - - - task: AzureCLI@2 - displayName: 'Run Cluster Inventory' - inputs: - azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name - scriptType: 'pscore' - scriptLocation: 'inlineScript' - # v0.8.5 thin-YAML: the inline run block (inventory query, dual - # CSV/JSON export, canonical CSV copy, README_Instructions.txt, - # per-pipeline summary, separate 'Generate Summary' task) has - # been condensed into the Public cmdlet - # Invoke-AzLocalClusterInventory. The cmdlet writes the four - # artifacts to -OutputDirectory, emits the markdown summary via - # ##vso[task.uploadsummary], and sets the four step outputs. - inlineScript: | - $ErrorActionPreference = 'Stop' - Import-Module AzLocal.UpdateManagement -Force - Invoke-AzLocalClusterInventory ` - -OutputDirectory "$(Build.ArtifactStagingDirectory)" ` - -SubscriptionFilter "${{ parameters.subscriptionFilter }}" ` - -InstalledModuleVersion "$(installedModuleVersion)" - - # compute a UTC timestamp variable so every downloadable artifact name is unique per run. - - pwsh: | - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') - Write-Host "##vso[task.setvariable variable=artifactStamp;isOutput=true]$stamp" - Write-Host "Artifact timestamp: $stamp" - displayName: 'Compute Artifact Timestamp' - condition: always() - name: stamp - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Inventory Artifact' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'azlocal-step.1-cluster-inventory_$(stamp.artifactStamp)' - publishLocation: 'Container' - diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml new file mode 100644 index 00000000..e45dc66d --- /dev/null +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml @@ -0,0 +1,256 @@ +# AZLOCAL-PIPELINE-ID: setup-validate-and-inventory +# Setup: 01 - Validate Auth and Inventory Azure Local Clusters +# +# PURPOSE: +# This is the first step in the setup workflow. It combines two operations: +# 1. VALIDATION: Validates the service connection (Workload Identity Federation), +# App Registration, and RBAC role assignments, and produces a Subscription +# Scope report listing every subscription the pipeline identity can see. +# 2. INVENTORY: Queries all Azure Local clusters and exports inventory with +# UpdateRing tag status. +# +# Run this pipeline: +# - BEFORE importing the operational pipelines (Fleet: 01-07). It narrows +# any failure to authentication/discovery rather than seven interacting +# pipelines. +# - PERIODICALLY (recommended monthly, or after every RBAC change in the +# tenant) to confirm the pipeline identity subscription scope and cluster +# inventory have not silently drifted. +# +# VALIDATION PROBES: +# 1. AzureCLI@2 task auth against the service connection (Workload Identity +# Federation auto-managed by ADO). +# 2. Service-connection -> App Registration -> subscription wiring. +# 3. Azure RBAC on the App Registration's service principal. +# 4. Resource Graph reachability (proves the SP can actually see clusters). +# 5. Subscription scope: total count and full Subscription ID / Name table. +# +# INVENTORY OUTPUT COLUMNS: +# ClusterName, ResourceGroup, SubscriptionId, SubscriptionName, +# UpdateRing, HasUpdateRingTag, UpdateStartWindow, UpdateExclusions, +# UpdateSideloaded, UpdateVersionInProgress, ResourceId +# +# - UpdateSideloaded (operator-set): True/False/1/0 - sideloaded-payload gate +# - UpdateVersionInProgress (module-managed; do not edit): staged update name +# See main module README, section "Sideloaded Payload Workflow". +# +# REPORTS GENERATED: +# - JUnit XML published via PublishTestResults@2 (renders in the ADO Tests tab). +# - Markdown summary uploaded via ##vso[task.uploadsummary] (renders in the Summary tab). +# - Artifacts: auth-report (JUnit XML + subscriptions.json + .csv) and +# cluster-inventory (ClusterUpdateRings.csv + .json + README_Instructions.txt). +# +# HOW TO RUN (after the file is committed and the pipeline is imported): +# Pipelines -> Setup: 01 - Validate Auth and Inventory Clusters -> Run pipeline. +# +# See Automation-Pipeline-Examples/README.md, section 5, for the full setup story. + +name: 'Setup: 01 - Validate Auth and Inventory Clusters' + +# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers +# Add or modify `schedules:` blocks here to enable cron-driven runs. Content +# between BEGIN/END markers is preserved by Update-AzLocalPipelineExample +# across module upgrades. +trigger: none + +schedules: + # Run weekly on Sunday at 8:00 AM UTC + - cron: '0 8 * * 0' + displayName: 'Weekly Setup Validation and Inventory' + branches: + include: + - main + always: true +# END-AZLOCAL-CUSTOMIZE:schedule-triggers + +parameters: + - name: subscriptionFilter + displayName: 'Subscription ID Filter (leave empty for all subscriptions)' + type: string + default: '' + - name: moduleVersion + displayName: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".' + type: string + default: '' + +variables: + # Module version this YAML was generated against. The install step compares this to + # 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. + - name: GENERATED_AGAINST_MODULE_VERSION + value: '0.8.85' + # 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). + - name: REQUIRED_MODULE_VERSION + value: '${{ parameters.moduleVersion }}' + +pool: + vmImage: 'windows-latest' + +stages: + - stage: Validate + displayName: 'Validate Workload Identity Federation + RBAC + Subscription Scope' + jobs: + - job: ValidateAuthAndScope + displayName: 'Run auth, RBAC, and Subscription Scope probes' + + steps: + - task: PowerShell@2 + displayName: 'Install AzLocal.UpdateManagement from PSGallery' + # v0.8.5 thin-YAML: drift detection + banner + step outputs are all + # produced by Add-AzLocalPipelineVersionBanner (Public cmdlet). + env: + REQUIRED_MODULE_VERSION: $(REQUIRED_MODULE_VERSION) + GENERATED_AGAINST_MODULE_VERSION: $(GENERATED_AGAINST_MODULE_VERSION) + inputs: + targetType: 'inline' + pwsh: true + script: | + $ErrorActionPreference = 'Stop' + $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } + if ($env:REQUIRED_MODULE_VERSION) { + $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION + Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." + } else { + Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." + } + Install-Module @installArgs + Import-Module AzLocal.UpdateManagement -Force + $banner = Add-AzLocalPipelineVersionBanner ` + -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` + -PinnedVersion $env:REQUIRED_MODULE_VERSION ` + -PassThru + $latestStr = if ($banner.LatestOnPSGallery) { $banner.LatestOnPSGallery } else { '' } + Write-Host "##vso[task.setvariable variable=installedModuleVersion;isOutput=false]$($banner.InstalledVersion)" + Write-Host "##vso[task.setvariable variable=generatedAgainstVersion;isOutput=false]$($banner.GeneratedAgainstVersion)" + Write-Host "##vso[task.setvariable variable=latestOnPsGallery;isOutput=false]$latestStr" + + - task: AzureCLI@2 + displayName: 'Collect Authentication and Subscription Scope Report' + inputs: + azureSubscription: 'AzureLocal-ServiceConnection' # Update if your service connection has a different name. + scriptType: 'pscore' + scriptLocation: 'inlineScript' + # v0.8.5 thin-YAML: Export-AzLocalAuthValidationReport performs all + # four probes (az account show / role assignment list / az account + # list / Resource Graph query), writes the JUnit XML + + # subscriptions.json + subscriptions.csv, uploads the markdown + # summary via ##vso[task.uploadsummary], and emits the three + # pipeline variables (subscription_count, cluster_count, auth_valid). + inlineScript: | + $ErrorActionPreference = 'Stop' + Import-Module AzLocal.UpdateManagement -Force + $reportDir = Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY 'auth-report' + Export-AzLocalAuthValidationReport ` + -ReportDirectory $reportDir ` + -InstalledModuleVersion "$(installedModuleVersion)" ` + -GeneratedAgainstVersion "$(generatedAgainstVersion)" ` + -LatestOnPSGallery "$(latestOnPsGallery)" + + - task: PublishTestResults@2 + displayName: 'Publish Authentication Validation JUnit Diagnostics' + condition: always() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(Build.ArtifactStagingDirectory)/auth-report/auth-report.xml' + testRunTitle: '[JUnit Debug] Setup: 01 - Authentication Validation' + failTaskOnFailedTests: false + mergeTestResults: false + + - pwsh: | + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') + Write-Host "##vso[task.setvariable variable=artifactStamp;isOutput=true]$stamp" + Write-Host "Artifact timestamp: $stamp" + displayName: 'Compute Artifact Timestamp' + condition: always() + name: stamp + + - task: PublishPipelineArtifact@1 + displayName: 'Publish auth-report artifact' + condition: always() + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)/auth-report' + artifact: 'azlocal-step.1-setup-auth-report_$(stamp.artifactStamp)' + publishLocation: 'pipeline' + + - stage: Inventory + displayName: 'Inventory Azure Local Clusters' + dependsOn: Validate + condition: succeeded() + jobs: + - job: InventoryClusters + displayName: 'Run Cluster Inventory' + + steps: + - checkout: self + displayName: 'Checkout Repository' + + - task: AzureCLI@2 + displayName: 'Install Resource Graph Extension' + inputs: + azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name. + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + az extension add --name resource-graph --yes + + - task: PowerShell@2 + displayName: 'Install AzLocal.UpdateManagement from PSGallery' + # v0.8.5 thin-YAML: drift detection + banner + step outputs are all + # produced by Add-AzLocalPipelineVersionBanner (Public cmdlet). + env: + REQUIRED_MODULE_VERSION: $(REQUIRED_MODULE_VERSION) + GENERATED_AGAINST_MODULE_VERSION: $(GENERATED_AGAINST_MODULE_VERSION) + inputs: + targetType: 'inline' + pwsh: true + script: | + $ErrorActionPreference = 'Stop' + $installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true } + if ($env:REQUIRED_MODULE_VERSION) { + $installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION + Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)." + } else { + Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)." + } + Install-Module @installArgs + Import-Module AzLocal.UpdateManagement -Force + $banner = Add-AzLocalPipelineVersionBanner ` + -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION ` + -PinnedVersion $env:REQUIRED_MODULE_VERSION ` + -PassThru + Write-Host "##vso[task.setvariable variable=installedModuleVersion;isOutput=false]$($banner.InstalledVersion)" + + - task: AzureCLI@2 + displayName: 'Run Cluster Inventory' + inputs: + azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name. + scriptType: 'pscore' + scriptLocation: 'inlineScript' + # v0.8.5 thin-YAML: Invoke-AzLocalClusterInventory writes the four + # artifacts to -OutputDirectory, emits the markdown summary via + # ##vso[task.uploadsummary], and sets the four step outputs. + inlineScript: | + $ErrorActionPreference = 'Stop' + Import-Module AzLocal.UpdateManagement -Force + Invoke-AzLocalClusterInventory ` + -OutputDirectory "$(Build.ArtifactStagingDirectory)" ` + -SubscriptionFilter "${{ parameters.subscriptionFilter }}" ` + -InstalledModuleVersion "$(installedModuleVersion)" + + - pwsh: | + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss') + Write-Host "##vso[task.setvariable variable=artifactStamp;isOutput=true]$stamp" + Write-Host "Artifact timestamp: $stamp" + displayName: 'Compute Artifact Timestamp' + condition: always() + name: stamp + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Inventory Artifact' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'azlocal-step.1-setup-cluster-inventory_$(stamp.artifactStamp)' + publishLocation: 'Container' diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 4172c740..35e08278 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -162,8 +162,8 @@ Describe 'Module: AzLocal.UpdateManagement' { # that disagree with the module they just installed. Every # bundled *.yml sample (both platforms) must pin to the current # manifest ModuleVersion. v0.8.7 de-numbered the filenames and - # authentication-test.yml now also consumes the module, so every - # sample (no exemptions) carries the pin. + # setup-validate-and-inventory.yml (the merged setup workflow) also + # consumes the module, so every sample (no exemptions) carries the pin. $manifestPath = Join-Path -Path $PSScriptRoot -ChildPath '..\AzLocal.UpdateManagement.psd1' $manifestVersion = (Import-PowerShellDataFile -Path $manifestPath).ModuleVersion $samplesDir = Join-Path -Path $PSScriptRoot -ChildPath "..\Automation-Pipeline-Examples\$Platform" @@ -642,9 +642,8 @@ Describe 'Module: AzLocal.UpdateManagement' { # with one Add-AzLocalPipelineVersionBanner call - count either # form as a banner emit. $perFileExpect = @{ - 'authentication-test.yml' = 1 - 'inventory-clusters.yml' = 1 - 'manage-updatering-tags.yml' = 1 + 'setup-validate-and-inventory.yml' = 2 + 'manage-updatering-tags.yml' = 1 'apply-updates-schedule-audit.yml' = 1 'fleet-connectivity-status.yml' = 1 'assess-update-readiness.yml' = 1 @@ -7082,7 +7081,7 @@ Describe 'Function: Copy-AzLocalPipelineExample' { $yamls = @(Get-ChildItem -LiteralPath $dest -Filter '*.yml' -File) $yamls.Count | Should -BeGreaterThan 0 - $yamls.Name | Should -Contain 'authentication-test.yml' + $yamls.Name | Should -Contain 'setup-validate-and-inventory.yml' Test-Path (Join-Path $dest 'azure-devops') | Should -BeFalse Test-Path (Join-Path $dest 'github-actions') | Should -BeFalse From 12328a7dd533f656982e2e821f7727cd3d0feb42 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 11:07:53 +0100 Subject: [PATCH 04/17] v0.8.85 follow-up: move collapsible subscription section into Export-AzLocalAuthValidationReport cmdlet; track test-run-timings.csv --- .../setup-validate-and-inventory.yml | 33 ------------------- .../Export-AzLocalAuthValidationReport.ps1 | 5 +++ .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 6 ++++ .../Tests/test-run-timings.csv | 2 ++ 4 files changed, 13 insertions(+), 33 deletions(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml index ca7cdd40..cf7deddb 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml @@ -151,39 +151,6 @@ jobs: -GeneratedAgainstVersion $env:GENERATED_AGAINST_VERSION ` -LatestOnPSGallery $env:LATEST_ON_PSGALLERY - - name: 'Format Subscription Details as Collapsible Section' - if: always() - shell: pwsh - run: | - $reportDir = './reports' - $csvPath = Join-Path $reportDir 'subscriptions.csv' - - if (Test-Path -LiteralPath $csvPath) { - $subs = Import-Csv -LiteralPath $csvPath -ErrorAction SilentlyContinue - $subCount = @($subs).Count - - # Append collapsible subscription details to GITHUB_STEP_SUMMARY - $summary = @" - - ### Count of subscriptions accessible = $subCount - -
- Expand for subscription details - - | Subscription ID | Subscription Name | - |---|---| - "@ - - if ($subs) { - foreach ($sub in $subs) { - $summary += "`n| $($sub.'Subscription ID') | $($sub.'Subscription Name') |" - } - } - - $summary += "`n
`n" - $summary | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append - } - - name: 'Compute Artifact Timestamp' id: artifact-stamp shell: pwsh diff --git a/AzLocal.UpdateManagement/Public/Export-AzLocalAuthValidationReport.ps1 b/AzLocal.UpdateManagement/Public/Export-AzLocalAuthValidationReport.ps1 index 56468d50..c42f409a 100644 --- a/AzLocal.UpdateManagement/Public/Export-AzLocalAuthValidationReport.ps1 +++ b/AzLocal.UpdateManagement/Public/Export-AzLocalAuthValidationReport.ps1 @@ -353,6 +353,9 @@ function Export-AzLocalAuthValidationReport { [void]$md.AppendLine('') [void]$md.AppendLine("### Count of subscriptions accessible = $subCount") [void]$md.AppendLine('') + [void]$md.AppendLine('
') + [void]$md.AppendLine('Expand for subscription details') + [void]$md.AppendLine('') [void]$md.AppendLine('| # | Subscription Name | Subscription ID | Tenant ID | State |') [void]$md.AppendLine('|---|---|---|---|---|') $i = 0 @@ -361,6 +364,8 @@ function Export-AzLocalAuthValidationReport { [void]$md.AppendLine("| $i | $($s.name) | ``$($s.subscriptionId)`` | ``$($s.tenantId)`` | $($s.state) |") } [void]$md.AppendLine('') + [void]$md.AppendLine('
') + [void]$md.AppendLine('') [void]$md.AppendLine("*Generated at $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss UTC'))*") [void](Add-AzLocalPipelineStepSummary -Markdown $md.ToString() -SummaryFileName 'auth-report-summary.md') diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 35e08278..8c7be779 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -14200,8 +14200,11 @@ Describe 'Thin-YAML Step.0: Export-AzLocalAuthValidationReport' { $summary = Get-Content -LiteralPath $script:_avr_ghSummaryFile -Raw $summary | Should -Match '## Step\.0 - Authentication Validation and Subscription Scope Report' $summary | Should -Match '### Count of subscriptions accessible = 2' + $summary | Should -Match '
' + $summary | Should -Match 'Expand for subscription details' $summary | Should -Match 'Alpha' $summary | Should -Match 'Beta' + $summary | Should -Match '
' # Step outputs: all three keys present. $outputs = Get-Content -LiteralPath $script:_avr_ghOutputFile -Raw $outputs | Should -Match 'subscription_count=2' @@ -14259,9 +14262,12 @@ Describe 'Thin-YAML Step.0: Export-AzLocalAuthValidationReport' { $result = Invoke-Step0Cmdlet -Params $params -Account $script:_avr_account -Subs $multi -Clusters $script:_avr_clusters -RoleRows $script:_avr_roleRows $result.SubscriptionCount | Should -Be 3 $summary = Get-Content -LiteralPath $script:_avr_ghSummaryFile -Raw + $summary | Should -Match '
' + $summary | Should -Match 'Expand for subscription details' $summary | Should -Match '\| 1 \| A \| `s1`' $summary | Should -Match '\| 2 \| B \| `s2`' $summary | Should -Match '\| 3 \| C \| `s3`' + $summary | Should -Match '
' } It 'Throws a helpful error when az account show fails' { diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 32261a76..7c0a5ed0 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -30,3 +30,5 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T09:44:48Z","0.8.84","1295","1254","2","1","122.97","120.97","14.12","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T09:47:44Z","0.8.85","1295","1253","3","1","50.17","48.88","3.54","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T09:49:41Z","0.8.85","1295","1256","0","1","50.8","49.53","3.77","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T09:57:12Z","0.8.85","1295","1256","0","1","100.7","98.72","8.95","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T10:07:33Z","0.8.85","1295","1256","0","1","126.88","123.98","5.19","Invoke-Tests.ps1","5.1.26100.8730" From f96c602e150ca1703b9dd4f952cfbb03f74d01ce Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 11:15:57 +0100 Subject: [PATCH 05/17] v0.8.85 follow-up: fix live integration test failures (Step.0 ErrorActionPreference, Step.3 path, Step.4 SummaryPath) --- AzLocal.UpdateManagement/Private/Invoke-AzCliJson.ps1 | 7 ++++++- .../Public/Export-AzLocalFleetConnectivityStatusReport.ps1 | 4 ++-- AzLocal.UpdateManagement/Tests/Live-Integration.Tests.ps1 | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/AzLocal.UpdateManagement/Private/Invoke-AzCliJson.ps1 b/AzLocal.UpdateManagement/Private/Invoke-AzCliJson.ps1 index 2f21ba7a..2cee3b54 100644 --- a/AzLocal.UpdateManagement/Private/Invoke-AzCliJson.ps1 +++ b/AzLocal.UpdateManagement/Private/Invoke-AzCliJson.ps1 @@ -54,7 +54,12 @@ function Invoke-AzCliJson { $env:PYTHONIOENCODING = 'utf-8' $azArgs = @($Arguments) + '--only-show-errors' - $raw = & az @azArgs 2>&1 + # Use Continue so ErrorRecord objects from native az stderr do not become + # terminating exceptions when the caller sets -ErrorAction Stop. The exit + # code ($LASTEXITCODE) is the authoritative failure signal; we handle it + # explicitly below. + $local:ErrorActionPreference = 'Continue' + $raw = @(& az @azArgs 2>&1) $exit = $LASTEXITCODE # Split merged stdout+stderr by stream type. Stderr lines (Python diff --git a/AzLocal.UpdateManagement/Public/Export-AzLocalFleetConnectivityStatusReport.ps1 b/AzLocal.UpdateManagement/Public/Export-AzLocalFleetConnectivityStatusReport.ps1 index 0ae11673..624d892c 100644 --- a/AzLocal.UpdateManagement/Public/Export-AzLocalFleetConnectivityStatusReport.ps1 +++ b/AzLocal.UpdateManagement/Public/Export-AzLocalFleetConnectivityStatusReport.ps1 @@ -397,7 +397,7 @@ function Export-AzLocalFleetConnectivityStatusReport { } $md = New-AzLocalFleetConnectivityStatusSummary @summaryArgs - Add-AzLocalPipelineStepSummary -Markdown $md -SummaryFileName $SummaryFileName | Out-Null + $summaryWrittenPath = Add-AzLocalPipelineStepSummary -Markdown $md -SummaryFileName $SummaryFileName # ---- Step outputs (lowercase snake_case, v0.8.5 convention) ----------- Set-AzLocalPipelineOutput -Name 'cluster_total' -Value ([string]$clusterRows.Count) @@ -442,7 +442,7 @@ function Export-AzLocalFleetConnectivityStatusReport { NicFailures = $nicFail ArbFailures = $arbFail JUnitXmlPath = $xmlPath - SummaryPath = (Join-Path -Path $OutputDirectory -ChildPath $SummaryFileName) + SummaryPath = $summaryWrittenPath } } } diff --git a/AzLocal.UpdateManagement/Tests/Live-Integration.Tests.ps1 b/AzLocal.UpdateManagement/Tests/Live-Integration.Tests.ps1 index f6d195b3..eccbff7c 100644 --- a/AzLocal.UpdateManagement/Tests/Live-Integration.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/Live-Integration.Tests.ps1 @@ -409,7 +409,7 @@ Describe 'Live-Integration: Export-*Report cmdlets emit non-empty artifacts (v0. It '[Step.3] Export-AzLocalApplyUpdatesScheduleAudit emits audit + matrix CSV, recommend MD, JUnit XML and PassThru exposes counts' { $modRoot = Split-Path -Path (Get-Module AzLocal.UpdateManagement | Select-Object -First 1).Path -Parent $schedule = Join-Path $modRoot 'Automation-Pipeline-Examples\apply-updates-schedule.example.yml' - $pipelineYml = Join-Path $modRoot 'Automation-Pipeline-Examples\github-actions\Step.3_apply-updates-schedule-audit.yml' + $pipelineYml = Join-Path $modRoot 'Automation-Pipeline-Examples\github-actions\apply-updates-schedule-audit.yml' Test-Path -LiteralPath $schedule | Should -BeTrue -Because 'bundled schedule example must exist' Test-Path -LiteralPath $pipelineYml | Should -BeTrue -Because 'bundled Step.3 pipeline yml must exist' From b2bc2c4e9ce421842ceec5967a0054db75fdce89 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 11:18:21 +0100 Subject: [PATCH 06/17] Add test-run-timings.csv to repo (test performance metrics) --- AzLocal.UpdateManagement/Tests/test-run-timings.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 7c0a5ed0..7d900c08 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -32,3 +32,5 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T09:49:41Z","0.8.85","1295","1256","0","1","50.8","49.53","3.77","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T09:57:12Z","0.8.85","1295","1256","0","1","100.7","98.72","8.95","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T10:07:33Z","0.8.85","1295","1256","0","1","126.88","123.98","5.19","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T10:09:11Z","0.8.85","1295","35","3","0","545.67","544.05","2.5","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T10:13:27Z","0.8.85","1295","1256","0","1","49.91","48.62","3.18","Invoke-Tests.ps1","5.1.26100.8730" From ebaa5871dfa4c617b6052261635fc3f45a791721 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 11:35:52 +0100 Subject: [PATCH 07/17] docs: align pipeline appendix with Setup/Fleet naming --- .../docs/appendix-pipelines.md | 110 +++++++++++------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md index 0b56013a..691a9bf6 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md @@ -1,27 +1,27 @@ # Pipeline reference -> **What you will find here:** A one-row-per-pipeline reference index for every bundled `Step.*.yml` workflow (GitHub Actions + Azure DevOps twins), with a one-line summary of the job, its triggers, the cmdlets it invokes, what it depends on, and the artefacts it produces. Use this as the at-a-glance reference card after you have read the per-step runbook in the main pipeline [README.md](../README.md). +> **What you will find here:** A comprehensive reference index for every bundled workflow (GitHub Actions + Azure DevOps twins), with a one-line summary of the job, its triggers, the cmdlets it invokes, what it depends on, and the artefacts it produces. Use this as the at-a-glance reference card after you have read the per-workflow runbook in the main pipeline [README.md](../README.md). -Each pipeline section heading below uses **`Step N - `** to mirror its bundled YAML filename `Step.N_.yml` exactly. The **Pipeline** column header in the at-a-glance table below is the column label; the `Step N -` form is the operator-facing identifier used everywhere else - workflow `name:` fields, ADO pipeline names, the parent README's section 1.1 table, and these section headings. The execution order in the [end-to-end runbook](../README.md#6-end-to-end-runbook-bring-an-estate-online) is operational and not strictly numerical - read it for the recommended sequence. +Each pipeline section heading below uses **`Setup: NN`** or **`Fleet: NN`** naming to match the operator-facing workflow names. These identifiers appear in the workflow `name:` fields (GH Actions), ADO pipeline import names, the parent README's section 1.1 table, and these section headings. The execution order in the [end-to-end runbook](../README.md#6-end-to-end-runbook-bring-an-estate-online) is operational - read it for the recommended sequence. --- ## Default triggers and schedules (at a glance) -The table below is the ground truth for what each shipped YAML does **out of the box**. Four of the ten pipelines (`fleet-connectivity-status`, `fleet-update-status`, `fleet-health-status`, `apply-updates-schedule-audit`) and one optional (`inventory-clusters` weekly) are pre-wired with `schedule:` (GH) / `schedules:` (ADO) blocks. The remaining five (`authentication-test`, `manage-updatering-tags`, `assess-update-readiness`, `apply-updates`, `monitor-updates`) are manual-only by design (`monitor-updates` ships a commented-out `*/30 * * * *` cron sample for in-wave use). +The table below is the ground truth for what each shipped YAML does **out of the box**. Four of the ten pipelines (`fleet-connectivity-status`, `fleet-update-status`, `fleet-health-status`, `apply-updates-schedule-audit`) are pre-wired with `schedule:` (GH) / `schedules:` (ADO) blocks. The remaining six (all Setup workflows + `assess-update-readiness`, `apply-updates`, `monitor-updates`) are manual-only by design (`monitor-updates` ships a commented-out `*/30 * * * *` cron sample for in-wave use). | Pipeline | GitHub Actions trigger | Azure DevOps trigger | Notes | |---|---|---|---| -| Step 0 - `authentication-test` | `workflow_dispatch` only | `trigger: none` (manual only) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Recommended monthly cadence; add your own `schedule:` block if you want it automated. | -| Step 1 - `inventory-clusters` | `workflow_dispatch` + `schedule: cron '0 6 * * 1'` (Mondays 06:00 UTC) | `trigger: none` + `schedules: cron '0 6 * * 1'` | Weekly drift detection. Edit the cron to change cadence. | -| Step 2 - `manage-updatering-tags` | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | -| Step 3 - `apply-updates-schedule-audit` | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | -| Step 4 - `fleet-connectivity-status` | `workflow_dispatch` + `schedule: cron '30 5 * * *'` (daily 05:30 UTC) | `trigger: none` + `schedules: cron '30 5 * * *'` | Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot with bidirectional node-coverage reconciliation. Runs 30 min before `fleet-update-status` so connectivity issues are visible upstream of update reporting. | -| Step 5 - `assess-update-readiness` | `workflow_dispatch` only | `trigger: none` (manual only) | **Recommended customisation** - schedule per ring 12-72h before the matching Step 7 `UpdateStartWindow` (see [Step 5 - Assess Update Readiness](#step-5---assess-update-readiness) below for the per-ring pattern + lead-time table). Always-green at pipeline level; per-cluster readiness gaps surface as JUnit `` entries. | -| Step 7 - `apply-updates` | `workflow_dispatch` only | `trigger: none` (manual only) | **No schedule shipped** - see the warning in [Step 7 - Apply Updates](#step-7---apply-updates) below. The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags only gate updates *while the pipeline is running*; they do **not** start the pipeline. | -| Step 8 - `monitor-updates` | `workflow_dispatch` only (commented-out `schedule: cron '*/30 * * * *'` sample shipped) | `trigger: none` only (commented-out `schedules: cron '*/30 * * * *'` sample shipped) | In-flight update progress / long-running-run flag. Disabled by default to avoid 48 idle runs/day between waves. Uncomment the cron when a wave is active. | -| Step 9 - `fleet-update-status` | `workflow_dispatch` + `schedule: cron '0 6 * * *'` (daily 06:00 UTC) | `trigger: none` + `schedules: cron '0 6 * * *'` | Daily fleet update snapshot. | -| Step 10 - `fleet-health-status` | `workflow_dispatch` + `schedule: cron '0 7 * * *'` (daily 07:00 UTC) | `trigger: none` + `schedules: cron '0 7 * * *'` | Daily 24-hour health-check snapshot. Offset by one hour from `fleet-update-status` to avoid contention. | +| **Setup: 01** - Validate Auth and Inventory Clusters | `workflow_dispatch` only (merged auth + inventory) | `trigger: none` (manual only) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Recommended monthly cadence; add your own `schedule:` block if you want it automated. | +| **Setup: 02** - Manage UpdateRing Tags | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | +| **Setup: 03** - Apply-Updates Schedule Coverage Audit | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | +| **Fleet: 01** - Assess Update Readiness | `workflow_dispatch` only | `trigger: none` (manual only) | **Recommended customisation** - schedule per ring 12-72h before the matching **Fleet: 04** `UpdateStartWindow` (see [Fleet: 01 - Assess Update Readiness](#fleet-01---assess-update-readiness) below for the per-ring pattern + lead-time table). Always-green at pipeline level; per-cluster readiness gaps surface as JUnit `` entries. | +| **Fleet: 02** - Fleet Connectivity Status | `workflow_dispatch` + `schedule: cron '30 5 * * *'` (daily 05:30 UTC) | `trigger: none` + `schedules: cron '30 5 * * *'` | Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot with bidirectional node-coverage reconciliation. Runs 30 min before **Fleet: 06** so connectivity issues are visible upstream of update reporting. | +| **Fleet: 03** - Sideload Updates (Opt-in) | `workflow_dispatch` only | `trigger: none` (manual only) | Optional pre-stage on-prem update media workflow for disconnected environments. | +| **Fleet: 04** - Apply Updates | `workflow_dispatch` only | `trigger: none` (manual only) | **No schedule shipped** - see the warning in [Fleet: 04 - Apply Updates](#fleet-04---apply-updates) below. The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags only gate updates *while the pipeline is running*; they do **not** start the pipeline. | +| **Fleet: 05** - Monitor In-Flight Updates | `workflow_dispatch` only (commented-out `schedule: cron '*/30 * * * *'` sample shipped) | `trigger: none` only (commented-out `schedules: cron '*/30 * * * *'` sample shipped) | In-flight update progress / long-running-run flag. Disabled by default to avoid 48 idle runs/day between waves. Uncomment the cron when a wave is active. | +| **Fleet: 06** - Fleet Update Status | `workflow_dispatch` + `schedule: cron '0 6 * * *'` (daily 06:00 UTC) | `trigger: none` + `schedules: cron '0 6 * * *'` | Daily fleet update snapshot. | +| **Fleet: 07** - Fleet Health Status | `workflow_dispatch` + `schedule: cron '0 7 * * *'` (daily 07:00 UTC) | `trigger: none` + `schedules: cron '0 7 * * *'` | Daily 24-hour health-check snapshot. Offset by one hour from **Fleet: 06** to avoid contention. | > **All times are UTC.** GitHub Actions and Azure DevOps schedules both run on UTC; convert from your local timezone when picking cron values. Both platforms can delay scheduled runs by several minutes during high-load periods - do not rely on second-precision alignment. > @@ -31,11 +31,11 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Step 0 - Authentication Validation and Subscription Scope Report +## Setup: 01 - Validate Auth and Inventory Azure Local Clusters | Aspect | Value | |---|---| -| **Purpose** | End-to-end probe of the federated identity, the Azure RBAC role assignment, and the subscriptions the pipeline identity can read. Emits a JUnit-rendered Authentication / Subscription Scope / Resource Graph Reachability report and the full subscription set (`subscriptions.csv` + `subscriptions.json`) so you can detect silent scope drift (a new tenant SP suddenly seeing more or fewer subscriptions than yesterday) before downstream fleet reports under- or over-count clusters. | +| **Purpose** | End-to-end probe of the federated identity, the Azure RBAC role assignment, and the subscriptions the pipeline identity can read. Also enumerates every Azure Local cluster the identity can see. Emits a JUnit-rendered Authentication / Subscription Scope / Resource Graph Reachability / Cluster Inventory report and exports the full cluster list (`cluster-inventory.csv` + JSON) with UpdateRing tag status so you can detect silent scope drift (a new tenant SP suddenly seeing more or fewer subscriptions or clusters than yesterday) before downstream fleet reports under- or over-count. | | **Inputs** | `environment` (optional - GitHub Actions only; leave blank to test the branch-scoped federated credential), `module_version` (optional). | | **Trigger** | Manual only (`workflow_dispatch` / **Run pipeline** button). No schedule shipped - run it on initial wiring, after every RBAC / federated-credential / subscription change, and on a recommended monthly cadence. Add your own `schedule:` / `schedules:` block if you want it automated. | | **Cmdlets invoked** | None (uses the `az` CLI directly for the auth / Resource Graph probes). | @@ -45,9 +45,13 @@ The table below is the ground truth for what each shipped YAML does **out of the | **RBAC** | Whatever the pipeline identity has - the probe itself is read-only and intentionally surfaces both over- and under-grants. | | **Exit conditions** | Pipeline run is green only when every probe passes. A red run means the identity, its role assignment, or its subscription scope is wrong - downstream pipelines will give nonsense results until this passes. | | **ITSM** | Not supported - this is an identity-only probe with no per-cluster failure surface to dispatch on. | -| **Introduced** | v0.7.70. | +| **Introduced** | v0.7.70 (separate auth + inventory); v0.8.85 (merged into Setup: 01). | -## Step 1 - Inventory Clusters +> **Note**: v0.8.85 consolidates the separate authentication and inventory pipelines into a single `setup-validate-and-inventory.yml` workflow. + +--- + +## Setup: 02 - Manage UpdateRing Tags | Aspect | Value | |---|---| @@ -77,7 +81,9 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Exit conditions** | Pipeline run is green when every CSV row is processed (added / updated / unchanged). Per-cluster tag-write failures surface in the run log; the pipeline does not currently fail on per-row errors. Re-run after triaging is safe (the cmdlet is idempotent). | | **ITSM** | Not supported - tag-write operation is operator-driven and idempotent; per-row failures are surfaced in the run log for direct triage rather than ticketed. | -## Step 3 - Apply-Updates Schedule Coverage Audit +--- + +## Setup: 03 - Apply-Updates Schedule Coverage Audit | Aspect | Value | |---|---| @@ -85,7 +91,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `pipeline_path` (file or folder; default `.github/workflows` on GitHub Actions, `.azure-pipelines` on Azure DevOps - the standard consumer locations for the bundled `apply-updates.yml` sample), `lead_time_minutes` (0-60, default 5), `include_untagged` (default false), `module_version` (optional). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled weekly on Mondays at 05:00 UTC (`cron '0 5 * * 1'`). Deliberately runs before the daily `fleet-connectivity-status` (05:30 UTC), `fleet-update-status` (06:00 UTC), and `fleet-health-status` (07:00 UTC) pipelines so its drift annotations land at the top of the operator's Monday-morning inbox. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Test-AzLocalApplyUpdatesScheduleCoverage`, `Get-AzLocalApplyUpdatesScheduleConfig` (when a `schedule.yml` is present in the repo). | -| **Depends on** | Step 1 (cluster inventory + `UpdateStartWindow` tags), Step 2 (tags applied) for non-trivial output. Runs fine on an empty fleet but the audit is meaningless. | +| **Depends on** | Setup: 01 (cluster inventory + `UpdateStartWindow` tags), Setup: 02 (tags applied) for non-trivial output. Runs fine on an empty fleet but the audit is meaningless. | | **Artefacts** | `schedule-coverage-audit.xml` (JUnit, one `` per `(UpdateRing, UpdateStartWindow)` pair, uncovered = ``), `schedule-coverage-audit.csv` (full Audit view with `Status` / `Recommendation` columns), `schedule-coverage-matrix.csv` (every distinct `(Ring, Window)` pair with its required cron), `schedule-coverage-recommend.md` (ready-to-paste GH Actions + Azure DevOps cron blocks), markdown step summary. | | **When to run** | Hands-off scheduled. Trigger manually whenever you have just tagged a new ring or changed a maintenance window - see the [end-to-end runbook in section 8.3](../README.md#83-end-to-end-runbook-apply-updates-schedule-coverage-audit). | | **RBAC** | Read-only - `Reader` on the cluster scope plus `Microsoft.ResourceGraph/resources/read`. No write actions are ever taken. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | @@ -93,7 +99,9 @@ The table below is the ground truth for what each shipped YAML does **out of the | **ITSM** | Not supported - schedule coverage drift is a one-time YAML-edit fix, not a per-incident triage queue. | | **Introduced** | v0.7.65. | -## Step 4 - Fleet Connectivity Status +--- + +## Fleet: 02 - Fleet Connectivity Status | Aspect | Value | |---|---| @@ -101,15 +109,17 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `subscription_ids` (optional - comma-separated; defaults to every subscription the federated identity can read), `module_version` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 05:30 UTC (`cron '30 5 * * *'`). Deliberately runs 30 minutes before `fleet-update-status` (06:00 UTC) so connectivity issues are visible upstream of update reporting. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalFleetConnectivityStatus`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Step 0 should be green at least once so the subscription scope is trusted. This is the upstream "can we see the fleet at all?" probe that other fleet pipelines silently rely on. | +| **Depends on** | None directly. Setup: 01 should be green at least once so the subscription scope is trusted. This is the upstream "can we see the fleet at all?" probe that other fleet pipelines silently rely on. | | **Artefacts** | `fleet-connectivity-status.xml` (JUnit, one `` per cluster), `fleet-cluster-connectivity.csv` (per-cluster reconciliation), `fleet-arc-status-summary.csv` (per-cluster Arc agent counts), `fleet-arc-non-connected-machines.csv` (per-machine triage list), `fleet-physical-nics.csv` (NIC issues only), `fleet-physical-nic-all.csv` (full NIC inventory), `fleet-physical-nic-stats.csv` (histogram by `NicType` + `NicStatus`), `fleet-arb-status.csv` (Azure Resource Bridge state), markdown job summary including the *"How to interpret + act on a non-zero reconciliation"* guidance subsection. | -| **When to run** | Hands-off scheduled. Trigger manually whenever the apply-updates chain returns "zero ready clusters" or the fleet count drops unexpectedly - Step 4 is the upstream "can we see the fleet at all?" probe. | +| **When to run** | Hands-off scheduled. Trigger manually whenever the apply-updates chain returns "zero ready clusters" or the fleet count drops unexpectedly - Fleet: 02 is the upstream "can we see the fleet at all?" probe. | | **RBAC** | Read-only - `Reader` plus `Microsoft.ResourceGraph/resources/read`, `Microsoft.AzureStackHCI/edgeDevices/read`, `Microsoft.HybridCompute/machines/read`, and `Microsoft.ResourceConnector/appliances/read`. All four already live in the **`Azure Stack HCI Update Operator (custom)`** custom role definition shipped in [section 3.1](../README.md#31-custom-role-azure-stack-hci-update-operator-custom). | | **Exit conditions** | Pipeline run is green when the snapshot completes. Per-cluster connectivity issues surface as JUnit `` entries; non-zero reconciliation deltas surface in the markdown summary with remediation guidance. | | **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per cluster with an Arc-disconnected machine or non-zero reconciliation delta; dedupe key is per-cluster + reason via the matrix at `./.itsm/azurelocal-itsm.yml`. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | | **Introduced** | v0.7.79 (reconciliation enhanced in v0.7.85). | -## Step 5 - Assess Update Readiness +--- + +## Fleet: 01 - Assess Update Readiness > **Behaviour change in v0.8.81**: status icons in the Summary counts, Not-Ready and All-clusters tables now render via the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: GitHub Markdown shortcodes on the `GitHubActions` host, Unicode glyphs everywhere else - fixes literal `:white_check_mark:` text on Azure DevOps step summaries). The Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink` and the standing **Ctrl-click tip** banner is single-sourced via `Get-AzLocalCtrlClickTip`. The Up-to-Date bucket gains the readiness-cascade check icon. @@ -121,24 +131,24 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `update_ring` (required), `throttle_limit` (optional). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule is shipped** because the `update_ring` input is required and there is no single ring value that would be correct for every consumer. See the **recommended customisation note below** for the per-ring scheduling pattern + lead-time guidance. | | **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Test-AzLocalClusterHealth`. | -| **Depends on** | Step 1 (an `UpdateRing` tag must exist on at least one cluster to scope the readiness query) and Step 2 (to apply that tag at scale). | +| **Depends on** | Setup: 01 (an `UpdateRing` tag must exist on at least one cluster to scope the readiness query) and Setup: 02 (to apply that tag at scale). | | **Artefacts** | `readiness.xml`, `readiness.csv`, `health-blocking.xml`, `health-blocking.csv`. | -| **When to run** | 12-72 hours before each Step 7 wave for the same ring (12-24h for small rings, 48-72h for large rings of 50+ clusters) - long enough for an operator to triage any `` entries before the maintenance window opens, short enough that the readiness signal is still fresh. See the **recommended customisation note below** for the cron pattern. | +| **When to run** | 12-72 hours before each Fleet: 04 wave for the same ring (12-24h for small rings, 48-72h for large rings of 50+ clusters) - long enough for an operator to triage any `` entries before the maintenance window opens, short enough that the readiness signal is still fresh. See the **recommended customisation note below** for the cron pattern. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | | **Exit conditions** | Pipeline run is always green - per-cluster readiness gaps and blocking health checks surface as JUnit `` entries in the Tests tab. **Caveat**: because the pipeline never goes red, a silently-empty `readiness.xml` (e.g. ring tag typo, zero clusters in scope) will not generate a red email - check the Tests tab / `readiness.xml` artefact after each run, or wire the JUnit reporter into your existing CI status surface. | | **ITSM** | Not supported - this is a pre-flight gate intended for operator review before the Step 7 wave; readiness gaps surface as JUnit `` entries and feed the Step 7 ITSM dispatch downstream rather than raising tickets here. | -> **RECOMMENDED CUSTOMISATION: schedule Step 5 per ring, 12-72h ahead of the matching Step 7 cron.** Unlike Step 7 (which silently does nothing if you forget to schedule it), Step 5 is *recommended* rather than *mandatory* - Step 7 does its own internal `Get-AzLocalClusterUpdateReadiness` per-cluster pre-flight, so skipping Step 5 will not break the apply step. What you lose by skipping it is the **human review window** - the chance to see "12 clusters in Ring2 have blocking health checks" and intervene before the maintenance window starts. +> **RECOMMENDED CUSTOMISATION: schedule Fleet: 01 per ring, 12-72h ahead of the matching Fleet: 04 cron.** Unlike Fleet: 04 (which silently does nothing if you forget to schedule it), Fleet: 01 is *recommended* rather than *mandatory* - Fleet: 04 does its own internal `Get-AzLocalClusterUpdateReadiness` per-cluster pre-flight, so skipping Fleet: 01 will not break the apply step. What you lose by skipping it is the **human review window** - the chance to see "12 clusters in Ring2 have blocking health checks" and intervene before the maintenance window starts. > -> Because `update_ring` is a required input there is no single bundled cron - **add one `schedule:` (GitHub Actions) / `schedules:` (Azure DevOps) entry per ring you intend to patch**, each setting `update_ring:` via the workflow inputs (GH Actions `with:` / ADO `parameters:`). Anchor each Step 5 cron to the matching Step 7 cron (same days, earlier by the lead time): +> Because `update_ring` is a required input there is no single bundled cron - **add one `schedule:` (GitHub Actions) / `schedules:` (Azure DevOps) entry per ring you intend to patch**, each setting `update_ring:` via the workflow inputs (GH Actions `with:` / ADO `parameters:`). Anchor each Fleet: 01 cron to the matching Fleet: 04 cron (same days, earlier by the lead time): > -> | Ring size | Recommended Step 5 lead time | Step 5 cron (UTC, anchoring on a Step 7 Sat 02:00 window) | Rationale | +> | Ring size | Recommended Fleet: 01 lead time | Fleet: 01 cron (UTC, anchoring on a Fleet: 04 Sat 02:00 window) | Rationale | > |---|---|---|---| -> | Small (<= 10 clusters) | 12-24 hours ahead | `'0 2 * * 5'` (Fri 02:00 ahead of a Sat 02:00 Step 7) | Enough to triage a handful of `` entries; fresh enough that the health signal is still meaningful at apply time. | -> | Medium (10-50 clusters) | 24-48 hours ahead | `'0 2 * * 4'` (Thu 02:00 ahead of a Sat 02:00 Step 7) | Lets you raise tickets / engage cluster owners on per-cluster issues before the weekend. | -> | Large (50+ clusters) | 48-72 hours ahead | `'0 2 * * 3'` (Wed 02:00 ahead of a Sat 02:00 Step 7) | Allows time to escalate, swap clusters into a deferral ring (`UpdateExcluded=True`), or stage parallel mitigation. | +> | Small (<= 10 clusters) | 12-24 hours ahead | `'0 2 * * 5'` (Fri 02:00 ahead of a Sat 02:00 Fleet: 04) | Enough to triage a handful of `` entries; fresh enough that the health signal is still meaningful at apply time. | +> | Medium (10-50 clusters) | 24-48 hours ahead | `'0 2 * * 4'` (Thu 02:00 ahead of a Sat 02:00 Fleet: 04) | Lets you raise tickets / engage cluster owners on per-cluster issues before the weekend. | +> | Large (50+ clusters) | 48-72 hours ahead | `'0 2 * * 3'` (Wed 02:00 ahead of a Sat 02:00 Fleet: 04) | Allows time to escalate, swap clusters into a deferral ring (`UpdateExcluded=True`), or stage parallel mitigation. | > -> **Known gap**: the Step 3 schedule-coverage audit currently validates Step 7 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Step 5 cron is correctly anchored to a Step 7 cron. Pair Step 5 and Step 7 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. +> **Known gap**: the Setup: 03 schedule-coverage audit currently validates Fleet: 04 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Fleet: 01 cron is correctly anchored to a Fleet: 04 cron. Pair Fleet: 01 and Fleet: 04 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. ## Step 6 - Sideload Updates @@ -146,21 +156,27 @@ The table below is the ground truth for what each shipped YAML does **out of the > **OPT-IN, OFF BY DEFAULT, self-hosted runner required.** Introduced in v0.8.7. This pipeline is inert unless the repository variable `SIDELOAD_UPDATES` is the literal string `'true'`. Full architecture, the scheduled-task survival model, the shared-state / multi-runner contract, the auth-map, and the catalog format are in [sideload.md](sideload.md); robocopy throttling guidance is in [sideload-robocopy.md](sideload-robocopy.md). +--- + +## Fleet: 03 - Sideload Updates (Opt-in) + | Aspect | Value | |---|---| -| **Purpose** | Pre-stage Azure Local solution-update media onto clusters that **cannot pull updates from Azure directly** (dark / air-gapped / restricted-egress fabrics). It Robocopies the `CombinedSolutionBundle` (or OEM SBE package) to each cluster's infrastructure `import` SMB share, verifies the SHA256 over WinRM, runs `Add-SolutionUpdate`, and flips `UpdateSideloaded=True` so the downstream Step 7 apply can proceed. | +| **Purpose** | Pre-stage Azure Local solution-update media onto clusters that **cannot pull updates from Azure directly** (dark / air-gapped / restricted-egress fabrics). It Robocopies the `CombinedSolutionBundle` (or OEM SBE package) to each cluster's infrastructure `import` SMB share, verifies the SHA256 over WinRM, runs `Add-SolutionUpdate`, and flips `UpdateSideloaded=True` so the downstream Fleet: 04 apply can proceed. | | **Inputs** | `update_ring` (optional - a single ring, a `;`-delimited list, or `***` for every tagged cluster; default `***`), `dry_run` (optional, default `true` - previews the plan + transitions with no staging / task / tag changes), `module_version` (optional pin). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule ships** because the pipeline requires an on-prem self-hosted runner labelled `azlocal-sideload` (GH) / a self-hosted agent in a pool with the `azlocal-sideload` demand (ADO) that most repos do not have. Once the runner/agent is online, uncomment the bundled `*/30 * * * *` cron inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to drive the state machine. | | **Cmdlets invoked** | `Resolve-AzLocalSideloadPlan` (selects clusters whose next apply window is within `SIDELOAD_LEAD_DAYS`), `Invoke-AzLocalSideloadUpdate` (the re-entrant state machine), `Export-AzLocalSideloadStatusReport` + `Add-AzLocalSideloadStepSummary` (JUnit + Markdown summary). | | **Re-entrant state machine** | Each run advances every in-scope cluster by **one** transition and exits: `Planned -> Copying -> Copied -> Verified -> Imported -> SideloadFlagged`. The multi-hour copy itself runs in a **detached Windows Scheduled Task** (driven by `Tools/Invoke-AzLocalSideloadCopyTask.ps1`) so no pipeline run is ever long-lived. Re-running is always safe; a stale `Copying` heartbeat (> `SIDELOAD_HEARTBEAT_STALE_MINUTES`) is re-driven. | -| **Depends on** | Step 1 (`UpdateRing` + sideload tags present, including `UpdateAuthAccountId`), Step 2 (to apply those tags at scale), and a populated `sideload-auth-map.csv` + `sideload-catalog.yml`. The downstream Step 7 apply consumes the `UpdateSideloaded=True` flag this pipeline sets. | +| **Depends on** | Setup: 01 (`UpdateRing` + sideload tags present, including `UpdateAuthAccountId`), Setup: 02 (to apply those tags at scale), and a populated `sideload-auth-map.csv` + `sideload-catalog.yml`. The downstream Fleet: 04 apply consumes the `UpdateSideloaded=True` flag this pipeline sets. | | **Artefacts** | `sideload-status.xml` (JUnit, one cluster per test), `sideload-status.csv`, plus per-run copy/verify logs under the shared `SIDELOAD_STATE_ROOT\logs`. | | **Shared state** | `SIDELOAD_STATE_ROOT` must be a UNC path that every runner/agent can read+write (`state\`, `logs\`, `cache\`). Verified media is cached under `SIDELOAD_CACHE_ROOT` (defaults to `\cache`) so a bundle is downloaded + hashed once and reused across clusters. | | **RBAC** | ARG fleet read + Key Vault secret read + `UpdateSideloaded` tag write for the pipeline identity; a separate Active Directory `[pscredential]` (built from two Key Vault secrets named in the auth-map row) is used for the cluster WinRM remoting and `Add-SolutionUpdate`. | | **Exit conditions** | Pipeline run is green when every in-scope cluster either advances cleanly or is correctly classified as a skip. Per-cluster copy / verify / import failures surface as JUnit `` entries. | | **ITSM** | Not supported in v0.8.7 (staging is a pre-apply preparation step; failures surface as JUnit `` entries for operator review). | -## Step 7 - Apply Updates +--- + +## Fleet: 04 - Apply Updates > **Behaviour change in v0.8.81**: the per-host apply-updates step summary (`Add-AzLocalApplyUpdatesStepSummary`) drops its inline `$iconSuccess` / `$iconFail` / `$iconBlock` / `$iconSkip` / `$iconWarn` definitions in favour of the new shared `Get-AzLocalStatusIconMap` private helper (fixes literal `:white_check_mark:` text on Azure DevOps step summaries). The Cluster column in both the Cluster Actions and Clusters Skipped at Readiness Gate tables now resolves each cluster's resource id from the readiness CSV (built upstream by `Get-AzLocalClusterUpdateReadiness`) and wraps the cell in a portal deep-link via `Get-AzLocalClusterPortalLink`. Adds the standing Ctrl-click tip via `Get-AzLocalCtrlClickTip`. The upstream apply-results JSON shape (`ClusterName` / `Status` / `UpdateName` / `Duration` / `Message`) is intentionally unchanged - the cluster id lookup is done in the renderer. @@ -176,18 +192,18 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `update_ring` (required), `update_name` (optional - leave blank for latest), `dry_run` (optional), `throttle_limit` (optional). **v0.7.4 adds** `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, defaults preserve existing behaviour). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule is shipped** - you must add one. See the **mandatory customisation note below** and the schedule-alignment guidance in [section 8](../README.md#8-scheduling-maintenance-windows-and-change-freeze-periods). | | **Cmdlets invoked** | `Get-AzLocalApplyUpdatesScheduleConfig`, `Get-AzLocalClusterUpdateReadiness`, `Start-AzLocalClusterUpdate`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | Step 1 (`UpdateRing` tags present), Step 5 (readiness reviewed for the wave). Step 3 (schedule coverage audit) should be green so the cron(s) that start this pipeline actually fire at every tagged `UpdateStartWindow`. | +| **Depends on** | Setup: 01 (`UpdateRing` tags present), Fleet: 01 (readiness reviewed for the wave). Setup: 03 (schedule coverage audit) should be green so the cron(s) that start this pipeline actually fire at every tagged `UpdateStartWindow`. | | **Artefacts** | `update-results.xml` (JUnit, one cluster per test), `update-logs/*` (CSV + detail). When ITSM is enabled: `itsm-results.csv`, `itsm-results.xml`. | -| **When to run** | During the maintenance window for each ring, after the readiness assessment is reviewed. | +| **When to run** | During the maintenance window for each ring, after the Fleet: 01 readiness assessment is reviewed. | | **RBAC** | Write to clusters required. Covered by the `Azure Stack HCI Update Operator (custom)` custom role (`Microsoft.AzureStackHCI/clusters/updates/apply/action` + cluster-update reads). | -| **Exit conditions** | Pipeline run is green when every in-scope cluster either succeeds or is correctly classified as `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` (these are skips, not failures - rendered as JUnit `` from v0.8.78). Per-cluster update failures and `HealthCheckBlocked` outcomes surface as JUnit `` entries; long-running runs are tracked by Step 8. | +| **Exit conditions** | Pipeline run is green when every in-scope cluster either succeeds or is correctly classified as `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` (these are skips, not failures - rendered as JUnit `` from v0.8.78). Per-cluster update failures and `HealthCheckBlocked` outcomes surface as JUnit `` entries; long-running runs are tracked by Fleet: 05. | | **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per cluster whose update run finished with a failure state; classification skips (`ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag`) do not generate tickets. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | > **MANDATORY CUSTOMISATION: the Apply Updates pipeline does not ship with a schedule.** The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags **only gate updates *while the pipeline is already running***; they do **not** start the pipeline. If you (a) use `UpdateStartWindow` tags to define when updates may be installed and (b) leave the shipped `apply-updates.yml` with `workflow_dispatch` only (GH) / `trigger: none` (ADO), **no updates will ever be applied automatically** - the pipeline will simply never start during the window. > > Add a `schedule:` (GitHub Actions) / `schedules:` (Azure DevOps) block to `apply-updates.yml` that fires at (or a few minutes before) the start of every `UpdateStartWindow` you have tagged. One cron entry per distinct window value. Worked examples and the per-cluster scheduling model are in [section 8](../README.md#8-scheduling-maintenance-windows-and-change-freeze-periods). > -> Pipeline summaries (Step 7 GH Actions + Azure DevOps) classify per-cluster skips as `ScheduleBlocked` (outside `UpdateStartWindow` or inside `UpdateExclusionsWindow`), `SideloadedBlocked` (`UpdateSideloaded=False` on a sideloaded-workflow cluster) and, new in v0.7.90, `ExcludedByTag` (`UpdateExcluded=True` operator hard override). The Actions Required callout points operators at the `UpdateExcluded` tag when one or more clusters are intentionally held out of automation. +> Pipeline summaries (Fleet: 04 GH Actions + Azure DevOps) classify per-cluster skips as `ScheduleBlocked` (outside `UpdateStartWindow` or inside `UpdateExclusionsWindow`), `SideloadedBlocked` (`UpdateSideloaded=False` on a sideloaded-workflow cluster) and, new in v0.7.90, `ExcludedByTag` (`UpdateExcluded=True` operator hard override). The Actions Required callout points operators at the `UpdateExcluded` tag when one or more clusters are intentionally held out of automation. ## Step 8 - Monitor In-Flight Updates @@ -199,7 +215,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `scope` (`all-clusters` (default) or `by-update-ring`), `update_ring` (only used when `scope=by-update-ring`; single ring, `Prod;Ring2` semicolon-list, or `***` wildcard), `long_running_threshold_hours` (0-48, default 6), `module_version` (optional). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). A commented-out `schedule: cron '*/30 * * * *'` (GH) / `schedules: cron '*/30 * * * *'` (ADO) sample is shipped inside the `# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers` block - uncomment it for the duration of an active wave, then re-comment it once the wave drains, so you do not generate 48 empty runs per day between waves. | | **Cmdlets invoked** | `Get-AzLocalUpdateRuns`, `Get-AzLocalClusterInventory` (used for ring-scope mode). | -| **Depends on** | Step 7 must be active (at least one `InProgress` run) for the output to be non-empty. Runs cleanly on an empty fleet (reports "no in-flight runs"). | +| **Depends on** | Fleet: 04 must be active (at least one `InProgress` run) for the output to be non-empty. Runs cleanly on an empty fleet (reports "no in-flight runs"). | | **Artefacts** | `update-monitor.xml` (JUnit, one `` per in-flight cluster - the failure message contains current step + progress + elapsed duration when the run is over `long_running_threshold_hours`, or the deepest-step error detail when `failureType='StepError'` fires under v0.7.96), `update-monitor.csv` (full per-cluster rows including the new `Status` + `ErrorMessage` columns), markdown job summary with the in-flight table (cluster + update names as portal links from v0.7.96) and long-running flag column. | | **Exit conditions** | Pipeline run is green when the snapshot completes. Long-running runs (over `long_running_threshold_hours`) AND stuck-step errors (`failureType='StepError'`, v0.7.96+) surface as JUnit `` entries so they appear in the Tests / Checks tab without failing the whole pipeline. v0.7.96 also adds two new `GITHUB_OUTPUT` values: `STEP_ERRORED` (count of runs that hit a step error inside the threshold) and `UNRESOLVED_FAILURES` (count of Failed runs surfaced from `Get-AzLocalUpdateRunFailures`). | | **When to run** | While a wave is active (manually, or via the commented cron). The two daily snapshot pipelines (Step 9 and Step 10) remain the steady-state daily reports; this pipeline is purpose-built for the "is the apply-updates run I started two hours ago still progressing?" question that those daily snapshots cannot answer between runs. | @@ -207,7 +223,9 @@ The table below is the ground truth for what each shipped YAML does **out of the | **ITSM** | Not supported - in-flight monitor exists to be a fast feedback loop during an active wave; long-running runs surface as JUnit `` entries for the operator on call, and post-wave failures are picked up + ticketed by the daily Step 9 snapshot. | | **Introduced** | v0.7.90. Enhanced in v0.7.96 with the `Status` + `ErrorMessage` columns, the new `StepError` JUnit failure type, the always-shown Failed-runs block, portal-linked Cluster Name / Update Name cells, and the new `STEP_ERRORED` / `UNRESOLVED_FAILURES` `GITHUB_OUTPUT` values. **Behaviour change in v0.7.98:** UX overhaul of the in-flight table - rows now sort by composite `SeverityScore` (`StepError severity x 1000 + RunSeverity x 100 + elapsed-hours bucket`) so stuck step errors and runs over 14 days bubble to the top regardless of cluster alphabetical order; each row carries per-cell `StateIcon` + `StatusIcon` icons and a horizontal chip stack of flags (`STEP-STUCK`, `RUN-STUCK`, `UNRESOLVED`, `RECENT-FAIL`); the job summary opens with a single `CRITICAL / WARN / OK` fleet status badge that collapses the worst row across the fleet into one line (e.g. `CRITICAL - 1 stuck step error(s), 1 run(s) > 14d, 1 step(s) > 4h`); each failed step's `errorMessage` is now wrapped in a collapsible `
Verbose error...
` block; and JUnit `` + `` are populated with real per-run elapsed seconds (was `time="0"` before, which made GitHub Test Reporter render `"5 tests were completed in 0ms"`). | -## Step 9 - Fleet Update Status +--- + +## Fleet: 06 - Fleet Update Status > **Behaviour change in v0.8.81**: the GitHub-Markdown shortcodes used by the Critical Health Status table, the Primary Status table, and the Version Distribution Support column are now routed through the shared `Get-AzLocalStatusIconMap` private helper using the new `Info` / `GreenCircle` / `YellowCircle` / `CycleArrows` / `GreyQuestion` / `SupportSupported` / `SupportUnsupported` / `SupportUnknown` keys (fixes literal `:green_circle:` / `:yellow_circle:` / `:information_source:` / etc. text on Azure DevOps step summaries). JUnit XML output and CSV columns are unchanged. @@ -217,15 +235,17 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | Scope (`-AllClusters` or `-ScopeByUpdateRingTag`), `throttle_limit` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 06:00 UTC (`cron '0 6 * * *'`). Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Get-AzLocalUpdateSummary`, `Get-AzLocalAvailableUpdates`, `Get-AzLocalUpdateRuns`, `Get-AzLocalUpdateRunFailures`, `Get-AzLocalLatestSolutionVersion`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Best-quality output when Step 4 (connectivity) has run earlier the same morning, since clusters Arc can't see won't appear in the readiness or update-summary results. | +| **Depends on** | None directly. Best-quality output when Fleet: 02 (connectivity) has run earlier the same morning, since clusters Arc can't see won't appear in the readiness or update-summary results. | | **Artefacts** | `readiness-status.xml` / `.csv` / `.json` (the `.json` carries the rolled-up bucket counts under `Summary.*` - `UpdateFailures`, `ActionRequired`, `HealthFailures`, `SbeBlocked`, `InProgress`, `ReadyForUpdate`, `UpToDate`, `Other` - and the new `primaryActionRequired` field mirrors the JUnit testsuite attribute from v0.7.96 for downstream automation), `cluster-inventory.csv`, `update-summaries.csv`, `available-updates.csv`, `update-runs.csv`. | | **When to run** | Hands-off scheduled. Trigger manually for ad-hoc reporting. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | | **Exit conditions** | Pipeline run is green when the snapshot completes. Per-cluster issues (outdated version, failed run history) surface as JUnit `` entries. v0.7.96 distinguishes `failureType='UpdateFailure'` (run executed and failed - landed in `NeedsAttention`) from `failureType='PreparationFailed'` (run never started - landed in the new `Action Required` bucket) so downstream automation can split paging behaviour per failure mode. | | **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per unresolved failed update run picked up by the daily snapshot; dedupe key combines cluster + update version + failure reason via `./.itsm/azurelocal-itsm.yml`. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | -| **Introduced** | Originally Step 7 in v0.7.4; renumbered to Step 8 in v0.7.90 when the in-flight monitor moved into Step 7; renumbered to Step 9 in v0.8.7 when the on-prem sideload pipeline took Step 6. v0.7.90 added the YYMM-pivoted Version Distribution table. v0.7.96 promoted `NeedsAttention` into the `Update Failed` bucket, added the new `Action Required` bucket for `PreparationFailed`, folded `PreparationInProgress` into `Update In Progress`, added the `primaryActionRequired` JUnit attribute, the `ACTION_REQUIRED` `GITHUB_OUTPUT`, `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and portal-linked Cluster Name / Update Name cells in the `Update Run History and Error Details` markdown table. **Behaviour change in v0.7.98:** each `` inside the `📜 Update Run History and Error Details` testsuite now emits `time` = `DurationMinutes * 60` (`DurationMinutes` from KQL `datetime_diff('minute', EndTime, StartTime)` via `Get-AzLocalUpdateRunFailures`), and the testsuite-level `time=` is the sum of those per-row seconds. The other two testsuites (`FleetVersionDistribution`, `AzureLocalFleetUpdateStatus`) intentionally stay at `time="0"` - they are instantaneous snapshot projections and a synthetic duration would be misleading. | +| **Introduced** | Originally Fleet: 04 in v0.7.4 (then Step 7); renumbered to Step 8 in v0.7.90 when the in-flight monitor moved into Step 7; renumbered to Step 9 in v0.8.7 when the on-prem sideload pipeline took Step 6. v0.7.90 added the YYMM-pivoted Version Distribution table. v0.7.96 promoted `NeedsAttention` into the `Update Failed` bucket, added the new `Action Required` bucket for `PreparationFailed`, folded `PreparationInProgress` into `Update In Progress`, added the `primaryActionRequired` JUnit attribute, the `ACTION_REQUIRED` `GITHUB_OUTPUT`, `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and portal-linked Cluster Name / Update Name cells in the `Update Run History and Error Details` markdown table. **Behaviour change in v0.7.98:** each `` inside the `📜 Update Run History and Error Details` testsuite now emits `time` = `DurationMinutes * 60` (`DurationMinutes` from KQL `datetime_diff('minute', EndTime, StartTime)` via `Get-AzLocalUpdateRunFailures`), and the testsuite-level `time=` is the sum of those per-row seconds. The other two testsuites (`FleetVersionDistribution`, `AzureLocalFleetUpdateStatus`) intentionally stay at `time="0"` - they are instantaneous snapshot projections and a synthetic duration would be misleading. | + +--- -## Step 10 - Fleet Health Status +## Fleet: 07 - Fleet Health Status > **Behaviour change in v0.8.81**: fixes the **"Healthy + Unhealthy != Total" KPI counting bug**. The previous KPI table summed only two buckets (Healthy + Unhealthy) and silently dropped clusters whose `HealthStatus` was `In progress` / `Unknown` / `Health check failed` AND had no failure rows. The KPI block is now split into two tables: **Cluster Counts** (Total / Healthy / Unhealthy / **Other**, where Other is computed as `Total - Healthy - Unhealthy` clamped non-negative so the three buckets always sum to Total) and **Failing Checks Breakdown** (Total / Critical / Warning / Distinct Reasons). New `other_clusters` pipeline step output; `-PassThru` PSCustomObject gains `OtherClusters`. **Detailed Results columns reordered** to put the most-specific identifier first: `Severity | Title | Failure Reason | Description | Health Check Name | Failure Remediation | Target Resource Name | Target Resource Type | Last Occurrence | Resource Group`. Description is a collapsible `
` block that surfaces drive/volume-level detail (e.g. the file path emitted by a `Microsoft.Health.FaultType.Volume.FileSystem.Corruption.Correctable` warning); Health Check Name is the raw fault-type name (preserved verbatim so operators can grep / triage by FaultType across runs). Icons in the table are wired through the new shared `Get-AzLocalStatusIconMap` private helper. @@ -235,7 +255,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `severity` (optional - `Critical`, `Warning`, or `All`; default `All`), `update_ring_tag` (optional - narrow to one wave), `throttle_limit` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 07:00 UTC (`cron '0 7 * * *'`). Deliberately offset by one hour from `fleet-update-status` (06:00 UTC) to avoid agent and ARM contention. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalFleetHealthFailures`, `Get-AzLocalFleetHealthOverview`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Best-quality output when Step 4 (connectivity) has run earlier the same morning. | +| **Depends on** | None directly. Best-quality output when Fleet: 02 (connectivity) has run earlier the same morning. | | **Artefacts** | `fleet-health-status.xml` (JUnit, one `` per failing check, grouped under `Critical Health Failures` / `Warning Health Failures` testsuites), `fleet-health-detail.csv` (one row per failing check), `fleet-health-summary.csv` (aggregated by `FailureReason` + `Severity`), markdown job summary. | | **When to run** | Hands-off scheduled. Trigger manually for ad-hoc fleet-wide health triage outside the daily schedule, especially after operational events (capacity changes, network maintenance, certificate rotations) where health-check failures are expected to spike. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | From 4e21ffcc9d8926d5e430af327922197f98f27f63 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 11:36:24 +0100 Subject: [PATCH 08/17] chore: update test run timings --- AzLocal.UpdateManagement/Tests/test-run-timings.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 7d900c08..aca0f887 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -34,3 +34,4 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T10:07:33Z","0.8.85","1295","1256","0","1","126.88","123.98","5.19","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T10:09:11Z","0.8.85","1295","35","3","0","545.67","544.05","2.5","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T10:13:27Z","0.8.85","1295","1256","0","1","49.91","48.62","3.18","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T10:25:03Z","0.8.85","1295","38","0","0","617.07","615.99","3.31","Invoke-Tests.ps1","5.1.26100.8730" From 286d10bfb332aa801cb0689bebfed66db53036f2 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 12:11:41 +0100 Subject: [PATCH 09/17] v0.8.85: address PR #91 review findings - manifest, docs, mojibake, prune tests - Get-AzLocalPipelineManifest: collapse legacy authentication-test + inventory-clusters rows into the merged setup-validate-and-inventory entry - appendix-pipelines.md / README.md: replace removed filenames + old Step naming with Setup:/Fleet: numbering; fix section 13 file-layout block and section 14 reference; repair broken cross-doc anchors (README 8.1.1 -> #fleet-01, sideload.md -> #fleet-03); update present-tense Step.7 reference - appendix-pipelines.md: reconcile Setup: 01 trigger docs to the shipped weekly Sunday 08:00 UTC cron - fleet-update-status.yml: fix mojibake in YAML comment - apply-updates-schedule.example.yml: replace Step.5/Step.7 with stable apply-updates.yml / Fleet: 04 references - Tests: add 10 -PruneDeprecated regression tests (6 Copy- + 4 Update-) covering both-removed, single-removed, ID-mismatch-preserved, omitted-flag and -WhatIf paths --- .../Automation-Pipeline-Examples/README.md | 40 ++--- .../apply-updates-schedule.example.yml | 23 +-- .../docs/appendix-pipelines.md | 49 ++---- .../docs/sideload.md | 2 +- .../github-actions/fleet-update-status.yml | 2 +- .../Private/Get-AzLocalPipelineManifest.ps1 | 14 +- .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 140 ++++++++++++++++++ .../Tests/test-run-timings.csv | 1 + 8 files changed, 202 insertions(+), 69 deletions(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index cc54771d..a7934f0c 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -1054,11 +1054,11 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp 1. **Run authentication validation first (strongly recommended).** Before importing the remaining pipelines, validate that the service connection (Workload Identity Federation), App Registration, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running the authentication validation pipeline. This narrows any failure to one small YAML file instead of debugging multiple interacting pipelines simultaneously. - The validation pipeline ships with the module at [`azure-devops/authentication-test.yml`](./azure-devops/authentication-test.yml). v0.7.70 emits a **JUnit XML** report (Authentication / Subscription Scope / Resource Graph Reachability) published via `PublishTestResults@2` and rendered in the run's **Tests** tab, a **markdown summary** with the subscription count + subscription detail table uploaded to the run's **Summary** tab via `##vso[task.uploadsummary]`, and a `auth-report` pipeline artifact (XML + `subscriptions.json` + `subscriptions.csv`) for ITSM / dashboard ingest. + The validation pipeline ships with the module at [`azure-devops/setup-validate-and-inventory.yml`](./azure-devops/setup-validate-and-inventory.yml). v0.8.85 merged the former `authentication-test.yml` + `inventory-clusters.yml` into this single workflow. It emits a **JUnit XML** report (Authentication / Subscription Scope / Resource Graph Reachability / Cluster Inventory) published via `PublishTestResults@2` and rendered in the run's **Tests** tab, a **markdown summary** with the subscription count + subscription detail table uploaded to the run's **Summary** tab via `##vso[task.uploadsummary]`, and a `auth-report` pipeline artifact (XML + `subscriptions.json` + `subscriptions.csv` + `cluster-inventory.csv`) for ITSM / dashboard ingest. If you used the `Copy-AzLocalPipelineExample` shortcut above, the file is already in your chosen pipelines folder alongside the other eight pipeline YAMLs - those YAMLs sit dormant until you import each one as a pipeline, so they're harmless at rest. Otherwise, copy just that one file into your repo. Either way, import it as a new pipeline: - - **Pipelines -> New pipeline -> Azure Repos Git -> your repo -> Existing Azure Pipelines YAML file -> `/azure-devops/authentication-test.yml`**. + - **Pipelines -> New pipeline -> Azure Repos Git -> your repo -> Existing Azure Pipelines YAML file -> `/azure-devops/setup-validate-and-inventory.yml`**. - **Save and run**. If your service connection has a name other than `AzureLocal-ServiceConnection`, edit `azureSubscription:` in the YAML first (the only configurable line). Unlike GitHub Actions, ADO does **not** have environment-scoped federated credentials at the auth layer - the service connection itself is the federation, so a single pipeline run validates everything. ADO **Environments** (Pipelines -> Environments) are approval gates only, layered on top of the service connection, and are not exercised by this validation pipeline. @@ -1192,7 +1192,7 @@ This is the canonical "nothing wired -> staged rollout working" sequence. Follow ```text +-----------------------------------------------------------------------+ | PHASE 1: INVENTORY | -| 6.1 inventory-clusters.yml -> cluster-inventory.csv | +| 6.1 setup-validate-and-inventory.yml -> cluster-inventory.csv | +-----------------------------------------------------------------------+ v +-----------------------------------------------------------------------+ @@ -1239,7 +1239,7 @@ Every pipeline emits one or more artifacts (CSV / Markdown / JUnit XML / HTML). ```text +-------------------------------+ - | inventory-clusters.yml | + | setup-validate-and-inventory | | (read-only ARG) | +-------------------------------+ | @@ -1501,7 +1501,7 @@ It calls the new [`Get-AzLocalFleetHealthFailures`](../README.md#get-azlocalflee Configure your CI/CD platform's alerting on the JUnit failures - GitHub Actions surfaces them in the run summary and Azure DevOps shows them in the Tests tab with trend analytics. -**Plus weekly: `apply-updates-schedule-audit.yml`** (read-only, runs Mondays at 05:00 UTC by default) catches drift between the cron schedule(s) committed to `apply-updates.yml` and the `UpdateRing` / `UpdateStartWindow` tags that operators apply to new clusters. It emits a JUnit + CSV + Markdown "Recommend" snippet that pastes straight back into Step.7 to close any coverage gap. **For the full audit runbook (tag a cluster -> see drift -> paste recommended cron -> re-run and watch it turn green), see [section 8.3](#83-end-to-end-runbook-apply-updates-schedule-coverage-audit).** +**Plus weekly: `apply-updates-schedule-audit.yml`** (read-only, runs Mondays at 05:00 UTC by default) catches drift between the cron schedule(s) committed to `apply-updates.yml` and the `UpdateRing` / `UpdateStartWindow` tags that operators apply to new clusters. It emits a JUnit + CSV + Markdown "Recommend" snippet that pastes straight back into `apply-updates.yml` to close any coverage gap. **For the full audit runbook (tag a cluster -> see drift -> paste recommended cron -> re-run and watch it turn green), see [section 8.3](#83-end-to-end-runbook-apply-updates-schedule-coverage-audit).** --- @@ -1619,7 +1619,7 @@ Copy this file once per ring (e.g. `assess-update-readiness-Pilot.yml`, `assess- **Why one YAML per ring**: GitHub Actions `schedule:`-triggered runs cannot supply `inputs:` values - they always use the workflow's default `inputs:`. So a single `assess-update-readiness*.yml` with three crons in one `schedule:` block (and `update_ring` required, no default) would never actually run on cron - every cron tick would fail input validation. Splitting one YAML per ring (each with its own default + single cron) is the cleanest fix. Azure DevOps has the same constraint - `schedules:`-triggered runs use the YAML's default `parameters:` values, so the same per-ring split pattern applies. -**Known gap**: the Step.3 schedule-coverage audit (`apply-updates-schedule-audit.yml`) currently validates Step.7 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Step.5 cron is correctly anchored ahead of a Step.7 cron. **Always pair Step.5 + Step.7 cron edits in the same PR** so the lead-time relationship is reviewable by a human at merge time. The per-pipeline appendix entry for [Step 5 - Assess Update Readiness](docs/appendix-pipelines.md#step-5---assess-update-readiness) repeats this guidance with the same lead-time table. +**Known gap**: the Step.3 schedule-coverage audit (`apply-updates-schedule-audit.yml`) currently validates Step.7 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Step.5 cron is correctly anchored ahead of a Step.7 cron. **Always pair Step.5 + Step.7 cron edits in the same PR** so the lead-time relationship is reviewable by a human at merge time. The per-pipeline appendix entry for [Fleet: 01 - Assess Update Readiness](docs/appendix-pipelines.md#fleet-01---assess-update-readiness) repeats this guidance with the same lead-time table. **Always-green caveat**: `assess-update-readiness.yml` never goes red at the pipeline level - per-cluster readiness gaps surface as JUnit `` entries in the Tests / Checks tab via `readiness.xml`. A silently-empty `readiness.xml` (e.g. an `update_ring` typo with zero clusters in scope) **will not generate a red-build email**. Either check the Tests tab after each scheduled run, or wire the JUnit reporter into the CI status surface you already monitor. @@ -2018,23 +2018,23 @@ Automation-Pipeline-Examples/ templates/ incident-body.md # - Mustache-style ticket body template. github-actions/ - authentication-test.yml # 0. Authentication validation + subscription scope report (manual; v0.7.70). - inventory-clusters.yml # 1. Inventory (weekly Mon 06:00 UTC + manual). - manage-updatering-tags.yml # 2. Apply UpdateRing / UpdateStartWindow / UpdateExclusionsWindow / UpdateExcluded tags (manual). - apply-updates-schedule-audit.yml # 3. Weekly read-only audit: UpdateStartWindow tags vs apply-updates cron (Mon 05:00 UTC, v0.7.65). - fleet-connectivity-status.yml # 4. Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot + node-coverage reconciliation (daily 05:30 UTC, v0.7.79+; reconciliation enhanced in v0.7.85). - assess-update-readiness.yml # 5. Pre-flight readiness report (manual; v0.7.0). - apply-updates.yml # 6. Apply updates to one UpdateRing (with optional ITSM step, v0.7.4). - monitor-updates.yml # 7. In-flight update monitor: per-cluster current step + elapsed duration; flags long-running runs (manual, optional */30 min cron; v0.7.90). - fleet-update-status.yml # 8. Scheduled fleet update-status snapshot (daily 06:00 UTC; formerly Step.8). - fleet-health-status.yml # 9. Scheduled fleet 24-hour health-check failure report (daily 07:00 UTC, v0.7.65; formerly Step.9). + setup-validate-and-inventory.yml # Setup: 01. Auth + subscription-scope validation and cluster inventory (merged auth+inventory in v0.8.85; manual + weekly Sun 08:00 UTC). + manage-updatering-tags.yml # Setup: 02. Apply UpdateRing / UpdateStartWindow / UpdateExclusionsWindow / UpdateExcluded tags (manual). + apply-updates-schedule-audit.yml # Setup: 03. Weekly read-only audit: UpdateStartWindow tags vs apply-updates cron (Mon 05:00 UTC, v0.7.65). + assess-update-readiness.yml # Fleet: 01. Pre-flight readiness report (manual; v0.7.0). + fleet-connectivity-status.yml # Fleet: 02. Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot + node-coverage reconciliation (daily 05:30 UTC, v0.7.79+; reconciliation enhanced in v0.7.85). + sideload-updates.yml # Fleet: 03. Opt-in self-hosted-runner workflow: Robocopy + WinRM sideload of solution-update media to clusters gated on UpdateSideloaded (manual; v0.8.7). + apply-updates.yml # Fleet: 04. Apply updates to one UpdateRing (with optional ITSM step, v0.7.4). + monitor-updates.yml # Fleet: 05. In-flight update monitor: per-cluster current step + elapsed duration; flags long-running runs (manual, optional cron; v0.7.90). + fleet-update-status.yml # Fleet: 06. Scheduled fleet update-status snapshot (daily 06:00 UTC). + fleet-health-status.yml # Fleet: 07. Scheduled fleet 24-hour health-check failure report (daily 07:00 UTC, v0.7.65). azure-devops/ - authentication-test.yml - inventory-clusters.yml + setup-validate-and-inventory.yml manage-updatering-tags.yml apply-updates-schedule-audit.yml - fleet-connectivity-status.yml assess-update-readiness.yml + fleet-connectivity-status.yml + sideload-updates.yml apply-updates.yml monitor-updates.yml fleet-update-status.yml @@ -2045,7 +2045,7 @@ Automation-Pipeline-Examples/ ## 14. Pipeline reference -Moved to [docs/appendix-pipelines.md](docs/appendix-pipelines.md) - one section per pipeline (`Step 0 - ...` ... `Step 10 - ...`) mapping 1:1 to the bundled `*.yml` workflows, with purpose, inputs, trigger, cmdlets invoked, dependencies, artefacts, RBAC, and exit conditions for each. Kept out-of-line to keep this README focused on the runbook. +Moved to [docs/appendix-pipelines.md](docs/appendix-pipelines.md) - one section per pipeline (`Setup: 01 - ...` ... `Fleet: 07 - ...`) mapping 1:1 to the bundled `*.yml` workflows, with purpose, inputs, trigger, cmdlets invoked, dependencies, artefacts, RBAC, and exit conditions for each. Kept out-of-line to keep this README focused on the runbook. ## Appendix B: Release history diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml index 1daf7859..80095dc3 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml @@ -19,17 +19,18 @@ # # 1. This file (day-grain): the calendar week / day of week says # WHICH UpdateRing tag values are eligible TODAY in UTC. -# 2. The Step.5 cron schedule (intra-day-grain): says HOW OFTEN the -# apply-updates job wakes up (e.g. hourly). +# 2. The apply-updates.yml cron schedule (intra-day-grain): says HOW +# OFTEN the apply-updates job wakes up (e.g. hourly). # 3. Per-cluster `UpdateStartWindow` tag (minute-grain): says WHEN, during # an eligible day, the actual update is allowed to start. # # A cron firing that lands on a day with NO matching schedule rows is # logged and exits 0 - no errors, no failures. # -# This file is REQUIRED for the "Step.5 Apply Updates" pipeline when -# triggered by a schedule (not workflow_dispatch). If the file is -# missing, Step.5 throws with the exact remediation command. +# This file is REQUIRED for the "Fleet: 04 - Apply Updates" +# (apply-updates.yml) pipeline when triggered by a schedule (not +# workflow_dispatch). If the file is missing, apply-updates.yml throws +# with the exact remediation command. # # Generate a starter file from your current fleet via: # New-AzLocalApplyUpdatesScheduleConfig -OutputPath .\config\apply-updates-schedule.yml @@ -59,14 +60,14 @@ cycleAnchorYear: 2026 # ---- AllowedUpdateVersions (schema v2, MANDATORY top-level) ------- # >>> ALLOWED-UPDATE-VERSIONS-V2 <<< -# Fleet-wide allow-list of Azure Local update identifiers that Step.7 -# (apply-updates) is permitted to install. +# Fleet-wide allow-list of Azure Local update identifiers that +# apply-updates.yml is permitted to install. # # Default 'Latest' (case-insensitive) is a reserved sentinel meaning # 'no constraint - install the latest Ready update on each cluster' # (the historic v0.7.88 default). Leave it as 'Latest' to keep that -# behaviour. When set to an explicit list, Step.7 only installs -# updates whose `name` OR `properties.version` is an EXACT +# behaviour. When set to an explicit list, apply-updates.yml only +# installs updates whose `name` OR `properties.version` is an EXACT # (case-insensitive) match for one of the entries. If a cluster has # no Ready update that matches, that cluster is SKIPPED with status # 'NotInAllowList' (strict no-op; never falls back to 'latest'). @@ -98,8 +99,8 @@ cycleAnchorYear: 2026 # customer ask in v0.7.89): install ONLY the YY04 + YY10 feature # updates plus the cumulative updates immediately preceding each # feature update (YY03 + YY09). With v2 schema you list those four -# update names here once and Step.7 ignores every other 'Ready' -# update. Example using real Azure Local update names: +# update names here once and apply-updates.yml ignores every other +# 'Ready' update. Example using real Azure Local update names: # allowedUpdateVersions: 'Solution12.2603.1002.15;Solution12.2604.1003.1005;Solution12.2609.1002.XX;Solution12.2610.1003.XX' # # Mixed Solution + OEM SBE example - allow the YY04 cumulative AND a diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md index 691a9bf6..f10e75d8 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md @@ -12,7 +12,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | Pipeline | GitHub Actions trigger | Azure DevOps trigger | Notes | |---|---|---|---| -| **Setup: 01** - Validate Auth and Inventory Clusters | `workflow_dispatch` only (merged auth + inventory) | `trigger: none` (manual only) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Recommended monthly cadence; add your own `schedule:` block if you want it automated. | +| **Setup: 01** - Validate Auth and Inventory Clusters | `workflow_dispatch` + weekly `schedule: cron '0 8 * * 0'` (Sun 08:00 UTC) | `trigger: none` + weekly `schedules: cron '0 8 * * 0'` (Sun 08:00 UTC) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Ships a weekly Sunday 08:00 UTC re-validation + inventory refresh; edit or remove the cron in the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to change the cadence. | | **Setup: 02** - Manage UpdateRing Tags | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | | **Setup: 03** - Apply-Updates Schedule Coverage Audit | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | | **Fleet: 01** - Assess Update Readiness | `workflow_dispatch` only | `trigger: none` (manual only) | **Recommended customisation** - schedule per ring 12-72h before the matching **Fleet: 04** `UpdateStartWindow` (see [Fleet: 01 - Assess Update Readiness](#fleet-01---assess-update-readiness) below for the per-ring pattern + lead-time table). Always-green at pipeline level; per-cluster readiness gaps surface as JUnit `` entries. | @@ -27,7 +27,7 @@ The table below is the ground truth for what each shipped YAML does **out of the > > **GitHub Actions only**: scheduled workflows are **automatically disabled after 60 days of repository inactivity** (no commits, PRs, or issue activity). Re-enable them via the Actions UI or run any push. Azure DevOps schedules do not have this auto-disable behaviour. -> **Common conventions for every row below**: `Cmdlets invoked` lists only AzLocal.UpdateManagement cmdlets the YAML calls directly (helper cmdlets they themselves call transitively are documented in each cmdlet's section of the module-level [`README.md`](../../README.md)). `Depends on` lists upstream pipelines whose output is consumed or whose work must be in place before this pipeline runs - `None` means the pipeline is self-contained. `Exit conditions` describes what a non-success YAML run state means in practice; per-cluster issues are always surfaced as JUnit `` entries rather than failing the whole pipeline unless noted otherwise. `ITSM` indicates whether the bundled YAMLs ship with the opt-in ServiceNow auto-raise connector (`Get-AzLocalItsmConfig` + per-failure ticket creation gated on the `raise_itsm_ticket` / `raiseItsmTicket` input); only Step 4, Step 7, Step 9, and Step 10 ship it - the matrix config lives at `./.itsm/azurelocal-itsm.yml` (see [`Automation-Pipeline-Examples/ITSM/`](../ITSM/) for the full recipe). +> **Common conventions for every row below**: `Cmdlets invoked` lists only AzLocal.UpdateManagement cmdlets the YAML calls directly (helper cmdlets they themselves call transitively are documented in each cmdlet's section of the module-level [`README.md`](../../README.md)). `Depends on` lists upstream pipelines whose output is consumed or whose work must be in place before this pipeline runs - `None` means the pipeline is self-contained. `Exit conditions` describes what a non-success YAML run state means in practice; per-cluster issues are always surfaced as JUnit `` entries rather than failing the whole pipeline unless noted otherwise. `ITSM` indicates whether the bundled YAMLs ship with the opt-in ServiceNow auto-raise connector (`Get-AzLocalItsmConfig` + per-failure ticket creation gated on the `raise_itsm_ticket` / `raiseItsmTicket` input); only Fleet: 02, Fleet: 04, Fleet: 06, and Fleet: 07 ship it - the matrix config lives at `./.itsm/azurelocal-itsm.yml` (see [`Automation-Pipeline-Examples/ITSM/`](../ITSM/) for the full recipe). --- @@ -37,7 +37,7 @@ The table below is the ground truth for what each shipped YAML does **out of the |---|---| | **Purpose** | End-to-end probe of the federated identity, the Azure RBAC role assignment, and the subscriptions the pipeline identity can read. Also enumerates every Azure Local cluster the identity can see. Emits a JUnit-rendered Authentication / Subscription Scope / Resource Graph Reachability / Cluster Inventory report and exports the full cluster list (`cluster-inventory.csv` + JSON) with UpdateRing tag status so you can detect silent scope drift (a new tenant SP suddenly seeing more or fewer subscriptions or clusters than yesterday) before downstream fleet reports under- or over-count. | | **Inputs** | `environment` (optional - GitHub Actions only; leave blank to test the branch-scoped federated credential), `module_version` (optional). | -| **Trigger** | Manual only (`workflow_dispatch` / **Run pipeline** button). No schedule shipped - run it on initial wiring, after every RBAC / federated-credential / subscription change, and on a recommended monthly cadence. Add your own `schedule:` / `schedules:` block if you want it automated. | +| **Trigger** | `workflow_dispatch` / **Run pipeline** button, **plus a shipped weekly cron** - GitHub `schedule: cron '0 8 * * 0'` / Azure DevOps `schedules: cron '0 8 * * 0'` (every Sunday at 08:00 UTC), inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block. Also run it manually on initial wiring and after every RBAC / federated-credential / subscription change. Edit the cron - or delete it from the customize block - to change the cadence. | | **Cmdlets invoked** | None (uses the `az` CLI directly for the auth / Resource Graph probes). | | **Depends on** | None. This is the first pipeline that must run on a freshly-wired identity. | | **Artefacts** | `auth-report.xml` (JUnit, one `` per probe), `subscriptions.json`, `subscriptions.csv`, markdown step / run summary with the subscription detail table. | @@ -53,29 +53,14 @@ The table below is the ground truth for what each shipped YAML does **out of the ## Setup: 02 - Manage UpdateRing Tags -| Aspect | Value | -|---|---| -| **Purpose** | Enumerate every Azure Local cluster the identity can see and export to CSV. Establishes the baseline cluster list and current tag values for every downstream pipeline. | -| **Inputs** | None. | -| **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** weekly scheduled run on Mondays at 06:00 UTC (`cron '0 6 * * 1'`). Edit the cron in the YAML (inside the `# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers` block) to change the day / time. | -| **Cmdlets invoked** | `Get-AzLocalClusterInventory`. | -| **Depends on** | Step 0 should be green at least once before relying on this output (otherwise the cluster list may be silently scoped to the wrong subscription set). | -| **Artefacts** | `cluster-inventory.csv` (one row per cluster, includes current `UpdateRing` / `UpdateStartWindow` / `UpdateExclusionsWindow` / `UpdateExcluded` and sideloaded-workflow tags). | -| **When to run** | First run of a new estate; periodically (default weekly) to detect new clusters or tag drift. The CSV is the input artefact for Step 2 (UpdateRing tag bulk-apply). | -| **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role (`Microsoft.AzureStackHCI/clusters/read`, `Microsoft.ResourceGraph/resources/read`, `Microsoft.Resources/tags/read`). | -| **Exit conditions** | Pipeline run is green when the inventory query succeeds and at least one cluster is returned. An empty inventory on a non-empty estate is a red flag for scope drift - re-run Step 0. | -| **ITSM** | Not supported - inventory snapshot has no per-cluster failure surface to ticket on. | - -## Step 2 - Manage UpdateRing Tags - | Aspect | Value | |---|---| | **Purpose** | Bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, and `UpdateExcluded` tags from a CSV. Default-stamps `UpdateExcluded=False` on any cluster that does not already carry the tag (v0.7.90) so the operator hard-override is always discoverable in the Azure portal. | | **Inputs** | `csv_path` (required). | | **Trigger** | Manual only (`workflow_dispatch` / **Run pipeline** button). No schedule shipped - this is a deliberate change-controlled operation that should follow a CSV edit + review. Add a `schedule:` / `schedules:` block if your CSV is auto-generated and you want periodic re-application. | | **Cmdlets invoked** | `Set-AzLocalClusterUpdateRingTag`. | -| **Depends on** | Step 1 produces the input CSV (the operator typically downloads `cluster-inventory.csv`, edits ring / window / exclusion values in a PR, then re-uploads it as the input to this pipeline). | -| **Artefacts** | `UpdateRingTag_YYYYMMDD_HHmmss.csv` (per-cluster CSV log: ClusterName, ResourceGroup, SubscriptionId, ResourceId, Action, PreviousTagValue, NewTagValue, Status, Message) and `UpdateRingTag_Results.json` (the same rows in JSON form, written from `-PassThru`) inside the published log artifact. Since v0.8.0 the pipeline job summary tab also renders a **result breakdown table** (Created / Updated / Already in sync / Skipped / WhatIf / Failed) plus a collapsible **per-cluster details** block, at parity with Step 3 / Step 5 / Step 7 / Step 9. | +| **Depends on** | Setup: 01 produces the input CSV (the operator typically downloads `cluster-inventory.csv`, edits ring / window / exclusion values in a PR, then re-uploads it as the input to this pipeline). | +| **Artefacts** | `UpdateRingTag_YYYYMMDD_HHmmss.csv` (per-cluster CSV log: ClusterName, ResourceGroup, SubscriptionId, ResourceId, Action, PreviousTagValue, NewTagValue, Status, Message) and `UpdateRingTag_Results.json` (the same rows in JSON form, written from `-PassThru`) inside the published log artifact. Since v0.8.0 the pipeline job summary tab also renders a **result breakdown table** (Created / Updated / Already in sync / Skipped / WhatIf / Failed) plus a collapsible **per-cluster details** block, at parity with Setup: 03 / Fleet: 01 / Fleet: 04 / Fleet: 06. | | **When to run** | After editing the inventory CSV; whenever ring membership or maintenance windows change. | | **RBAC** | Write to tags only. The built-in **Tag Contributor** role on the cluster scope is sufficient (`Microsoft.Resources/tags/*`). Since v0.7.65, `Set-AzLocalClusterUpdateRingTag` writes via `Microsoft.Resources/tags/default` PATCH, so the broader `Microsoft.AzureStackHCI/clusters/write` is not required. | | **Exit conditions** | Pipeline run is green when every CSV row is processed (added / updated / unchanged). Per-cluster tag-write failures surface in the run log; the pipeline does not currently fail on per-row errors. Re-run after triaging is safe (the cmdlet is idempotent). | @@ -123,7 +108,7 @@ The table below is the ground truth for what each shipped YAML does **out of the > **Behaviour change in v0.8.81**: status icons in the Summary counts, Not-Ready and All-clusters tables now render via the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: GitHub Markdown shortcodes on the `GitHubActions` host, Unicode glyphs everywhere else - fixes literal `:white_check_mark:` text on Azure DevOps step summaries). The Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink` and the standing **Ctrl-click tip** banner is single-sourced via `Get-AzLocalCtrlClickTip`. The Up-to-Date bucket gains the readiness-cascade check icon. -> **Behaviour change in v0.8.74**: clusters that have already applied every required update are now classified as **Up to Date** (counted separately) instead of being lumped into the Not-Ready bucket. The summary's "Up to date" count is now accurate, the "Not-Ready clusters (review first)" table excludes Up-to-Date and Ready clusters, and the "All clusters detail" table shows a readable `Status` column in place of the old `Ready` boolean. Classification is shared with Step 7 and Step 9 via the `Get-AzLocalClusterReadinessStatus` helper. +> **Behaviour change in v0.8.74**: clusters that have already applied every required update are now classified as **Up to Date** (counted separately) instead of being lumped into the Not-Ready bucket. The summary's "Up to date" count is now accurate, the "Not-Ready clusters (review first)" table excludes Up-to-Date and Ready clusters, and the "All clusters detail" table shows a readable `Status` column in place of the old `Ready` boolean. Classification is shared with Fleet: 04 and Fleet: 06 via the `Get-AzLocalClusterReadinessStatus` helper. | Aspect | Value | |---|---| @@ -136,7 +121,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **When to run** | 12-72 hours before each Fleet: 04 wave for the same ring (12-24h for small rings, 48-72h for large rings of 50+ clusters) - long enough for an operator to triage any `` entries before the maintenance window opens, short enough that the readiness signal is still fresh. See the **recommended customisation note below** for the cron pattern. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | | **Exit conditions** | Pipeline run is always green - per-cluster readiness gaps and blocking health checks surface as JUnit `` entries in the Tests tab. **Caveat**: because the pipeline never goes red, a silently-empty `readiness.xml` (e.g. ring tag typo, zero clusters in scope) will not generate a red email - check the Tests tab / `readiness.xml` artefact after each run, or wire the JUnit reporter into your existing CI status surface. | -| **ITSM** | Not supported - this is a pre-flight gate intended for operator review before the Step 7 wave; readiness gaps surface as JUnit `` entries and feed the Step 7 ITSM dispatch downstream rather than raising tickets here. | +| **ITSM** | Not supported - this is a pre-flight gate intended for operator review before the Fleet: 04 wave; readiness gaps surface as JUnit `` entries and feed the Fleet: 04 ITSM dispatch downstream rather than raising tickets here. | > **RECOMMENDED CUSTOMISATION: schedule Fleet: 01 per ring, 12-72h ahead of the matching Fleet: 04 cron.** Unlike Fleet: 04 (which silently does nothing if you forget to schedule it), Fleet: 01 is *recommended* rather than *mandatory* - Fleet: 04 does its own internal `Get-AzLocalClusterUpdateReadiness` per-cluster pre-flight, so skipping Fleet: 01 will not break the apply step. What you lose by skipping it is the **human review window** - the chance to see "12 clusters in Ring2 have blocking health checks" and intervene before the maintenance window starts. > @@ -150,16 +135,12 @@ The table below is the ground truth for what each shipped YAML does **out of the > > **Known gap**: the Setup: 03 schedule-coverage audit currently validates Fleet: 04 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Fleet: 01 cron is correctly anchored to a Fleet: 04 cron. Pair Fleet: 01 and Fleet: 04 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. -## Step 6 - Sideload Updates +## Fleet: 03 - Sideload Updates (Opt-in) -> **Behaviour change in v0.8.81 (Step 6 - Apply-Updates Readiness Gate)**: NOTE this callout describes the **Apply-Updates Readiness Gate** report (`Export-AzLocalClusterReadinessGateReport`), which is internal to the Step 7 apply-updates pipeline (not the Sideload Updates pipeline documented in the table below). The gate report drops its inline icon switch in favour of the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: fixes literal `:white_check_mark:` text on Azure DevOps step summaries); the Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink`; and the standing Ctrl-click tip banner is single-sourced via `Get-AzLocalCtrlClickTip`. +> **Behaviour change in v0.8.81 (Fleet: 04 Apply-Updates Readiness Gate)**: NOTE this callout describes the **Apply-Updates Readiness Gate** report (`Export-AzLocalClusterReadinessGateReport`), which is internal to the Fleet: 04 apply-updates pipeline (not the Sideload Updates pipeline documented in the table below). The gate report drops its inline icon switch in favour of the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: fixes literal `:white_check_mark:` text on Azure DevOps step summaries); the Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink`; and the standing Ctrl-click tip banner is single-sourced via `Get-AzLocalCtrlClickTip`. > **OPT-IN, OFF BY DEFAULT, self-hosted runner required.** Introduced in v0.8.7. This pipeline is inert unless the repository variable `SIDELOAD_UPDATES` is the literal string `'true'`. Full architecture, the scheduled-task survival model, the shared-state / multi-runner contract, the auth-map, and the catalog format are in [sideload.md](sideload.md); robocopy throttling guidance is in [sideload-robocopy.md](sideload-robocopy.md). ---- - -## Fleet: 03 - Sideload Updates (Opt-in) - | Aspect | Value | |---|---| | **Purpose** | Pre-stage Azure Local solution-update media onto clusters that **cannot pull updates from Azure directly** (dark / air-gapped / restricted-egress fabrics). It Robocopies the `CombinedSolutionBundle` (or OEM SBE package) to each cluster's infrastructure `import` SMB share, verifies the SHA256 over WinRM, runs `Add-SolutionUpdate`, and flips `UpdateSideloaded=True` so the downstream Fleet: 04 apply can proceed. | @@ -180,9 +161,9 @@ The table below is the ground truth for what each shipped YAML does **out of the > **Behaviour change in v0.8.81**: the per-host apply-updates step summary (`Add-AzLocalApplyUpdatesStepSummary`) drops its inline `$iconSuccess` / `$iconFail` / `$iconBlock` / `$iconSkip` / `$iconWarn` definitions in favour of the new shared `Get-AzLocalStatusIconMap` private helper (fixes literal `:white_check_mark:` text on Azure DevOps step summaries). The Cluster column in both the Cluster Actions and Clusters Skipped at Readiness Gate tables now resolves each cluster's resource id from the readiness CSV (built upstream by `Get-AzLocalClusterUpdateReadiness`) and wraps the cell in a portal deep-link via `Get-AzLocalClusterPortalLink`. Adds the standing Ctrl-click tip via `Get-AzLocalCtrlClickTip`. The upstream apply-results JSON shape (`ClusterName` / `Status` / `UpdateName` / `Duration` / `Message`) is intentionally unchanged - the cluster id lookup is done in the renderer. -> **Behaviour change in v0.8.74**: the pre-apply readiness gate report (`Export-AzLocalClusterReadinessGateReport`) replaces its binary `Ready?` column with a readable `Status` column so fully-patched clusters now show **Up to Date** instead of a no-entry icon that implied failure. The header line shows a distinct `Up to Date` count and the step emits a new additive `UP_TO_DATE_COUNT` / `UpToDateCount` output (the `READY_COUNT` gate that drives the apply is unchanged). Classification is shared with Step 5 and Step 9 via the `Get-AzLocalClusterReadinessStatus` helper. When the gate finds no clusters ready, the "No Clusters Ready" summary (`Add-AzLocalNoReadyClustersStepSummary`) now renders an Up-to-Date vs Not-Ready breakdown - so an idle run makes clear that already-patched clusters are a healthy steady state (a notice, not a warning, when every cluster is up to date) rather than a fault. +> **Behaviour change in v0.8.74**: the pre-apply readiness gate report (`Export-AzLocalClusterReadinessGateReport`) replaces its binary `Ready?` column with a readable `Status` column so fully-patched clusters now show **Up to Date** instead of a no-entry icon that implied failure. The header line shows a distinct `Up to Date` count and the step emits a new additive `UP_TO_DATE_COUNT` / `UpToDateCount` output (the `READY_COUNT` gate that drives the apply is unchanged). Classification is shared with Fleet: 01 and Fleet: 06 via the `Get-AzLocalClusterReadinessStatus` helper. When the gate finds no clusters ready, the "No Clusters Ready" summary (`Add-AzLocalNoReadyClustersStepSummary`) now renders an Up-to-Date vs Not-Ready breakdown - so an idle run makes clear that already-patched clusters are a healthy steady state (a notice, not a warning, when every cluster is up to date) rather than a fault. -> **Behaviour change in v0.8.78**: per-cluster `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips the Step.07 check RED on by-design gate-respect outcomes - `HealthCheckBlocked` deliberately stays a `` because a critical health failure IS actionable. The Step 7 Readiness KPI table also gains optional **Already Up to Date** and **Not Ready (needs attention before updating)** rows, wired in both shipped `apply-updates.yml` templates from the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (emitted since v0.8.74), so operators see the full ring breakdown - eligible / started / skipped / blocked / up-to-date / needs-attention - in one place. GitHub Actions only: `actions/download-artifact@v6 -> @v7` to silence the Node 20 deprecation warning (v7 chosen over v8 because v8 has a breaking `digest-mismatch=error` default). +> **Behaviour change in v0.8.78**: per-cluster `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips the Step.07 check RED on by-design gate-respect outcomes - `HealthCheckBlocked` deliberately stays a `` because a critical health failure IS actionable. The Fleet: 04 Readiness KPI table also gains optional **Already Up to Date** and **Not Ready (needs attention before updating)** rows, wired in both shipped `apply-updates.yml` templates from the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (emitted since v0.8.74), so operators see the full ring breakdown - eligible / started / skipped / blocked / up-to-date / needs-attention - in one place. GitHub Actions only: `actions/download-artifact@v6 -> @v7` to silence the Node 20 deprecation warning (v7 chosen over v8 because v8 has a breaking `digest-mismatch=error` default). > **New in v0.8.79 - operator break-glass override (`Force Immediate Update`)**: optional pipeline parameter on `apply-updates.yml` (`force_immediate_update` GitHub Actions choice input / `forceImmediateUpdate` Azure DevOps boolean parameter) defaulting to `false`. When set to `true` on a **manual run / queue only**, bypasses the per-cluster `UpdateStartWindow` / `UpdateExclusionsWindow` Step 3c maintenance-window gate for emergency / out-of-window patching. Defence in depth: GitHub Actions YAML collapses the flag to `'false'` for any non-`workflow_dispatch` event (`${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}`); Azure DevOps rechecks `$(Build.Reason) -eq 'Manual'` at runtime before honouring the parameter. A `WARNING:` GUI label is prepended on both pipeline hosts; a high-visibility `::warning::` (GHA) / `##vso[task.logissue type=warning]` (ADO) banner is emitted into the run log when the override fires. The override is unreachable from the `apply-updates-schedule.yml` configuration file (no `forceImmediateUpdate` field exists on `New-AzLocalApplyUpdatesScheduleConfig` / `Resolve-AzLocalPipelineUpdateRing` / `Get-AzLocalApplyUpdatesScheduleAudit`). Other readiness gates (connectivity, health, sideload status, `UpdateExcluded` operator hard-override) continue to apply. Maps to `Invoke-AzLocalReadinessGatedClusterUpdate -ForceImmediateUpdate` -> `Start-AzLocalClusterUpdate -IgnoreScheduleTags` for direct cmdlet use. @@ -205,7 +186,7 @@ The table below is the ground truth for what each shipped YAML does **out of the > > Pipeline summaries (Fleet: 04 GH Actions + Azure DevOps) classify per-cluster skips as `ScheduleBlocked` (outside `UpdateStartWindow` or inside `UpdateExclusionsWindow`), `SideloadedBlocked` (`UpdateSideloaded=False` on a sideloaded-workflow cluster) and, new in v0.7.90, `ExcludedByTag` (`UpdateExcluded=True` operator hard override). The Actions Required callout points operators at the `UpdateExcluded` tag when one or more clusters are intentionally held out of automation. -## Step 8 - Monitor In-Flight Updates +## Fleet: 05 - Monitor In-Flight Updates > **Behaviour change in v0.8.81**: the `$stateIcon` (`State` column) and `$statusIcon` (`Current Step Status` column) switches in `Export-AzLocalUpdateRunMonitorReport` no longer hard-code GitHub-Markdown shortcodes (`:large_blue_circle:` / `:hourglass_flowing_sand:` / `:red_circle:` / etc.) which previously rendered as literal text on Azure DevOps step summaries. Both switches now read from the shared `Get-AzLocalStatusIconMap` private helper using the new `State*` (`StateInProgress` / `StateSucceeded` / `StateFailed` / `StateNotStarted` / `StateUnknown`) and `Status*` (`StatusInProgress` / `StatusCancelled`) keys. JUnit XML output and CSV columns are unchanged. @@ -218,9 +199,9 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Depends on** | Fleet: 04 must be active (at least one `InProgress` run) for the output to be non-empty. Runs cleanly on an empty fleet (reports "no in-flight runs"). | | **Artefacts** | `update-monitor.xml` (JUnit, one `` per in-flight cluster - the failure message contains current step + progress + elapsed duration when the run is over `long_running_threshold_hours`, or the deepest-step error detail when `failureType='StepError'` fires under v0.7.96), `update-monitor.csv` (full per-cluster rows including the new `Status` + `ErrorMessage` columns), markdown job summary with the in-flight table (cluster + update names as portal links from v0.7.96) and long-running flag column. | | **Exit conditions** | Pipeline run is green when the snapshot completes. Long-running runs (over `long_running_threshold_hours`) AND stuck-step errors (`failureType='StepError'`, v0.7.96+) surface as JUnit `` entries so they appear in the Tests / Checks tab without failing the whole pipeline. v0.7.96 also adds two new `GITHUB_OUTPUT` values: `STEP_ERRORED` (count of runs that hit a step error inside the threshold) and `UNRESOLVED_FAILURES` (count of Failed runs surfaced from `Get-AzLocalUpdateRunFailures`). | -| **When to run** | While a wave is active (manually, or via the commented cron). The two daily snapshot pipelines (Step 9 and Step 10) remain the steady-state daily reports; this pipeline is purpose-built for the "is the apply-updates run I started two hours ago still progressing?" question that those daily snapshots cannot answer between runs. | +| **When to run** | While a wave is active (manually, or via the commented cron). The two daily snapshot pipelines (Fleet: 06 and Fleet: 07) remain the steady-state daily reports; this pipeline is purpose-built for the "is the apply-updates run I started two hours ago still progressing?" question that those daily snapshots cannot answer between runs. | | **RBAC** | Read-only - `Reader` on the cluster scope, plus the cluster-update API read paths already enumerated in the `Azure Stack HCI Update Operator (custom)` custom role. | -| **ITSM** | Not supported - in-flight monitor exists to be a fast feedback loop during an active wave; long-running runs surface as JUnit `` entries for the operator on call, and post-wave failures are picked up + ticketed by the daily Step 9 snapshot. | +| **ITSM** | Not supported - in-flight monitor exists to be a fast feedback loop during an active wave; long-running runs surface as JUnit `` entries for the operator on call, and post-wave failures are picked up + ticketed by the daily Fleet: 06 snapshot. | | **Introduced** | v0.7.90. Enhanced in v0.7.96 with the `Status` + `ErrorMessage` columns, the new `StepError` JUnit failure type, the always-shown Failed-runs block, portal-linked Cluster Name / Update Name cells, and the new `STEP_ERRORED` / `UNRESOLVED_FAILURES` `GITHUB_OUTPUT` values. **Behaviour change in v0.7.98:** UX overhaul of the in-flight table - rows now sort by composite `SeverityScore` (`StepError severity x 1000 + RunSeverity x 100 + elapsed-hours bucket`) so stuck step errors and runs over 14 days bubble to the top regardless of cluster alphabetical order; each row carries per-cell `StateIcon` + `StatusIcon` icons and a horizontal chip stack of flags (`STEP-STUCK`, `RUN-STUCK`, `UNRESOLVED`, `RECENT-FAIL`); the job summary opens with a single `CRITICAL / WARN / OK` fleet status badge that collapses the worst row across the fleet into one line (e.g. `CRITICAL - 1 stuck step error(s), 1 run(s) > 14d, 1 step(s) > 4h`); each failed step's `errorMessage` is now wrapped in a collapsible `
Verbose error...
` block; and JUnit `` + `` are populated with real per-run elapsed seconds (was `time="0"` before, which made GitHub Test Reporter render `"5 tests were completed in 0ms"`). | --- @@ -231,7 +212,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | Aspect | Value | |---|---| -| **Purpose** | Daily fleet-wide snapshot of cluster update state. Read-only. **v0.7.90 pivots the Version Distribution markdown table by YYMM** (leading column `Version` = `2511`, `2604`, ...; new `Update Versions` column lists each distinct full version installed within that YYMM as ` x ` separated by `
`; rows sorted ascending by YYMM so the oldest YYMM is at the top). The underlying `` JUnit XML is unchanged - still one `` per distinct full `CurrentVersion` - so machine-readable consumers and CI test-reporters are unaffected. **v0.7.96 reworks the Primary Status bucket cascade to align with the Azure portal Update Manager `state` filter:** `NeedsAttention` is now promoted into the **Update Failed** bucket; `PreparationFailed` lands in a new **Action Required** bucket (with its own actionable guidance in the Actions Required callout because the run never started, so re-arming is required); `PreparationInProgress` is folded into **Update In Progress**. The new bucket surfaces via (1) a new `` JUnit attribute on the primary testsuite, (2) per-testcase `failureType='PreparationFailed'` instead of the generic `UpdateFailure`, (3) a new `ACTION_REQUIRED` `GITHUB_OUTPUT` (`actionRequired` pipeline variable in ADO), (4) `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and (5) a new "Action Required (PreparationFailed)" row in the Primary Status markdown table with separate actionable prose in the Actions Required callout. The `📜 Update Run History and Error Details` markdown table renders Cluster Name and Update Name cells as `` deep-links (cluster's Updates blade + the single-instance update-run history view) - same linking as the Step.8 in-flight table. | +| **Purpose** | Daily fleet-wide snapshot of cluster update state. Read-only. **v0.7.90 pivots the Version Distribution markdown table by YYMM** (leading column `Version` = `2511`, `2604`, ...; new `Update Versions` column lists each distinct full version installed within that YYMM as ` x ` separated by `
`; rows sorted ascending by YYMM so the oldest YYMM is at the top). The underlying `` JUnit XML is unchanged - still one `` per distinct full `CurrentVersion` - so machine-readable consumers and CI test-reporters are unaffected. **v0.7.96 reworks the Primary Status bucket cascade to align with the Azure portal Update Manager `state` filter:** `NeedsAttention` is now promoted into the **Update Failed** bucket; `PreparationFailed` lands in a new **Action Required** bucket (with its own actionable guidance in the Actions Required callout because the run never started, so re-arming is required); `PreparationInProgress` is folded into **Update In Progress**. The new bucket surfaces via (1) a new `` JUnit attribute on the primary testsuite, (2) per-testcase `failureType='PreparationFailed'` instead of the generic `UpdateFailure`, (3) a new `ACTION_REQUIRED` `GITHUB_OUTPUT` (`actionRequired` pipeline variable in ADO), (4) `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and (5) a new "Action Required (PreparationFailed)" row in the Primary Status markdown table with separate actionable prose in the Actions Required callout. The `📜 Update Run History and Error Details` markdown table renders Cluster Name and Update Name cells as `
` deep-links (cluster's Updates blade + the single-instance update-run history view) - same linking as the Fleet: 05 in-flight table. | | **Inputs** | Scope (`-AllClusters` or `-ScopeByUpdateRingTag`), `throttle_limit` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 06:00 UTC (`cron '0 6 * * *'`). Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Get-AzLocalUpdateSummary`, `Get-AzLocalAvailableUpdates`, `Get-AzLocalUpdateRuns`, `Get-AzLocalUpdateRunFailures`, `Get-AzLocalLatestSolutionVersion`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md index e16067dc..141361c6 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md @@ -20,7 +20,7 @@ imported, the pipeline flips the `UpdateSideloaded=True` gate so the downstream internet-connected clusters. For the per-pipeline reference card (inputs, artefacts, RBAC, exit conditions) see -[appendix-pipelines.md - Step 6](appendix-pipelines.md#step-6---sideload-updates). +[appendix-pipelines.md - Fleet: 03](appendix-pipelines.md#fleet-03---sideload-updates-opt-in). For robocopy throttling guidance see [sideload-robocopy.md](sideload-robocopy.md). --- 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 c94625db..9a17dafe 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 @@ -66,7 +66,7 @@ on: default: '' # --- ITSM Connector (ServiceNow auto-raise on fleet-update-status) --- # Set raise_itsm_ticket=true to open ServiceNow incidents from each unresolved - # Failed update run reported by the '📜 Update Run History and Error Details' + # Failed update run reported by the 'Update Run History and Error Details' # testsuite (and any failed AzureLocalFleetUpdateStatus row). Default is false # so existing schedules stay byte-identical until you opt in. The connector reads # the JUnit file Step.7 already produces (./reports/readiness-status.xml) and diff --git a/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 b/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 index 091fca7a..60407e5f 100644 --- a/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 +++ b/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 @@ -79,9 +79,19 @@ function Get-AzLocalPipelineManifest { # the pre-renumber legacy filename so Update can recover a customer's CRONs # regardless of which copy they started from. sideload-updates is net-new # in v0.8.7 and therefore has no aliases. + # + # v0.8.85: the two standalone onboarding workflows authentication-test.yml + # (DisplayStep 0) and inventory-clusters.yml (DisplayStep 1) were merged + # into a single setup-validate-and-inventory.yml (Setup: 01). Its Aliases + # list BOTH superseded filenames (and their Step.N_ ancestors) so + # Update-AzLocalPipelineExample can rename-match a customer who still has + # either old file and carry their schedule CRONs into the merged workflow. + # inventory-clusters.yml is listed first because it is the alias that + # historically carried the schedule trigger; the rename path adopts the + # first existing alias. The companion authentication-test.yml is then + # cleaned up by -PruneDeprecated. @( - [PSCustomObject]@{ Id = 'authentication-test'; FileName = 'authentication-test.yml'; DisplayStep = 0; IntroducedIn = '0.7.0'; Aliases = @('Step.0_authentication-test.yml') } - [PSCustomObject]@{ Id = 'inventory-clusters'; FileName = 'inventory-clusters.yml'; DisplayStep = 1; IntroducedIn = '0.7.0'; Aliases = @('Step.1_inventory-clusters.yml') } + [PSCustomObject]@{ Id = 'setup-validate-and-inventory'; FileName = 'setup-validate-and-inventory.yml'; DisplayStep = 0; IntroducedIn = '0.8.85'; Aliases = @('inventory-clusters.yml', 'authentication-test.yml', 'Step.1_inventory-clusters.yml', 'Step.0_authentication-test.yml') } [PSCustomObject]@{ Id = 'manage-updatering-tags'; FileName = 'manage-updatering-tags.yml'; DisplayStep = 2; IntroducedIn = '0.7.0'; Aliases = @('Step.2_manage-updatering-tags.yml') } [PSCustomObject]@{ Id = 'apply-updates-schedule-audit'; FileName = 'apply-updates-schedule-audit.yml'; DisplayStep = 3; IntroducedIn = '0.7.0'; Aliases = @('Step.3_apply-updates-schedule-audit.yml') } [PSCustomObject]@{ Id = 'fleet-connectivity-status'; FileName = 'fleet-connectivity-status.yml'; DisplayStep = 4; IntroducedIn = '0.7.0'; Aliases = @('Step.4_fleet-connectivity-status.yml') } diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 8c7be779..83999ed2 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -7434,6 +7434,83 @@ Describe 'Function: Copy-AzLocalPipelineExample' { Test-Path (Join-Path $repoRoot 'config\sideload-auth-map.csv') | Should -BeFalse Test-Path (Join-Path $repoRoot 'config\sideload-catalog.yml') | Should -BeFalse } + + # v0.8.85: -PruneDeprecated removes the legacy authentication-test.yml / + # inventory-clusters.yml sample files that were merged into the single + # setup-validate-and-inventory.yml workflow. The cleanup is opt-in, + # GitHub-only, ID-verified (each candidate's AZLOCAL-PIPELINE-ID must match + # the expected value before deletion) and ShouldProcess-gated. + Context 'v0.8.85 -PruneDeprecated cleanup of merged setup workflow files' { + + It '-PruneDeprecated is exposed as a [switch] parameter' { + $cmd = Get-Command -Name 'Copy-AzLocalPipelineExample' -ErrorAction Stop + $cmd.Parameters.ContainsKey('PruneDeprecated') | Should -BeTrue + $cmd.Parameters['PruneDeprecated'].ParameterType | Should -Be ([switch]) + } + + It 'removes BOTH deprecated files when their AZLOCAL-PIPELINE-IDs match' { + $dest = Join-Path $script:cpDestRoot 'prune-both' + New-Item -Path $dest -ItemType Directory -Force | Out-Null + # Seed the two legacy files with their canonical IDs so the + # ID-verification gate allows deletion. + Set-Content -LiteralPath (Join-Path $dest 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: authentication-test' -Encoding ASCII + Set-Content -LiteralPath (Join-Path $dest 'inventory-clusters.yml') -Value '# AZLOCAL-PIPELINE-ID: inventory-clusters' -Encoding ASCII + + Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub -PruneDeprecated -Confirm:$false 6>$null | Out-Null + + Test-Path (Join-Path $dest 'authentication-test.yml') | Should -BeFalse + Test-Path (Join-Path $dest 'inventory-clusters.yml') | Should -BeFalse + # The merged replacement must be present. + Test-Path (Join-Path $dest 'setup-validate-and-inventory.yml') | Should -BeTrue + } + + It 'removes only the single deprecated file that is present' { + $dest = Join-Path $script:cpDestRoot 'prune-single' + New-Item -Path $dest -ItemType Directory -Force | Out-Null + Set-Content -LiteralPath (Join-Path $dest 'inventory-clusters.yml') -Value '# AZLOCAL-PIPELINE-ID: inventory-clusters' -Encoding ASCII + + Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub -PruneDeprecated -Confirm:$false 6>$null | Out-Null + + Test-Path (Join-Path $dest 'inventory-clusters.yml') | Should -BeFalse + Test-Path (Join-Path $dest 'setup-validate-and-inventory.yml') | Should -BeTrue + } + + It 'preserves a deprecated-named file whose AZLOCAL-PIPELINE-ID does not match' { + $dest = Join-Path $script:cpDestRoot 'prune-mismatch' + New-Item -Path $dest -ItemType Directory -Force | Out-Null + # An operator-owned file that merely happens to share the legacy + # name but carries a different ID must NOT be deleted. + Set-Content -LiteralPath (Join-Path $dest 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: my-custom-auth-workflow' -Encoding ASCII + + Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub -PruneDeprecated -Confirm:$false 6>$null 3>$null | Out-Null + + Test-Path (Join-Path $dest 'authentication-test.yml') | Should -BeTrue + } + + It 'leaves deprecated files untouched when -PruneDeprecated is NOT supplied' { + $dest = Join-Path $script:cpDestRoot 'prune-omitted' + New-Item -Path $dest -ItemType Directory -Force | Out-Null + Set-Content -LiteralPath (Join-Path $dest 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: authentication-test' -Encoding ASCII + Set-Content -LiteralPath (Join-Path $dest 'inventory-clusters.yml') -Value '# AZLOCAL-PIPELINE-ID: inventory-clusters' -Encoding ASCII + + Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub 6>$null 3>$null | Out-Null + + Test-Path (Join-Path $dest 'authentication-test.yml') | Should -BeTrue + Test-Path (Join-Path $dest 'inventory-clusters.yml') | Should -BeTrue + } + + It '-PruneDeprecated -WhatIf does not delete the deprecated files' { + $dest = Join-Path $script:cpDestRoot 'prune-whatif' + New-Item -Path $dest -ItemType Directory -Force | Out-Null + Set-Content -LiteralPath (Join-Path $dest 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: authentication-test' -Encoding ASCII + Set-Content -LiteralPath (Join-Path $dest 'inventory-clusters.yml') -Value '# AZLOCAL-PIPELINE-ID: inventory-clusters' -Encoding ASCII + + Copy-AzLocalPipelineExample -Destination $dest -Platform GitHub -PruneDeprecated -WhatIf 6>$null | Out-Null + + Test-Path (Join-Path $dest 'authentication-test.yml') | Should -BeTrue + Test-Path (Join-Path $dest 'inventory-clusters.yml') | Should -BeTrue + } + } } #endregion Copy-AzLocalPipelineExample (v0.7.4, updated in v0.7.50, v0.7.92) @@ -9434,6 +9511,69 @@ jobs: $afterContent | Should -Match 'operator hand-edit' } } + + # v0.8.85: -PruneDeprecated removes the legacy authentication-test.yml / + # inventory-clusters.yml sample files once the merged + # setup-validate-and-inventory.yml has been written by the refresh. The + # cleanup is opt-in, GitHub-only, ID-verified and ShouldProcess-gated. + Context 'v0.8.85 -PruneDeprecated cleanup of merged setup workflow files' { + + It '-PruneDeprecated is exposed as a [switch] parameter' { + $cmd = Get-Command -Name 'Update-AzLocalPipelineExample' -ErrorAction Stop + $cmd.Parameters.ContainsKey('PruneDeprecated') | Should -BeTrue + $cmd.Parameters['PruneDeprecated'].ParameterType | Should -Be ([switch]) + } + + It 'removes both deprecated files when their AZLOCAL-PIPELINE-IDs match' { + $temp = Join-Path $env:TEMP "upe-prune-both-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $temp -Force | Out-Null + try { + # Seed the merged replacement so the prune precondition (the + # canonical setup-validate-and-inventory.yml exists) is met via a + # plain canonical match - no rename-merge of the legacy aliases. + Copy-Item -LiteralPath (Join-Path $script:UpePlatformSrcGh 'setup-validate-and-inventory.yml') -Destination $temp + Set-Content -LiteralPath (Join-Path $temp 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: authentication-test' -Encoding ASCII + Set-Content -LiteralPath (Join-Path $temp 'inventory-clusters.yml') -Value '# AZLOCAL-PIPELINE-ID: inventory-clusters' -Encoding ASCII + + Update-AzLocalPipelineExample -Destination $temp -Platform GitHub -PruneDeprecated -Confirm:$false 6>$null 4>$null | Out-Null + + Test-Path -LiteralPath (Join-Path $temp 'authentication-test.yml') | Should -BeFalse + Test-Path -LiteralPath (Join-Path $temp 'inventory-clusters.yml') | Should -BeFalse + Test-Path -LiteralPath (Join-Path $temp 'setup-validate-and-inventory.yml') | Should -BeTrue + } + finally { Remove-Item -Path $temp -Recurse -Force -ErrorAction SilentlyContinue } + } + + It 'preserves a deprecated-named file whose AZLOCAL-PIPELINE-ID does not match' { + $temp = Join-Path $env:TEMP "upe-prune-mismatch-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $temp -Force | Out-Null + try { + Copy-Item -LiteralPath (Join-Path $script:UpePlatformSrcGh 'setup-validate-and-inventory.yml') -Destination $temp + Set-Content -LiteralPath (Join-Path $temp 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: my-custom-auth' -Encoding ASCII + + Update-AzLocalPipelineExample -Destination $temp -Platform GitHub -PruneDeprecated -Confirm:$false 6>$null 4>$null | Out-Null + + Test-Path -LiteralPath (Join-Path $temp 'authentication-test.yml') | Should -BeTrue + } + finally { Remove-Item -Path $temp -Recurse -Force -ErrorAction SilentlyContinue } + } + + It 'leaves deprecated files untouched when -PruneDeprecated is NOT supplied' { + $temp = Join-Path $env:TEMP "upe-prune-omitted-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $temp -Force | Out-Null + try { + Copy-Item -LiteralPath (Join-Path $script:UpePlatformSrcGh 'setup-validate-and-inventory.yml') -Destination $temp + Set-Content -LiteralPath (Join-Path $temp 'authentication-test.yml') -Value '# AZLOCAL-PIPELINE-ID: authentication-test' -Encoding ASCII + Set-Content -LiteralPath (Join-Path $temp 'inventory-clusters.yml') -Value '# AZLOCAL-PIPELINE-ID: inventory-clusters' -Encoding ASCII + + Update-AzLocalPipelineExample -Destination $temp -Platform GitHub -Confirm:$false 6>$null 4>$null | Out-Null + + Test-Path -LiteralPath (Join-Path $temp 'authentication-test.yml') | Should -BeTrue + Test-Path -LiteralPath (Join-Path $temp 'inventory-clusters.yml') | Should -BeTrue + } + finally { Remove-Item -Path $temp -Recurse -Force -ErrorAction SilentlyContinue } + } + } } #endregion v0.7.68 Update-AzLocalPipelineExample + marker parser diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index aca0f887..0bd034f5 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -35,3 +35,4 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T10:09:11Z","0.8.85","1295","35","3","0","545.67","544.05","2.5","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T10:13:27Z","0.8.85","1295","1256","0","1","49.91","48.62","3.18","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T10:25:03Z","0.8.85","1295","38","0","0","617.07","615.99","3.31","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T11:05:53Z","0.8.85","1305","1265","1","1","148.49","145.99","15.16","Invoke-Tests.ps1","5.1.26100.8730" From 715efc7f0b00a35c00ff832ed7d79a38b24c1497 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 12:11:59 +0100 Subject: [PATCH 10/17] docs: clarify sideload-catalog.example.yml header comment (v0.8.7 on-prem solution/SBE update sideloading) --- .../Automation-Pipeline-Examples/sideload-catalog.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml index c274278b..fe8908f1 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml @@ -1,5 +1,5 @@ # --------------------------------------------------------------------------- -# Sideload catalog (v0.8.7 on-prem solution-update sideloading automation) +# Sideload catalog (introduce in module version 0.8.7 to provide on-premises solution or SBE update sideloading automation) # --------------------------------------------------------------------------- # Source-controlled list of the Azure Local solution-update media (and OEM SBE # packages) the sideloading automation is allowed to stage. The runtime copy From 43b8c8608629462bd1c2ac97258ef9d86b68efce Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 12:12:20 +0100 Subject: [PATCH 11/17] docs: fix grammar in sideload-catalog.example.yml header (introduce -> introduced) --- .../Automation-Pipeline-Examples/sideload-catalog.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml index fe8908f1..3a3399f2 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/sideload-catalog.example.yml @@ -1,5 +1,5 @@ # --------------------------------------------------------------------------- -# Sideload catalog (introduce in module version 0.8.7 to provide on-premises solution or SBE update sideloading automation) +# Sideload catalog (introduced in module version 0.8.7 to provide on-premises solution or SBE update sideloading automation) # --------------------------------------------------------------------------- # Source-controlled list of the Azure Local solution-update media (and OEM SBE # packages) the sideloading automation is allowed to stage. The runtime copy From a1d4ff366725153fbd2b190d7b2cd21affdcd147 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 12:44:06 +0100 Subject: [PATCH 12/17] v0.8.86: rename onboarding pipelines Setup -> Config + clarify GitHub Environments are optional - Rename the three onboarding workflow display names from `Setup: 0N` to `Config: 0N` (GitHub Actions `name:` and Azure DevOps `displayName:`/`name:`) so the Actions sidebar / ADO Pipelines list sorts the onboarding workflows ahead of the `Fleet: 0N` operational workflows (C sorts before F; the old `Setup:` prefix sorted after `Fleet:`). Display-name-only change - filenames, AZLOCAL-PIPELINE-ID values, aliases and prune logic are unchanged. - Update bundled pipeline README.md + docs/appendix-pipelines.md (headings, cross-references, anchors, naming rationale) to the Config: NN naming. - README.md (Automation-Pipeline-Examples): clarify that GitHub Environments are OPTIONAL. The branch-scoped federated credential is all OIDC needs, so a blank `environment` input is the supported minimal setup. Environments are a governance wrapper (approval gates, deployment-branch rules, wait timers, per-ring identities); environment-scoped federated credentials are only needed when a run actually names an environment. Mark the environment table, the inline + PowerShell env-scoped credential blocks, the subject-claim table row, and the section-5 secrets script accordingly. - Version bump to 0.8.86: psd1 ModuleVersion + ReleaseNotes, psm1 $script:ModuleVersion, CHANGELOG.md, both README files, docs/release-history, Tests version assertion, and all GENERATED_AGAINST_MODULE_VERSION pins across the bundled GitHub Actions and Azure DevOps templates. Full Pester suite: 1266 passed, 0 failed, 1 skipped. --- .../Automation-Pipeline-Examples/README.md | 78 +++++++++++-------- .../apply-updates-schedule-audit.yml | 6 +- .../azure-devops/apply-updates.yml | 2 +- .../azure-devops/assess-update-readiness.yml | 2 +- .../fleet-connectivity-status.yml | 2 +- .../azure-devops/fleet-health-status.yml | 2 +- .../azure-devops/fleet-update-status.yml | 2 +- .../azure-devops/manage-updatering-tags.yml | 6 +- .../azure-devops/monitor-updates.yml | 2 +- .../setup-validate-and-inventory.yml | 10 +-- .../azure-devops/sideload-updates.yml | 2 +- .../docs/appendix-pipelines.md | 34 ++++---- .../apply-updates-schedule-audit.yml | 4 +- .../github-actions/apply-updates.yml | 2 +- .../assess-update-readiness.yml | 2 +- .../fleet-connectivity-status.yml | 2 +- .../github-actions/fleet-health-status.yml | 2 +- .../github-actions/fleet-update-status.yml | 2 +- .../github-actions/manage-updatering-tags.yml | 4 +- .../github-actions/monitor-updates.yml | 2 +- .../setup-validate-and-inventory.yml | 8 +- .../github-actions/sideload-updates.yml | 2 +- .../AzLocal.UpdateManagement.psd1 | 4 +- .../AzLocal.UpdateManagement.psm1 | 2 +- AzLocal.UpdateManagement/CHANGELOG.md | 30 +++++++ .../Private/Get-AzLocalPipelineManifest.ps1 | 2 +- AzLocal.UpdateManagement/README.md | 25 +++--- .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 4 +- .../Tests/test-run-timings.csv | 1 + .../docs/release-history.md | 14 +++- 30 files changed, 162 insertions(+), 98 deletions(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index a7934f0c..6a4cb5c0 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -56,10 +56,10 @@ It is written in the same step-by-step style as [`ITSM/README.md`](../ITSM/READM By the end of this guide you will have: - A federated identity (no client secrets) wired into your CI/CD platform with the **minimum** Azure RBAC needed for cluster update management. -- Setup/Fleet workflows committed to your repo and visible in the Actions / Pipelines UI: - - **Setup: 01 - Validate Auth and Inventory Clusters** (GitHub) - merged auth + inventory flow with a clear setup-first summary (including collapsible subscription details) and cluster inventory export. - - **Setup: 02 - Manage UpdateRing Tags** - bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, `UpdateExcluded` tags from CSV. - - **Setup: 03 - Apply-Updates Schedule Coverage Audit** - read-only audit that validates your schedule against `UpdateStartWindow` tags. +- Config/Fleet workflows committed to your repo and visible in the Actions / Pipelines UI: + - **Config: 01 - Validate Auth and Inventory Clusters** (GitHub) - merged auth + inventory flow with a clear setup-first summary (including collapsible subscription details) and cluster inventory export. + - **Config: 02 - Manage UpdateRing Tags** - bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, `UpdateExcluded` tags from CSV. + - **Config: 03 - Apply-Updates Schedule Coverage Audit** - read-only audit that validates your schedule against `UpdateStartWindow` tags. - **Fleet: 01 - Assess Update Readiness** - pre-flight readiness + blocking-health gate report. - **Fleet: 02 - Fleet Connectivity Status** - Arc connectivity, NIC health, and ARB status snapshot. - **Fleet: 03 - Sideload Updates (opt-in)** - on-prem media pre-stage workflow for disconnected environments. @@ -72,18 +72,18 @@ By the end of this guide you will have: The pipelines are **fully opt-in additive layers** over the module. The PowerShell functions also work without any pipeline at all - see [section 10](#10-standalone-html-report-no-pipeline) for the ad-hoc / desktop story. -### 1.1 Why the pipelines are named `Setup: NN` and `Fleet: NN` +### 1.1 Why the pipelines are named `Config: NN` and `Fleet: NN` The active workflow model uses two clear groups: -- `Setup: 01-03` for onboarding and configuration. +- `Config: 01-03` for onboarding and configuration. - `Fleet: 01-07` for day-2 operational monitoring and update execution. | Group | Workflow name | GH Actions | Azure DevOps | |---|---|---|---| -| Setup | Setup: 01 - Validate Auth and Inventory Clusters | `setup-validate-and-inventory.yml` | `authentication-test.yml` + `inventory-clusters.yml` | -| Setup | Setup: 02 - Manage UpdateRing Tags | `manage-updatering-tags.yml` | `manage-updatering-tags.yml` | -| Setup | Setup: 03 - Apply-Updates Schedule Coverage Audit | `apply-updates-schedule-audit.yml` | `apply-updates-schedule-audit.yml` | +| Config | Config: 01 - Validate Auth and Inventory Clusters | `setup-validate-and-inventory.yml` | `authentication-test.yml` + `inventory-clusters.yml` | +| Config | Config: 02 - Manage UpdateRing Tags | `manage-updatering-tags.yml` | `manage-updatering-tags.yml` | +| Config | Config: 03 - Apply-Updates Schedule Coverage Audit | `apply-updates-schedule-audit.yml` | `apply-updates-schedule-audit.yml` | | Fleet | Fleet: 01 - Assess Update Readiness | `assess-update-readiness.yml` | `assess-update-readiness.yml` | | Fleet | Fleet: 02 - Fleet Connectivity Status | `fleet-connectivity-status.yml` | `fleet-connectivity-status.yml` | | Fleet | Fleet: 03 - Sideload Updates (Opt-in) | `sideload-updates.yml` | `sideload-updates.yml` | @@ -92,13 +92,13 @@ The active workflow model uses two clear groups: | Fleet | Fleet: 06 - Fleet Update Status | `fleet-update-status.yml` | `fleet-update-status.yml` | | Fleet | Fleet: 07 - Fleet Health Status | `fleet-health-status.yml` | `fleet-health-status.yml` | -- **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field. Prefixing names with `Setup: NN` and `Fleet: NN` keeps the sidebar in intended execution order. +- **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field. Prefixing names with `Config: NN` and `Fleet: NN` keeps the sidebar in intended execution order (the `Config:` group sorts ahead of `Fleet:` so onboarding pipelines appear first). - ![GitHub Actions sidebar showing Setup and Fleet workflows in execution order](../docs/images/github-actions-10-pipelines-view.png) + ![GitHub Actions sidebar showing Config and Fleet workflows in execution order](../docs/images/github-actions-10-pipelines-view.png) - *The Setup/Fleet numeric prefixes keep the GitHub Actions sidebar in practical execution order rather than a purely alphabetical scatter.* + *The Config/Fleet numeric prefixes keep the GitHub Actions sidebar in practical execution order rather than a purely alphabetical scatter.* -- **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at import time (not by filename). Use the same `Setup: NN` / `Fleet: NN` naming when you import so the list stays in operational order. +- **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at import time (not by filename). Use the same `Config: NN` / `Fleet: NN` naming when you import so the list stays in operational order. If you prefer a different naming scheme (e.g. `00 - Auth`, `01 - Inventory`, ...), just change the `name:` field in each GH Actions YAML and / or pick a different prefix at ADO import time. Nothing else in the module depends on these display names. @@ -499,7 +499,17 @@ az role assignment create ` **Step 3 - federate the workflow** -> **Plan your GitHub environments now**: environment-scoped subjects (`...:environment:`) only succeed at workflow run time if a GitHub environment with the exact same name exists in the repo (names are **case-sensitive**). The `az` command will accept any string you put in `subject` - Entra ID does **not** validate it against GitHub - but a missing or mistyped environment fails the OIDC exchange at runtime with `AADSTS70021: No matching federated identity record found`. The create order does not technically matter, but it is easiest to decide on environment names now (and ideally create them up-front under **your repo -> Settings -> Environments -> New environment**) so the strings you put into the federated credentials definitely match what GitHub will later send in the token. For the ring-based rollout pattern this guide describes, three are recommended: +> **Are GitHub environments required? No - they are optional.** The pipelines authenticate to Azure with a **branch-scoped** federated credential (`...:ref:refs/heads/main`, the first `az ad app federated-credential create` block below). That single credential is all OIDC needs - leave the `environment` input **blank** (as in the screenshot most operators see) and every workflow runs under the branch-scoped subject claim. The `environment:` line in each job evaluates to an empty string, GitHub attaches no environment, and `azure/login` exchanges the branch-scoped token. This is the correct, fully-supported minimal setup. +> +> **What environments add (and when to bother).** A GitHub environment is a *governance* wrapper, **not** an OIDC requirement. Create them only when you want one or more of: +> - **Required reviewers / manual approval gates** - e.g. a human must approve before the `apply-updates` job runs against the `Production` ring. +> - **Deployment-branch restrictions** - only allow the workflow to target an environment from `main`. +> - **Wait timers** - enforce a soak period between rings. +> - **Per-environment secrets / variables** - e.g. a *different* `AZURE_CLIENT_ID` (a separate App Registration) per ring, so the pilot ring and the production ring use distinct identities with distinct RBAC scopes. +> +> If you do **not** need any of those, you can skip the environment-scoped credentials, the environment table, and the `environment` input entirely - the branch-scoped credential covers all runs. You can also add environments later without re-doing anything: create the environment, add its environment-scoped federated credential, and start passing its name in the `environment` input. **Is OIDC the reason?** Yes - the only auth-level effect of naming an environment is that GitHub puts `environment:` into the token's `subject` claim instead of `ref:refs/heads/main`, which is why a *named* run needs a matching **environment-scoped** federated credential (the loop block further below). A *blank* run never needs one. +> +> **Plan your GitHub environments now** (only if you decided you want them above): environment-scoped subjects (`...:environment:`) only succeed at workflow run time if a GitHub environment with the exact same name exists in the repo (names are **case-sensitive**). The `az` command will accept any string you put in `subject` - Entra ID does **not** validate it against GitHub - but a missing or mistyped environment fails the OIDC exchange at runtime with `AADSTS70021: No matching federated identity record found`. The create order does not technically matter, but it is easiest to decide on environment names now (and ideally create them up-front under **your repo -> Settings -> Environments -> New environment**) so the strings you put into the federated credentials definitely match what GitHub will later send in the token. **This whole table is optional** - it only applies if you opted into environments above. For the ring-based rollout pattern this guide describes, three are *suggested* (none are required): > > | Environment | Purpose | Suggested protection rules | > |---|---|---| @@ -514,7 +524,10 @@ az role assignment create ` > **GitHub environments and `UpdateRing` tag values are independent.** The `UpdateRing` tag lives on the cluster ARM resource and is what the PowerShell functions filter on (`-UpdateRing Wave1`). A GitHub environment is just an approval gate and federated credential subject. They do **not** have to share names, and the mapping is many-to-many: one GitHub environment can run updates across multiple `UpdateRing` values (different workflow runs pass different `-UpdateRing` parameters under the same approval gate), and multiple environments can target the same `UpdateRing` (e.g. a `PreProductionDryRun` environment that runs with `-WhatIf` against the `Production` ring). The workflow YAML decides which ring tag a given environment-gated run applies to. ```bash -# Branch-scoped credential (for default-branch / scheduled runs). +# REQUIRED - branch-scoped credential (for default-branch / scheduled runs). +# This single credential is enough to run every pipeline with the `environment` +# input left blank. If you are not using GitHub environments, this is the ONLY +# federated credential you need - skip the environment-scoped block below. az ad app federated-credential create ` --id ` --parameters '{ @@ -524,8 +537,10 @@ az ad app federated-credential create ` "audiences": ["api://AzureADTokenExchange"] }' -# Environment-scoped credential - one per GitHub environment (DevTest, PreProduction, Production). -# Repeat this command three times, substituting both `name` and `subject` to match each environment. +# OPTIONAL - environment-scoped credential, one per GitHub environment you chose +# to create (e.g. DevTest, PreProduction, Production). Only needed if you pass an +# environment name in the workflow `environment` input. Skip entirely otherwise. +# Repeat this command once per environment, substituting both `name` and `subject`. az ad app federated-credential create ` --id ` --parameters '{ @@ -542,7 +557,7 @@ Subject-claim patterns for other trigger types: |---|---| | Push to a branch | `repo:/:ref:refs/heads/` | | Pull request | `repo:/:pull_request` | -| Environment | `repo:/:environment:` | +| Environment *(optional)* | `repo:/:environment:` | | Tag | `repo:/:ref:refs/tags/` | > **PowerShell on Windows**: passing the `--parameters` JSON as an inline string (as shown above) fails on Windows PowerShell - and on PowerShell 7+ on Windows - with `Failed to parse string as JSON: ... Expecting property name enclosed in double quotes`. The `az` CLI on Windows is a `.cmd` shim, and cmd.exe strips the inner double quotes from the JSON before `az` ever sees them. Microsoft's [quoting guidance](https://learn.microsoft.com/cli/azure/use-azure-cli-successfully-quoting#json-strings) recommends bypassing the shell entirely by writing the JSON to a file and passing it with the `@` prefix - this is the universally safe pattern and works on Linux/macOS too: @@ -563,8 +578,9 @@ Subject-claim patterns for other trigger types: > --id ` > --parameters "@$paramsFile" > -> # Environment-scoped credentials - one per GitHub environment (names are case-sensitive -> # and must match the environments that will exist in your repo at workflow run time) +> # OPTIONAL - environment-scoped credentials, one per GitHub environment (names are +> # case-sensitive and must match the environments in your repo at workflow run time). +> # Skip this foreach entirely if you are running with the `environment` input blank. > foreach ($envName in 'DevTest','PreProduction','Production') { > @{ > name = "GitHubActions-$envName" @@ -628,7 +644,7 @@ You can add the secrets via the **GitHub UI** (**Settings -> Secrets and variabl > > `gh` reuses the credentials of the signed-in account, so it can write secrets to any repo that account can write to. No personal access token needed for interactive use. -**Script the secrets and environments (recommended)** - end-to-end: creates the GitHub environments your federated credentials reference, writes the three repo-level secrets, and (optionally) pins `AZURE_CLIENT_ID` at each environment. Substitute `/` for your target repo: +**Script the secrets and environments** - end-to-end: creates the GitHub environments your federated credentials reference (**optional** - only needed if you opted into environments in step 3; if you are running with the `environment` input blank, you can skip the environment-creation loop and keep just the secret/variable writes), writes the three repo-level secrets, and (optionally) pins `AZURE_CLIENT_ID` at each environment. Substitute `/` for your target repo: ```powershell # Inputs - reuse the variables from the federation step where you can @@ -940,13 +956,13 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp > Copy-AzLocalPipelineExample -Destination .\pipelines -Platform AzureDevOps > ``` > -> The function prints a short "next steps" summary pointing at the copied YAML location with the recommended workflow / pipeline to run first (GitHub: **Setup: 01 - Validate Auth and Inventory Clusters**, Azure DevOps: auth validation + inventory onboarding). Supports `-Platform GitHub | AzureDevOps | All`, `-PassThru`, `-WhatIf`, `-Confirm`. +> The function prints a short "next steps" summary pointing at the copied YAML location with the recommended workflow / pipeline to run first (GitHub: **Config: 01 - Validate Auth and Inventory Clusters**, Azure DevOps: auth validation + inventory onboarding). Supports `-Platform GitHub | AzureDevOps | All`, `-PassThru`, `-WhatIf`, `-Confirm`. > > **Refusing to overwrite**: the function will refuse to overwrite any file that already exists in `-Destination`, listing the conflicts in the error message. To refresh after a module upgrade, delete the existing copies first (`Remove-Item .\.github\workflows\*.yml`) and re-run. ### 5.1 GitHub Actions -1. **Run Setup: 01 first (strongly recommended).** Before exercising the Fleet workflows, validate that the App Registration, federated credentials, GitHub secrets, environments, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Setup: 01 - Validate Auth and Inventory Clusters`**. This narrows any failure to one onboarding workflow instead of debugging multiple operational workflows simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed. +1. **Run Config: 01 first (strongly recommended).** Before exercising the Fleet workflows, validate that the App Registration, federated credentials, GitHub secrets, environments, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Config: 01 - Validate Auth and Inventory Clusters`**. This narrows any failure to one onboarding workflow instead of debugging multiple operational workflows simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed. The onboarding workflow ships with the module at [`github-actions/setup-validate-and-inventory.yml`](./github-actions/setup-validate-and-inventory.yml). It emits the auth validation JUnit report (Authentication / Subscription Scope / Resource Graph Reachability), writes a setup-focused markdown summary (including a collapsible subscription-details section), and exports both auth and inventory artifacts for downstream workflows. @@ -971,8 +987,8 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp At the `gh` CLI level, `gh run watch` shows the run summary as the steps complete (`gh` actually renders these as Unicode check marks; reproduced here in ASCII): ```text - ? Select a workflow run * Setup: 01 - Validate Auth and Inventory Clusters, Setup: 01 - Validate Auth and Inventory Clusters [main] 12s ago - [OK] main Setup: 01 - Validate Auth and Inventory Clusters - + ? Select a workflow run * Config: 01 - Validate Auth and Inventory Clusters, Config: 01 - Validate Auth and Inventory Clusters [main] 12s ago + [OK] main Config: 01 - Validate Auth and Inventory Clusters - Triggered via workflow_dispatch less than a minute ago JOBS @@ -985,7 +1001,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp [OK] Post Azure login (OIDC) [OK] Complete job - [OK] Run Setup: 01 - Validate Auth and Inventory Clusters () completed with 'success' + [OK] Run Config: 01 - Validate Auth and Inventory Clusters () completed with 'success' ``` Inside the `Collect Authentication and Subscription Scope Report` step, the run log shows: @@ -1006,7 +1022,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp ``` - ![Setup: 01 - Validate Auth and Inventory Clusters run, showing authentication validation output and subscription scope details](../docs/images/auth-smoke-test-validate-oidc.png) + ![Config: 01 - Validate Auth and Inventory Clusters run, showing authentication validation output and subscription scope details](../docs/images/auth-smoke-test-validate-oidc.png) You may see one informational `windows-latest` -> `windows-2025-vs2026` migration notice in the run annotations. The sample workflows pin `runs-on: windows-latest` (the module is a Windows-side PowerShell module), and GitHub will retarget the alias to the new image automatically when it becomes the default - no action required on your part. As of v0.7.60 the previously-seen Node.js 20 deprecation banner (against `actions/checkout@v4`, `azure/login@v2`, `actions/upload-artifact@v4`, `dorny/test-reporter@v1`) is gone: the sample workflows have been refreshed to Node 24-compatible majors (`@v5`, `@v3`, `@v6`, `@v3` respectively). @@ -2018,9 +2034,9 @@ Automation-Pipeline-Examples/ templates/ incident-body.md # - Mustache-style ticket body template. github-actions/ - setup-validate-and-inventory.yml # Setup: 01. Auth + subscription-scope validation and cluster inventory (merged auth+inventory in v0.8.85; manual + weekly Sun 08:00 UTC). - manage-updatering-tags.yml # Setup: 02. Apply UpdateRing / UpdateStartWindow / UpdateExclusionsWindow / UpdateExcluded tags (manual). - apply-updates-schedule-audit.yml # Setup: 03. Weekly read-only audit: UpdateStartWindow tags vs apply-updates cron (Mon 05:00 UTC, v0.7.65). + setup-validate-and-inventory.yml # Config: 01. Auth + subscription-scope validation and cluster inventory (merged auth+inventory in v0.8.85; manual + weekly Sun 08:00 UTC). + manage-updatering-tags.yml # Config: 02. Apply UpdateRing / UpdateStartWindow / UpdateExclusionsWindow / UpdateExcluded tags (manual). + apply-updates-schedule-audit.yml # Config: 03. Weekly read-only audit: UpdateStartWindow tags vs apply-updates cron (Mon 05:00 UTC, v0.7.65). assess-update-readiness.yml # Fleet: 01. Pre-flight readiness report (manual; v0.7.0). fleet-connectivity-status.yml # Fleet: 02. Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot + node-coverage reconciliation (daily 05:30 UTC, v0.7.79+; reconciliation enhanced in v0.7.85). sideload-updates.yml # Fleet: 03. Opt-in self-hosted-runner workflow: Robocopy + WinRM sideload of solution-update media to clusters gated on UpdateSideloaded (manual; v0.8.7). @@ -2045,7 +2061,7 @@ Automation-Pipeline-Examples/ ## 14. Pipeline reference -Moved to [docs/appendix-pipelines.md](docs/appendix-pipelines.md) - one section per pipeline (`Setup: 01 - ...` ... `Fleet: 07 - ...`) mapping 1:1 to the bundled `*.yml` workflows, with purpose, inputs, trigger, cmdlets invoked, dependencies, artefacts, RBAC, and exit conditions for each. Kept out-of-line to keep this README focused on the runbook. +Moved to [docs/appendix-pipelines.md](docs/appendix-pipelines.md) - one section per pipeline (`Config: 01 - ...` ... `Fleet: 07 - ...`) mapping 1:1 to the bundled `*.yml` workflows, with purpose, inputs, trigger, cmdlets invoked, dependencies, artefacts, RBAC, and exit conditions for each. Kept out-of-line to keep this README focused on the runbook. ## Appendix B: Release history 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 e235ec60..f3cae97b 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: apply-updates-schedule-audit -# Setup: 03 - Apply-Updates Schedule Coverage Audit Pipeline +# Config: 03 - Apply-Updates Schedule Coverage Audit Pipeline # This pipeline runs the read-only Test-AzLocalApplyUpdatesScheduleCoverage advisor # weekly to detect drift between the cron schedule(s) in your apply-updates pipeline # YAML and the UpdateStartWindow tags that operators have placed on Azure Local clusters. @@ -87,7 +87,7 @@ parameters: default: false variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' # v0.8.7 sideload advisor defaults. Override at the pipeline / variable-group @@ -98,7 +98,7 @@ variables: stages: - stage: ScheduleCoverage - displayName: 'Setup: 03 - Audit Apply-Updates Schedule Coverage' + displayName: 'Config: 03 - Audit Apply-Updates Schedule Coverage' jobs: - job: Audit displayName: 'Run Schedule Coverage Advisor' 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 5d4558ca..2ecb5cea 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -129,7 +129,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.85' + value: '0.8.86' # 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 aaedbcb0..d4458972 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 9c4fe23d..60fabc7c 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 2e32cc7c..8e85f945 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 b05f415d..efb2b9e1 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 b095dac9..26140ab6 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: manage-updatering-tags -# Setup: 02 - Manage UpdateRing Tags +# Config: 02 - Manage UpdateRing Tags # This pipeline creates or updates UpdateRing tags on Azure Local clusters from a CSV file # # WORKFLOW: @@ -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.85' + value: '0.8.86' # 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). @@ -58,7 +58,7 @@ pool: stages: - stage: ManageTags - displayName: 'Setup: 02 - Manage UpdateRing Tags' + displayName: 'Config: 02 - Manage UpdateRing Tags' jobs: - job: ApplyTags displayName: 'Apply UpdateRing Tags' 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 015aa066..2005dcc4 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml index e45dc66d..05944b11 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: setup-validate-and-inventory -# Setup: 01 - Validate Auth and Inventory Azure Local Clusters +# Config: 01 - Validate Auth and Inventory Azure Local Clusters # # PURPOSE: # This is the first step in the setup workflow. It combines two operations: @@ -41,11 +41,11 @@ # cluster-inventory (ClusterUpdateRings.csv + .json + README_Instructions.txt). # # HOW TO RUN (after the file is committed and the pipeline is imported): -# Pipelines -> Setup: 01 - Validate Auth and Inventory Clusters -> Run pipeline. +# Pipelines -> Config: 01 - Validate Auth and Inventory Clusters -> Run pipeline. # # See Automation-Pipeline-Examples/README.md, section 5, for the full setup story. -name: 'Setup: 01 - Validate Auth and Inventory Clusters' +name: 'Config: 01 - Validate Auth and Inventory Clusters' # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers # Add or modify `schedules:` blocks here to enable cron-driven runs. Content @@ -79,7 +79,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.85' + value: '0.8.86' # 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). @@ -155,7 +155,7 @@ stages: inputs: testResultsFormat: 'JUnit' testResultsFiles: '$(Build.ArtifactStagingDirectory)/auth-report/auth-report.xml' - testRunTitle: '[JUnit Debug] Setup: 01 - Authentication Validation' + testRunTitle: '[JUnit Debug] Config: 01 - Authentication Validation' failTaskOnFailedTests: false mergeTestResults: false 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 06b44210..2975fb5b 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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/docs/appendix-pipelines.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md index f10e75d8..2270ece2 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md @@ -2,19 +2,19 @@ > **What you will find here:** A comprehensive reference index for every bundled workflow (GitHub Actions + Azure DevOps twins), with a one-line summary of the job, its triggers, the cmdlets it invokes, what it depends on, and the artefacts it produces. Use this as the at-a-glance reference card after you have read the per-workflow runbook in the main pipeline [README.md](../README.md). -Each pipeline section heading below uses **`Setup: NN`** or **`Fleet: NN`** naming to match the operator-facing workflow names. These identifiers appear in the workflow `name:` fields (GH Actions), ADO pipeline import names, the parent README's section 1.1 table, and these section headings. The execution order in the [end-to-end runbook](../README.md#6-end-to-end-runbook-bring-an-estate-online) is operational - read it for the recommended sequence. +Each pipeline section heading below uses **`Config: NN`** or **`Fleet: NN`** naming to match the operator-facing workflow names. These identifiers appear in the workflow `name:` fields (GH Actions), ADO pipeline import names, the parent README's section 1.1 table, and these section headings. The execution order in the [end-to-end runbook](../README.md#6-end-to-end-runbook-bring-an-estate-online) is operational - read it for the recommended sequence. --- ## Default triggers and schedules (at a glance) -The table below is the ground truth for what each shipped YAML does **out of the box**. Four of the ten pipelines (`fleet-connectivity-status`, `fleet-update-status`, `fleet-health-status`, `apply-updates-schedule-audit`) are pre-wired with `schedule:` (GH) / `schedules:` (ADO) blocks. The remaining six (all Setup workflows + `assess-update-readiness`, `apply-updates`, `monitor-updates`) are manual-only by design (`monitor-updates` ships a commented-out `*/30 * * * *` cron sample for in-wave use). +The table below is the ground truth for what each shipped YAML does **out of the box**. Four of the ten pipelines (`fleet-connectivity-status`, `fleet-update-status`, `fleet-health-status`, `apply-updates-schedule-audit`) are pre-wired with `schedule:` (GH) / `schedules:` (ADO) blocks. The remaining six (all Config workflows + `assess-update-readiness`, `apply-updates`, `monitor-updates`) are manual-only by design (`monitor-updates` ships a commented-out `*/30 * * * *` cron sample for in-wave use). | Pipeline | GitHub Actions trigger | Azure DevOps trigger | Notes | |---|---|---|---| -| **Setup: 01** - Validate Auth and Inventory Clusters | `workflow_dispatch` + weekly `schedule: cron '0 8 * * 0'` (Sun 08:00 UTC) | `trigger: none` + weekly `schedules: cron '0 8 * * 0'` (Sun 08:00 UTC) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Ships a weekly Sunday 08:00 UTC re-validation + inventory refresh; edit or remove the cron in the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to change the cadence. | -| **Setup: 02** - Manage UpdateRing Tags | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | -| **Setup: 03** - Apply-Updates Schedule Coverage Audit | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | +| **Config: 01** - Validate Auth and Inventory Clusters | `workflow_dispatch` + weekly `schedule: cron '0 8 * * 0'` (Sun 08:00 UTC) | `trigger: none` + weekly `schedules: cron '0 8 * * 0'` (Sun 08:00 UTC) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Ships a weekly Sunday 08:00 UTC re-validation + inventory refresh; edit or remove the cron in the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to change the cadence. | +| **Config: 02** - Manage UpdateRing Tags | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | +| **Config: 03** - Apply-Updates Schedule Coverage Audit | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | | **Fleet: 01** - Assess Update Readiness | `workflow_dispatch` only | `trigger: none` (manual only) | **Recommended customisation** - schedule per ring 12-72h before the matching **Fleet: 04** `UpdateStartWindow` (see [Fleet: 01 - Assess Update Readiness](#fleet-01---assess-update-readiness) below for the per-ring pattern + lead-time table). Always-green at pipeline level; per-cluster readiness gaps surface as JUnit `` entries. | | **Fleet: 02** - Fleet Connectivity Status | `workflow_dispatch` + `schedule: cron '30 5 * * *'` (daily 05:30 UTC) | `trigger: none` + `schedules: cron '30 5 * * *'` | Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot with bidirectional node-coverage reconciliation. Runs 30 min before **Fleet: 06** so connectivity issues are visible upstream of update reporting. | | **Fleet: 03** - Sideload Updates (Opt-in) | `workflow_dispatch` only | `trigger: none` (manual only) | Optional pre-stage on-prem update media workflow for disconnected environments. | @@ -31,7 +31,7 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Setup: 01 - Validate Auth and Inventory Azure Local Clusters +## Config: 01 - Validate Auth and Inventory Azure Local Clusters | Aspect | Value | |---|---| @@ -45,13 +45,13 @@ The table below is the ground truth for what each shipped YAML does **out of the | **RBAC** | Whatever the pipeline identity has - the probe itself is read-only and intentionally surfaces both over- and under-grants. | | **Exit conditions** | Pipeline run is green only when every probe passes. A red run means the identity, its role assignment, or its subscription scope is wrong - downstream pipelines will give nonsense results until this passes. | | **ITSM** | Not supported - this is an identity-only probe with no per-cluster failure surface to dispatch on. | -| **Introduced** | v0.7.70 (separate auth + inventory); v0.8.85 (merged into Setup: 01). | +| **Introduced** | v0.7.70 (separate auth + inventory); v0.8.85 (merged into Config: 01). | > **Note**: v0.8.85 consolidates the separate authentication and inventory pipelines into a single `setup-validate-and-inventory.yml` workflow. --- -## Setup: 02 - Manage UpdateRing Tags +## Config: 02 - Manage UpdateRing Tags | Aspect | Value | |---|---| @@ -59,8 +59,8 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `csv_path` (required). | | **Trigger** | Manual only (`workflow_dispatch` / **Run pipeline** button). No schedule shipped - this is a deliberate change-controlled operation that should follow a CSV edit + review. Add a `schedule:` / `schedules:` block if your CSV is auto-generated and you want periodic re-application. | | **Cmdlets invoked** | `Set-AzLocalClusterUpdateRingTag`. | -| **Depends on** | Setup: 01 produces the input CSV (the operator typically downloads `cluster-inventory.csv`, edits ring / window / exclusion values in a PR, then re-uploads it as the input to this pipeline). | -| **Artefacts** | `UpdateRingTag_YYYYMMDD_HHmmss.csv` (per-cluster CSV log: ClusterName, ResourceGroup, SubscriptionId, ResourceId, Action, PreviousTagValue, NewTagValue, Status, Message) and `UpdateRingTag_Results.json` (the same rows in JSON form, written from `-PassThru`) inside the published log artifact. Since v0.8.0 the pipeline job summary tab also renders a **result breakdown table** (Created / Updated / Already in sync / Skipped / WhatIf / Failed) plus a collapsible **per-cluster details** block, at parity with Setup: 03 / Fleet: 01 / Fleet: 04 / Fleet: 06. | +| **Depends on** | Config: 01 produces the input CSV (the operator typically downloads `cluster-inventory.csv`, edits ring / window / exclusion values in a PR, then re-uploads it as the input to this pipeline). | +| **Artefacts** | `UpdateRingTag_YYYYMMDD_HHmmss.csv` (per-cluster CSV log: ClusterName, ResourceGroup, SubscriptionId, ResourceId, Action, PreviousTagValue, NewTagValue, Status, Message) and `UpdateRingTag_Results.json` (the same rows in JSON form, written from `-PassThru`) inside the published log artifact. Since v0.8.0 the pipeline job summary tab also renders a **result breakdown table** (Created / Updated / Already in sync / Skipped / WhatIf / Failed) plus a collapsible **per-cluster details** block, at parity with Config: 03 / Fleet: 01 / Fleet: 04 / Fleet: 06. | | **When to run** | After editing the inventory CSV; whenever ring membership or maintenance windows change. | | **RBAC** | Write to tags only. The built-in **Tag Contributor** role on the cluster scope is sufficient (`Microsoft.Resources/tags/*`). Since v0.7.65, `Set-AzLocalClusterUpdateRingTag` writes via `Microsoft.Resources/tags/default` PATCH, so the broader `Microsoft.AzureStackHCI/clusters/write` is not required. | | **Exit conditions** | Pipeline run is green when every CSV row is processed (added / updated / unchanged). Per-cluster tag-write failures surface in the run log; the pipeline does not currently fail on per-row errors. Re-run after triaging is safe (the cmdlet is idempotent). | @@ -68,7 +68,7 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Setup: 03 - Apply-Updates Schedule Coverage Audit +## Config: 03 - Apply-Updates Schedule Coverage Audit | Aspect | Value | |---|---| @@ -76,7 +76,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `pipeline_path` (file or folder; default `.github/workflows` on GitHub Actions, `.azure-pipelines` on Azure DevOps - the standard consumer locations for the bundled `apply-updates.yml` sample), `lead_time_minutes` (0-60, default 5), `include_untagged` (default false), `module_version` (optional). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled weekly on Mondays at 05:00 UTC (`cron '0 5 * * 1'`). Deliberately runs before the daily `fleet-connectivity-status` (05:30 UTC), `fleet-update-status` (06:00 UTC), and `fleet-health-status` (07:00 UTC) pipelines so its drift annotations land at the top of the operator's Monday-morning inbox. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Test-AzLocalApplyUpdatesScheduleCoverage`, `Get-AzLocalApplyUpdatesScheduleConfig` (when a `schedule.yml` is present in the repo). | -| **Depends on** | Setup: 01 (cluster inventory + `UpdateStartWindow` tags), Setup: 02 (tags applied) for non-trivial output. Runs fine on an empty fleet but the audit is meaningless. | +| **Depends on** | Config: 01 (cluster inventory + `UpdateStartWindow` tags), Config: 02 (tags applied) for non-trivial output. Runs fine on an empty fleet but the audit is meaningless. | | **Artefacts** | `schedule-coverage-audit.xml` (JUnit, one `` per `(UpdateRing, UpdateStartWindow)` pair, uncovered = ``), `schedule-coverage-audit.csv` (full Audit view with `Status` / `Recommendation` columns), `schedule-coverage-matrix.csv` (every distinct `(Ring, Window)` pair with its required cron), `schedule-coverage-recommend.md` (ready-to-paste GH Actions + Azure DevOps cron blocks), markdown step summary. | | **When to run** | Hands-off scheduled. Trigger manually whenever you have just tagged a new ring or changed a maintenance window - see the [end-to-end runbook in section 8.3](../README.md#83-end-to-end-runbook-apply-updates-schedule-coverage-audit). | | **RBAC** | Read-only - `Reader` on the cluster scope plus `Microsoft.ResourceGraph/resources/read`. No write actions are ever taken. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | @@ -94,7 +94,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `subscription_ids` (optional - comma-separated; defaults to every subscription the federated identity can read), `module_version` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 05:30 UTC (`cron '30 5 * * *'`). Deliberately runs 30 minutes before `fleet-update-status` (06:00 UTC) so connectivity issues are visible upstream of update reporting. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalFleetConnectivityStatus`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Setup: 01 should be green at least once so the subscription scope is trusted. This is the upstream "can we see the fleet at all?" probe that other fleet pipelines silently rely on. | +| **Depends on** | None directly. Config: 01 should be green at least once so the subscription scope is trusted. This is the upstream "can we see the fleet at all?" probe that other fleet pipelines silently rely on. | | **Artefacts** | `fleet-connectivity-status.xml` (JUnit, one `` per cluster), `fleet-cluster-connectivity.csv` (per-cluster reconciliation), `fleet-arc-status-summary.csv` (per-cluster Arc agent counts), `fleet-arc-non-connected-machines.csv` (per-machine triage list), `fleet-physical-nics.csv` (NIC issues only), `fleet-physical-nic-all.csv` (full NIC inventory), `fleet-physical-nic-stats.csv` (histogram by `NicType` + `NicStatus`), `fleet-arb-status.csv` (Azure Resource Bridge state), markdown job summary including the *"How to interpret + act on a non-zero reconciliation"* guidance subsection. | | **When to run** | Hands-off scheduled. Trigger manually whenever the apply-updates chain returns "zero ready clusters" or the fleet count drops unexpectedly - Fleet: 02 is the upstream "can we see the fleet at all?" probe. | | **RBAC** | Read-only - `Reader` plus `Microsoft.ResourceGraph/resources/read`, `Microsoft.AzureStackHCI/edgeDevices/read`, `Microsoft.HybridCompute/machines/read`, and `Microsoft.ResourceConnector/appliances/read`. All four already live in the **`Azure Stack HCI Update Operator (custom)`** custom role definition shipped in [section 3.1](../README.md#31-custom-role-azure-stack-hci-update-operator-custom). | @@ -116,7 +116,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `update_ring` (required), `throttle_limit` (optional). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule is shipped** because the `update_ring` input is required and there is no single ring value that would be correct for every consumer. See the **recommended customisation note below** for the per-ring scheduling pattern + lead-time guidance. | | **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Test-AzLocalClusterHealth`. | -| **Depends on** | Setup: 01 (an `UpdateRing` tag must exist on at least one cluster to scope the readiness query) and Setup: 02 (to apply that tag at scale). | +| **Depends on** | Config: 01 (an `UpdateRing` tag must exist on at least one cluster to scope the readiness query) and Config: 02 (to apply that tag at scale). | | **Artefacts** | `readiness.xml`, `readiness.csv`, `health-blocking.xml`, `health-blocking.csv`. | | **When to run** | 12-72 hours before each Fleet: 04 wave for the same ring (12-24h for small rings, 48-72h for large rings of 50+ clusters) - long enough for an operator to triage any `` entries before the maintenance window opens, short enough that the readiness signal is still fresh. See the **recommended customisation note below** for the cron pattern. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | @@ -133,7 +133,7 @@ The table below is the ground truth for what each shipped YAML does **out of the > | Medium (10-50 clusters) | 24-48 hours ahead | `'0 2 * * 4'` (Thu 02:00 ahead of a Sat 02:00 Fleet: 04) | Lets you raise tickets / engage cluster owners on per-cluster issues before the weekend. | > | Large (50+ clusters) | 48-72 hours ahead | `'0 2 * * 3'` (Wed 02:00 ahead of a Sat 02:00 Fleet: 04) | Allows time to escalate, swap clusters into a deferral ring (`UpdateExcluded=True`), or stage parallel mitigation. | > -> **Known gap**: the Setup: 03 schedule-coverage audit currently validates Fleet: 04 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Fleet: 01 cron is correctly anchored to a Fleet: 04 cron. Pair Fleet: 01 and Fleet: 04 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. +> **Known gap**: the Config: 03 schedule-coverage audit currently validates Fleet: 04 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Fleet: 01 cron is correctly anchored to a Fleet: 04 cron. Pair Fleet: 01 and Fleet: 04 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. ## Fleet: 03 - Sideload Updates (Opt-in) @@ -148,7 +148,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule ships** because the pipeline requires an on-prem self-hosted runner labelled `azlocal-sideload` (GH) / a self-hosted agent in a pool with the `azlocal-sideload` demand (ADO) that most repos do not have. Once the runner/agent is online, uncomment the bundled `*/30 * * * *` cron inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to drive the state machine. | | **Cmdlets invoked** | `Resolve-AzLocalSideloadPlan` (selects clusters whose next apply window is within `SIDELOAD_LEAD_DAYS`), `Invoke-AzLocalSideloadUpdate` (the re-entrant state machine), `Export-AzLocalSideloadStatusReport` + `Add-AzLocalSideloadStepSummary` (JUnit + Markdown summary). | | **Re-entrant state machine** | Each run advances every in-scope cluster by **one** transition and exits: `Planned -> Copying -> Copied -> Verified -> Imported -> SideloadFlagged`. The multi-hour copy itself runs in a **detached Windows Scheduled Task** (driven by `Tools/Invoke-AzLocalSideloadCopyTask.ps1`) so no pipeline run is ever long-lived. Re-running is always safe; a stale `Copying` heartbeat (> `SIDELOAD_HEARTBEAT_STALE_MINUTES`) is re-driven. | -| **Depends on** | Setup: 01 (`UpdateRing` + sideload tags present, including `UpdateAuthAccountId`), Setup: 02 (to apply those tags at scale), and a populated `sideload-auth-map.csv` + `sideload-catalog.yml`. The downstream Fleet: 04 apply consumes the `UpdateSideloaded=True` flag this pipeline sets. | +| **Depends on** | Config: 01 (`UpdateRing` + sideload tags present, including `UpdateAuthAccountId`), Config: 02 (to apply those tags at scale), and a populated `sideload-auth-map.csv` + `sideload-catalog.yml`. The downstream Fleet: 04 apply consumes the `UpdateSideloaded=True` flag this pipeline sets. | | **Artefacts** | `sideload-status.xml` (JUnit, one cluster per test), `sideload-status.csv`, plus per-run copy/verify logs under the shared `SIDELOAD_STATE_ROOT\logs`. | | **Shared state** | `SIDELOAD_STATE_ROOT` must be a UNC path that every runner/agent can read+write (`state\`, `logs\`, `cache\`). Verified media is cached under `SIDELOAD_CACHE_ROOT` (defaults to `\cache`) so a bundle is downloaded + hashed once and reused across clusters. | | **RBAC** | ARG fleet read + Key Vault secret read + `UpdateSideloaded` tag write for the pipeline identity; a separate Active Directory `[pscredential]` (built from two Key Vault secrets named in the auth-map row) is used for the cluster WinRM remoting and `Add-SolutionUpdate`. | @@ -173,7 +173,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `update_ring` (required), `update_name` (optional - leave blank for latest), `dry_run` (optional), `throttle_limit` (optional). **v0.7.4 adds** `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, defaults preserve existing behaviour). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule is shipped** - you must add one. See the **mandatory customisation note below** and the schedule-alignment guidance in [section 8](../README.md#8-scheduling-maintenance-windows-and-change-freeze-periods). | | **Cmdlets invoked** | `Get-AzLocalApplyUpdatesScheduleConfig`, `Get-AzLocalClusterUpdateReadiness`, `Start-AzLocalClusterUpdate`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | Setup: 01 (`UpdateRing` tags present), Fleet: 01 (readiness reviewed for the wave). Setup: 03 (schedule coverage audit) should be green so the cron(s) that start this pipeline actually fire at every tagged `UpdateStartWindow`. | +| **Depends on** | Config: 01 (`UpdateRing` tags present), Fleet: 01 (readiness reviewed for the wave). Config: 03 (schedule coverage audit) should be green so the cron(s) that start this pipeline actually fire at every tagged `UpdateStartWindow`. | | **Artefacts** | `update-results.xml` (JUnit, one cluster per test), `update-logs/*` (CSV + detail). When ITSM is enabled: `itsm-results.csv`, `itsm-results.xml`. | | **When to run** | During the maintenance window for each ring, after the Fleet: 01 readiness assessment is reviewed. | | **RBAC** | Write to clusters required. Covered by the `Azure Stack HCI Update Operator (custom)` custom role (`Microsoft.AzureStackHCI/clusters/updates/apply/action` + cluster-update reads). | 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 7cdb7454..6df7771b 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 @@ -31,7 +31,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: 'Setup: 03 - Apply-Updates Schedule Coverage Audit' +name: 'Config: 03 - Apply-Updates Schedule Coverage Audit' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' 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 36d408ee..d81ec4c2 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -148,7 +148,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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 33f0910d..924a8ca5 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 f14a80f4..0ce0ad0a 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 ea52f0e7..1a988bfe 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 9a17dafe..d267508c 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 b5577735..c9ed17e5 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 @@ -22,7 +22,7 @@ # Workflow name carries the same Setup prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the setup and operational pipelines in execution order. -name: 'Setup: 02 - Manage UpdateRing Tags' +name: 'Config: 02 - Manage UpdateRing Tags' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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 546c8f30..087fa286 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml index cf7deddb..4aa08f3b 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: setup-validate-and-inventory -# Setup: 01 - Validate Auth and Inventory Azure Local Clusters +# Config: 01 - Validate Auth and Inventory Azure Local Clusters # # PURPOSE: # This is the first step in the setup workflow. It combines two operations: @@ -42,7 +42,7 @@ # # See Automation-Pipeline-Examples/README.md for full setup story. -name: 'Setup: 01 - Validate Auth and Inventory Clusters' +name: 'Config: 01 - Validate Auth and Inventory Clusters' on: workflow_dispatch: @@ -73,7 +73,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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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). @@ -171,7 +171,7 @@ jobs: if: always() uses: dorny/test-reporter@v3 with: - name: '[JUnit Debug] Setup: 01 - Authentication Validation' + name: '[JUnit Debug] Config: 01 - Authentication Validation' path: ./reports/auth-report.xml reporter: java-junit fail-on-error: false 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 aef5becc..b5ee2a81 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.85' + GENERATED_AGAINST_MODULE_VERSION: '0.8.86' # 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/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 01327d92..3a3b643d 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.85' + ModuleVersion = '0.8.86' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -317,6 +317,8 @@ # ReleaseNotes of this module ReleaseNotes = @' +## Version 0.8.86 - Patch: renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet); the previous `Setup:` prefix sorted AFTER `Fleet:`. Only the operator-facing `name:` (GitHub Actions) / `displayName:` (Azure DevOps) fields change - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, prune logic, and all cmdlet behaviour are unchanged. Bundled pipeline README + appendix docs updated to match. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.85'` to `'0.8.86'`. + ## Version 0.8.85 - Patch: introduces Setup/Fleet naming in bundled pipeline templates, adds merged GitHub onboarding workflow `setup-validate-and-inventory.yml`, and updates pipeline refresh tooling with optional deprecated-file pruning guarded by AZLOCAL-PIPELINE-ID verification. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.84'` to `'0.8.85'`. ## Version 0.8.83 - Patch: fix-forward for v0.8.82 Item-5. The Step.08 `UpdateLastAttempt` reconciliation in `Export-AzLocalUpdateRunMonitorReport` reads `$inv.tags` from `Get-AzLocalClusterInventory`, but the v0.8.82 inventory projection did not carry the raw ARM `tags` bag - so the "Recent update attempts with no observable updateRun" section was silently always empty in production. v0.8.83 surfaces the raw `tags` bag on every inventory row (in-memory only; the CSV / JSON export keeps its explicit `$selectColumns` whitelist - new regression test asserts both). Also wires `attempts_without_run` into the GitHub Actions `monitor-updates.yml` `jobs.outputs:` block (ADO `Set-AzLocalPipelineOutput` auto-publishes so only an ADO docstring refresh needed), and corrects the `Export-AzLocalUpdateRunMonitorReport` "6 step outputs" docstring to "7 step outputs". No public API change. Export count unchanged (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.82'` to `'0.8.83'`. diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index 9b8ffc56..f593905f 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.85' +$script:ModuleVersion = '0.8.86' $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 de1dac21..a591afa2 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,36 @@ 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.86] - 2026-06-16 + +Patch release. Renames the three onboarding pipeline templates from `Setup: 0N` +to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines +list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` +operational workflows. Both surfaces sort alphabetically by the workflow `name:` +/ definition name, and `C` (Config) sorts before `F` (Fleet); the previous +`Setup:` prefix sorted *after* `Fleet:`. No public API change. Export count +unchanged (still 60). + +### Changed + +- Renames the operator-facing display names of the three onboarding workflows + from `Setup: 01/02/03` to `Config: 01/02/03` across both GitHub Actions + (`name:`) and Azure DevOps (`displayName:` / `name:`) templates. Filenames, + `AZLOCAL-PIPELINE-ID` values, aliases, and `-PruneDeprecated` logic are + unchanged - this is a display-name-only change. +- Updates the bundled pipeline `README.md` and `docs/appendix-pipelines.md` + (section headings, dependency cross-references, naming rationale) to use the + `Config: NN` naming. +- Clarifies in the bundled pipeline `README.md` (step 3, "federate the + workflow") that **GitHub environments are optional**: the branch-scoped + federated credential is all OIDC requires, so leaving the `environment` input + blank is the supported minimal setup. Environments are a governance wrapper + (approval gates, deployment-branch restrictions, wait timers, per-environment + identities) and only need environment-scoped federated credentials when a run + actually names one. +- `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.85'` to `'0.8.86'` + across all bundled GitHub Actions and Azure DevOps pipeline templates. + ## [0.8.85] - 2026-06-16 Patch release. Introduces Setup/Fleet naming in bundled pipeline templates, diff --git a/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 b/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 index 60407e5f..5db6430d 100644 --- a/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 +++ b/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 @@ -82,7 +82,7 @@ function Get-AzLocalPipelineManifest { # # v0.8.85: the two standalone onboarding workflows authentication-test.yml # (DisplayStep 0) and inventory-clusters.yml (DisplayStep 1) were merged - # into a single setup-validate-and-inventory.yml (Setup: 01). Its Aliases + # into a single setup-validate-and-inventory.yml (Config: 01). Its Aliases # list BOTH superseded filenames (and their Step.N_ ancestors) so # Update-AzLocalPipelineExample can rename-match a customer who still has # either old file and carry their schedule CRONs into the merged workflow. diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 8d1e2716..66990113 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.85 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.85) +**Latest Version:** v0.8.86 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.86) > 📢 **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.85](#whats-new-in-v0885) +- [What's New in v0.8.86](#whats-new-in-v0886) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,24 +86,23 @@ 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.85 +## What's New in v0.8.86 -**Pipeline UX consolidation release.** Introduces Setup/Fleet naming in bundled pipelines, adds the merged GitHub onboarding workflow `setup-validate-and-inventory.yml` (Setup: 01), and updates pipeline refresh tooling to safely handle deprecated workflow cleanup with pipeline-ID verification. Also bumps bundled pipeline `GENERATED_AGAINST_MODULE_VERSION` pins to `0.8.85`. +**Pipeline sidebar-ordering fix.** Renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) lists the onboarding / configuration workflows *ahead of* the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet) - the previous `Setup:` prefix sorted *after* `Fleet:`. -### Added +### Changed -- Bundled GitHub workflows now surface Setup/Fleet ordering and include `Setup: 01 - Validate Auth and Inventory Clusters`. -- Legacy GitHub onboarding workflows (`authentication-test.yml`, `inventory-clusters.yml`) are superseded by the merged Setup workflow in the bundled set. -- `Copy-AzLocalPipelineExample` and `Update-AzLocalPipelineExample` gain optional `-PruneDeprecated` cleanup for superseded workflow files, with AZLOCAL-PIPELINE-ID verification before removal. +- The three onboarding workflows are renamed `Config: 01 - Validate Auth and Inventory Clusters`, `Config: 02 - Manage UpdateRing Tags`, and `Config: 03 - Apply-Updates Schedule Coverage Audit` (was `Setup: 0N`). Display-name (`name:` / `displayName:`) change only - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, and `-PruneDeprecated` logic are unchanged. +- Bundled pipeline `README.md` + `docs/appendix-pipelines.md` updated to use `Config: NN` naming throughout. ### Notes - **No new exports** (count unchanged at 60). -- **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.84` to `0.8.85` across bundled pipeline templates. +- **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.85` to `0.8.86` across bundled pipeline templates. > Previous release notes have moved into the [Release History](#release-history) appendix at the bottom of this document. -See [CHANGELOG.md](CHANGELOG.md) for full release details. See [`What's New in v0.8.84`](#whats-new-in-v0884) in the Release History for the previous release. +See [CHANGELOG.md](CHANGELOG.md) for full release details. See [`What's New in v0.8.85`](#whats-new-in-v0885) in the Release History for the previous release. ## Files @@ -582,7 +581,11 @@ 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.84** stay above under [`What's New in v0.8.84`](#whats-new-in-v0884). +The most recent release notes for **v0.8.86** stay above under [`What's New in v0.8.86`](#whats-new-in-v0886). + +### What's New in v0.8.85 + +**Pipeline UX consolidation release.** Introduces Setup/Fleet naming in bundled pipelines, adds the merged GitHub onboarding workflow `setup-validate-and-inventory.yml` (Setup: 01), and updates pipeline refresh tooling to safely handle deprecated workflow cleanup with pipeline-ID verification. Also bumps bundled pipeline `GENERATED_AGAINST_MODULE_VERSION` pins to `0.8.85`. (Superseded in v0.8.86, which renames the `Setup: 0N` workflows to `Config: 0N`.) See [CHANGELOG.md](CHANGELOG.md#0885---2026-06-16) for the full v0.8.85 entry. ### What's New in v0.8.83 diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 83999ed2..69b929e2 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.85' { - $script:ModuleInfo.Version | Should -Be '0.8.85' + It 'Should have version 0.8.86' { + $script:ModuleInfo.Version | Should -Be '0.8.86' } It 'Module version constants are in sync between .psm1 and .psd1' { diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 0bd034f5..947cb7dd 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -36,3 +36,4 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T10:13:27Z","0.8.85","1295","1256","0","1","49.91","48.62","3.18","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T10:25:03Z","0.8.85","1295","38","0","0","617.07","615.99","3.31","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T11:05:53Z","0.8.85","1305","1265","1","1","148.49","145.99","15.16","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T11:43:15Z","0.8.86","1305","1266","0","1","77.33","75.24","3.83","Invoke-Tests.ps1","5.1.26100.8730" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index 15cf1f29..dfa3a4e5 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,19 @@ > > **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.84 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0884) `What's New in v0.8.84` section. +> **For v0.8.86 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0886) `What's New in v0.8.86` section. + +--- + +### What's New in v0.8.86 + +Patch release. Renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet); the previous `Setup:` prefix sorted *after* `Fleet:`. Display-name (`name:` / `displayName:`) change only - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, and `-PruneDeprecated` logic are unchanged. Bundled pipeline README + appendix docs updated to match. No public API change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.85` to `0.8.86` across all bundled pipeline templates. + +--- + +### What's New in v0.8.85 + +Patch release. Introduces Setup/Fleet naming in bundled pipeline templates, adds the merged GitHub onboarding workflow `setup-validate-and-inventory.yml`, and updates pipeline refresh tooling with optional deprecated-file pruning guarded by AZLOCAL-PIPELINE-ID verification. (Superseded in v0.8.86, which renames the `Setup: 0N` workflows to `Config: 0N`.) No public API change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.84` to `0.8.85`. --- From 6f01258db47ad2732171b844336659797da75103 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 13:04:33 +0100 Subject: [PATCH 13/17] ci: disable hosted Pester workflow (comment out pull_request + push triggers; keep workflow_dispatch). Suite is run locally before every push. --- .../AzLocal.UpdateManagement-pester.yml | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/AzLocal.UpdateManagement-pester.yml b/.github/workflows/AzLocal.UpdateManagement-pester.yml index 61e8c43b..6a23b73e 100644 --- a/.github/workflows/AzLocal.UpdateManagement-pester.yml +++ b/.github/workflows/AzLocal.UpdateManagement-pester.yml @@ -10,17 +10,22 @@ name: AzLocal.UpdateManagement - Pester # Cost mitigation: path-filtered to AzLocal.UpdateManagement/** only, and only # fires on the three useful PR activities (opened, synchronize, reopened). +# DISABLED: automatic CI triggers are commented out because the Pester suite is +# run locally before every push (see AzLocal.UpdateManagement/Tests/Invoke-Tests.ps1). +# To re-enable hosted CI, uncomment the pull_request + push blocks below. +# workflow_dispatch is kept so the workflow remains valid (GitHub requires at +# least one trigger) and can still be run on demand from the Actions tab. on: - pull_request: - types: [opened, synchronize, reopened] - paths: - - 'AzLocal.UpdateManagement/**' - - '.github/workflows/AzLocal.UpdateManagement-pester.yml' - push: - branches: [main] - paths: - - 'AzLocal.UpdateManagement/**' - - '.github/workflows/AzLocal.UpdateManagement-pester.yml' + # pull_request: + # types: [opened, synchronize, reopened] + # paths: + # - 'AzLocal.UpdateManagement/**' + # - '.github/workflows/AzLocal.UpdateManagement-pester.yml' + # push: + # branches: [main] + # paths: + # - 'AzLocal.UpdateManagement/**' + # - '.github/workflows/AzLocal.UpdateManagement-pester.yml' workflow_dispatch: concurrency: From fc7bf6c6a44009e4038dd261b607c8d6d591a6cc Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 13:47:43 +0100 Subject: [PATCH 14/17] v0.8.87: rename 'Orphan ARBs' section to 'Non-Azure Local and/or Orphan ARB appliances' New-AzLocalFleetConnectivityStatusSummary (Fleet: 02) markdown output: rename the Orphan ARBs section, add intro line, and expand the caveat to note that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM. Adds investigate-before-acting guidance. KPI note, causes list, and cluster-table cross-reference updated to match. Output text only; no API or behavioural change. Lock-step version bump 0.8.86 -> 0.8.87: psd1 (ModuleVersion + ReleaseNotes), psm1 ($script:ModuleVersion), CHANGELOG, both README sections + appendix, Tests assertion, docs/release-history.md, and all 20 GENERATED_AGAINST_MODULE_VERSION pins. Full Pester suite green (1266 passed, 0 failed, 1 skipped, 38 not-run). --- .../apply-updates-schedule-audit.yml | 2 +- .../azure-devops/apply-updates.yml | 2 +- .../azure-devops/assess-update-readiness.yml | 2 +- .../fleet-connectivity-status.yml | 2 +- .../azure-devops/fleet-health-status.yml | 2 +- .../azure-devops/fleet-update-status.yml | 2 +- .../azure-devops/manage-updatering-tags.yml | 2 +- .../azure-devops/monitor-updates.yml | 2 +- .../setup-validate-and-inventory.yml | 2 +- .../azure-devops/sideload-updates.yml | 2 +- .../apply-updates-schedule-audit.yml | 2 +- .../github-actions/apply-updates.yml | 2 +- .../assess-update-readiness.yml | 2 +- .../fleet-connectivity-status.yml | 2 +- .../github-actions/fleet-health-status.yml | 2 +- .../github-actions/fleet-update-status.yml | 2 +- .../github-actions/manage-updatering-tags.yml | 2 +- .../github-actions/monitor-updates.yml | 2 +- .../setup-validate-and-inventory.yml | 2 +- .../github-actions/sideload-updates.yml | 2 +- .../AzLocal.UpdateManagement.psd1 | 4 ++- .../AzLocal.UpdateManagement.psm1 | 2 +- AzLocal.UpdateManagement/CHANGELOG.md | 27 +++++++++++++++++++ ...-AzLocalFleetConnectivityStatusSummary.ps1 | 13 +++++---- AzLocal.UpdateManagement/README.md | 23 +++++++++------- .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 8 +++--- .../Tests/test-run-timings.csv | 1 + .../docs/release-history.md | 8 +++++- 28 files changed, 85 insertions(+), 41 deletions(-) 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 f3cae97b..1839a17b 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' 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 2ecb5cea..2c0e21fc 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -129,7 +129,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.86' + value: '0.8.87' # 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 d4458972..a9430a47 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 60fabc7c..3248b595 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 8e85f945..22dc9bbb 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 efb2b9e1..ec4ce3e8 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 26140ab6..f8cd4a19 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.86' + value: '0.8.87' # 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 2005dcc4..bb416a44 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml index 05944b11..a99a0f41 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml @@ -79,7 +79,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.86' + value: '0.8.87' # 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/sideload-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml index 2975fb5b..aabe77bf 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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/github-actions/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml index 6df7771b..67fe3898 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' 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 d81ec4c2..340b7af5 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -148,7 +148,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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 924a8ca5..65e8acb6 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 0ce0ad0a..9302b219 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 1a988bfe..16d7e647 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 d267508c..31206f5b 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 c9ed17e5..ac989219 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 087fa286..49c494d6 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml index 4aa08f3b..2505e4d5 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml @@ -73,7 +73,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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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 b5ee2a81..9db0ec10 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.86' + GENERATED_AGAINST_MODULE_VERSION: '0.8.87' # 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/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 3a3b643d..986c733c 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.86' + ModuleVersion = '0.8.87' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -317,6 +317,8 @@ # ReleaseNotes of this module ReleaseNotes = @' +## Version 0.8.87 - Patch: renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM, so a listed appliance may be a healthy bridge for a non-Azure Local platform. Adds investigate-before-acting guidance (check resource type, custom location, platform served; do not delete until confirmed orphaned). KPI note, causes list, and cluster-table cross-reference updated to match. Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. + ## Version 0.8.86 - Patch: renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet); the previous `Setup:` prefix sorted AFTER `Fleet:`. Only the operator-facing `name:` (GitHub Actions) / `displayName:` (Azure DevOps) fields change - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, prune logic, and all cmdlet behaviour are unchanged. Bundled pipeline README + appendix docs updated to match. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.85'` to `'0.8.86'`. ## Version 0.8.85 - Patch: introduces Setup/Fleet naming in bundled pipeline templates, adds merged GitHub onboarding workflow `setup-validate-and-inventory.yml`, and updates pipeline refresh tooling with optional deprecated-file pruning guarded by AZLOCAL-PIPELINE-ID verification. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.84'` to `'0.8.85'`. diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index f593905f..bf79b8de 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.86' +$script:ModuleVersion = '0.8.87' $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 a591afa2..15167bc9 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,33 @@ 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.87] - 2026-06-16 + +Patch release. Renames the "Orphan ARBs" section in the +`New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity +Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and +adds an explicit caveat that an Arc resource bridge with no matching in-scope +Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is +also used by VMware vSphere and System Center Virtual Machine Manager (SCVMM) +Arc-enabled deployments, so a listed appliance may be a healthy, in-use bridge +for a non-Azure Local platform. Output text only - no public API change. Export +count unchanged (still 60). + +### Changed + +- `New-AzLocalFleetConnectivityStatusSummary` renames the `### Orphan ARBs (no + matching cluster in scope)` section heading to `### Non-Azure Local and/or + Orphan ARB appliances`, adds an intro line ("These ARB appliances do not have + a matching Azure Local instance (cluster) in scope."), and expands the italic + caveat to call out VMware vSphere and SCVMM Arc resource bridges and to add + investigate-before-acting guidance (check resource type, custom location, and + the platform served; do not delete an ARB until confirmed genuinely orphaned). +- The KPI-table "Orphan ARBs" note, the "how to interpret" causes list (new + leading bullet about non-Azure Local resource bridges), and the cluster-table + cross-reference now point at the renamed section. +- `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all + bundled pipeline templates. + ## [0.8.86] - 2026-06-16 Patch release. Renames the three onboarding pipeline templates from `Setup: 0N` diff --git a/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 b/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 index 8d61a059..e7ebe47b 100644 --- a/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 +++ b/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 @@ -334,7 +334,7 @@ function New-AzLocalFleetConnectivityStatusSummary { [void]$sb.AppendLine("| ARBs in scope | $arbTotal | One row per ``resourceconnector/appliances`` (multi-cluster-per-RG collapsed via summarize/make_set) |") [void]$sb.AppendLine("| Clusters with an ARB | $clustersWithArb | Clusters matched to an ARB by ClusterId |") [void]$sb.AppendLine("| Clusters without an ARB | $clustersWithoutArb | Clusters that show ``_(no ARB)_`` in the per-cluster table below |") - [void]$sb.AppendLine("| Orphan ARBs | $($orphanArbs.Count) | ARBs whose RG contains no in-scope HCI cluster (listed in the Orphan ARBs section below when > 0) |") + [void]$sb.AppendLine("| Orphan ARBs | $($orphanArbs.Count) | ARBs whose RG contains no in-scope HCI cluster (listed in the 'Non-Azure Local and/or Orphan ARB appliances' section below when > 0; may include VMware/SCVMM resource bridges) |") [void]$sb.AppendLine('') # 5c. "How to interpret + act" static prose @@ -379,8 +379,9 @@ function New-AzLocalFleetConnectivityStatusSummary { [void]$sb.AppendLine('') [void]$sb.AppendLine('**``Orphan ARBs`` > 0** _(ARBs whose RG contains no in-scope HCI cluster)_') [void]$sb.AppendLine('') - [void]$sb.AppendLine('Inspect the ''Orphan ARBs (no matching cluster in scope)'' section below for the full resource IDs. Causes:') + [void]$sb.AppendLine('Inspect the ''Non-Azure Local and/or Orphan ARB appliances'' section below for the full resource IDs. Causes:') [void]$sb.AppendLine('') + [void]$sb.AppendLine('- **ARB serves a different platform** - Azure Arc resource bridge is also used by VMware vSphere and SCVMM; the appliance may be a healthy bridge for a non-Azure Local deployment, not an orphan. Confirm the platform before acting.') [void]$sb.AppendLine('- **Cluster deleted but its ARB was not cleaned up** - delete the orphan ARB.') [void]$sb.AppendLine('- **ARB in a different sub/RG from the cluster it serves** - scope-list drift; correct the input parameter list to include the matching cluster.') [void]$sb.AppendLine('- **ARB created for a cluster excluded from this run** (e.g. environment filter) - verify the filter; no action if expected.') @@ -389,7 +390,7 @@ function New-AzLocalFleetConnectivityStatusSummary { # 5d. Cluster Connectivity (with ARB Status) per-cluster table [void]$sb.AppendLine('### Cluster Connectivity (with ARB Status)') [void]$sb.AppendLine('') - [void]$sb.AppendLine('_One row per cluster, left-joined to the cluster''s Azure Resource Bridge (ARB) appliance status. Each cluster has at most one ARB. ARBs without a matching cluster in scope are listed separately as ''Orphan ARBs''._') + [void]$sb.AppendLine('_One row per cluster, left-joined to the cluster''s Azure Resource Bridge (ARB) appliance status. Each cluster has at most one ARB. ARBs without a matching cluster in scope are listed separately under ''Non-Azure Local and/or Orphan ARB appliances''._') [void]$sb.AppendLine('') if ($ClusterRows.Count -eq 0) { [void]$sb.AppendLine('*No clusters returned.*') @@ -429,9 +430,11 @@ function New-AzLocalFleetConnectivityStatusSummary { # 5e. Orphan ARBs table (conditional) if ($orphanArbs.Count -gt 0) { [void]$sb.AppendLine('') - [void]$sb.AppendLine('### Orphan ARBs (no matching cluster in scope)') + [void]$sb.AppendLine('### Non-Azure Local and/or Orphan ARB appliances') [void]$sb.AppendLine('') - [void]$sb.AppendLine('_ARB appliances whose resource group does not contain any HCI cluster visible to this run. May indicate a stale appliance, or a cluster outside the configured scope._') + [void]$sb.AppendLine('These ARB appliances do not have a matching Azure Local instance (cluster) in scope.') + [void]$sb.AppendLine('') + [void]$sb.AppendLine('_ARB appliances whose resource group does not contain any HCI cluster visible to this run. This does **not** by itself mean the appliance is stale or orphaned: Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. Other causes include a stale appliance left behind after a cluster was deleted, or a cluster that is outside the configured scope of this run. **Fully investigate each appliance - check its resource type, associated custom location, and the platform it serves - before taking any action; do not delete an ARB until you have confirmed it is genuinely orphaned.**_') [void]$sb.AppendLine('') [void]$sb.AppendLine('| ARB | Status | Resource Group | Days Since LastModified |') [void]$sb.AppendLine('|-----|--------|----------------|--------------------------|') diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 66990113..970269be 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.86 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.86) +**Latest Version:** v0.8.87 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.87) > 📢 **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.86](#whats-new-in-v0886) +- [What's New in v0.8.87](#whats-new-in-v0887) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,23 +86,24 @@ 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.86 +## What's New in v0.8.87 -**Pipeline sidebar-ordering fix.** Renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) lists the onboarding / configuration workflows *ahead of* the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet) - the previous `Setup:` prefix sorted *after* `Fleet:`. +**Fleet Connectivity Status output clarification.** Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to **"Non-Azure Local and/or Orphan ARB appliances"** and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is *not necessarily orphaned*. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so a listed appliance may be a healthy, in-use resource bridge for a non-Azure Local platform. ### Changed -- The three onboarding workflows are renamed `Config: 01 - Validate Auth and Inventory Clusters`, `Config: 02 - Manage UpdateRing Tags`, and `Config: 03 - Apply-Updates Schedule Coverage Audit` (was `Setup: 0N`). Display-name (`name:` / `displayName:`) change only - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, and `-PruneDeprecated` logic are unchanged. -- Bundled pipeline `README.md` + `docs/appendix-pipelines.md` updated to use `Config: NN` naming throughout. +- The section heading `### Orphan ARBs (no matching cluster in scope)` becomes `### Non-Azure Local and/or Orphan ARB appliances`, with an intro line and an expanded caveat calling out VMware vSphere / SCVMM resource bridges and adding investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). +- The KPI-table "Orphan ARBs" note, the "how to interpret" causes list (new leading bullet about non-Azure Local resource bridges), and the cluster-table cross-reference now point at the renamed section. ### Notes +- **Output text only** - no public API, parameter, or behavioural change. - **No new exports** (count unchanged at 60). -- **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.85` to `0.8.86` across bundled pipeline templates. +- **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.86` to `0.8.87` across bundled pipeline templates. > Previous release notes have moved into the [Release History](#release-history) appendix at the bottom of this document. -See [CHANGELOG.md](CHANGELOG.md) for full release details. See [`What's New in v0.8.85`](#whats-new-in-v0885) in the Release History for the previous release. +See [CHANGELOG.md](CHANGELOG.md) for full release details. See [`What's New in v0.8.86`](#whats-new-in-v0886) in the Release History for the previous release. ## Files @@ -581,7 +582,11 @@ 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.86** stay above under [`What's New in v0.8.86`](#whats-new-in-v0886). +The most recent release notes for **v0.8.87** stay above under [`What's New in v0.8.87`](#whats-new-in-v0887). + +### What's New in v0.8.86 + +**Pipeline sidebar-ordering fix.** Renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) lists the onboarding / configuration workflows *ahead of* the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet) - the previous `Setup:` prefix sorted *after* `Fleet:`. Display-name change only - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, and `-PruneDeprecated` logic are unchanged. See [CHANGELOG.md](CHANGELOG.md#0886---2026-06-16) for the full v0.8.86 entry. ### What's New in v0.8.85 diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 69b929e2..d042ea37 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.86' { - $script:ModuleInfo.Version | Should -Be '0.8.86' + It 'Should have version 0.8.87' { + $script:ModuleInfo.Version | Should -Be '0.8.87' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -12664,7 +12664,7 @@ Describe 'Function: New-AzLocalFleetConnectivityStatusSummary' { } It 'Does NOT include the Orphan ARBs section when there are no orphans' { - $script:mdEmpty | Should -Not -Match '### Orphan ARBs' + $script:mdEmpty | Should -Not -Match '### Non-Azure Local and/or Orphan ARB appliances' } It 'Stays well under the 21K cap with empty inputs' { @@ -12698,7 +12698,7 @@ Describe 'Function: New-AzLocalFleetConnectivityStatusSummary' { } It 'Includes the Orphan ARBs section heading' { - $script:mdOrphan | Should -Match '### Orphan ARBs \(no matching cluster in scope\)' + $script:mdOrphan | Should -Match '### Non-Azure Local and/or Orphan ARB appliances' } It 'Lists the orphan ARB by name' { diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 947cb7dd..b0530be3 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -37,3 +37,4 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T10:25:03Z","0.8.85","1295","38","0","0","617.07","615.99","3.31","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T11:05:53Z","0.8.85","1305","1265","1","1","148.49","145.99","15.16","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T11:43:15Z","0.8.86","1305","1266","0","1","77.33","75.24","3.83","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T12:47:00Z","0.8.87","1305","1266","0","1","116.33","113.88","4.43","Invoke-Tests.ps1","5.1.26100.8730" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index dfa3a4e5..48104a57 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,13 @@ > > **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.86 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0886) `What's New in v0.8.86` section. +> **For v0.8.87 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0887) `What's New in v0.8.87` section. + +--- + +### What's New in v0.8.87 + +Patch release. Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. --- From c46da8f581fa0f0bc998a0f7c4fb8af5301d092d Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 14:00:16 +0100 Subject: [PATCH 15/17] v0.8.87: collapse Cluster Connectivity (with ARB Status) table by default New-AzLocalFleetConnectivityStatusSummary now wraps the per-cluster Cluster Connectivity (with ARB Status) table in a collapsed
block with an 'Expand to view clusters' summary, so the Fleet: 02 step summary stays compact. Added a Pester assertion for the details/summary wrapper. Output text only; no API or behavioural change. Release notes (psd1/CHANGELOG/README/release-history) updated within the in-flight v0.8.87. Full Pester suite green (1267 passed, 0 failed, 1 skipped). --- AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 | 2 +- AzLocal.UpdateManagement/CHANGELOG.md | 3 +++ .../Public/New-AzLocalFleetConnectivityStatusSummary.ps1 | 5 +++++ AzLocal.UpdateManagement/README.md | 1 + .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 5 +++++ AzLocal.UpdateManagement/Tests/test-run-timings.csv | 1 + AzLocal.UpdateManagement/docs/release-history.md | 2 +- 7 files changed, 17 insertions(+), 2 deletions(-) diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 986c733c..b3a2839d 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 @@ -317,7 +317,7 @@ # ReleaseNotes of this module ReleaseNotes = @' -## Version 0.8.87 - Patch: renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM, so a listed appliance may be a healthy bridge for a non-Azure Local platform. Adds investigate-before-acting guidance (check resource type, custom location, platform served; do not delete until confirmed orphaned). KPI note, causes list, and cluster-table cross-reference updated to match. Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. +## Version 0.8.87 - Patch: renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM, so a listed appliance may be a healthy bridge for a non-Azure Local platform. Adds investigate-before-acting guidance (check resource type, custom location, platform served; do not delete until confirmed orphaned). KPI note, causes list, and cluster-table cross-reference updated to match. The Cluster Connectivity per-cluster table now collapses behind an "Expand to view clusters" details block. Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. ## Version 0.8.86 - Patch: renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet); the previous `Setup:` prefix sorted AFTER `Fleet:`. Only the operator-facing `name:` (GitHub Actions) / `displayName:` (Azure DevOps) fields change - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, prune logic, and all cmdlet behaviour are unchanged. Bundled pipeline README + appendix docs updated to match. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.85'` to `'0.8.86'`. diff --git a/AzLocal.UpdateManagement/CHANGELOG.md b/AzLocal.UpdateManagement/CHANGELOG.md index 15167bc9..21991fb3 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -29,6 +29,9 @@ count unchanged (still 60). - The KPI-table "Orphan ARBs" note, the "how to interpret" causes list (new leading bullet about non-Azure Local resource bridges), and the cluster-table cross-reference now point at the renamed section. +- The `### Cluster Connectivity (with ARB Status)` per-cluster table is now + wrapped in a collapsed `
` block (`Expand to view + clusters`) so the step summary stays compact by default. - `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. diff --git a/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 b/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 index e7ebe47b..d55c652e 100644 --- a/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 +++ b/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 @@ -396,6 +396,9 @@ function New-AzLocalFleetConnectivityStatusSummary { [void]$sb.AppendLine('*No clusters returned.*') } else { + [void]$sb.AppendLine('
') + [void]$sb.AppendLine('Expand to view clusters') + [void]$sb.AppendLine('') [void]$sb.AppendLine('| Cluster | Connectivity | Cluster Status | Nodes | ARB | ARB Status | ARB Days Since LastModified | Resource Group | Location |') [void]$sb.AppendLine('|---------|---------------|-----------------|-------|-----|-------------|------------------------------|----------------|----------|') foreach ($r in ($ClusterRows | Select-Object -First 100)) { @@ -425,6 +428,8 @@ function New-AzLocalFleetConnectivityStatusSummary { [void]$sb.AppendLine('') [void]$sb.AppendLine("*Showing first 100 of $($ClusterRows.Count); see ``fleet-cluster-connectivity.csv`` and ``fleet-arb-status.csv`` for the full lists.*") } + [void]$sb.AppendLine('') + [void]$sb.AppendLine('
') } # 5e. Orphan ARBs table (conditional) diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 970269be..9b6cd90f 100644 --- a/AzLocal.UpdateManagement/README.md +++ b/AzLocal.UpdateManagement/README.md @@ -94,6 +94,7 @@ If you are new to this module, work through these in order from a regular PowerS - The section heading `### Orphan ARBs (no matching cluster in scope)` becomes `### Non-Azure Local and/or Orphan ARB appliances`, with an intro line and an expanded caveat calling out VMware vSphere / SCVMM resource bridges and adding investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). - The KPI-table "Orphan ARBs" note, the "how to interpret" causes list (new leading bullet about non-Azure Local resource bridges), and the cluster-table cross-reference now point at the renamed section. +- The `### Cluster Connectivity (with ARB Status)` per-cluster table is now wrapped in a collapsed `
` block (`Expand to view clusters`) so the step summary stays compact by default. ### Notes diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index d042ea37..f8d50382 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -12565,6 +12565,11 @@ Describe 'Function: New-AzLocalFleetConnectivityStatusSummary' { $script:md | Should -Match '### Cluster Connectivity \(with ARB Status\)' } + It 'Wraps the cluster connectivity table in a collapsed details block' { + $script:md | Should -Match '
\s*\r?\nExpand to view clusters' + $script:md | Should -Match '
' + } + It 'Renders cluster rows with portal links when ClusterId is present' { $script:md | Should -Match '\[Mobile\]\(https://portal\.azure\.com/#@/resource/sub/abc/rg/x/p/m\.azurestackhci/clusters/Mobile\)' } diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index b0530be3..a28e16ad 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -38,3 +38,4 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T11:05:53Z","0.8.85","1305","1265","1","1","148.49","145.99","15.16","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T11:43:15Z","0.8.86","1305","1266","0","1","77.33","75.24","3.83","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T12:47:00Z","0.8.87","1305","1266","0","1","116.33","113.88","4.43","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T13:00:02Z","0.8.87","1306","1267","0","1","96.98","95.68","3.11","Invoke-Tests.ps1","5.1.26100.8730" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index 48104a57..48d4bb88 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -10,7 +10,7 @@ ### What's New in v0.8.87 -Patch release. Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. +Patch release. Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. The `### Cluster Connectivity (with ARB Status)` per-cluster table is now wrapped in a collapsed `
` block (`Expand to view clusters`) so the step summary stays compact by default. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. --- From be7bbfdda096acdc3fc1e44fd77a3a00dc4c3748 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 14:07:29 +0100 Subject: [PATCH 16/17] v0.8.87: split Cluster Connectivity table into with/without connectivity issues New-AzLocalFleetConnectivityStatusSummary now renders two per-cluster tables instead of one: - '### Cluster with Connectivity Issues' - clusters that are NOT (Connectivity = Connected AND ARB Status = Running). Shown first and expanded. - '### Cluster without Connectivity Issues' - clusters that ARE Connected with a Running ARB. Collapsed behind an 'Expand to view clusters' details block. Both tables share the same heading columns. Row rendering is factored into a plain scriptblock (no .GetNewClosure()) reused by both tables. Added/updated Pester assertions for both headings, the partitioning, and the collapsed block; fixed the CSV-input context assertion that referenced the old single heading. Release notes (psd1/CHANGELOG/README/release-history) updated within the in-flight v0.8.87 (ReleaseNotes 9872 chars, under the 10000 limit). Full Pester suite green (1270 passed, 0 failed, 1 skipped). Output text only; no API change. --- .../AzLocal.UpdateManagement.psd1 | 2 +- AzLocal.UpdateManagement/CHANGELOG.md | 9 +- ...-AzLocalFleetConnectivityStatusSummary.ps1 | 112 +++++++++++++----- AzLocal.UpdateManagement/README.md | 2 +- .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 27 ++++- .../Tests/test-run-timings.csv | 1 + .../docs/release-history.md | 2 +- 7 files changed, 115 insertions(+), 40 deletions(-) diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index b3a2839d..3e8955c3 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 @@ -317,7 +317,7 @@ # ReleaseNotes of this module ReleaseNotes = @' -## Version 0.8.87 - Patch: renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM, so a listed appliance may be a healthy bridge for a non-Azure Local platform. Adds investigate-before-acting guidance (check resource type, custom location, platform served; do not delete until confirmed orphaned). KPI note, causes list, and cluster-table cross-reference updated to match. The Cluster Connectivity per-cluster table now collapses behind an "Expand to view clusters" details block. Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. +## Version 0.8.87 - Patch: renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM. Adds investigate-before-acting guidance. KPI note, causes list, and cluster-table cross-reference updated to match. The Cluster Connectivity table is split into "Cluster with Connectivity Issues" (shown first) and "Cluster without Connectivity Issues" (Connected + ARB Running, collapsed). Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. ## Version 0.8.86 - Patch: renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet); the previous `Setup:` prefix sorted AFTER `Fleet:`. Only the operator-facing `name:` (GitHub Actions) / `displayName:` (Azure DevOps) fields change - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, prune logic, and all cmdlet behaviour are unchanged. Bundled pipeline README + appendix docs updated to match. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.85'` to `'0.8.86'`. diff --git a/AzLocal.UpdateManagement/CHANGELOG.md b/AzLocal.UpdateManagement/CHANGELOG.md index 21991fb3..1e6078d5 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -29,9 +29,12 @@ count unchanged (still 60). - The KPI-table "Orphan ARBs" note, the "how to interpret" causes list (new leading bullet about non-Azure Local resource bridges), and the cluster-table cross-reference now point at the renamed section. -- The `### Cluster Connectivity (with ARB Status)` per-cluster table is now - wrapped in a collapsed `
` block (`Expand to view - clusters`) so the step summary stays compact by default. +- The `### Cluster Connectivity (with ARB Status)` table is split into two: a + `### Cluster with Connectivity Issues` table (clusters that are not Connected + with a Running ARB) shown first and expanded, and a `### Cluster without + Connectivity Issues` table (Connectivity = Connected AND ARB Status = Running) + collapsed behind a `Expand to view clusters` details block. + Both tables share the same columns. - `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. diff --git a/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 b/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 index d55c652e..15967b61 100644 --- a/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 +++ b/AzLocal.UpdateManagement/Public/New-AzLocalFleetConnectivityStatusSummary.ps1 @@ -387,46 +387,100 @@ function New-AzLocalFleetConnectivityStatusSummary { [void]$sb.AppendLine('- **ARB created for a cluster excluded from this run** (e.g. environment filter) - verify the filter; no action if expected.') [void]$sb.AppendLine('') - # 5d. Cluster Connectivity (with ARB Status) per-cluster table - [void]$sb.AppendLine('### Cluster Connectivity (with ARB Status)') + # 5d. Cluster Connectivity (with ARB Status) per-cluster tables. + # Clusters are split into two tables: those WITH connectivity issues (anything + # that is not Connected with a Running ARB) shown first and expanded, and those + # WITHOUT connectivity issues (Connectivity = Connected AND ARB Status = Running) + # collapsed behind an "Expand to view clusters" details block. + $clusterIntro = '_One row per cluster, left-joined to the cluster''s Azure Resource Bridge (ARB) appliance status. Each cluster has at most one ARB. ARBs without a matching cluster in scope are listed separately under ''Non-Azure Local and/or Orphan ARB appliances''._' + $tableHeader = '| Cluster | Connectivity | Cluster Status | Nodes | ARB | ARB Status | ARB Days Since LastModified | Resource Group | Location |' + $tableDivider = '|---------|---------------|-----------------|-------|-----|-------------|------------------------------|----------------|----------|' + + # Row renderer - plain scriptblock (NOT .GetNewClosure()) so it retains the + # module SessionState (access to Get-ClusterSev / Get-StatusIcon / Get-ArbSev) + # and lexical access to $arbByClusterId. + $renderClusterRow = { + param($r) + $sev = Get-ClusterSev $r.ConnectivityStatus + $icon = Get-StatusIcon $sev + $clusterCell = if ($r.ClusterId) { '[{0}](https://portal.azure.com/#@/resource{1})' -f $r.ClusterName, $r.ClusterId } else { $r.ClusterName } + + $arb = $null + if ($r.ClusterId) { $arb = $arbByClusterId[$r.ClusterId.ToLowerInvariant()] } + + if ($arb) { + $arbSev = Get-ArbSev $arb.ArbStatus + $arbIcon = Get-StatusIcon $arbSev + $arbCell = if ($arb.ArbId) { '[{0}](https://portal.azure.com/#@/resource{1})' -f $arb.ArbName, $arb.ArbId } else { $arb.ArbName } + $arbStatusCell = '{0} {1}' -f $arbIcon, $arb.ArbStatus + $arbDaysCell = $arb.DaysSinceLastModified + } + else { + $arbCell = '_(no ARB)_' + $arbStatusCell = '-' + $arbDaysCell = '-' + } + + ('| {0} | {1} {2} | {3} | {4} | {5} | {6} | {7} | {8} | {9} |' -f $clusterCell, $icon, $r.ConnectivityStatus, $r.ClusterStatus, $r.NodeCount, $arbCell, $arbStatusCell, $arbDaysCell, $r.ResourceGroup, $r.Location) + } + + # Classify each cluster as healthy (Connected + matched ARB Running) or with issues. + $clustersWithIssues = New-Object 'System.Collections.Generic.List[object]' + $clustersHealthy = New-Object 'System.Collections.Generic.List[object]' + foreach ($r in $ClusterRows) { + $arbForRow = $null + if ($r.ClusterId) { $arbForRow = $arbByClusterId[$r.ClusterId.ToLowerInvariant()] } + $isHealthy = ($r.ConnectivityStatus -eq 'Connected') -and ($null -ne $arbForRow) -and ($arbForRow.ArbStatus -eq 'Running') + if ($isHealthy) { [void]$clustersHealthy.Add($r) } else { [void]$clustersWithIssues.Add($r) } + } + + # 5d-i. Clusters WITH connectivity issues (shown first, not collapsed). + [void]$sb.AppendLine('### Cluster with Connectivity Issues') [void]$sb.AppendLine('') - [void]$sb.AppendLine('_One row per cluster, left-joined to the cluster''s Azure Resource Bridge (ARB) appliance status. Each cluster has at most one ARB. ARBs without a matching cluster in scope are listed separately under ''Non-Azure Local and/or Orphan ARB appliances''._') + [void]$sb.AppendLine($clusterIntro) [void]$sb.AppendLine('') if ($ClusterRows.Count -eq 0) { [void]$sb.AppendLine('*No clusters returned.*') } + elseif ($clustersWithIssues.Count -eq 0) { + [void]$sb.AppendLine('*No clusters with connectivity issues - every cluster is Connected with a Running ARB.*') + } + else { + [void]$sb.AppendLine($tableHeader) + [void]$sb.AppendLine($tableDivider) + foreach ($r in ($clustersWithIssues | Select-Object -First 100)) { + [void]$sb.AppendLine((& $renderClusterRow $r)) + } + if ($clustersWithIssues.Count -gt 100) { + [void]$sb.AppendLine('') + [void]$sb.AppendLine("*Showing first 100 of $($clustersWithIssues.Count); see ``fleet-cluster-connectivity.csv`` and ``fleet-arb-status.csv`` for the full lists.*") + } + } + [void]$sb.AppendLine('') + + # 5d-ii. Clusters WITHOUT connectivity issues (collapsed by default). + [void]$sb.AppendLine('### Cluster without Connectivity Issues') + [void]$sb.AppendLine('') + [void]$sb.AppendLine($clusterIntro) + [void]$sb.AppendLine('') + if ($ClusterRows.Count -eq 0) { + [void]$sb.AppendLine('*No clusters returned.*') + } + elseif ($clustersHealthy.Count -eq 0) { + [void]$sb.AppendLine('*No clusters are currently Connected with a Running ARB.*') + } else { [void]$sb.AppendLine('
') [void]$sb.AppendLine('Expand to view clusters') [void]$sb.AppendLine('') - [void]$sb.AppendLine('| Cluster | Connectivity | Cluster Status | Nodes | ARB | ARB Status | ARB Days Since LastModified | Resource Group | Location |') - [void]$sb.AppendLine('|---------|---------------|-----------------|-------|-----|-------------|------------------------------|----------------|----------|') - foreach ($r in ($ClusterRows | Select-Object -First 100)) { - $sev = Get-ClusterSev $r.ConnectivityStatus - $icon = Get-StatusIcon $sev - $clusterCell = if ($r.ClusterId) { '[{0}](https://portal.azure.com/#@/resource{1})' -f $r.ClusterName, $r.ClusterId } else { $r.ClusterName } - - $arb = $null - if ($r.ClusterId) { $arb = $arbByClusterId[$r.ClusterId.ToLowerInvariant()] } - - if ($arb) { - $arbSev = Get-ArbSev $arb.ArbStatus - $arbIcon = Get-StatusIcon $arbSev - $arbCell = if ($arb.ArbId) { '[{0}](https://portal.azure.com/#@/resource{1})' -f $arb.ArbName, $arb.ArbId } else { $arb.ArbName } - $arbStatusCell = '{0} {1}' -f $arbIcon, $arb.ArbStatus - $arbDaysCell = $arb.DaysSinceLastModified - } - else { - $arbCell = '_(no ARB)_' - $arbStatusCell = '-' - $arbDaysCell = '-' - } - - [void]$sb.AppendLine(('| {0} | {1} {2} | {3} | {4} | {5} | {6} | {7} | {8} | {9} |' -f $clusterCell, $icon, $r.ConnectivityStatus, $r.ClusterStatus, $r.NodeCount, $arbCell, $arbStatusCell, $arbDaysCell, $r.ResourceGroup, $r.Location)) + [void]$sb.AppendLine($tableHeader) + [void]$sb.AppendLine($tableDivider) + foreach ($r in ($clustersHealthy | Select-Object -First 100)) { + [void]$sb.AppendLine((& $renderClusterRow $r)) } - if ($ClusterRows.Count -gt 100) { + if ($clustersHealthy.Count -gt 100) { [void]$sb.AppendLine('') - [void]$sb.AppendLine("*Showing first 100 of $($ClusterRows.Count); see ``fleet-cluster-connectivity.csv`` and ``fleet-arb-status.csv`` for the full lists.*") + [void]$sb.AppendLine("*Showing first 100 of $($clustersHealthy.Count); see ``fleet-cluster-connectivity.csv`` and ``fleet-arb-status.csv`` for the full lists.*") } [void]$sb.AppendLine('') [void]$sb.AppendLine('
') diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 9b6cd90f..45b5dbe4 100644 --- a/AzLocal.UpdateManagement/README.md +++ b/AzLocal.UpdateManagement/README.md @@ -94,7 +94,7 @@ If you are new to this module, work through these in order from a regular PowerS - The section heading `### Orphan ARBs (no matching cluster in scope)` becomes `### Non-Azure Local and/or Orphan ARB appliances`, with an intro line and an expanded caveat calling out VMware vSphere / SCVMM resource bridges and adding investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). - The KPI-table "Orphan ARBs" note, the "how to interpret" causes list (new leading bullet about non-Azure Local resource bridges), and the cluster-table cross-reference now point at the renamed section. -- The `### Cluster Connectivity (with ARB Status)` per-cluster table is now wrapped in a collapsed `
` block (`Expand to view clusters`) so the step summary stays compact by default. +- The `### Cluster Connectivity (with ARB Status)` table is split into a `### Cluster with Connectivity Issues` table (clusters not Connected with a Running ARB) shown first and expanded, and a `### Cluster without Connectivity Issues` table (Connectivity = Connected AND ARB Status = Running) collapsed behind an `Expand to view clusters` details block. Both tables share the same columns. ### Notes diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index f8d50382..b8e6b716 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -12561,15 +12561,31 @@ Describe 'Function: New-AzLocalFleetConnectivityStatusSummary' { $script:md | Should -Match '### How to interpret \+ act on a non-zero reconciliation' } - It 'Includes the Cluster Connectivity (with ARB Status) table heading' { - $script:md | Should -Match '### Cluster Connectivity \(with ARB Status\)' + It 'Includes the Cluster with Connectivity Issues table heading' { + $script:md | Should -Match '### Cluster with Connectivity Issues' } - It 'Wraps the cluster connectivity table in a collapsed details block' { - $script:md | Should -Match '
\s*\r?\nExpand to view clusters' + It 'Includes the Cluster without Connectivity Issues table heading' { + $script:md | Should -Match '### Cluster without Connectivity Issues' + } + + It 'Collapses only the without-issues table in a details block' { + $script:md | Should -Match '### Cluster without Connectivity Issues[\s\S]*?
\s*\r?\nExpand to view clusters' $script:md | Should -Match '
' } + It 'Lists a Connected cluster with a Running ARB under the without-issues table' { + # Mobile is Connected and arb-mobile is Running, so it must appear after + # the "without Connectivity Issues" heading. + $script:md | Should -Match '### Cluster without Connectivity Issues[\s\S]*\[Mobile\]\(https://portal\.azure\.com/#@/resource/sub/abc/rg/x/p/m\.azurestackhci/clusters/Mobile\)' + } + + It 'Lists a Disconnected cluster under the with-issues table' { + # NewYork is Disconnected, so it must appear under "with Connectivity Issues" + # and before the "without Connectivity Issues" heading. + $script:md | Should -Match '### Cluster with Connectivity Issues[\s\S]*\| \[NewYork\][\s\S]*### Cluster without Connectivity Issues' + } + It 'Renders cluster rows with portal links when ClusterId is present' { $script:md | Should -Match '\[Mobile\]\(https://portal\.azure\.com/#@/resource/sub/abc/rg/x/p/m\.azurestackhci/clusters/Mobile\)' } @@ -12854,7 +12870,8 @@ Describe 'Function: New-AzLocalFleetConnectivityStatusSummary' { It 'Treats missing CSV files as empty (no error)' { # No fleet-arb-status.csv, no fleet-physical-nics.csv etc. # Should still produce a complete markdown document. - $script:mdCsv | Should -Match '### Cluster Connectivity \(with ARB Status\)' + $script:mdCsv | Should -Match '### Cluster with Connectivity Issues' + $script:mdCsv | Should -Match '### Cluster without Connectivity Issues' $script:mdCsv | Should -Match '\*No physical NIC issues\.' } diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index a28e16ad..01d3e5ea 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -39,3 +39,4 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T11:43:15Z","0.8.86","1305","1266","0","1","77.33","75.24","3.83","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T12:47:00Z","0.8.87","1305","1266","0","1","116.33","113.88","4.43","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T13:00:02Z","0.8.87","1306","1267","0","1","96.98","95.68","3.11","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T13:06:07Z","0.8.87","1309","1270","0","1","76.61","75.29","2.85","Invoke-Tests.ps1","5.1.26100.8730" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index 48d4bb88..b63a096c 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -10,7 +10,7 @@ ### What's New in v0.8.87 -Patch release. Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. The `### Cluster Connectivity (with ARB Status)` per-cluster table is now wrapped in a collapsed `
` block (`Expand to view clusters`) so the step summary stays compact by default. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. +Patch release. Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. The `### Cluster Connectivity (with ARB Status)` table is split into a `### Cluster with Connectivity Issues` table (clusters not Connected with a Running ARB) shown first and expanded, and a `### Cluster without Connectivity Issues` table (Connectivity = Connected AND ARB Status = Running) collapsed behind an `Expand to view clusters` details block; both tables share the same columns. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. --- From 1a3110cce8416b1045d2e54b1f8302e5981e61f3 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 16:06:22 +0100 Subject: [PATCH 17/17] v0.8.87: rename bundled pipeline display names to Config:/Monitor:/Update: scheme Renames all bundled pipeline display names (name: on GitHub Actions, displayName:/name: on Azure DevOps) into a three-group single-digit scheme: Config: 1-3 (onboarding/config), Monitor: 1-3 (day-2 fleet monitoring), Update: 1-4 (update lifecycle), replacing the former Setup: 0N / Fleet: 0N prefixes. Monitor: 2 is Fleet Health Status and Monitor: 3 is Fleet Update Status so the numbering matches alphabetical order. Display name only - filenames, AZLOCAL-PIPELINE-ID values, aliases, and -PruneDeprecated logic are unchanged. Updates the bundled README, appendix-pipelines.md (grouped/ordered conventions, alphabetical file layout), ITSM docs, release docs, the manifest ReleaseNotes, CHANGELOG, the schedule auditor's emitted markdown (Update: 4), and the matching Pester assertions. Full suite green: 1282 passed, 0 failed, 1 skipped. --- .../.itsm/azurelocal-itsm.yml | 18 +++ .../Automation-Pipeline-Examples/README.md | 110 ++++++------- .../apply-updates-schedule.example.yml | 2 +- .../apply-updates-schedule-audit.yml | 4 +- .../azure-devops/apply-updates.yml | 4 +- .../azure-devops/assess-update-readiness.yml | 4 +- .../fleet-connectivity-status.yml | 4 +- .../azure-devops/fleet-health-status.yml | 2 +- .../azure-devops/fleet-update-status.yml | 2 +- .../azure-devops/manage-updatering-tags.yml | 4 +- .../azure-devops/monitor-updates.yml | 146 +++++++++++++++++- .../setup-validate-and-inventory.yml | 10 +- .../azure-devops/sideload-updates.yml | 2 +- .../docs/appendix-pipelines.md | 142 ++++++++--------- .../docs/sideload.md | 2 +- .../apply-updates-schedule-audit.yml | 2 +- .../github-actions/apply-updates.yml | 2 +- .../assess-update-readiness.yml | 2 +- .../fleet-connectivity-status.yml | 2 +- .../github-actions/fleet-health-status.yml | 2 +- .../github-actions/fleet-update-status.yml | 2 +- .../github-actions/manage-updatering-tags.yml | 2 +- .../github-actions/monitor-updates.yml | 133 +++++++++++++++- .../setup-validate-and-inventory.yml | 8 +- .../github-actions/sideload-updates.yml | 2 +- .../AzLocal.UpdateManagement.psd1 | 14 +- AzLocal.UpdateManagement/CHANGELOG.md | 60 ++++++- .../ITSM/ITSM-Config-Reference.md | 9 ++ AzLocal.UpdateManagement/ITSM/README.md | 2 + .../Private/Get-AzLocalPipelineManifest.ps1 | 2 +- ...xport-AzLocalApplyUpdatesScheduleAudit.ps1 | 104 +++++++++++++ .../Export-AzLocalUpdateRunMonitorReport.ps1 | 33 ++++ AzLocal.UpdateManagement/README.md | 14 +- .../Tests/AzLocal.UpdateManagement.Tests.ps1 | 81 ++++++++++ .../Tests/test-run-timings.csv | 3 + .../docs/release-history.md | 4 +- 36 files changed, 766 insertions(+), 173 deletions(-) diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml index 5a174742..5a206160 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml @@ -48,3 +48,21 @@ triggers: raiseTicket: false NotReady: raiseTicket: false + + # ----- Update: 4 - Monitor In-Flight Updates statuses (v0.8.87) ----- + # Emitted by Export-AzLocalUpdateRunMonitorReport in update-monitor.xml. + # Wire monitor-updates.yml with raise_itsm_ticket=true to open tickets for + # these. Dedupe key = SHA256(ClusterResourceId|UpdateName|TriggerCategory), + # so a stuck/attempt-gap cluster opens at most ONE incident until it clears. + AttemptWithoutRun: + raiseTicket: true + severity: 3 + category: "Update attempt with no observable updateRun" # UpdateLastAttempt tag set, but no updateRun materialised (commonly a URP package pre-install health-check failure) + StepError: + raiseTicket: true + severity: 2 + category: "Update step stuck in error" # progress.status == Error while state is still InProgress + LongRunningOverall: + raiseTicket: false # informational backstop; opt-in if you want a ticket per run exceeding the overall-elapsed threshold + LongRunningStep: + raiseTicket: false # informational; opt-in if you want a ticket per run whose current step exceeds the per-step threshold diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index 6a4cb5c0..18637ff6 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -56,49 +56,50 @@ It is written in the same step-by-step style as [`ITSM/README.md`](../ITSM/READM By the end of this guide you will have: - A federated identity (no client secrets) wired into your CI/CD platform with the **minimum** Azure RBAC needed for cluster update management. -- Config/Fleet workflows committed to your repo and visible in the Actions / Pipelines UI: - - **Config: 01 - Validate Auth and Inventory Clusters** (GitHub) - merged auth + inventory flow with a clear setup-first summary (including collapsible subscription details) and cluster inventory export. - - **Config: 02 - Manage UpdateRing Tags** - bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, `UpdateExcluded` tags from CSV. - - **Config: 03 - Apply-Updates Schedule Coverage Audit** - read-only audit that validates your schedule against `UpdateStartWindow` tags. - - **Fleet: 01 - Assess Update Readiness** - pre-flight readiness + blocking-health gate report. - - **Fleet: 02 - Fleet Connectivity Status** - Arc connectivity, NIC health, and ARB status snapshot. - - **Fleet: 03 - Sideload Updates (opt-in)** - on-prem media pre-stage workflow for disconnected environments. - - **Fleet: 04 - Apply Updates** - ring-scoped update execution with WhatIf support and schedule-aware gating. - - **Fleet: 05 - Monitor In-Flight Updates** - active-run progress and stuck-run diagnostics. - - **Fleet: 06 - Fleet Update Status** - daily fleet-wide update-state summary. - - **Fleet: 07 - Fleet Health Status** - daily 24-hour health-failure summary. +- Config/Monitor/Update workflows committed to your repo and visible in the Actions / Pipelines UI: + - **Config: 1 - Validate Auth and Inventory Clusters** (GitHub) - merged auth + inventory flow with a clear setup-first summary (including collapsible subscription details) and cluster inventory export. + - **Config: 2 - Manage UpdateRing Tags** - bulk-apply `UpdateRing`, `UpdateStartWindow`, `UpdateExclusionsWindow`, `UpdateExcluded` tags from CSV. + - **Config: 3 - Apply-Updates Schedule Coverage Audit** - read-only audit that validates your schedule against `UpdateStartWindow` tags. + - **Monitor: 1 - Fleet Connectivity Status** - Arc connectivity, NIC health, and ARB status snapshot. + - **Monitor: 2 - Fleet Health Status** - daily 24-hour health-failure summary. + - **Monitor: 3 - Fleet Update Status** - daily fleet-wide update-state summary. + - **Update: 1 - Assess Update Readiness** - pre-flight readiness + blocking-health gate report. + - **Update: 2 - Sideload Updates (opt-in)** - on-prem media pre-stage workflow for disconnected environments. + - **Update: 3 - Apply Updates** - ring-scoped update execution with WhatIf support and schedule-aware gating. + - **Update: 4 - Monitor In-Flight Updates** - active-run progress and stuck-run diagnostics. - An end-to-end "ring-based" rollout pattern: Pilot -> Wave2 -> Production, with each ring gated on the previous wave's success. - **Optional**: a ServiceNow integration that opens deduped incidents for clusters whose run status indicates the module's own retries cannot recover (failures, blocking health checks, sideloaded payload missing) - see [section 7](#7-optional-open-itsm-tickets-for-clusters-needing-operator-action). The pipelines are **fully opt-in additive layers** over the module. The PowerShell functions also work without any pipeline at all - see [section 10](#10-standalone-html-report-no-pipeline) for the ad-hoc / desktop story. -### 1.1 Why the pipelines are named `Config: NN` and `Fleet: NN` +### 1.1 Why the pipelines are named `Config: N`, `Monitor: N`, and `Update: N` -The active workflow model uses two clear groups: +The active workflow model uses three clear groups: -- `Config: 01-03` for onboarding and configuration. -- `Fleet: 01-07` for day-2 operational monitoring and update execution. +- `Config: 1-3` for onboarding and configuration. +- `Monitor: 1-3` for day-2 fleet monitoring (connectivity, update status, health). +- `Update: 1-4` for the update lifecycle (readiness, sideload, apply, in-flight monitor). | Group | Workflow name | GH Actions | Azure DevOps | |---|---|---|---| -| Config | Config: 01 - Validate Auth and Inventory Clusters | `setup-validate-and-inventory.yml` | `authentication-test.yml` + `inventory-clusters.yml` | -| Config | Config: 02 - Manage UpdateRing Tags | `manage-updatering-tags.yml` | `manage-updatering-tags.yml` | -| Config | Config: 03 - Apply-Updates Schedule Coverage Audit | `apply-updates-schedule-audit.yml` | `apply-updates-schedule-audit.yml` | -| Fleet | Fleet: 01 - Assess Update Readiness | `assess-update-readiness.yml` | `assess-update-readiness.yml` | -| Fleet | Fleet: 02 - Fleet Connectivity Status | `fleet-connectivity-status.yml` | `fleet-connectivity-status.yml` | -| Fleet | Fleet: 03 - Sideload Updates (Opt-in) | `sideload-updates.yml` | `sideload-updates.yml` | -| Fleet | Fleet: 04 - Apply Updates | `apply-updates.yml` | `apply-updates.yml` | -| Fleet | Fleet: 05 - Monitor In-Flight Updates | `monitor-updates.yml` | `monitor-updates.yml` | -| Fleet | Fleet: 06 - Fleet Update Status | `fleet-update-status.yml` | `fleet-update-status.yml` | -| Fleet | Fleet: 07 - Fleet Health Status | `fleet-health-status.yml` | `fleet-health-status.yml` | +| Config | Config: 1 - Validate Auth and Inventory Clusters | `setup-validate-and-inventory.yml` | `authentication-test.yml` + `inventory-clusters.yml` | +| Config | Config: 2 - Manage UpdateRing Tags | `manage-updatering-tags.yml` | `manage-updatering-tags.yml` | +| Config | Config: 3 - Apply-Updates Schedule Coverage Audit | `apply-updates-schedule-audit.yml` | `apply-updates-schedule-audit.yml` | +| Monitor | Monitor: 1 - Fleet Connectivity Status | `fleet-connectivity-status.yml` | `fleet-connectivity-status.yml` | +| Monitor | Monitor: 2 - Fleet Health Status | `fleet-health-status.yml` | `fleet-health-status.yml` | +| Monitor | Monitor: 3 - Fleet Update Status | `fleet-update-status.yml` | `fleet-update-status.yml` | +| Update | Update: 1 - Assess Update Readiness | `assess-update-readiness.yml` | `assess-update-readiness.yml` | +| Update | Update: 2 - Sideload Updates (Opt-in) | `sideload-updates.yml` | `sideload-updates.yml` | +| Update | Update: 3 - Apply Updates | `apply-updates.yml` | `apply-updates.yml` | +| Update | Update: 4 - Monitor In-Flight Updates | `monitor-updates.yml` | `monitor-updates.yml` | -- **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field. Prefixing names with `Config: NN` and `Fleet: NN` keeps the sidebar in intended execution order (the `Config:` group sorts ahead of `Fleet:` so onboarding pipelines appear first). +- **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field. Prefixing names with `Config: N`, `Monitor: N`, and `Update: N` keeps the sidebar grouped (the `Config:` onboarding group sorts first, then `Monitor:` day-2 reporting, then the `Update:` lifecycle pipelines). - ![GitHub Actions sidebar showing Config and Fleet workflows in execution order](../docs/images/github-actions-10-pipelines-view.png) + ![GitHub Actions sidebar showing Config, Monitor, and Update workflows grouped](../docs/images/github-actions-10-pipelines-view.png) - *The Config/Fleet numeric prefixes keep the GitHub Actions sidebar in practical execution order rather than a purely alphabetical scatter.* + *The Config/Monitor/Update numeric prefixes keep the GitHub Actions sidebar grouped by purpose rather than a purely alphabetical scatter.* -- **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at import time (not by filename). Use the same `Config: NN` / `Fleet: NN` naming when you import so the list stays in operational order. +- **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at import time (not by filename). Use the same `Config: N` / `Monitor: N` / `Update: N` naming when you import so the list stays grouped by purpose. If you prefer a different naming scheme (e.g. `00 - Auth`, `01 - Inventory`, ...), just change the `name:` field in each GH Actions YAML and / or pick a different prefix at ADO import time. Nothing else in the module depends on these display names. @@ -956,13 +957,13 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp > Copy-AzLocalPipelineExample -Destination .\pipelines -Platform AzureDevOps > ``` > -> The function prints a short "next steps" summary pointing at the copied YAML location with the recommended workflow / pipeline to run first (GitHub: **Config: 01 - Validate Auth and Inventory Clusters**, Azure DevOps: auth validation + inventory onboarding). Supports `-Platform GitHub | AzureDevOps | All`, `-PassThru`, `-WhatIf`, `-Confirm`. +> The function prints a short "next steps" summary pointing at the copied YAML location with the recommended workflow / pipeline to run first (GitHub: **Config: 1 - Validate Auth and Inventory Clusters**, Azure DevOps: auth validation + inventory onboarding). Supports `-Platform GitHub | AzureDevOps | All`, `-PassThru`, `-WhatIf`, `-Confirm`. > > **Refusing to overwrite**: the function will refuse to overwrite any file that already exists in `-Destination`, listing the conflicts in the error message. To refresh after a module upgrade, delete the existing copies first (`Remove-Item .\.github\workflows\*.yml`) and re-run. ### 5.1 GitHub Actions -1. **Run Config: 01 first (strongly recommended).** Before exercising the Fleet workflows, validate that the App Registration, federated credentials, GitHub secrets, environments, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Config: 01 - Validate Auth and Inventory Clusters`**. This narrows any failure to one onboarding workflow instead of debugging multiple operational workflows simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed. +1. **Run Config: 1 first (strongly recommended).** Before exercising the Update and Monitor workflows, validate that the App Registration, federated credentials, GitHub secrets, environments, and RBAC role assignment all line up - and capture the count + per-subscription detail of subscriptions visible to the pipeline identity - by running **`Config: 1 - Validate Auth and Inventory Clusters`**. This narrows any failure to one onboarding workflow instead of debugging multiple operational workflows simultaneously. **Re-run periodically** (recommended monthly, or after any RBAC change in the tenant) to confirm the pipeline identity's subscription scope has not silently widened or narrowed. The onboarding workflow ships with the module at [`github-actions/setup-validate-and-inventory.yml`](./github-actions/setup-validate-and-inventory.yml). It emits the auth validation JUnit report (Authentication / Subscription Scope / Resource Graph Reachability), writes a setup-focused markdown summary (including a collapsible subscription-details section), and exports both auth and inventory artifacts for downstream workflows. @@ -987,8 +988,8 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp At the `gh` CLI level, `gh run watch` shows the run summary as the steps complete (`gh` actually renders these as Unicode check marks; reproduced here in ASCII): ```text - ? Select a workflow run * Config: 01 - Validate Auth and Inventory Clusters, Config: 01 - Validate Auth and Inventory Clusters [main] 12s ago - [OK] main Config: 01 - Validate Auth and Inventory Clusters - + ? Select a workflow run * Config: 1 - Validate Auth and Inventory Clusters, Config: 1 - Validate Auth and Inventory Clusters [main] 12s ago + [OK] main Config: 1 - Validate Auth and Inventory Clusters - Triggered via workflow_dispatch less than a minute ago JOBS @@ -1001,7 +1002,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp [OK] Post Azure login (OIDC) [OK] Complete job - [OK] Run Config: 01 - Validate Auth and Inventory Clusters () completed with 'success' + [OK] Run Config: 1 - Validate Auth and Inventory Clusters () completed with 'success' ``` Inside the `Collect Authentication and Subscription Scope Report` step, the run log shows: @@ -1022,7 +1023,7 @@ Both platforms expect the YAML files inside this folder to land in a platform-sp ``` - ![Config: 01 - Validate Auth and Inventory Clusters run, showing authentication validation output and subscription scope details](../docs/images/auth-smoke-test-validate-oidc.png) + ![Config: 1 - Validate Auth and Inventory Clusters run, showing authentication validation output and subscription scope details](../docs/images/auth-smoke-test-validate-oidc.png) You may see one informational `windows-latest` -> `windows-2025-vs2026` migration notice in the run annotations. The sample workflows pin `runs-on: windows-latest` (the module is a Windows-side PowerShell module), and GitHub will retarget the alias to the new image automatically when it becomes the default - no action required on your part. As of v0.7.60 the previously-seen Node.js 20 deprecation banner (against `actions/checkout@v4`, `azure/login@v2`, `actions/upload-artifact@v4`, `dorny/test-reporter@v1`) is gone: the sample workflows have been refreshed to Node 24-compatible majors (`@v5`, `@v3`, `@v6`, `@v3` respectively). @@ -1527,6 +1528,8 @@ Configure your CI/CD platform's alerting on the JUnit failures - GitHub Actions The connector reads the JUnit results the Apply Updates pipeline already publishes and, for each cluster whose status matches your configured trigger matrix (default: `Failed`, `Error`, `HealthCheckBlocked`, `SideloadedBlocked`), opens a deduped ServiceNow incident via the Table API. Idempotency is enforced via a SHA256 dedupe key written to a custom `u_azlocal_dedupe_key` column, so re-running the same workflow does not create duplicates. +> **v0.8.87: the Update: 4 in-flight monitor (`monitor-updates.yml`) now supports the same opt-in ITSM step.** Toggle `raise_itsm_ticket=true` (`raiseItsmTicket` on Azure DevOps) and it reads `./reports/update-monitor.xml` after publishing the JUnit. The monitor JUnit now emits per-testcase `ClusterResourceId` / `UpdateName` / `Status` properties (`Status` is `StepError` / `LongRunningStep` / `LongRunningOverall` / `InProgress` / `Failed` / `AttemptWithoutRun`); the sample matrix raises on `AttemptWithoutRun` + `StepError` and leaves `LongRunning*` opt-in. The monitor stays report-only and always green - ITSM failures never affect its result. To pick a poll cadence, the **Config: 3** schedule auditor now prints a "Recommended in-flight monitor schedule (Update: 4)" cron derived from your apply windows (`-MonitorFiresPerHour`, `-MonitorTrailingDays`). + This README does not duplicate the setup - it is a single-source-of-truth in [`../ITSM/README.md`](../ITSM/README.md). Here is the high-level wiring you'll do over there: > **Shortcut for getting the sample into your repo**: from the repo root, run @@ -1635,7 +1638,7 @@ Copy this file once per ring (e.g. `assess-update-readiness-Pilot.yml`, `assess- **Why one YAML per ring**: GitHub Actions `schedule:`-triggered runs cannot supply `inputs:` values - they always use the workflow's default `inputs:`. So a single `assess-update-readiness*.yml` with three crons in one `schedule:` block (and `update_ring` required, no default) would never actually run on cron - every cron tick would fail input validation. Splitting one YAML per ring (each with its own default + single cron) is the cleanest fix. Azure DevOps has the same constraint - `schedules:`-triggered runs use the YAML's default `parameters:` values, so the same per-ring split pattern applies. -**Known gap**: the Step.3 schedule-coverage audit (`apply-updates-schedule-audit.yml`) currently validates Step.7 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Step.5 cron is correctly anchored ahead of a Step.7 cron. **Always pair Step.5 + Step.7 cron edits in the same PR** so the lead-time relationship is reviewable by a human at merge time. The per-pipeline appendix entry for [Fleet: 01 - Assess Update Readiness](docs/appendix-pipelines.md#fleet-01---assess-update-readiness) repeats this guidance with the same lead-time table. +**Known gap**: the Step.3 schedule-coverage audit (`apply-updates-schedule-audit.yml`) currently validates Step.7 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Step.5 cron is correctly anchored ahead of a Step.7 cron. **Always pair Step.5 + Step.7 cron edits in the same PR** so the lead-time relationship is reviewable by a human at merge time. The per-pipeline appendix entry for [Update: 1 - Assess Update Readiness](docs/appendix-pipelines.md#update-1---assess-update-readiness) repeats this guidance with the same lead-time table. **Always-green caveat**: `assess-update-readiness.yml` never goes red at the pipeline level - per-cluster readiness gaps surface as JUnit `` entries in the Tests / Checks tab via `readiness.xml`. A silently-empty `readiness.xml` (e.g. an `update_ring` typo with zero clusters in scope) **will not generate a red-build email**. Either check the Tests tab after each scheduled run, or wire the JUnit reporter into the CI status surface you already monitor. @@ -2034,34 +2037,35 @@ Automation-Pipeline-Examples/ templates/ incident-body.md # - Mustache-style ticket body template. github-actions/ - setup-validate-and-inventory.yml # Config: 01. Auth + subscription-scope validation and cluster inventory (merged auth+inventory in v0.8.85; manual + weekly Sun 08:00 UTC). - manage-updatering-tags.yml # Config: 02. Apply UpdateRing / UpdateStartWindow / UpdateExclusionsWindow / UpdateExcluded tags (manual). - apply-updates-schedule-audit.yml # Config: 03. Weekly read-only audit: UpdateStartWindow tags vs apply-updates cron (Mon 05:00 UTC, v0.7.65). - assess-update-readiness.yml # Fleet: 01. Pre-flight readiness report (manual; v0.7.0). - fleet-connectivity-status.yml # Fleet: 02. Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot + node-coverage reconciliation (daily 05:30 UTC, v0.7.79+; reconciliation enhanced in v0.7.85). - sideload-updates.yml # Fleet: 03. Opt-in self-hosted-runner workflow: Robocopy + WinRM sideload of solution-update media to clusters gated on UpdateSideloaded (manual; v0.8.7). - apply-updates.yml # Fleet: 04. Apply updates to one UpdateRing (with optional ITSM step, v0.7.4). - monitor-updates.yml # Fleet: 05. In-flight update monitor: per-cluster current step + elapsed duration; flags long-running runs (manual, optional cron; v0.7.90). - fleet-update-status.yml # Fleet: 06. Scheduled fleet update-status snapshot (daily 06:00 UTC). - fleet-health-status.yml # Fleet: 07. Scheduled fleet 24-hour health-check failure report (daily 07:00 UTC, v0.7.65). + apply-updates-schedule-audit.yml # Config: 3. Weekly read-only audit: UpdateStartWindow tags vs apply-updates cron (Mon 05:00 UTC, v0.7.65). + apply-updates.yml # Update: 3. Apply updates to one UpdateRing (with optional ITSM step, v0.7.4). + assess-update-readiness.yml # Update: 1. Pre-flight readiness report (manual; v0.7.0). + fleet-connectivity-status.yml # Monitor: 1. Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot + node-coverage reconciliation (daily 05:30 UTC, v0.7.79+; reconciliation enhanced in v0.7.85). + fleet-health-status.yml # Monitor: 2. Scheduled fleet 24-hour health-check failure report (daily 07:00 UTC, v0.7.65). + fleet-update-status.yml # Monitor: 3. Scheduled fleet update-status snapshot (daily 06:00 UTC). + manage-updatering-tags.yml # Config: 2. Apply UpdateRing / UpdateStartWindow / UpdateExclusionsWindow / UpdateExcluded tags (manual). + monitor-updates.yml # Update: 4. In-flight update monitor: per-cluster current step + elapsed duration; flags long-running runs (manual, optional cron; v0.7.90). + setup-validate-and-inventory.yml # Config: 1. Auth + subscription-scope validation and cluster inventory (merged auth+inventory in v0.8.85; manual + weekly Sun 08:00 UTC). + sideload-updates.yml # Update: 2. Opt-in self-hosted-runner workflow: Robocopy + WinRM sideload of solution-update media to clusters gated on UpdateSideloaded (manual; v0.8.7). + azure-devops/ - setup-validate-and-inventory.yml - manage-updatering-tags.yml apply-updates-schedule-audit.yml + apply-updates.yml assess-update-readiness.yml fleet-connectivity-status.yml - sideload-updates.yml - apply-updates.yml - monitor-updates.yml - fleet-update-status.yml fleet-health-status.yml + fleet-update-status.yml + manage-updatering-tags.yml + monitor-updates.yml + setup-validate-and-inventory.yml + sideload-updates.yml ``` --- ## 14. Pipeline reference -Moved to [docs/appendix-pipelines.md](docs/appendix-pipelines.md) - one section per pipeline (`Config: 01 - ...` ... `Fleet: 07 - ...`) mapping 1:1 to the bundled `*.yml` workflows, with purpose, inputs, trigger, cmdlets invoked, dependencies, artefacts, RBAC, and exit conditions for each. Kept out-of-line to keep this README focused on the runbook. +Moved to [docs/appendix-pipelines.md](docs/appendix-pipelines.md) - one section per pipeline (`Config: 1 - ...` ... `Update: 4 - ...`) mapping 1:1 to the bundled `*.yml` workflows, with purpose, inputs, trigger, cmdlets invoked, dependencies, artefacts, RBAC, and exit conditions for each. Kept out-of-line to keep this README focused on the runbook. ## Appendix B: Release history diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml index 80095dc3..98f0d7ae 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/apply-updates-schedule.example.yml @@ -27,7 +27,7 @@ # A cron firing that lands on a day with NO matching schedule rows is # logged and exits 0 - no errors, no failures. # -# This file is REQUIRED for the "Fleet: 04 - Apply Updates" +# This file is REQUIRED for the "Update: 3 - Apply Updates" # (apply-updates.yml) pipeline when triggered by a schedule (not # workflow_dispatch). If the file is missing, apply-updates.yml throws # with the exact remediation command. 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 1839a17b..3f1c0343 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: apply-updates-schedule-audit -# Config: 03 - Apply-Updates Schedule Coverage Audit Pipeline +# Config: 3 - Apply-Updates Schedule Coverage Audit Pipeline # This pipeline runs the read-only Test-AzLocalApplyUpdatesScheduleCoverage advisor # weekly to detect drift between the cron schedule(s) in your apply-updates pipeline # YAML and the UpdateStartWindow tags that operators have placed on Azure Local clusters. @@ -98,7 +98,7 @@ variables: stages: - stage: ScheduleCoverage - displayName: 'Config: 03 - Audit Apply-Updates Schedule Coverage' + displayName: 'Config: 3 - Audit Apply-Updates Schedule Coverage' jobs: - job: Audit displayName: 'Run Schedule Coverage Advisor' 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 2c0e21fc..6b70a930 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: apply-updates -# Fleet: 04 - Apply Updates to Azure Local Clusters +# Update: 3 - Apply Updates to Azure Local Clusters # This pipeline applies updates to clusters filtered by UpdateRing tag value # Author guidance lives OUTSIDE the customise markers so module refreshes @@ -252,7 +252,7 @@ stages: # Stage 2: Apply Updates (with approval gate for production) - stage: ApplyUpdates - displayName: 'Fleet: 04 - Apply Updates' + displayName: 'Update: 3 - Apply Updates' dependsOn: CheckReadiness condition: and(succeeded(), gt(dependencies.CheckReadiness.outputs['ReadinessCheck.readiness.ReadyCount'], 0)) 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 a9430a47..e7c41790 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: assess-update-readiness -# Fleet: 01 - Assess Update Readiness (Pre-flight go/no-go gate) +# Update: 1 - Assess Update Readiness (Pre-flight go/no-go gate) # -------------------------------------------------- # Runs Get-AzLocalClusterUpdateReadiness and Test-AzLocalClusterHealth -BlockingOnly # against a target UpdateRing (or the whole fleet) BEFORE apply-updates.yml runs. @@ -82,7 +82,7 @@ pool: stages: - stage: Assess - displayName: 'Fleet: 01 - Assess Update Readiness' + displayName: 'Update: 1 - Assess Update Readiness' jobs: - job: AssessReadiness displayName: 'Readiness + blocking health gate' 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 3248b595..dc772d32 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: fleet-connectivity-status -# Fleet: 02 - Fleet Connectivity Status Monitoring Pipeline +# Monitor: 1 - Fleet Connectivity Status Monitoring Pipeline # This pipeline surfaces network-connectivity issues across every Azure Local # cluster the service connection can read - four scopes in one pipeline: # @@ -49,7 +49,7 @@ # Pipeline display name carries the same Step.N - prefix as the filename so the # ADO pipelines list (sorts by `name:` field) lists pipelines in execution order. -name: 'Fleet: 02 - Fleet Connectivity Status' +name: 'Monitor: 1 - Fleet Connectivity Status' # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers # Edits inside this block are preserved across Update-AzLocalPipelineExample 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 22dc9bbb..807ddb14 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: fleet-health-status -# Fleet: 07 - Fleet Health Status Monitoring Pipeline +# Monitor: 2 - Fleet Health Status Monitoring Pipeline # This pipeline surfaces 24-hour system health-check failures across every Azure Local # cluster the service connection can read - including clusters that are already # "up to date" with no available updates. The 24-hour health checks continue to run 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 ec4ce3e8..51d704b3 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: fleet-update-status -# Fleet: 06 - Fleet Update Status Monitoring Pipeline +# Monitor: 3 - Fleet Update Status Monitoring Pipeline # This pipeline monitors update status across all Azure Local clusters and generates reports # # USE CASES: 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 f8cd4a19..d0d7857f 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 @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: manage-updatering-tags -# Config: 02 - Manage UpdateRing Tags +# Config: 2 - Manage UpdateRing Tags # This pipeline creates or updates UpdateRing tags on Azure Local clusters from a CSV file # # WORKFLOW: @@ -58,7 +58,7 @@ pool: stages: - stage: ManageTags - displayName: 'Config: 02 - Manage UpdateRing Tags' + displayName: 'Config: 2 - Manage UpdateRing Tags' jobs: - job: ApplyTags displayName: 'Apply UpdateRing Tags' 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 bb416a44..e85d5e28 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: monitor-updates -# Fleet: 05 - Monitor In-Flight Updates (Azure DevOps) +# Update: 4 - Monitor In-Flight Updates (Azure DevOps) # ----------------------------------------------------------- # Reports clusters whose latest update run is currently in flight, with the # CURRENT STEP each cluster is on and the ELAPSED DURATION of the run. Built @@ -83,10 +83,54 @@ parameters: type: string default: '' + # --- ITSM Connector (ServiceNow auto-raise on in-flight monitor failures) --- + # Set raiseItsmTicket=true to open ServiceNow incidents (v0.8.87) from each + # stuck-step / failed / attempt-without-run cluster surfaced in + # update-monitor.xml. Default false so existing schedules stay byte-identical + # until you opt in. The connector reads the JUnit file this pipeline already + # produces (update-monitor.xml) and the trigger matrix in + # ./.itsm/azurelocal-itsm.yml (defaults raise AttemptWithoutRun / StepError / + # Failed; LongRunningStep / LongRunningOverall are opt-in). + - name: raiseItsmTicket + displayName: 'Open ITSM tickets (ServiceNow) for in-flight monitor failures' + type: boolean + default: false + + - name: itsmConfigPath + displayName: 'Path to ITSM matrix config (YAML or JSON)' + type: string + default: './.itsm/azurelocal-itsm.yml' + + - name: itsmDryRun + displayName: 'ITSM: build payloads + run read-only dedupe but do NOT create tickets' + type: boolean + default: false + + - name: itsmForceCreate + displayName: 'ITSM: bypass dedupe and always create new tickets (use with caution)' + type: boolean + default: false + variables: GENERATED_AGAINST_MODULE_VERSION: '0.8.87' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' + # ITSM connector. When raiseItsmTicket=true the Raise ITSM task below + # references $(ITSM_SN_INSTANCE_URL), $(ITSM_SN_CLIENT_ID), + # $(ITSM_SN_CLIENT_SECRET). The simplest binding is an ADO variable group + # named 'AzureLocal-ITSM-Secrets'. ADO does not allow mixing mapping-form + # variables with '- group:' entries in the same block, so to enable the + # group reference convert this entire 'variables:' block to list form: + # variables: + # - name: GENERATED_AGAINST_MODULE_VERSION + # value: '0.8.87' + # - name: REQUIRED_MODULE_VERSION + # value: '${{ parameters.moduleVersion }}' + # - name: reportsPath + # value: '$(Build.ArtifactStagingDirectory)/reports' + # - group: AzureLocal-ITSM-Secrets + # When raiseItsmTicket=false the Raise ITSM task is skipped and the secrets + # are not consulted, so the conversion is only required for opt-in users. stages: - stage: MonitorInFlight @@ -187,3 +231,103 @@ stages: testRunTitle: 'In-Flight Update Monitor' mergeTestResults: false failTaskOnFailedTests: false + + # ---------------------------------------------------------------------- + # ITSM Connector (ServiceNow auto-raise on in-flight monitor failures) + # + # Fully opt-in (gated on parameters.raiseItsmTicket == true). Runs AFTER + # the JUnit file is published, so the ITSM task is strictly additive - + # failures here never affect the monitor result (which is always green; + # this is a report-only pipeline). Reads the same update-monitor.xml that + # powers the summary. The JUnit emitter writes per-testcase + # (ClusterName / ClusterResourceId / UpdateName / Status / CurrentStep / + # ClusterPortalUrl / UpdateRunPortalUrl) so New-AzLocalIncident can compute + # the SHA256 dedupe key (one incident per stuck/failed/attempt-gap cluster + # until it clears) and the Mustache body template can deep-link into the + # Azure portal. + # ---------------------------------------------------------------------- + - task: PowerShell@2 + displayName: 'Install powershell-yaml (ITSM config parser)' + condition: and(succeeded(), eq('${{ parameters.raiseItsmTicket }}', 'true')) + inputs: + targetType: 'inline' + pwsh: true + script: | + if (-not (Get-Module -ListAvailable -Name powershell-yaml)) { + Install-Module powershell-yaml -Scope CurrentUser -Force -AllowClobber + } + + - task: AzureCLI@2 + displayName: 'Raise ITSM tickets' + name: itsm + condition: and(succeeded(), eq('${{ parameters.raiseItsmTicket }}', 'true')) + env: + # BEGIN-AZLOCAL-CUSTOMIZE:itsm-secrets + # Bind your ITSM connector secrets from the variable group above. + # Defaults assume the ServiceNow OAuth client_credentials naming used + # by azurelocal-itsm.yml (env://ITSM_SN_*). If your secret names differ, + # update them here - this block is preserved by + # Update-AzLocalPipelineExample across module upgrades. + ITSM_SN_INSTANCE_URL: $(ITSM_SN_INSTANCE_URL) + ITSM_SN_CLIENT_ID: $(ITSM_SN_CLIENT_ID) + ITSM_SN_CLIENT_SECRET: $(ITSM_SN_CLIENT_SECRET) + # END-AZLOCAL-CUSTOMIZE:itsm-secrets + inputs: + azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + Import-Module AzLocal.UpdateManagement -Force + + $configPath = "${{ parameters.itsmConfigPath }}" + $dryRun = [System.Convert]::ToBoolean("${{ parameters.itsmDryRun }}") + $force = [System.Convert]::ToBoolean("${{ parameters.itsmForceCreate }}") + + if (-not (Test-Path $configPath)) { + Write-Host "##vso[task.logissue type=warning]ITSM config not found at '$configPath' - skipping ticket creation." + exit 0 + } + + $cfg = Get-AzLocalItsmConfig -Path $configPath + + $junitInput = "$(reportsPath)/update-monitor.xml" + if (-not (Test-Path $junitInput)) { + Write-Host "##vso[task.logissue type=warning]No update-monitor.xml found at '$junitInput' - skipping ticket creation." + exit 0 + } + + $params = @{ + InputArtifactPath = $junitInput + Config = $cfg + RunMetadata = @{ + Platform = 'azure-devops' + RunId = $env:BUILD_BUILDID + RunUrl = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECT)/_build/results?buildId=$($env:BUILD_BUILDID)" + Branch = $env:BUILD_SOURCEBRANCH + } + DryRun = $dryRun + ForceCreate = $force + ExportPath = "$(reportsPath)/itsm-results.csv" + ExportJUnitPath = "$(reportsPath)/itsm-results.xml" + } + + $results = New-AzLocalIncident @params + $results | Format-Table ClusterName, Action, TicketId, Severity -AutoSize + + - task: PublishBuildArtifacts@1 + displayName: 'Publish ITSM Artefacts' + condition: and(always(), eq('${{ parameters.raiseItsmTicket }}', 'true')) + inputs: + PathtoPublish: '$(reportsPath)' + ArtifactName: 'azlocal-step.8-update-monitor-itsm-results_$(stamp.artifactStamp)' + publishLocation: 'Container' + continueOnError: true + + - task: PublishTestResults@2 + displayName: 'Publish ITSM Test Results' + condition: and(always(), eq('${{ parameters.raiseItsmTicket }}', 'true')) + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(reportsPath)/itsm-results.xml' + testRunTitle: 'ITSM Tickets (Update: 4 Monitor) - $(Build.BuildNumber)' + failTaskOnFailedTests: false diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml index a99a0f41..80e5191c 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/setup-validate-and-inventory.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: setup-validate-and-inventory -# Config: 01 - Validate Auth and Inventory Azure Local Clusters +# Config: 1 - Validate Auth and Inventory Azure Local Clusters # # PURPOSE: # This is the first step in the setup workflow. It combines two operations: @@ -10,7 +10,7 @@ # UpdateRing tag status. # # Run this pipeline: -# - BEFORE importing the operational pipelines (Fleet: 01-07). It narrows +# - BEFORE importing the operational pipelines (Update: 1-4 and Monitor: 1-3). It narrows # any failure to authentication/discovery rather than seven interacting # pipelines. # - PERIODICALLY (recommended monthly, or after every RBAC change in the @@ -41,11 +41,11 @@ # cluster-inventory (ClusterUpdateRings.csv + .json + README_Instructions.txt). # # HOW TO RUN (after the file is committed and the pipeline is imported): -# Pipelines -> Config: 01 - Validate Auth and Inventory Clusters -> Run pipeline. +# Pipelines -> Config: 1 - Validate Auth and Inventory Clusters -> Run pipeline. # # See Automation-Pipeline-Examples/README.md, section 5, for the full setup story. -name: 'Config: 01 - Validate Auth and Inventory Clusters' +name: 'Config: 1 - Validate Auth and Inventory Clusters' # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers # Add or modify `schedules:` blocks here to enable cron-driven runs. Content @@ -155,7 +155,7 @@ stages: inputs: testResultsFormat: 'JUnit' testResultsFiles: '$(Build.ArtifactStagingDirectory)/auth-report/auth-report.xml' - testRunTitle: '[JUnit Debug] Config: 01 - Authentication Validation' + testRunTitle: '[JUnit Debug] Config: 1 - Authentication Validation' failTaskOnFailedTests: false mergeTestResults: false 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 aabe77bf..059eff6b 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: sideload-updates -# Fleet: 03 - Sideload Updates (Opt-in, on-prem self-hosted agent) +# Update: 2 - Sideload Updates (Opt-in, on-prem self-hosted agent) # # OPT-IN, OFF BY DEFAULT. This pipeline automates the on-prem sideloading of # Azure Local solution-update media for clusters that cannot pull updates from diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md index 2270ece2..9b759b3d 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md @@ -2,7 +2,7 @@ > **What you will find here:** A comprehensive reference index for every bundled workflow (GitHub Actions + Azure DevOps twins), with a one-line summary of the job, its triggers, the cmdlets it invokes, what it depends on, and the artefacts it produces. Use this as the at-a-glance reference card after you have read the per-workflow runbook in the main pipeline [README.md](../README.md). -Each pipeline section heading below uses **`Config: NN`** or **`Fleet: NN`** naming to match the operator-facing workflow names. These identifiers appear in the workflow `name:` fields (GH Actions), ADO pipeline import names, the parent README's section 1.1 table, and these section headings. The execution order in the [end-to-end runbook](../README.md#6-end-to-end-runbook-bring-an-estate-online) is operational - read it for the recommended sequence. +Each pipeline section heading below uses **`Config: N`**, **`Monitor: N`**, or **`Update: N`** naming to match the operator-facing workflow names. These identifiers appear in the workflow `name:` fields (GH Actions), ADO pipeline import names, the parent README's section 1.1 table, and these section headings. The execution order in the [end-to-end runbook](../README.md#6-end-to-end-runbook-bring-an-estate-online) is operational - read it for the recommended sequence. --- @@ -12,26 +12,26 @@ The table below is the ground truth for what each shipped YAML does **out of the | Pipeline | GitHub Actions trigger | Azure DevOps trigger | Notes | |---|---|---|---| -| **Config: 01** - Validate Auth and Inventory Clusters | `workflow_dispatch` + weekly `schedule: cron '0 8 * * 0'` (Sun 08:00 UTC) | `trigger: none` + weekly `schedules: cron '0 8 * * 0'` (Sun 08:00 UTC) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Ships a weekly Sunday 08:00 UTC re-validation + inventory refresh; edit or remove the cron in the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to change the cadence. | -| **Config: 02** - Manage UpdateRing Tags | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | -| **Config: 03** - Apply-Updates Schedule Coverage Audit | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | -| **Fleet: 01** - Assess Update Readiness | `workflow_dispatch` only | `trigger: none` (manual only) | **Recommended customisation** - schedule per ring 12-72h before the matching **Fleet: 04** `UpdateStartWindow` (see [Fleet: 01 - Assess Update Readiness](#fleet-01---assess-update-readiness) below for the per-ring pattern + lead-time table). Always-green at pipeline level; per-cluster readiness gaps surface as JUnit `` entries. | -| **Fleet: 02** - Fleet Connectivity Status | `workflow_dispatch` + `schedule: cron '30 5 * * *'` (daily 05:30 UTC) | `trigger: none` + `schedules: cron '30 5 * * *'` | Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot with bidirectional node-coverage reconciliation. Runs 30 min before **Fleet: 06** so connectivity issues are visible upstream of update reporting. | -| **Fleet: 03** - Sideload Updates (Opt-in) | `workflow_dispatch` only | `trigger: none` (manual only) | Optional pre-stage on-prem update media workflow for disconnected environments. | -| **Fleet: 04** - Apply Updates | `workflow_dispatch` only | `trigger: none` (manual only) | **No schedule shipped** - see the warning in [Fleet: 04 - Apply Updates](#fleet-04---apply-updates) below. The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags only gate updates *while the pipeline is running*; they do **not** start the pipeline. | -| **Fleet: 05** - Monitor In-Flight Updates | `workflow_dispatch` only (commented-out `schedule: cron '*/30 * * * *'` sample shipped) | `trigger: none` only (commented-out `schedules: cron '*/30 * * * *'` sample shipped) | In-flight update progress / long-running-run flag. Disabled by default to avoid 48 idle runs/day between waves. Uncomment the cron when a wave is active. | -| **Fleet: 06** - Fleet Update Status | `workflow_dispatch` + `schedule: cron '0 6 * * *'` (daily 06:00 UTC) | `trigger: none` + `schedules: cron '0 6 * * *'` | Daily fleet update snapshot. | -| **Fleet: 07** - Fleet Health Status | `workflow_dispatch` + `schedule: cron '0 7 * * *'` (daily 07:00 UTC) | `trigger: none` + `schedules: cron '0 7 * * *'` | Daily 24-hour health-check snapshot. Offset by one hour from **Fleet: 06** to avoid contention. | +| **Config: 1** - Validate Auth and Inventory Clusters | `workflow_dispatch` + weekly `schedule: cron '0 8 * * 0'` (Sun 08:00 UTC) | `trigger: none` + weekly `schedules: cron '0 8 * * 0'` (Sun 08:00 UTC) | Run on initial wiring + after every RBAC / federated-credential / subscription change. Ships a weekly Sunday 08:00 UTC re-validation + inventory refresh; edit or remove the cron in the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to change the cadence. | +| **Config: 2** - Manage UpdateRing Tags | `workflow_dispatch` only | `trigger: none` (manual only) | Runs on-demand whenever you edit the CSV. | +| **Config: 3** - Apply-Updates Schedule Coverage Audit | `workflow_dispatch` + `schedule: cron '0 5 * * 1'` (Mondays 05:00 UTC) | `trigger: none` + `schedules: cron '0 5 * * 1'` | Weekly read-only drift advisor: compares apply-updates cron(s) to `UpdateStartWindow` tags. Runs before the daily fleet pipelines so its annotations are first on Monday mornings. | +| **Monitor: 1** - Fleet Connectivity Status | `workflow_dispatch` + `schedule: cron '30 5 * * *'` (daily 05:30 UTC) | `trigger: none` + `schedules: cron '30 5 * * *'` | Daily fleet connectivity / Arc / NIC / Resource Bridge snapshot with bidirectional node-coverage reconciliation. Runs 30 min before **Monitor: 3** so connectivity issues are visible upstream of update reporting. | +| **Monitor: 2** - Fleet Health Status | `workflow_dispatch` + `schedule: cron '0 7 * * *'` (daily 07:00 UTC) | `trigger: none` + `schedules: cron '0 7 * * *'` | Daily 24-hour health-check snapshot. Offset by one hour from **Monitor: 3** to avoid contention. | +| **Monitor: 3** - Fleet Update Status | `workflow_dispatch` + `schedule: cron '0 6 * * *'` (daily 06:00 UTC) | `trigger: none` + `schedules: cron '0 6 * * *'` | Daily fleet update snapshot. | +| **Update: 1** - Assess Update Readiness | `workflow_dispatch` only | `trigger: none` (manual only) | **Recommended customisation** - schedule per ring 12-72h before the matching **Update: 3** `UpdateStartWindow` (see [Update: 1 - Assess Update Readiness](#update-1---assess-update-readiness) below for the per-ring pattern + lead-time table). Always-green at pipeline level; per-cluster readiness gaps surface as JUnit `` entries. | +| **Update: 2** - Sideload Updates (Opt-in) | `workflow_dispatch` only | `trigger: none` (manual only) | Optional pre-stage on-prem update media workflow for disconnected environments. | +| **Update: 3** - Apply Updates | `workflow_dispatch` only | `trigger: none` (manual only) | **No schedule shipped** - see the warning in [Update: 3 - Apply Updates](#update-3---apply-updates) below. The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags only gate updates *while the pipeline is running*; they do **not** start the pipeline. | +| **Update: 4** - Monitor In-Flight Updates | `workflow_dispatch` only (commented-out `schedule: cron '*/30 * * * *'` sample shipped) | `trigger: none` only (commented-out `schedules: cron '*/30 * * * *'` sample shipped) | In-flight update progress / long-running-run flag. Disabled by default to avoid 48 idle runs/day between waves. Uncomment the cron when a wave is active. | > **All times are UTC.** GitHub Actions and Azure DevOps schedules both run on UTC; convert from your local timezone when picking cron values. Both platforms can delay scheduled runs by several minutes during high-load periods - do not rely on second-precision alignment. > > **GitHub Actions only**: scheduled workflows are **automatically disabled after 60 days of repository inactivity** (no commits, PRs, or issue activity). Re-enable them via the Actions UI or run any push. Azure DevOps schedules do not have this auto-disable behaviour. -> **Common conventions for every row below**: `Cmdlets invoked` lists only AzLocal.UpdateManagement cmdlets the YAML calls directly (helper cmdlets they themselves call transitively are documented in each cmdlet's section of the module-level [`README.md`](../../README.md)). `Depends on` lists upstream pipelines whose output is consumed or whose work must be in place before this pipeline runs - `None` means the pipeline is self-contained. `Exit conditions` describes what a non-success YAML run state means in practice; per-cluster issues are always surfaced as JUnit `` entries rather than failing the whole pipeline unless noted otherwise. `ITSM` indicates whether the bundled YAMLs ship with the opt-in ServiceNow auto-raise connector (`Get-AzLocalItsmConfig` + per-failure ticket creation gated on the `raise_itsm_ticket` / `raiseItsmTicket` input); only Fleet: 02, Fleet: 04, Fleet: 06, and Fleet: 07 ship it - the matrix config lives at `./.itsm/azurelocal-itsm.yml` (see [`Automation-Pipeline-Examples/ITSM/`](../ITSM/) for the full recipe). +> **Common conventions for every row below**: `Cmdlets invoked` lists only AzLocal.UpdateManagement cmdlets the YAML calls directly (helper cmdlets they themselves call transitively are documented in each cmdlet's section of the module-level [`README.md`](../../README.md)). `Depends on` lists upstream pipelines whose output is consumed or whose work must be in place before this pipeline runs - `None` means the pipeline is self-contained. `Exit conditions` describes what a non-success YAML run state means in practice; per-cluster issues are always surfaced as JUnit `` entries rather than failing the whole pipeline unless noted otherwise. `ITSM` indicates whether the bundled YAMLs ship with the opt-in ServiceNow auto-raise connector (`Get-AzLocalItsmConfig` + per-failure ticket creation gated on the `raise_itsm_ticket` / `raiseItsmTicket` input); only Monitor: 1, Monitor: 2, Monitor: 3, and Update: 3 ship it - the matrix config lives at `./.itsm/azurelocal-itsm.yml` (see [`Automation-Pipeline-Examples/ITSM/`](../ITSM/) for the full recipe). --- -## Config: 01 - Validate Auth and Inventory Azure Local Clusters +## Config: 1 - Validate Auth and Inventory Azure Local Clusters | Aspect | Value | |---|---| @@ -45,13 +45,13 @@ The table below is the ground truth for what each shipped YAML does **out of the | **RBAC** | Whatever the pipeline identity has - the probe itself is read-only and intentionally surfaces both over- and under-grants. | | **Exit conditions** | Pipeline run is green only when every probe passes. A red run means the identity, its role assignment, or its subscription scope is wrong - downstream pipelines will give nonsense results until this passes. | | **ITSM** | Not supported - this is an identity-only probe with no per-cluster failure surface to dispatch on. | -| **Introduced** | v0.7.70 (separate auth + inventory); v0.8.85 (merged into Config: 01). | +| **Introduced** | v0.7.70 (separate auth + inventory); v0.8.85 (merged into Config: 1). | > **Note**: v0.8.85 consolidates the separate authentication and inventory pipelines into a single `setup-validate-and-inventory.yml` workflow. --- -## Config: 02 - Manage UpdateRing Tags +## Config: 2 - Manage UpdateRing Tags | Aspect | Value | |---|---| @@ -59,8 +59,8 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `csv_path` (required). | | **Trigger** | Manual only (`workflow_dispatch` / **Run pipeline** button). No schedule shipped - this is a deliberate change-controlled operation that should follow a CSV edit + review. Add a `schedule:` / `schedules:` block if your CSV is auto-generated and you want periodic re-application. | | **Cmdlets invoked** | `Set-AzLocalClusterUpdateRingTag`. | -| **Depends on** | Config: 01 produces the input CSV (the operator typically downloads `cluster-inventory.csv`, edits ring / window / exclusion values in a PR, then re-uploads it as the input to this pipeline). | -| **Artefacts** | `UpdateRingTag_YYYYMMDD_HHmmss.csv` (per-cluster CSV log: ClusterName, ResourceGroup, SubscriptionId, ResourceId, Action, PreviousTagValue, NewTagValue, Status, Message) and `UpdateRingTag_Results.json` (the same rows in JSON form, written from `-PassThru`) inside the published log artifact. Since v0.8.0 the pipeline job summary tab also renders a **result breakdown table** (Created / Updated / Already in sync / Skipped / WhatIf / Failed) plus a collapsible **per-cluster details** block, at parity with Config: 03 / Fleet: 01 / Fleet: 04 / Fleet: 06. | +| **Depends on** | Config: 1 produces the input CSV (the operator typically downloads `cluster-inventory.csv`, edits ring / window / exclusion values in a PR, then re-uploads it as the input to this pipeline). | +| **Artefacts** | `UpdateRingTag_YYYYMMDD_HHmmss.csv` (per-cluster CSV log: ClusterName, ResourceGroup, SubscriptionId, ResourceId, Action, PreviousTagValue, NewTagValue, Status, Message) and `UpdateRingTag_Results.json` (the same rows in JSON form, written from `-PassThru`) inside the published log artifact. Since v0.8.0 the pipeline job summary tab also renders a **result breakdown table** (Created / Updated / Already in sync / Skipped / WhatIf / Failed) plus a collapsible **per-cluster details** block, at parity with Config: 3 / Monitor: 3 / Update: 1 / Update: 3. | | **When to run** | After editing the inventory CSV; whenever ring membership or maintenance windows change. | | **RBAC** | Write to tags only. The built-in **Tag Contributor** role on the cluster scope is sufficient (`Microsoft.Resources/tags/*`). Since v0.7.65, `Set-AzLocalClusterUpdateRingTag` writes via `Microsoft.Resources/tags/default` PATCH, so the broader `Microsoft.AzureStackHCI/clusters/write` is not required. | | **Exit conditions** | Pipeline run is green when every CSV row is processed (added / updated / unchanged). Per-cluster tag-write failures surface in the run log; the pipeline does not currently fail on per-row errors. Re-run after triaging is safe (the cmdlet is idempotent). | @@ -68,7 +68,7 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Config: 03 - Apply-Updates Schedule Coverage Audit +## Config: 3 - Apply-Updates Schedule Coverage Audit | Aspect | Value | |---|---| @@ -76,7 +76,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `pipeline_path` (file or folder; default `.github/workflows` on GitHub Actions, `.azure-pipelines` on Azure DevOps - the standard consumer locations for the bundled `apply-updates.yml` sample), `lead_time_minutes` (0-60, default 5), `include_untagged` (default false), `module_version` (optional). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled weekly on Mondays at 05:00 UTC (`cron '0 5 * * 1'`). Deliberately runs before the daily `fleet-connectivity-status` (05:30 UTC), `fleet-update-status` (06:00 UTC), and `fleet-health-status` (07:00 UTC) pipelines so its drift annotations land at the top of the operator's Monday-morning inbox. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Test-AzLocalApplyUpdatesScheduleCoverage`, `Get-AzLocalApplyUpdatesScheduleConfig` (when a `schedule.yml` is present in the repo). | -| **Depends on** | Config: 01 (cluster inventory + `UpdateStartWindow` tags), Config: 02 (tags applied) for non-trivial output. Runs fine on an empty fleet but the audit is meaningless. | +| **Depends on** | Config: 1 (cluster inventory + `UpdateStartWindow` tags), Config: 2 (tags applied) for non-trivial output. Runs fine on an empty fleet but the audit is meaningless. | | **Artefacts** | `schedule-coverage-audit.xml` (JUnit, one `` per `(UpdateRing, UpdateStartWindow)` pair, uncovered = ``), `schedule-coverage-audit.csv` (full Audit view with `Status` / `Recommendation` columns), `schedule-coverage-matrix.csv` (every distinct `(Ring, Window)` pair with its required cron), `schedule-coverage-recommend.md` (ready-to-paste GH Actions + Azure DevOps cron blocks), markdown step summary. | | **When to run** | Hands-off scheduled. Trigger manually whenever you have just tagged a new ring or changed a maintenance window - see the [end-to-end runbook in section 8.3](../README.md#83-end-to-end-runbook-apply-updates-schedule-coverage-audit). | | **RBAC** | Read-only - `Reader` on the cluster scope plus `Microsoft.ResourceGraph/resources/read`. No write actions are ever taken. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | @@ -86,7 +86,7 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Fleet: 02 - Fleet Connectivity Status +## Monitor: 1 - Fleet Connectivity Status | Aspect | Value | |---|---| @@ -94,9 +94,9 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `subscription_ids` (optional - comma-separated; defaults to every subscription the federated identity can read), `module_version` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 05:30 UTC (`cron '30 5 * * *'`). Deliberately runs 30 minutes before `fleet-update-status` (06:00 UTC) so connectivity issues are visible upstream of update reporting. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalFleetConnectivityStatus`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Config: 01 should be green at least once so the subscription scope is trusted. This is the upstream "can we see the fleet at all?" probe that other fleet pipelines silently rely on. | +| **Depends on** | None directly. Config: 1 should be green at least once so the subscription scope is trusted. This is the upstream "can we see the fleet at all?" probe that other fleet pipelines silently rely on. | | **Artefacts** | `fleet-connectivity-status.xml` (JUnit, one `` per cluster), `fleet-cluster-connectivity.csv` (per-cluster reconciliation), `fleet-arc-status-summary.csv` (per-cluster Arc agent counts), `fleet-arc-non-connected-machines.csv` (per-machine triage list), `fleet-physical-nics.csv` (NIC issues only), `fleet-physical-nic-all.csv` (full NIC inventory), `fleet-physical-nic-stats.csv` (histogram by `NicType` + `NicStatus`), `fleet-arb-status.csv` (Azure Resource Bridge state), markdown job summary including the *"How to interpret + act on a non-zero reconciliation"* guidance subsection. | -| **When to run** | Hands-off scheduled. Trigger manually whenever the apply-updates chain returns "zero ready clusters" or the fleet count drops unexpectedly - Fleet: 02 is the upstream "can we see the fleet at all?" probe. | +| **When to run** | Hands-off scheduled. Trigger manually whenever the apply-updates chain returns "zero ready clusters" or the fleet count drops unexpectedly - Monitor: 1 is the upstream "can we see the fleet at all?" probe. | | **RBAC** | Read-only - `Reader` plus `Microsoft.ResourceGraph/resources/read`, `Microsoft.AzureStackHCI/edgeDevices/read`, `Microsoft.HybridCompute/machines/read`, and `Microsoft.ResourceConnector/appliances/read`. All four already live in the **`Azure Stack HCI Update Operator (custom)`** custom role definition shipped in [section 3.1](../README.md#31-custom-role-azure-stack-hci-update-operator-custom). | | **Exit conditions** | Pipeline run is green when the snapshot completes. Per-cluster connectivity issues surface as JUnit `` entries; non-zero reconciliation deltas surface in the markdown summary with remediation guidance. | | **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per cluster with an Arc-disconnected machine or non-zero reconciliation delta; dedupe key is per-cluster + reason via the matrix at `./.itsm/azurelocal-itsm.yml`. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | @@ -104,11 +104,11 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Fleet: 01 - Assess Update Readiness +## Update: 1 - Assess Update Readiness > **Behaviour change in v0.8.81**: status icons in the Summary counts, Not-Ready and All-clusters tables now render via the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: GitHub Markdown shortcodes on the `GitHubActions` host, Unicode glyphs everywhere else - fixes literal `:white_check_mark:` text on Azure DevOps step summaries). The Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink` and the standing **Ctrl-click tip** banner is single-sourced via `Get-AzLocalCtrlClickTip`. The Up-to-Date bucket gains the readiness-cascade check icon. -> **Behaviour change in v0.8.74**: clusters that have already applied every required update are now classified as **Up to Date** (counted separately) instead of being lumped into the Not-Ready bucket. The summary's "Up to date" count is now accurate, the "Not-Ready clusters (review first)" table excludes Up-to-Date and Ready clusters, and the "All clusters detail" table shows a readable `Status` column in place of the old `Ready` boolean. Classification is shared with Fleet: 04 and Fleet: 06 via the `Get-AzLocalClusterReadinessStatus` helper. +> **Behaviour change in v0.8.74**: clusters that have already applied every required update are now classified as **Up to Date** (counted separately) instead of being lumped into the Not-Ready bucket. The summary's "Up to date" count is now accurate, the "Not-Ready clusters (review first)" table excludes Up-to-Date and Ready clusters, and the "All clusters detail" table shows a readable `Status` column in place of the old `Ready` boolean. Classification is shared with Update: 3 and Monitor: 3 via the `Get-AzLocalClusterReadinessStatus` helper. | Aspect | Value | |---|---| @@ -116,39 +116,39 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `update_ring` (required), `throttle_limit` (optional). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule is shipped** because the `update_ring` input is required and there is no single ring value that would be correct for every consumer. See the **recommended customisation note below** for the per-ring scheduling pattern + lead-time guidance. | | **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Test-AzLocalClusterHealth`. | -| **Depends on** | Config: 01 (an `UpdateRing` tag must exist on at least one cluster to scope the readiness query) and Config: 02 (to apply that tag at scale). | +| **Depends on** | Config: 1 (an `UpdateRing` tag must exist on at least one cluster to scope the readiness query) and Config: 2 (to apply that tag at scale). | | **Artefacts** | `readiness.xml`, `readiness.csv`, `health-blocking.xml`, `health-blocking.csv`. | -| **When to run** | 12-72 hours before each Fleet: 04 wave for the same ring (12-24h for small rings, 48-72h for large rings of 50+ clusters) - long enough for an operator to triage any `` entries before the maintenance window opens, short enough that the readiness signal is still fresh. See the **recommended customisation note below** for the cron pattern. | +| **When to run** | 12-72 hours before each Update: 3 wave for the same ring (12-24h for small rings, 48-72h for large rings of 50+ clusters) - long enough for an operator to triage any `` entries before the maintenance window opens, short enough that the readiness signal is still fresh. See the **recommended customisation note below** for the cron pattern. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | | **Exit conditions** | Pipeline run is always green - per-cluster readiness gaps and blocking health checks surface as JUnit `` entries in the Tests tab. **Caveat**: because the pipeline never goes red, a silently-empty `readiness.xml` (e.g. ring tag typo, zero clusters in scope) will not generate a red email - check the Tests tab / `readiness.xml` artefact after each run, or wire the JUnit reporter into your existing CI status surface. | -| **ITSM** | Not supported - this is a pre-flight gate intended for operator review before the Fleet: 04 wave; readiness gaps surface as JUnit `` entries and feed the Fleet: 04 ITSM dispatch downstream rather than raising tickets here. | +| **ITSM** | Not supported - this is a pre-flight gate intended for operator review before the Update: 3 wave; readiness gaps surface as JUnit `` entries and feed the Update: 3 ITSM dispatch downstream rather than raising tickets here. | -> **RECOMMENDED CUSTOMISATION: schedule Fleet: 01 per ring, 12-72h ahead of the matching Fleet: 04 cron.** Unlike Fleet: 04 (which silently does nothing if you forget to schedule it), Fleet: 01 is *recommended* rather than *mandatory* - Fleet: 04 does its own internal `Get-AzLocalClusterUpdateReadiness` per-cluster pre-flight, so skipping Fleet: 01 will not break the apply step. What you lose by skipping it is the **human review window** - the chance to see "12 clusters in Ring2 have blocking health checks" and intervene before the maintenance window starts. +> **RECOMMENDED CUSTOMISATION: schedule Update: 1 per ring, 12-72h ahead of the matching Update: 3 cron.** Unlike Update: 3 (which silently does nothing if you forget to schedule it), Update: 1 is *recommended* rather than *mandatory* - Update: 3 does its own internal `Get-AzLocalClusterUpdateReadiness` per-cluster pre-flight, so skipping Update: 1 will not break the apply step. What you lose by skipping it is the **human review window** - the chance to see "12 clusters in Ring2 have blocking health checks" and intervene before the maintenance window starts. > -> Because `update_ring` is a required input there is no single bundled cron - **add one `schedule:` (GitHub Actions) / `schedules:` (Azure DevOps) entry per ring you intend to patch**, each setting `update_ring:` via the workflow inputs (GH Actions `with:` / ADO `parameters:`). Anchor each Fleet: 01 cron to the matching Fleet: 04 cron (same days, earlier by the lead time): +> Because `update_ring` is a required input there is no single bundled cron - **add one `schedule:` (GitHub Actions) / `schedules:` (Azure DevOps) entry per ring you intend to patch**, each setting `update_ring:` via the workflow inputs (GH Actions `with:` / ADO `parameters:`). Anchor each Update: 1 cron to the matching Update: 3 cron (same days, earlier by the lead time): > -> | Ring size | Recommended Fleet: 01 lead time | Fleet: 01 cron (UTC, anchoring on a Fleet: 04 Sat 02:00 window) | Rationale | +> | Ring size | Recommended Update: 1 lead time | Update: 1 cron (UTC, anchoring on a Update: 3 Sat 02:00 window) | Rationale | > |---|---|---|---| -> | Small (<= 10 clusters) | 12-24 hours ahead | `'0 2 * * 5'` (Fri 02:00 ahead of a Sat 02:00 Fleet: 04) | Enough to triage a handful of `` entries; fresh enough that the health signal is still meaningful at apply time. | -> | Medium (10-50 clusters) | 24-48 hours ahead | `'0 2 * * 4'` (Thu 02:00 ahead of a Sat 02:00 Fleet: 04) | Lets you raise tickets / engage cluster owners on per-cluster issues before the weekend. | -> | Large (50+ clusters) | 48-72 hours ahead | `'0 2 * * 3'` (Wed 02:00 ahead of a Sat 02:00 Fleet: 04) | Allows time to escalate, swap clusters into a deferral ring (`UpdateExcluded=True`), or stage parallel mitigation. | +> | Small (<= 10 clusters) | 12-24 hours ahead | `'0 2 * * 5'` (Fri 02:00 ahead of a Sat 02:00 Update: 3) | Enough to triage a handful of `` entries; fresh enough that the health signal is still meaningful at apply time. | +> | Medium (10-50 clusters) | 24-48 hours ahead | `'0 2 * * 4'` (Thu 02:00 ahead of a Sat 02:00 Update: 3) | Lets you raise tickets / engage cluster owners on per-cluster issues before the weekend. | +> | Large (50+ clusters) | 48-72 hours ahead | `'0 2 * * 3'` (Wed 02:00 ahead of a Sat 02:00 Update: 3) | Allows time to escalate, swap clusters into a deferral ring (`UpdateExcluded=True`), or stage parallel mitigation. | > -> **Known gap**: the Config: 03 schedule-coverage audit currently validates Fleet: 04 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Fleet: 01 cron is correctly anchored to a Fleet: 04 cron. Pair Fleet: 01 and Fleet: 04 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. +> **Known gap**: the Config: 3 schedule-coverage audit currently validates Update: 3 cron-to-`UpdateStartWindow` coverage only - it does **not** audit whether each Update: 1 cron is correctly anchored to a Update: 3 cron. Pair Update: 1 and Update: 3 cron edits in the same PR so the lead-time relationship is reviewable. The end-to-end runbook in the parent README's [section 8.1.1](../README.md#811-recommended-step5-pre-flight-schedule-per-ring) has worked examples for the most common ring layouts. -## Fleet: 03 - Sideload Updates (Opt-in) +## Update: 2 - Sideload Updates (Opt-in) -> **Behaviour change in v0.8.81 (Fleet: 04 Apply-Updates Readiness Gate)**: NOTE this callout describes the **Apply-Updates Readiness Gate** report (`Export-AzLocalClusterReadinessGateReport`), which is internal to the Fleet: 04 apply-updates pipeline (not the Sideload Updates pipeline documented in the table below). The gate report drops its inline icon switch in favour of the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: fixes literal `:white_check_mark:` text on Azure DevOps step summaries); the Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink`; and the standing Ctrl-click tip banner is single-sourced via `Get-AzLocalCtrlClickTip`. +> **Behaviour change in v0.8.81 (Update: 3 Apply-Updates Readiness Gate)**: NOTE this callout describes the **Apply-Updates Readiness Gate** report (`Export-AzLocalClusterReadinessGateReport`), which is internal to the Update: 3 apply-updates pipeline (not the Sideload Updates pipeline documented in the table below). The gate report drops its inline icon switch in favour of the new shared `Get-AzLocalStatusIconMap` private helper (host-aware: fixes literal `:white_check_mark:` text on Azure DevOps step summaries); the Cluster column wraps in a portal deep-link via `Get-AzLocalClusterPortalLink`; and the standing Ctrl-click tip banner is single-sourced via `Get-AzLocalCtrlClickTip`. > **OPT-IN, OFF BY DEFAULT, self-hosted runner required.** Introduced in v0.8.7. This pipeline is inert unless the repository variable `SIDELOAD_UPDATES` is the literal string `'true'`. Full architecture, the scheduled-task survival model, the shared-state / multi-runner contract, the auth-map, and the catalog format are in [sideload.md](sideload.md); robocopy throttling guidance is in [sideload-robocopy.md](sideload-robocopy.md). | Aspect | Value | |---|---| -| **Purpose** | Pre-stage Azure Local solution-update media onto clusters that **cannot pull updates from Azure directly** (dark / air-gapped / restricted-egress fabrics). It Robocopies the `CombinedSolutionBundle` (or OEM SBE package) to each cluster's infrastructure `import` SMB share, verifies the SHA256 over WinRM, runs `Add-SolutionUpdate`, and flips `UpdateSideloaded=True` so the downstream Fleet: 04 apply can proceed. | +| **Purpose** | Pre-stage Azure Local solution-update media onto clusters that **cannot pull updates from Azure directly** (dark / air-gapped / restricted-egress fabrics). It Robocopies the `CombinedSolutionBundle` (or OEM SBE package) to each cluster's infrastructure `import` SMB share, verifies the SHA256 over WinRM, runs `Add-SolutionUpdate`, and flips `UpdateSideloaded=True` so the downstream Update: 3 apply can proceed. | | **Inputs** | `update_ring` (optional - a single ring, a `;`-delimited list, or `***` for every tagged cluster; default `***`), `dry_run` (optional, default `true` - previews the plan + transitions with no staging / task / tag changes), `module_version` (optional pin). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule ships** because the pipeline requires an on-prem self-hosted runner labelled `azlocal-sideload` (GH) / a self-hosted agent in a pool with the `azlocal-sideload` demand (ADO) that most repos do not have. Once the runner/agent is online, uncomment the bundled `*/30 * * * *` cron inside the `BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block to drive the state machine. | | **Cmdlets invoked** | `Resolve-AzLocalSideloadPlan` (selects clusters whose next apply window is within `SIDELOAD_LEAD_DAYS`), `Invoke-AzLocalSideloadUpdate` (the re-entrant state machine), `Export-AzLocalSideloadStatusReport` + `Add-AzLocalSideloadStepSummary` (JUnit + Markdown summary). | | **Re-entrant state machine** | Each run advances every in-scope cluster by **one** transition and exits: `Planned -> Copying -> Copied -> Verified -> Imported -> SideloadFlagged`. The multi-hour copy itself runs in a **detached Windows Scheduled Task** (driven by `Tools/Invoke-AzLocalSideloadCopyTask.ps1`) so no pipeline run is ever long-lived. Re-running is always safe; a stale `Copying` heartbeat (> `SIDELOAD_HEARTBEAT_STALE_MINUTES`) is re-driven. | -| **Depends on** | Config: 01 (`UpdateRing` + sideload tags present, including `UpdateAuthAccountId`), Config: 02 (to apply those tags at scale), and a populated `sideload-auth-map.csv` + `sideload-catalog.yml`. The downstream Fleet: 04 apply consumes the `UpdateSideloaded=True` flag this pipeline sets. | +| **Depends on** | Config: 1 (`UpdateRing` + sideload tags present, including `UpdateAuthAccountId`), Config: 2 (to apply those tags at scale), and a populated `sideload-auth-map.csv` + `sideload-catalog.yml`. The downstream Update: 3 apply consumes the `UpdateSideloaded=True` flag this pipeline sets. | | **Artefacts** | `sideload-status.xml` (JUnit, one cluster per test), `sideload-status.csv`, plus per-run copy/verify logs under the shared `SIDELOAD_STATE_ROOT\logs`. | | **Shared state** | `SIDELOAD_STATE_ROOT` must be a UNC path that every runner/agent can read+write (`state\`, `logs\`, `cache\`). Verified media is cached under `SIDELOAD_CACHE_ROOT` (defaults to `\cache`) so a bundle is downloaded + hashed once and reused across clusters. | | **RBAC** | ARG fleet read + Key Vault secret read + `UpdateSideloaded` tag write for the pipeline identity; a separate Active Directory `[pscredential]` (built from two Key Vault secrets named in the auth-map row) is used for the cluster WinRM remoting and `Add-SolutionUpdate`. | @@ -157,13 +157,13 @@ The table below is the ground truth for what each shipped YAML does **out of the --- -## Fleet: 04 - Apply Updates +## Update: 3 - Apply Updates > **Behaviour change in v0.8.81**: the per-host apply-updates step summary (`Add-AzLocalApplyUpdatesStepSummary`) drops its inline `$iconSuccess` / `$iconFail` / `$iconBlock` / `$iconSkip` / `$iconWarn` definitions in favour of the new shared `Get-AzLocalStatusIconMap` private helper (fixes literal `:white_check_mark:` text on Azure DevOps step summaries). The Cluster column in both the Cluster Actions and Clusters Skipped at Readiness Gate tables now resolves each cluster's resource id from the readiness CSV (built upstream by `Get-AzLocalClusterUpdateReadiness`) and wraps the cell in a portal deep-link via `Get-AzLocalClusterPortalLink`. Adds the standing Ctrl-click tip via `Get-AzLocalCtrlClickTip`. The upstream apply-results JSON shape (`ClusterName` / `Status` / `UpdateName` / `Duration` / `Message`) is intentionally unchanged - the cluster id lookup is done in the renderer. -> **Behaviour change in v0.8.74**: the pre-apply readiness gate report (`Export-AzLocalClusterReadinessGateReport`) replaces its binary `Ready?` column with a readable `Status` column so fully-patched clusters now show **Up to Date** instead of a no-entry icon that implied failure. The header line shows a distinct `Up to Date` count and the step emits a new additive `UP_TO_DATE_COUNT` / `UpToDateCount` output (the `READY_COUNT` gate that drives the apply is unchanged). Classification is shared with Fleet: 01 and Fleet: 06 via the `Get-AzLocalClusterReadinessStatus` helper. When the gate finds no clusters ready, the "No Clusters Ready" summary (`Add-AzLocalNoReadyClustersStepSummary`) now renders an Up-to-Date vs Not-Ready breakdown - so an idle run makes clear that already-patched clusters are a healthy steady state (a notice, not a warning, when every cluster is up to date) rather than a fault. +> **Behaviour change in v0.8.74**: the pre-apply readiness gate report (`Export-AzLocalClusterReadinessGateReport`) replaces its binary `Ready?` column with a readable `Status` column so fully-patched clusters now show **Up to Date** instead of a no-entry icon that implied failure. The header line shows a distinct `Up to Date` count and the step emits a new additive `UP_TO_DATE_COUNT` / `UpToDateCount` output (the `READY_COUNT` gate that drives the apply is unchanged). Classification is shared with Update: 1 and Monitor: 3 via the `Get-AzLocalClusterReadinessStatus` helper. When the gate finds no clusters ready, the "No Clusters Ready" summary (`Add-AzLocalNoReadyClustersStepSummary`) now renders an Up-to-Date vs Not-Ready breakdown - so an idle run makes clear that already-patched clusters are a healthy steady state (a notice, not a warning, when every cluster is up to date) rather than a fault. -> **Behaviour change in v0.8.78**: per-cluster `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips the Step.07 check RED on by-design gate-respect outcomes - `HealthCheckBlocked` deliberately stays a `` because a critical health failure IS actionable. The Fleet: 04 Readiness KPI table also gains optional **Already Up to Date** and **Not Ready (needs attention before updating)** rows, wired in both shipped `apply-updates.yml` templates from the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (emitted since v0.8.74), so operators see the full ring breakdown - eligible / started / skipped / blocked / up-to-date / needs-attention - in one place. GitHub Actions only: `actions/download-artifact@v6 -> @v7` to silence the Node 20 deprecation warning (v7 chosen over v8 because v8 has a breaking `digest-mismatch=error` default). +> **Behaviour change in v0.8.78**: per-cluster `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips the Step.07 check RED on by-design gate-respect outcomes - `HealthCheckBlocked` deliberately stays a `` because a critical health failure IS actionable. The Update: 3 Readiness KPI table also gains optional **Already Up to Date** and **Not Ready (needs attention before updating)** rows, wired in both shipped `apply-updates.yml` templates from the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (emitted since v0.8.74), so operators see the full ring breakdown - eligible / started / skipped / blocked / up-to-date / needs-attention - in one place. GitHub Actions only: `actions/download-artifact@v6 -> @v7` to silence the Node 20 deprecation warning (v7 chosen over v8 because v8 has a breaking `digest-mismatch=error` default). > **New in v0.8.79 - operator break-glass override (`Force Immediate Update`)**: optional pipeline parameter on `apply-updates.yml` (`force_immediate_update` GitHub Actions choice input / `forceImmediateUpdate` Azure DevOps boolean parameter) defaulting to `false`. When set to `true` on a **manual run / queue only**, bypasses the per-cluster `UpdateStartWindow` / `UpdateExclusionsWindow` Step 3c maintenance-window gate for emergency / out-of-window patching. Defence in depth: GitHub Actions YAML collapses the flag to `'false'` for any non-`workflow_dispatch` event (`${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}`); Azure DevOps rechecks `$(Build.Reason) -eq 'Manual'` at runtime before honouring the parameter. A `WARNING:` GUI label is prepended on both pipeline hosts; a high-visibility `::warning::` (GHA) / `##vso[task.logissue type=warning]` (ADO) banner is emitted into the run log when the override fires. The override is unreachable from the `apply-updates-schedule.yml` configuration file (no `forceImmediateUpdate` field exists on `New-AzLocalApplyUpdatesScheduleConfig` / `Resolve-AzLocalPipelineUpdateRing` / `Get-AzLocalApplyUpdatesScheduleAudit`). Other readiness gates (connectivity, health, sideload status, `UpdateExcluded` operator hard-override) continue to apply. Maps to `Invoke-AzLocalReadinessGatedClusterUpdate -ForceImmediateUpdate` -> `Start-AzLocalClusterUpdate -IgnoreScheduleTags` for direct cmdlet use. @@ -173,20 +173,20 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `update_ring` (required), `update_name` (optional - leave blank for latest), `dry_run` (optional), `throttle_limit` (optional). **v0.7.4 adds** `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, defaults preserve existing behaviour). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). **No schedule is shipped** - you must add one. See the **mandatory customisation note below** and the schedule-alignment guidance in [section 8](../README.md#8-scheduling-maintenance-windows-and-change-freeze-periods). | | **Cmdlets invoked** | `Get-AzLocalApplyUpdatesScheduleConfig`, `Get-AzLocalClusterUpdateReadiness`, `Start-AzLocalClusterUpdate`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | Config: 01 (`UpdateRing` tags present), Fleet: 01 (readiness reviewed for the wave). Config: 03 (schedule coverage audit) should be green so the cron(s) that start this pipeline actually fire at every tagged `UpdateStartWindow`. | +| **Depends on** | Config: 1 (`UpdateRing` tags present), Update: 1 (readiness reviewed for the wave). Config: 3 (schedule coverage audit) should be green so the cron(s) that start this pipeline actually fire at every tagged `UpdateStartWindow`. | | **Artefacts** | `update-results.xml` (JUnit, one cluster per test), `update-logs/*` (CSV + detail). When ITSM is enabled: `itsm-results.csv`, `itsm-results.xml`. | -| **When to run** | During the maintenance window for each ring, after the Fleet: 01 readiness assessment is reviewed. | +| **When to run** | During the maintenance window for each ring, after the Update: 1 readiness assessment is reviewed. | | **RBAC** | Write to clusters required. Covered by the `Azure Stack HCI Update Operator (custom)` custom role (`Microsoft.AzureStackHCI/clusters/updates/apply/action` + cluster-update reads). | -| **Exit conditions** | Pipeline run is green when every in-scope cluster either succeeds or is correctly classified as `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` (these are skips, not failures - rendered as JUnit `` from v0.8.78). Per-cluster update failures and `HealthCheckBlocked` outcomes surface as JUnit `` entries; long-running runs are tracked by Fleet: 05. | +| **Exit conditions** | Pipeline run is green when every in-scope cluster either succeeds or is correctly classified as `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` (these are skips, not failures - rendered as JUnit `` from v0.8.78). Per-cluster update failures and `HealthCheckBlocked` outcomes surface as JUnit `` entries; long-running runs are tracked by Update: 4. | | **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per cluster whose update run finished with a failure state; classification skips (`ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag`) do not generate tickets. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | > **MANDATORY CUSTOMISATION: the Apply Updates pipeline does not ship with a schedule.** The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags **only gate updates *while the pipeline is already running***; they do **not** start the pipeline. If you (a) use `UpdateStartWindow` tags to define when updates may be installed and (b) leave the shipped `apply-updates.yml` with `workflow_dispatch` only (GH) / `trigger: none` (ADO), **no updates will ever be applied automatically** - the pipeline will simply never start during the window. > > Add a `schedule:` (GitHub Actions) / `schedules:` (Azure DevOps) block to `apply-updates.yml` that fires at (or a few minutes before) the start of every `UpdateStartWindow` you have tagged. One cron entry per distinct window value. Worked examples and the per-cluster scheduling model are in [section 8](../README.md#8-scheduling-maintenance-windows-and-change-freeze-periods). > -> Pipeline summaries (Fleet: 04 GH Actions + Azure DevOps) classify per-cluster skips as `ScheduleBlocked` (outside `UpdateStartWindow` or inside `UpdateExclusionsWindow`), `SideloadedBlocked` (`UpdateSideloaded=False` on a sideloaded-workflow cluster) and, new in v0.7.90, `ExcludedByTag` (`UpdateExcluded=True` operator hard override). The Actions Required callout points operators at the `UpdateExcluded` tag when one or more clusters are intentionally held out of automation. +> Pipeline summaries (Update: 3 GH Actions + Azure DevOps) classify per-cluster skips as `ScheduleBlocked` (outside `UpdateStartWindow` or inside `UpdateExclusionsWindow`), `SideloadedBlocked` (`UpdateSideloaded=False` on a sideloaded-workflow cluster) and, new in v0.7.90, `ExcludedByTag` (`UpdateExcluded=True` operator hard override). The Actions Required callout points operators at the `UpdateExcluded` tag when one or more clusters are intentionally held out of automation. -## Fleet: 05 - Monitor In-Flight Updates +## Update: 4 - Monitor In-Flight Updates > **Behaviour change in v0.8.81**: the `$stateIcon` (`State` column) and `$statusIcon` (`Current Step Status` column) switches in `Export-AzLocalUpdateRunMonitorReport` no longer hard-code GitHub-Markdown shortcodes (`:large_blue_circle:` / `:hourglass_flowing_sand:` / `:red_circle:` / etc.) which previously rendered as literal text on Azure DevOps step summaries. Both switches now read from the shared `Get-AzLocalStatusIconMap` private helper using the new `State*` (`StateInProgress` / `StateSucceeded` / `StateFailed` / `StateNotStarted` / `StateUnknown`) and `Status*` (`StatusInProgress` / `StatusCancelled`) keys. JUnit XML output and CSV columns are unchanged. @@ -196,37 +196,17 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `scope` (`all-clusters` (default) or `by-update-ring`), `update_ring` (only used when `scope=by-update-ring`; single ring, `Prod;Ring2` semicolon-list, or `***` wildcard), `long_running_threshold_hours` (0-48, default 6), `module_version` (optional). | | **Trigger** | **Manual only by default** (`workflow_dispatch` / **Run pipeline** button). A commented-out `schedule: cron '*/30 * * * *'` (GH) / `schedules: cron '*/30 * * * *'` (ADO) sample is shipped inside the `# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers` block - uncomment it for the duration of an active wave, then re-comment it once the wave drains, so you do not generate 48 empty runs per day between waves. | | **Cmdlets invoked** | `Get-AzLocalUpdateRuns`, `Get-AzLocalClusterInventory` (used for ring-scope mode). | -| **Depends on** | Fleet: 04 must be active (at least one `InProgress` run) for the output to be non-empty. Runs cleanly on an empty fleet (reports "no in-flight runs"). | +| **Depends on** | Update: 3 must be active (at least one `InProgress` run) for the output to be non-empty. Runs cleanly on an empty fleet (reports "no in-flight runs"). | | **Artefacts** | `update-monitor.xml` (JUnit, one `` per in-flight cluster - the failure message contains current step + progress + elapsed duration when the run is over `long_running_threshold_hours`, or the deepest-step error detail when `failureType='StepError'` fires under v0.7.96), `update-monitor.csv` (full per-cluster rows including the new `Status` + `ErrorMessage` columns), markdown job summary with the in-flight table (cluster + update names as portal links from v0.7.96) and long-running flag column. | | **Exit conditions** | Pipeline run is green when the snapshot completes. Long-running runs (over `long_running_threshold_hours`) AND stuck-step errors (`failureType='StepError'`, v0.7.96+) surface as JUnit `` entries so they appear in the Tests / Checks tab without failing the whole pipeline. v0.7.96 also adds two new `GITHUB_OUTPUT` values: `STEP_ERRORED` (count of runs that hit a step error inside the threshold) and `UNRESOLVED_FAILURES` (count of Failed runs surfaced from `Get-AzLocalUpdateRunFailures`). | -| **When to run** | While a wave is active (manually, or via the commented cron). The two daily snapshot pipelines (Fleet: 06 and Fleet: 07) remain the steady-state daily reports; this pipeline is purpose-built for the "is the apply-updates run I started two hours ago still progressing?" question that those daily snapshots cannot answer between runs. | +| **When to run** | While a wave is active (manually, or via the commented cron). The two daily snapshot pipelines (Monitor: 2 and Monitor: 3) remain the steady-state daily reports; this pipeline is purpose-built for the "is the apply-updates run I started two hours ago still progressing?" question that those daily snapshots cannot answer between runs. | | **RBAC** | Read-only - `Reader` on the cluster scope, plus the cluster-update API read paths already enumerated in the `Azure Stack HCI Update Operator (custom)` custom role. | -| **ITSM** | Not supported - in-flight monitor exists to be a fast feedback loop during an active wave; long-running runs surface as JUnit `` entries for the operator on call, and post-wave failures are picked up + ticketed by the daily Fleet: 06 snapshot. | +| **ITSM** | Not supported - in-flight monitor exists to be a fast feedback loop during an active wave; long-running runs surface as JUnit `` entries for the operator on call, and post-wave failures are picked up + ticketed by the daily Monitor: 3 snapshot. | | **Introduced** | v0.7.90. Enhanced in v0.7.96 with the `Status` + `ErrorMessage` columns, the new `StepError` JUnit failure type, the always-shown Failed-runs block, portal-linked Cluster Name / Update Name cells, and the new `STEP_ERRORED` / `UNRESOLVED_FAILURES` `GITHUB_OUTPUT` values. **Behaviour change in v0.7.98:** UX overhaul of the in-flight table - rows now sort by composite `SeverityScore` (`StepError severity x 1000 + RunSeverity x 100 + elapsed-hours bucket`) so stuck step errors and runs over 14 days bubble to the top regardless of cluster alphabetical order; each row carries per-cell `StateIcon` + `StatusIcon` icons and a horizontal chip stack of flags (`STEP-STUCK`, `RUN-STUCK`, `UNRESOLVED`, `RECENT-FAIL`); the job summary opens with a single `CRITICAL / WARN / OK` fleet status badge that collapses the worst row across the fleet into one line (e.g. `CRITICAL - 1 stuck step error(s), 1 run(s) > 14d, 1 step(s) > 4h`); each failed step's `errorMessage` is now wrapped in a collapsible `
Verbose error...
` block; and JUnit `` + `` are populated with real per-run elapsed seconds (was `time="0"` before, which made GitHub Test Reporter render `"5 tests were completed in 0ms"`). | --- -## Fleet: 06 - Fleet Update Status - -> **Behaviour change in v0.8.81**: the GitHub-Markdown shortcodes used by the Critical Health Status table, the Primary Status table, and the Version Distribution Support column are now routed through the shared `Get-AzLocalStatusIconMap` private helper using the new `Info` / `GreenCircle` / `YellowCircle` / `CycleArrows` / `GreyQuestion` / `SupportSupported` / `SupportUnsupported` / `SupportUnknown` keys (fixes literal `:green_circle:` / `:yellow_circle:` / `:information_source:` / etc. text on Azure DevOps step summaries). JUnit XML output and CSV columns are unchanged. - -| Aspect | Value | -|---|---| -| **Purpose** | Daily fleet-wide snapshot of cluster update state. Read-only. **v0.7.90 pivots the Version Distribution markdown table by YYMM** (leading column `Version` = `2511`, `2604`, ...; new `Update Versions` column lists each distinct full version installed within that YYMM as ` x ` separated by `
`; rows sorted ascending by YYMM so the oldest YYMM is at the top). The underlying `` JUnit XML is unchanged - still one `` per distinct full `CurrentVersion` - so machine-readable consumers and CI test-reporters are unaffected. **v0.7.96 reworks the Primary Status bucket cascade to align with the Azure portal Update Manager `state` filter:** `NeedsAttention` is now promoted into the **Update Failed** bucket; `PreparationFailed` lands in a new **Action Required** bucket (with its own actionable guidance in the Actions Required callout because the run never started, so re-arming is required); `PreparationInProgress` is folded into **Update In Progress**. The new bucket surfaces via (1) a new `` JUnit attribute on the primary testsuite, (2) per-testcase `failureType='PreparationFailed'` instead of the generic `UpdateFailure`, (3) a new `ACTION_REQUIRED` `GITHUB_OUTPUT` (`actionRequired` pipeline variable in ADO), (4) `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and (5) a new "Action Required (PreparationFailed)" row in the Primary Status markdown table with separate actionable prose in the Actions Required callout. The `📜 Update Run History and Error Details` markdown table renders Cluster Name and Update Name cells as `` deep-links (cluster's Updates blade + the single-instance update-run history view) - same linking as the Fleet: 05 in-flight table. | -| **Inputs** | Scope (`-AllClusters` or `-ScopeByUpdateRingTag`), `throttle_limit` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | -| **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 06:00 UTC (`cron '0 6 * * *'`). Edit the cron in the YAML to change cadence. | -| **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Get-AzLocalUpdateSummary`, `Get-AzLocalAvailableUpdates`, `Get-AzLocalUpdateRuns`, `Get-AzLocalUpdateRunFailures`, `Get-AzLocalLatestSolutionVersion`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Best-quality output when Fleet: 02 (connectivity) has run earlier the same morning, since clusters Arc can't see won't appear in the readiness or update-summary results. | -| **Artefacts** | `readiness-status.xml` / `.csv` / `.json` (the `.json` carries the rolled-up bucket counts under `Summary.*` - `UpdateFailures`, `ActionRequired`, `HealthFailures`, `SbeBlocked`, `InProgress`, `ReadyForUpdate`, `UpToDate`, `Other` - and the new `primaryActionRequired` field mirrors the JUnit testsuite attribute from v0.7.96 for downstream automation), `cluster-inventory.csv`, `update-summaries.csv`, `available-updates.csv`, `update-runs.csv`. | -| **When to run** | Hands-off scheduled. Trigger manually for ad-hoc reporting. | -| **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | -| **Exit conditions** | Pipeline run is green when the snapshot completes. Per-cluster issues (outdated version, failed run history) surface as JUnit `` entries. v0.7.96 distinguishes `failureType='UpdateFailure'` (run executed and failed - landed in `NeedsAttention`) from `failureType='PreparationFailed'` (run never started - landed in the new `Action Required` bucket) so downstream automation can split paging behaviour per failure mode. | -| **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per unresolved failed update run picked up by the daily snapshot; dedupe key combines cluster + update version + failure reason via `./.itsm/azurelocal-itsm.yml`. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | -| **Introduced** | Originally Fleet: 04 in v0.7.4 (then Step 7); renumbered to Step 8 in v0.7.90 when the in-flight monitor moved into Step 7; renumbered to Step 9 in v0.8.7 when the on-prem sideload pipeline took Step 6. v0.7.90 added the YYMM-pivoted Version Distribution table. v0.7.96 promoted `NeedsAttention` into the `Update Failed` bucket, added the new `Action Required` bucket for `PreparationFailed`, folded `PreparationInProgress` into `Update In Progress`, added the `primaryActionRequired` JUnit attribute, the `ACTION_REQUIRED` `GITHUB_OUTPUT`, `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and portal-linked Cluster Name / Update Name cells in the `Update Run History and Error Details` markdown table. **Behaviour change in v0.7.98:** each `` inside the `📜 Update Run History and Error Details` testsuite now emits `time` = `DurationMinutes * 60` (`DurationMinutes` from KQL `datetime_diff('minute', EndTime, StartTime)` via `Get-AzLocalUpdateRunFailures`), and the testsuite-level `time=` is the sum of those per-row seconds. The other two testsuites (`FleetVersionDistribution`, `AzureLocalFleetUpdateStatus`) intentionally stay at `time="0"` - they are instantaneous snapshot projections and a synthetic duration would be misleading. | - ---- - -## Fleet: 07 - Fleet Health Status +## Monitor: 2 - Fleet Health Status > **Behaviour change in v0.8.81**: fixes the **"Healthy + Unhealthy != Total" KPI counting bug**. The previous KPI table summed only two buckets (Healthy + Unhealthy) and silently dropped clusters whose `HealthStatus` was `In progress` / `Unknown` / `Health check failed` AND had no failure rows. The KPI block is now split into two tables: **Cluster Counts** (Total / Healthy / Unhealthy / **Other**, where Other is computed as `Total - Healthy - Unhealthy` clamped non-negative so the three buckets always sum to Total) and **Failing Checks Breakdown** (Total / Critical / Warning / Distinct Reasons). New `other_clusters` pipeline step output; `-PassThru` PSCustomObject gains `OtherClusters`. **Detailed Results columns reordered** to put the most-specific identifier first: `Severity | Title | Failure Reason | Description | Health Check Name | Failure Remediation | Target Resource Name | Target Resource Type | Last Occurrence | Resource Group`. Description is a collapsible `
` block that surfaces drive/volume-level detail (e.g. the file path emitted by a `Microsoft.Health.FaultType.Volume.FileSystem.Corruption.Correctable` warning); Health Check Name is the raw fault-type name (preserved verbatim so operators can grep / triage by FaultType across runs). Icons in the table are wired through the new shared `Get-AzLocalStatusIconMap` private helper. @@ -236,7 +216,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Inputs** | `severity` (optional - `Critical`, `Warning`, or `All`; default `All`), `update_ring_tag` (optional - narrow to one wave), `throttle_limit` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | | **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 07:00 UTC (`cron '0 7 * * *'`). Deliberately offset by one hour from `fleet-update-status` (06:00 UTC) to avoid agent and ARM contention. Edit the cron in the YAML to change cadence. | | **Cmdlets invoked** | `Get-AzLocalFleetHealthFailures`, `Get-AzLocalFleetHealthOverview`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | -| **Depends on** | None directly. Best-quality output when Fleet: 02 (connectivity) has run earlier the same morning. | +| **Depends on** | None directly. Best-quality output when Monitor: 1 (connectivity) has run earlier the same morning. | | **Artefacts** | `fleet-health-status.xml` (JUnit, one `` per failing check, grouped under `Critical Health Failures` / `Warning Health Failures` testsuites), `fleet-health-detail.csv` (one row per failing check), `fleet-health-summary.csv` (aggregated by `FailureReason` + `Severity`), markdown job summary. | | **When to run** | Hands-off scheduled. Trigger manually for ad-hoc fleet-wide health triage outside the daily schedule, especially after operational events (capacity changes, network maintenance, certificate rotations) where health-check failures are expected to spike. | | **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | @@ -245,3 +225,23 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Introduced** | v0.7.65. | --- + +## Monitor: 3 - Fleet Update Status + +> **Behaviour change in v0.8.81**: the GitHub-Markdown shortcodes used by the Critical Health Status table, the Primary Status table, and the Version Distribution Support column are now routed through the shared `Get-AzLocalStatusIconMap` private helper using the new `Info` / `GreenCircle` / `YellowCircle` / `CycleArrows` / `GreyQuestion` / `SupportSupported` / `SupportUnsupported` / `SupportUnknown` keys (fixes literal `:green_circle:` / `:yellow_circle:` / `:information_source:` / etc. text on Azure DevOps step summaries). JUnit XML output and CSV columns are unchanged. + +| Aspect | Value | +|---|---| +| **Purpose** | Daily fleet-wide snapshot of cluster update state. Read-only. **v0.7.90 pivots the Version Distribution markdown table by YYMM** (leading column `Version` = `2511`, `2604`, ...; new `Update Versions` column lists each distinct full version installed within that YYMM as ` x ` separated by `
`; rows sorted ascending by YYMM so the oldest YYMM is at the top). The underlying `` JUnit XML is unchanged - still one `` per distinct full `CurrentVersion` - so machine-readable consumers and CI test-reporters are unaffected. **v0.7.96 reworks the Primary Status bucket cascade to align with the Azure portal Update Manager `state` filter:** `NeedsAttention` is now promoted into the **Update Failed** bucket; `PreparationFailed` lands in a new **Action Required** bucket (with its own actionable guidance in the Actions Required callout because the run never started, so re-arming is required); `PreparationInProgress` is folded into **Update In Progress**. The new bucket surfaces via (1) a new `` JUnit attribute on the primary testsuite, (2) per-testcase `failureType='PreparationFailed'` instead of the generic `UpdateFailure`, (3) a new `ACTION_REQUIRED` `GITHUB_OUTPUT` (`actionRequired` pipeline variable in ADO), (4) `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and (5) a new "Action Required (PreparationFailed)" row in the Primary Status markdown table with separate actionable prose in the Actions Required callout. The `📜 Update Run History and Error Details` markdown table renders Cluster Name and Update Name cells as `
` deep-links (cluster's Updates blade + the single-instance update-run history view) - same linking as the Update: 4 in-flight table. | +| **Inputs** | Scope (`-AllClusters` or `-ScopeByUpdateRingTag`), `throttle_limit` (optional). v0.7.4-style ITSM toggles are also exposed: `raise_itsm_ticket`, `itsm_config_path`, `itsm_dry_run`, `itsm_force_create` (all optional, default off). | +| **Trigger** | Manual (`workflow_dispatch` / **Run pipeline** button) **plus** scheduled daily at 06:00 UTC (`cron '0 6 * * *'`). Edit the cron in the YAML to change cadence. | +| **Cmdlets invoked** | `Get-AzLocalClusterInventory`, `Get-AzLocalClusterUpdateReadiness`, `Get-AzLocalUpdateSummary`, `Get-AzLocalAvailableUpdates`, `Get-AzLocalUpdateRuns`, `Get-AzLocalUpdateRunFailures`, `Get-AzLocalLatestSolutionVersion`. When ITSM is enabled: `Get-AzLocalItsmConfig`. | +| **Depends on** | None directly. Best-quality output when Monitor: 1 (connectivity) has run earlier the same morning, since clusters Arc can't see won't appear in the readiness or update-summary results. | +| **Artefacts** | `readiness-status.xml` / `.csv` / `.json` (the `.json` carries the rolled-up bucket counts under `Summary.*` - `UpdateFailures`, `ActionRequired`, `HealthFailures`, `SbeBlocked`, `InProgress`, `ReadyForUpdate`, `UpToDate`, `Other` - and the new `primaryActionRequired` field mirrors the JUnit testsuite attribute from v0.7.96 for downstream automation), `cluster-inventory.csv`, `update-summaries.csv`, `available-updates.csv`, `update-runs.csv`. | +| **When to run** | Hands-off scheduled. Trigger manually for ad-hoc reporting. | +| **RBAC** | Read-only. Covered by the `Azure Stack HCI Update Operator (custom)` custom role. | +| **Exit conditions** | Pipeline run is green when the snapshot completes. Per-cluster issues (outdated version, failed run history) surface as JUnit `` entries. v0.7.96 distinguishes `failureType='UpdateFailure'` (run executed and failed - landed in `NeedsAttention`) from `failureType='PreparationFailed'` (run never started - landed in the new `Action Required` bucket) so downstream automation can split paging behaviour per failure mode. | +| **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per unresolved failed update run picked up by the daily snapshot; dedupe key combines cluster + update version + failure reason via `./.itsm/azurelocal-itsm.yml`. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | +| **Introduced** | Originally shipped in v0.7.4 (as Step 7); renumbered to Step 8 in v0.7.90 when the in-flight monitor moved into Step 7; renumbered to Step 9 in v0.8.7 when the on-prem sideload pipeline took Step 6. v0.7.90 added the YYMM-pivoted Version Distribution table. v0.7.96 promoted `NeedsAttention` into the `Update Failed` bucket, added the new `Action Required` bucket for `PreparationFailed`, folded `PreparationInProgress` into `Update In Progress`, added the `primaryActionRequired` JUnit attribute, the `ACTION_REQUIRED` `GITHUB_OUTPUT`, `Summary.UpdateFailures` + `Summary.ActionRequired` in `readiness-status.json`, and portal-linked Cluster Name / Update Name cells in the `Update Run History and Error Details` markdown table. **Behaviour change in v0.7.98:** each `` inside the `📜 Update Run History and Error Details` testsuite now emits `time` = `DurationMinutes * 60` (`DurationMinutes` from KQL `datetime_diff('minute', EndTime, StartTime)` via `Get-AzLocalUpdateRunFailures`), and the testsuite-level `time=` is the sum of those per-row seconds. The other two testsuites (`FleetVersionDistribution`, `AzureLocalFleetUpdateStatus`) intentionally stay at `time="0"` - they are instantaneous snapshot projections and a synthetic duration would be misleading. | + +--- diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md index 141361c6..db747e31 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/sideload.md @@ -20,7 +20,7 @@ imported, the pipeline flips the `UpdateSideloaded=True` gate so the downstream internet-connected clusters. For the per-pipeline reference card (inputs, artefacts, RBAC, exit conditions) see -[appendix-pipelines.md - Fleet: 03](appendix-pipelines.md#fleet-03---sideload-updates-opt-in). +[appendix-pipelines.md - Update: 2](appendix-pipelines.md#update-2---sideload-updates-opt-in). For robocopy throttling guidance see [sideload-robocopy.md](sideload-robocopy.md). --- 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 67fe3898..73d557de 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 @@ -31,7 +31,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: 'Config: 03 - Apply-Updates Schedule Coverage Audit' +name: 'Config: 3 - Apply-Updates Schedule Coverage Audit' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers 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 340b7af5..2acce124 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -12,7 +12,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: 'Fleet: 04 - Apply Updates' +name: 'Update: 3 - Apply Updates' on: # PREFERRED PATTERN - drive automated update runs from a ring-aware 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 65e8acb6..a8f2a444 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 @@ -36,7 +36,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: 'Fleet: 01 - Assess Update Readiness' +name: 'Update: 1 - Assess Update Readiness' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers 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 9302b219..65dad3d0 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 @@ -50,7 +50,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the pipelines in execution order. -name: 'Fleet: 02 - Fleet Connectivity Status' +name: 'Monitor: 1 - Fleet Connectivity Status' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers 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 16d7e647..bab6f10e 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 @@ -32,7 +32,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: 'Fleet: 07 - Fleet Health Status' +name: 'Monitor: 2 - Fleet Health Status' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers 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 31206f5b..a339d85c 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 @@ -24,7 +24,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the eight pipelines in execution order. -name: 'Fleet: 06 - Fleet Update Status' +name: 'Monitor: 3 - Fleet Update Status' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers 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 ac989219..dfbfa3ff 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 @@ -22,7 +22,7 @@ # Workflow name carries the same Setup prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the setup and operational pipelines in execution order. -name: 'Config: 02 - Manage UpdateRing Tags' +name: 'Config: 2 - Manage UpdateRing Tags' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers 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 49c494d6..1c513ecd 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml @@ -23,7 +23,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the pipelines in execution order. -name: 'Fleet: 05 - Monitor In-Flight Updates' +name: 'Update: 4 - Monitor In-Flight Updates' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers @@ -76,6 +76,39 @@ on: description: 'CRITICAL tier for overall-elapsed (default 3 days). In-flight runs older than this get a :rotating_light: chip; older than 2x this get a :skull: chip. Strongest visual signal that human intervention is needed - a 19-day-stuck run will no longer be hidden behind a single "step errored" chip.' required: false default: '3' + # ITSM (ServiceNow) auto-ticketing - opt-in (v0.8.87). When enabled the + # connector reads the update-monitor.xml this pipeline already produces and, + # for each cluster whose Status matches the trigger matrix (default raises + # for AttemptWithoutRun / StepError / Failed), opens a deduped incident. + # Default 'false' so existing schedules stay byte-identical until you opt in. + raise_itsm_ticket: + description: 'Open ITSM tickets (ServiceNow) for in-flight monitor failures (stuck steps, failed runs, attempt-without-run)' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' + itsm_config_path: + description: 'Path to ITSM matrix config (YAML or JSON)' + required: false + default: './.itsm/azurelocal-itsm.yml' + itsm_dry_run: + description: 'ITSM: build payloads + run read-only dedupe but do NOT create tickets' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' + itsm_force_create: + description: 'ITSM: bypass dedupe and always create new tickets (use with caution)' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' module_version: description: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".' required: false @@ -235,3 +268,101 @@ jobs: list-suites: failed list-tests: failed continue-on-error: true + + # ---------------------------------------------------------------------- + # ITSM Connector (ServiceNow auto-raise on in-flight monitor failures) + # + # Fully opt-in (gated on inputs.raise_itsm_ticket == 'true'). Runs AFTER + # the JUnit file is uploaded + published, so the ITSM action is strictly + # additive - failures here never affect the monitor exit code (which is + # always green; this is a report-only pipeline). + # + # Reads the same update-monitor.xml that powers the run summary. The + # JUnit emitter writes per-testcase (ClusterName / + # ClusterResourceId / UpdateName / Status / CurrentStep / + # ClusterPortalUrl / UpdateRunPortalUrl) so New-AzLocalIncident can + # compute the SHA256 dedupe key (one incident per stuck/failed/attempt-gap + # cluster until it clears) and the Mustache body template can deep-link + # into the Azure portal. Configure which monitor Status values raise a + # ticket in ./.itsm/azurelocal-itsm.yml (defaults raise AttemptWithoutRun / + # StepError / Failed; LongRunningStep / LongRunningOverall are opt-in). + # ---------------------------------------------------------------------- + - name: Install powershell-yaml (ITSM config parser) + if: ${{ github.event.inputs.raise_itsm_ticket == 'true' }} + shell: pwsh + run: | + if (-not (Get-Module -ListAvailable -Name powershell-yaml)) { + Install-Module powershell-yaml -Scope CurrentUser -Force -AllowClobber + } + + - name: Raise ITSM tickets + if: ${{ github.event.inputs.raise_itsm_ticket == 'true' }} + shell: pwsh + id: itsm + env: + # BEGIN-AZLOCAL-CUSTOMIZE:itsm-secrets + # Bind your ITSM connector secrets here. Defaults match the + # ServiceNow OAuth client_credentials naming used by azurelocal-itsm.yml. + # Preserved by Update-AzLocalPipelineExample across module upgrades. + ITSM_SN_INSTANCE_URL: ${{ secrets.ITSM_SN_INSTANCE_URL }} + ITSM_SN_CLIENT_ID: ${{ secrets.ITSM_SN_CLIENT_ID }} + ITSM_SN_CLIENT_SECRET: ${{ secrets.ITSM_SN_CLIENT_SECRET }} + # END-AZLOCAL-CUSTOMIZE:itsm-secrets + INPUT_ITSM_CONFIG_PATH: ${{ github.event.inputs.itsm_config_path }} + INPUT_ITSM_DRY_RUN: ${{ github.event.inputs.itsm_dry_run }} + INPUT_ITSM_FORCE_CREATE: ${{ github.event.inputs.itsm_force_create }} + run: | + Import-Module AzLocal.UpdateManagement -Force + + $configPath = $env:INPUT_ITSM_CONFIG_PATH + $dryRun = $env:INPUT_ITSM_DRY_RUN -eq 'true' + $force = $env:INPUT_ITSM_FORCE_CREATE -eq 'true' + + if (-not (Test-Path $configPath)) { + Write-Host "::warning::ITSM config not found at '$configPath' - skipping ticket creation." + exit 0 + } + + $cfg = Get-AzLocalItsmConfig -Path $configPath + + $junitInput = './reports/update-monitor.xml' + if (-not (Test-Path $junitInput)) { + Write-Host "::warning::No update-monitor.xml found at '$junitInput' - skipping ticket creation." + exit 0 + } + + $params = @{ + InputArtifactPath = $junitInput + Config = $cfg + RunMetadata = @{ + Platform = 'github' + RunId = $env:GITHUB_RUN_ID + RunUrl = "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID" + Branch = $env:GITHUB_REF + } + DryRun = $dryRun + ForceCreate = $force + ExportPath = './reports/itsm-results.csv' + ExportJUnitPath = './reports/itsm-results.xml' + } + + $results = New-AzLocalIncident @params + $results | Format-Table ClusterName, Action, TicketId, Severity -AutoSize + + - name: Upload ITSM Artefacts + if: ${{ github.event.inputs.raise_itsm_ticket == 'true' }} + uses: actions/upload-artifact@v6 + with: + name: azlocal-step.8-update-monitor-itsm-results_${{ steps.artifact-stamp.outputs.timestamp }} + path: ./reports/itsm-*.* + retention-days: 30 + continue-on-error: true + + - name: Publish ITSM Test Results + if: ${{ github.event.inputs.raise_itsm_ticket == 'true' }} + uses: dorny/test-reporter@v3 + with: + name: 'ITSM Tickets (Update: 4 Monitor)' + path: ./reports/itsm-results.xml + reporter: java-junit + continue-on-error: true diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml index 2505e4d5..a9d4ce9b 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/setup-validate-and-inventory.yml @@ -1,5 +1,5 @@ # AZLOCAL-PIPELINE-ID: setup-validate-and-inventory -# Config: 01 - Validate Auth and Inventory Azure Local Clusters +# Config: 1 - Validate Auth and Inventory Azure Local Clusters # # PURPOSE: # This is the first step in the setup workflow. It combines two operations: @@ -7,7 +7,7 @@ # 2. INVENTORY: Queries all Azure Local clusters and exports inventory with UpdateRing tag status # # Run this workflow: -# - BEFORE adding the operational workflows (Fleet: 01-07). It narrows any failure +# - BEFORE adding the operational workflows (Update: 1-4 and Monitor: 1-3). It narrows any failure # to authentication/discovery rather than seven interacting pipelines. # - PERIODICALLY (recommended monthly, or after every RBAC change in the tenant) # to confirm subscription scope and cluster inventory are accurate. @@ -42,7 +42,7 @@ # # See Automation-Pipeline-Examples/README.md for full setup story. -name: 'Config: 01 - Validate Auth and Inventory Clusters' +name: 'Config: 1 - Validate Auth and Inventory Clusters' on: workflow_dispatch: @@ -171,7 +171,7 @@ jobs: if: always() uses: dorny/test-reporter@v3 with: - name: '[JUnit Debug] Config: 01 - Authentication Validation' + name: '[JUnit Debug] Config: 1 - Authentication Validation' path: ./reports/auth-report.xml reporter: java-junit fail-on-error: false 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 9db0ec10..7306b3e4 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml @@ -38,7 +38,7 @@ # Workflow name carries the same Step.N - prefix as the filename so the GitHub # Actions sidebar (which sorts workflows alphabetically by this `name:` field) # lists the pipelines in execution order. -name: 'Fleet: 03 - Sideload Updates (Opt-in)' +name: 'Update: 2 - Sideload Updates (Opt-in)' on: # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 3e8955c3..39b7cca5 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 @@ -317,11 +317,11 @@ # ReleaseNotes of this module ReleaseNotes = @' -## Version 0.8.87 - Patch: renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM. Adds investigate-before-acting guidance. KPI note, causes list, and cluster-table cross-reference updated to match. The Cluster Connectivity table is split into "Cluster with Connectivity Issues" (shown first) and "Cluster without Connectivity Issues" (Connected + ARB Running, collapsed). Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. +## Version 0.8.87 - Renames the bundled pipeline display names into a three-group `Config: N` / `Monitor: N` / `Update: N` scheme (single-digit, e.g. `Monitor: 1 - Fleet Connectivity Status`, `Update: 4 - Monitor In-Flight Updates`), replacing the former `Setup: 0N` / `Fleet: 0N` prefixes; filenames and `AZLOCAL-PIPELINE-ID` values are unchanged. Also renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Monitor: 1 - Fleet Connectivity Status) output to "Non-Azure Local and/or Orphan ARB appliances" and adds a caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned - Arc resource bridge is also used by VMware vSphere and SCVMM. Adds investigate-before-acting guidance. KPI note, causes list, and cluster-table cross-reference updated to match. The Cluster Connectivity table is split into "Cluster with Connectivity Issues" (shown first) and "Cluster without Connectivity Issues" (Connected + ARB Running, collapsed). Output text only - no public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.87'`. Also: `Export-AzLocalUpdateRunMonitorReport` (Update: 4) now writes per-`` `` (ClusterName/ClusterResourceId/UpdateName/Status/CurrentStep/portal URLs) into `update-monitor.xml`, with Status values StepError/LongRunningStep/LongRunningOverall/InProgress/Failed/AttemptWithoutRun, so `New-AzLocalIncident` can dedupe and deep-link stuck/failed/attempt-without-run clusters. The bundled `monitor-updates.yml` (GitHub + Azure DevOps) gains an opt-in ITSM ticketing step (`raise_itsm_ticket`/`raiseItsmTicket`, default off). The sample ITSM matrix gains AttemptWithoutRun/StepError (raise) and opt-in LongRunningOverall/LongRunningStep entries. `Export-AzLocalApplyUpdatesScheduleAudit` (Config: 3) gains an always-on "Recommended in-flight monitor schedule (Update: 4)" section plus `-MonitorFiresPerHour` (1-12, default 2) and `-MonitorTrailingDays` (0-14, default 3). Additive. -## Version 0.8.86 - Patch: renames the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` so the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sorts the onboarding / configuration workflows ahead of the `Fleet: 0N` operational workflows. Both surfaces sort alphabetically by the workflow `name:` / definition name, and `C` (Config) sorts before `F` (Fleet); the previous `Setup:` prefix sorted AFTER `Fleet:`. Only the operator-facing `name:` (GitHub Actions) / `displayName:` (Azure DevOps) fields change - filenames, `AZLOCAL-PIPELINE-ID` values, aliases, prune logic, and all cmdlet behaviour are unchanged. Bundled pipeline README + appendix docs updated to match. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.85'` to `'0.8.86'`. +## Version 0.8.86 - Patch: renamed the three onboarding pipeline templates from `Setup: 0N` to `Config: 0N` (operator-facing display name only; filenames, `AZLOCAL-PIPELINE-ID` values, aliases, prune logic, and cmdlet behaviour unchanged). Superseded by the v0.8.87 `Config:`/`Monitor:`/`Update:` naming scheme. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.86'`. -## Version 0.8.85 - Patch: introduces Setup/Fleet naming in bundled pipeline templates, adds merged GitHub onboarding workflow `setup-validate-and-inventory.yml`, and updates pipeline refresh tooling with optional deprecated-file pruning guarded by AZLOCAL-PIPELINE-ID verification. No public API or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.84'` to `'0.8.85'`. +## Version 0.8.85 - Patch: introduced Setup/Fleet pipeline naming, added the merged GitHub onboarding workflow `setup-validate-and-inventory.yml`, and added optional deprecated-file pruning guarded by AZLOCAL-PIPELINE-ID verification. Superseded by later naming. No public API or behavioural change. `GENERATED_AGAINST_MODULE_VERSION` bumped to `'0.8.85'`. ## Version 0.8.83 - Patch: fix-forward for v0.8.82 Item-5. The Step.08 `UpdateLastAttempt` reconciliation in `Export-AzLocalUpdateRunMonitorReport` reads `$inv.tags` from `Get-AzLocalClusterInventory`, but the v0.8.82 inventory projection did not carry the raw ARM `tags` bag - so the "Recent update attempts with no observable updateRun" section was silently always empty in production. v0.8.83 surfaces the raw `tags` bag on every inventory row (in-memory only; the CSV / JSON export keeps its explicit `$selectColumns` whitelist - new regression test asserts both). Also wires `attempts_without_run` into the GitHub Actions `monitor-updates.yml` `jobs.outputs:` block (ADO `Set-AzLocalPipelineOutput` auto-publishes so only an ADO docstring refresh needed), and corrects the `Export-AzLocalUpdateRunMonitorReport` "6 step outputs" docstring to "7 step outputs". No public API change. Export count unchanged (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.82'` to `'0.8.83'`. @@ -329,17 +329,13 @@ ## Version 0.8.81 - Step summary polish across Steps 05-10. Step.10 fixes the KPI bug where Healthy + Unhealthy did not sum to Total (splits into Cluster Counts + Failing Checks Breakdown tables, adds `OtherClusters` step output; Detailed Results gains Title, raw FailureName and collapsible Description columns surfacing drive/volume detail e.g. file paths from `...FileSystem.Corruption.Correctable`). Steps 05-09 adopt three shared private helpers: `Get-AzLocalStatusIconMap` (host-aware GitHub Unicode vs Azure DevOps shortcodes), `Get-AzLocalClusterPortalLink` (portal deep-link wrapper) and `Get-AzLocalCtrlClickTip` (single-source Ctrl-click banner). Fixes literal shortcode text (`:white_check_mark:` etc.) rendering on Azure DevOps step summaries in Step.08 monitor + Step.09 fleet-update-status. No public API change (still 60 exports). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.80'` to `'0.8.81'`. -## Version 0.8.80 - Minor: three additive pipeline failure-rendering improvements across Step.05 / Step.08 / Step.09 / Step.10 step summaries. Q1 - Step.09 `Get-AzLocalUpdateRunFailures` attaches a `HealthCheckEvidence` array column (same-cluster Critical health-check entries within +/-2h) to HealthCheck-category rows; new private helper `Get-AzLocalUpdateRunHealthEvidence`; `-EnrichWithHealthEvidence` opt-out. Q2 - Step.05 `Test-AzLocalClusterHealth` and Step.10 `Get-AzLocalFleetHealthFailures` add per-check `Title` + full `TargetResourceID`; `Export-AzLocalFleetHealthStatusReport` wraps TargetResourceName in a portal hyperlink. Q3 - both deepest-error walkers capture step `description` alongside `errorMessage`; new `DeepestStepDescription` / `ErrorDescription` fields on `Get-AzLocalUpdateRunFailures` / `Format-AzLocalUpdateRun` / `Get-AzLocalUpdateRuns -PassThru`; Step.08 + Step.09 renderers combine the two in markdown failure cells and JUnit bodies. No new exports (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.79'` to `'0.8.80'`. See CHANGELOG.md for the full v0.8.80 entry. +## Version 0.8.80 - Minor: three additive pipeline failure-rendering improvements across Step.05 / Step.08 / Step.09 / Step.10 step summaries. Q1 - Step.09 `Get-AzLocalUpdateRunFailures` attaches a `HealthCheckEvidence` array column (same-cluster Critical health-check entries within +/-2h) to HealthCheck-category rows; new private helper `Get-AzLocalUpdateRunHealthEvidence`; `-EnrichWithHealthEvidence` opt-out. Q2 - Step.05 `Test-AzLocalClusterHealth` and Step.10 `Get-AzLocalFleetHealthFailures` add per-check `Title` + full `TargetResourceID`; `Export-AzLocalFleetHealthStatusReport` wraps TargetResourceName in a portal hyperlink. Q3 - both deepest-error walkers capture step `description` alongside `errorMessage`; new `DeepestStepDescription` / `ErrorDescription` fields on `Get-AzLocalUpdateRunFailures` / `Format-AzLocalUpdateRun` / `Get-AzLocalUpdateRuns -PassThru`; Step.08 + Step.09 renderers combine the two in markdown failure cells and JUnit bodies. No new exports (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `'0.8.79'` to `'0.8.80'`. ## Version 0.8.79 - Patch: operator break-glass override for Step.07. Adds `Force Immediate Update` to `Invoke-AzLocalReadinessGatedClusterUpdate` (`-ForceImmediateUpdate`) and `Start-AzLocalClusterUpdate` (`-IgnoreScheduleTags`), plus matching pipeline parameters `force_immediate_update` (GitHub Actions `workflow_dispatch` input) / `forceImmediateUpdate` (Azure DevOps `parameters:` boolean) both defaulting to `false`. When enabled, the per-cluster Step 3c maintenance-window gate (`UpdateStartWindow` / `UpdateExclusionsWindow` tags) is bypassed and updates start regardless of the current UTC time. Intended for emergency / out-of-window patching driven by an on-call operator. The override is UNREACHABLE from the apply-updates-schedule.yml configuration file - GHA wiring collapses the flag to `'false'` for any non-`workflow_dispatch` trigger (`${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}`), and ADO rechecks `$(Build.Reason) -eq 'Manual'` at runtime before honouring the parameter. A `WARNING:` GUI label is prepended on both pipeline hosts and a high-visibility `::warning::` (GHA) / `##vso[task.logissue type=warning]` (ADO) banner is emitted into the run log when the override fires. No new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.78'` to `'0.8.79'`. -- **CHANGE (`Start-AzLocalClusterUpdate`)**: new `[switch]$IgnoreScheduleTags` parameter; when set, the Step 3c maintenance-window block is skipped and a per-cluster `Warning` is logged with the bypassed tag values. Does NOT bypass any other readiness gate. -- **CHANGE (`Invoke-AzLocalReadinessGatedClusterUpdate`)**: new `[switch]$ForceImmediateUpdate` parameter; forwards `-IgnoreScheduleTags` per cluster and emits a host-aware warning banner at the top of the apply. -- **CHANGE (`apply-updates.yml`)**: new `force_immediate_update` GHA choice / `forceImmediateUpdate` ADO boolean parameter; both enforce the manual-only constraint at the YAML layer so a scheduled cron firing can never honour the flag. - ## Version 0.8.78 - Patch: pipeline-summary UX polish (`ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` now JUnit ``; `Add-AzLocalApplyUpdatesStepSummary` gains `-UpToDateCount` / `-NotReadyCount`; `actions/download-artifact@v6` -> `@v7`). See CHANGELOG.md / docs/release-history.md. -For full v0.7.x and v0.8.x release notes see: +For full release notes see: https://github.com/NeilBird/Azure-Local/blob/main/AzLocal.UpdateManagement/CHANGELOG.md '@ diff --git a/AzLocal.UpdateManagement/CHANGELOG.md b/AzLocal.UpdateManagement/CHANGELOG.md index 1e6078d5..1c43dea7 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -7,8 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.8.87] - 2026-06-16 -Patch release. Renames the "Orphan ARBs" section in the -`New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity +Renames the bundled pipeline display names into a three-group `Config: N` / +`Monitor: N` / `Update: N` scheme (single-digit, e.g. +`Monitor: 1 - Fleet Connectivity Status`, `Update: 4 - Monitor In-Flight +Updates`), replacing the former `Setup: 0N` / `Fleet: 0N` prefixes so the +GitHub Actions and Azure DevOps lists group onboarding, day-2 monitoring, and +update-lifecycle workflows logically. Display name (`name:` / `displayName:`) +only - filenames and `AZLOCAL-PIPELINE-ID` values are unchanged. + +Also renames the "Orphan ARBs" section in the +`New-AzLocalFleetConnectivityStatusSummary` (Monitor: 1 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is @@ -17,8 +25,56 @@ Arc-enabled deployments, so a listed appliance may be a healthy, in-use bridge for a non-Azure Local platform. Output text only - no public API change. Export count unchanged (still 60). +Also extends the in-flight monitor (Update: 4) and ITSM connector so stuck / +failed / attempt-without-run clusters surfaced by +`Export-AzLocalUpdateRunMonitorReport` can auto-raise deduped ServiceNow +incidents, and teaches the Config: 3 schedule auditor +(`Export-AzLocalApplyUpdatesScheduleAudit`) to recommend an Update: 4 monitor +poll cadence. New parameters are additive with backward-compatible defaults; +export count unchanged (still 60). + +### Added + +- `Export-AzLocalUpdateRunMonitorReport` now writes per-`` + `` into `update-monitor.xml` (`ClusterName`, `ClusterResourceId`, + `UpdateName`, `Status`, `CurrentStep`, `ClusterPortalUrl`, + `UpdateRunPortalUrl`). The `Status` values are `StepError`, `LongRunningStep`, + `LongRunningOverall`, `InProgress`, `Failed`, and `AttemptWithoutRun` (the + UpdateLastAttempt-reconciliation rows, which use a `(no update name)` + `UpdateName` fallback so the ITSM dedupe key stays stable per cluster). This + is what lets `New-AzLocalIncident` dedupe and deep-link from the monitor + pipeline (previously every monitor row was skipped for missing + ClusterResourceId / UpdateName). +- `Export-AzLocalApplyUpdatesScheduleAudit` (Config: 3) gains a new always-on + "Recommended in-flight monitor schedule (Update: 4)" section that derives a + `monitor-updates.yml` poll cron from the apply-window weekday(s), covering the + apply day plus a configurable trailing window for multi-day runs. Two new + parameters: `-MonitorFiresPerHour` (1-12, default 2 = every 30 min) and + `-MonitorTrailingDays` (0-14, default 3, mirroring the Update: 4 CRITICAL + elapsed tier). +- The bundled `monitor-updates.yml` (GitHub Actions and Azure DevOps) gains an + opt-in ITSM ticketing step (`raise_itsm_ticket` / `raiseItsmTicket`, + default off) that runs `New-AzLocalIncident` against the monitor JUnit after + it is published. Strictly additive - the monitor remains report-only and + always green. +- The sample ITSM trigger matrix + (`Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml`) gains entries for + the Update: 4 monitor statuses: `AttemptWithoutRun` (raise, severity 3), + `StepError` (raise, severity 2), and opt-in `LongRunningOverall` / + `LongRunningStep`. Documented in `ITSM/ITSM-Config-Reference.md`. + ### Changed +- Renames the bundled pipeline display names (`name:` on GitHub Actions, + `displayName:` / `name:` on Azure DevOps) into the three-group + `Config: N` / `Monitor: N` / `Update: N` scheme with single-digit numbering: + `Config: 1-3` (onboarding/config), `Monitor: 1-3` (day-2 fleet monitoring), + and `Update: 1-4` (update lifecycle). `Monitor: 2` is Fleet Health Status and + `Monitor: 3` is Fleet Update Status (numbered to match alphabetical order). + Filenames, `AZLOCAL-PIPELINE-ID` values, aliases, and `-PruneDeprecated` + logic are unchanged - this is a display-name-only change. Bundled pipeline + `README.md`, `docs/appendix-pipelines.md`, and the ITSM / release docs are + updated to match. - `New-AzLocalFleetConnectivityStatusSummary` renames the `### Orphan ARBs (no matching cluster in scope)` section heading to `### Non-Azure Local and/or Orphan ARB appliances`, adds an intro line ("These ARB appliances do not have diff --git a/AzLocal.UpdateManagement/ITSM/ITSM-Config-Reference.md b/AzLocal.UpdateManagement/ITSM/ITSM-Config-Reference.md index 19119cdc..de991813 100644 --- a/AzLocal.UpdateManagement/ITSM/ITSM-Config-Reference.md +++ b/AzLocal.UpdateManagement/ITSM/ITSM-Config-Reference.md @@ -56,6 +56,15 @@ Top-level keys are JUnit `Status` values produced by `Get-AzLocalUpdateRuns` / `Invoke-AzLocalFleetOperation` (e.g. `Failed`, `Error`, `HealthCheckBlocked`, `SideloadedBlocked`, `ExcludedByTag`, `ScheduleBlocked`, `Skipped`, `NotReady`). +From v0.8.87 the **Update: 4 - Monitor In-Flight Updates** pipeline +(`Export-AzLocalUpdateRunMonitorReport` -> `update-monitor.xml`) also emits +per-testcase `Status` values you can target: `AttemptWithoutRun` (an +`UpdateLastAttempt` tag with no observable updateRun, commonly a URP package +pre-install health-check failure), `StepError` (a step stuck in `Error` while +the run is still `InProgress`), and the informational `LongRunningOverall` / +`LongRunningStep` thresholds. Wire `monitor-updates.yml` with +`raise_itsm_ticket=true` to action them. + | Field | Type | Notes | |---|---|---| | `triggers..raiseTicket` | bool | Required. `true` opens a ticket; `false` (default) skips. | diff --git a/AzLocal.UpdateManagement/ITSM/README.md b/AzLocal.UpdateManagement/ITSM/README.md index 58b92e5b..e9854cbc 100644 --- a/AzLocal.UpdateManagement/ITSM/README.md +++ b/AzLocal.UpdateManagement/ITSM/README.md @@ -30,6 +30,8 @@ When any of the four "operator-attention" pipelines finishes - **`Step.4_fleet-c > **v0.7.76: Step.4 fleet-connectivity ticketing added.** When the `Step.4_fleet-connectivity-status` pipeline was introduced in v0.7.76 it shipped with the same opt-in ITSM wiring as Step.7 / Step.8 - gated on `raise_itsm_ticket=true`, reading `./reports/fleet-connectivity-status.xml`, and using the existing `azurelocal-itsm.yml` trigger matrix. Each row in the JUnit file emits `Status=Critical` or `Status=Warning` (Disconnected / Offline / partial-connectivity Arc-agent / ARB / NIC / cluster rows), so the same `Critical` / `Warning` entries from the trigger matrix in [Section 5](#5-author-the-trigger-matrix) drive Step.4 with no extra config. Step.4 emits a row-specific `UpdateName` per testcase (e.g. `ClusterConnectivity=Disconnected`, `ArcAgent=Disconnected []`, `PhysicalNic=Down [/]`, `ARB=Offline []`), so the SHA256 dedupe key naturally separates a cluster-level disconnect from an individual NIC / Arc-agent / ARB failure even when several rows fire for the same cluster on the same day. +> **v0.8.87: Update: 4 in-flight monitor ticketing added.** The `monitor-updates.yml` pipeline (Update: 4 - Monitor In-Flight Updates, produced by `Export-AzLocalUpdateRunMonitorReport`) now ships with the same opt-in ITSM wiring - gated on `raise_itsm_ticket=true` (`raiseItsmTicket` on Azure DevOps), reading `./reports/update-monitor.xml`, and using the existing `azurelocal-itsm.yml` trigger matrix. Until v0.8.87 the monitor JUnit carried no per-`` ``, so every monitor row was skipped by `New-AzLocalIncident` for a missing `ClusterResourceId` / `UpdateName`. The monitor now emits `ClusterName` / `ClusterResourceId` / `UpdateName` / `Status` / `CurrentStep` / `ClusterPortalUrl` / `UpdateRunPortalUrl` per row, with `Status` one of `StepError` / `LongRunningStep` / `LongRunningOverall` / `InProgress` / `Failed` / `AttemptWithoutRun`. The trigger matrix gains `AttemptWithoutRun` (raise, severity 3) and `StepError` (raise, severity 2) plus opt-in `LongRunningOverall` / `LongRunningStep` entries (see [Section 5](#5-author-the-trigger-matrix)). `AttemptWithoutRun` rows (UpdateLastAttempt tag with no observable updateRun) use a `(no update name)` `UpdateName` fallback so the SHA256 dedupe key stays stable per cluster. The monitor remains report-only and always green - ITSM failures never affect its result. + What it deliberately does **not** do in Phase 1: open Jira / ADO Work Items, send Teams / Slack notifications, or close tickets on success. See [ITSM-Connector-Plan.md Sections 2 + 9](./ITSM-Connector-Plan.md) for the phased roadmap. --- diff --git a/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 b/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 index 5db6430d..f1ae650b 100644 --- a/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 +++ b/AzLocal.UpdateManagement/Private/Get-AzLocalPipelineManifest.ps1 @@ -82,7 +82,7 @@ function Get-AzLocalPipelineManifest { # # v0.8.85: the two standalone onboarding workflows authentication-test.yml # (DisplayStep 0) and inventory-clusters.yml (DisplayStep 1) were merged - # into a single setup-validate-and-inventory.yml (Config: 01). Its Aliases + # into a single setup-validate-and-inventory.yml (Config: 1). Its Aliases # list BOTH superseded filenames (and their Step.N_ ancestors) so # Update-AzLocalPipelineExample can rename-match a customer who still has # either old file and carry their schedule CRONs into the merged workflow. diff --git a/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 b/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 index b60aa2a3..3681a16c 100644 --- a/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 +++ b/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 @@ -136,6 +136,18 @@ function Export-AzLocalApplyUpdatesScheduleAudit { shifted back this many days). When not explicitly bound it is resolved from the SIDELOAD_LEAD_DAYS environment variable, defaulting to 7. + .PARAMETER MonitorFiresPerHour + How often the v0.8.87 "Recommended in-flight monitor schedule" section + suggests the Update: 4 monitor-updates pipeline should poll while an + update run is in flight (1-12, default 2 = every 30 minutes). Drives + the minute field of the recommended monitor cron. + + .PARAMETER MonitorTrailingDays + How many days after an apply window opens an update run may still be + in flight (0-14, default 3, mirroring the Update: 4 monitor's CRITICAL + elapsed tier). The recommended monitor cron covers the apply weekday(s) + plus this many trailing days so multi-day runs stay observed. + .PARAMETER InstalledModuleVersion Optional [string] used in the markdown footer ('Generated by AzLocal.UpdateManagement v'). @@ -225,6 +237,14 @@ function Export-AzLocalApplyUpdatesScheduleAudit { [ValidateRange(0, 365)] [int]$SideloadLeadDays = 7, + [Parameter(Mandatory = $false)] + [ValidateRange(1, 12)] + [int]$MonitorFiresPerHour = 2, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, 14)] + [int]$MonitorTrailingDays = 3, + [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] @@ -279,6 +299,8 @@ function Export-AzLocalApplyUpdatesScheduleAudit { Write-Host "IncludeUntagged : $IncludeUntagged" Write-Host "ClusterCsvPath : $(if ($haveCsv) { $ClusterCsvPath } else { '(skipped - empty or missing)' })" Write-Host "Platform : $Platform" + Write-Host "MonitorFires/hr : $MonitorFiresPerHour" + Write-Host "MonitorTrailDays : $MonitorTrailingDays" Write-Host '' # ---- Audit view (rows + CSV) ------------------------------------------ @@ -859,6 +881,88 @@ function Export-AzLocalApplyUpdatesScheduleAudit { $sideloadScheduleIncluded = $true } + # ---- Recommended in-flight monitor schedule (Update: 4) ---------------- + # v0.8.87: recommend a poll cron for the 'Update: 4 - Monitor In-Flight + # Updates' (monitor-updates.yml) pipeline, scaled to how often updates are + # applied. Once an apply window fires, an update run can stay in-flight for + # hours to days (the monitor's own CRITICAL elapsed tier defaults to 3 + # days), so the monitor must poll frequently across the apply weekday(s) + # AND the trailing days an update may still be running. Cadence (fires/hour) + # is driven by -MonitorFiresPerHour; trailing coverage by -MonitorTrailingDays. + # Always emitted (independent of the sideload opt-in). + $monitorFires = $MonitorFiresPerHour + $monitorIntervalMinutes = [int][math]::Floor(60 / $monitorFires) + if ($monitorIntervalMinutes -lt 1) { $monitorIntervalMinutes = 1 } + $monitorMinuteField = if ($monitorFires -le 1) { '0' } else { ('*/{0}' -f $monitorIntervalMinutes) } + + # Distinct apply-window weekly firings (DayOfWeek) from the apply pipeline + # crons. Read-AzLocalApplyUpdatesYamlCrons uses a unary-comma return - + # direct assignment only (NEVER @() wrap). + $monitorApplyFirings = New-Object 'System.Collections.Generic.List[psobject]' + try { + $monitorCronTriggers = Read-AzLocalApplyUpdatesYamlCrons -Path $PipelineYamlPath -ErrorAction Stop + foreach ($mct in $monitorCronTriggers) { + try { + $mparsed = ConvertFrom-AzLocalCronExpression -Expression $mct.CronExpression -ErrorAction Stop + if ($mparsed.IsValid -and -not $mparsed.IsComplex) { + foreach ($mft in @($mparsed.FireTimes)) { + $monitorApplyFirings.Add([pscustomobject]@{ + DayOfWeek = [int]$mft.DayOfWeek + }) | Out-Null + } + } + } + catch { + Write-Verbose "Monitor section: cron parse failed for '$($mct.CronExpression)': $($_.Exception.Message)" + } + } + } + catch { + Write-Warning "Failed to read apply-updates crons for the monitor schedule recommendation: $($_.Exception.Message)" + } + + [void]$md.Add('### Recommended in-flight monitor schedule (Update: 4)') + [void]$md.Add('') + [void]$md.Add(('The **Update: 4 - Monitor In-Flight Updates** (`monitor-updates.yml`) pipeline should poll frequently while any update run is in flight. An update can stay in-flight for hours to days after its apply window opens, so poll **{0}x/hour** (every {1} min) across the apply weekday(s) plus the next **{2} day(s)** to catch multi-day runs.' -f $monitorFires, $monitorIntervalMinutes, $MonitorTrailingDays)) + [void]$md.Add('') + + $monitorDayNames = @('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat') + $monitorApplyDows = @($monitorApplyFirings | ForEach-Object { [int]$_.DayOfWeek } | Sort-Object -Unique) + if ($monitorApplyDows.Count -gt 0) { + # Expand each apply weekday across the trailing coverage days. + $monitorCoverageSet = New-Object 'System.Collections.Generic.HashSet[int]' + foreach ($applyDow in $monitorApplyDows) { + for ($k = 0; $k -le $MonitorTrailingDays; $k++) { + [void]$monitorCoverageSet.Add((($applyDow + $k) % 7)) + } + } + $monitorCoverageDows = @($monitorCoverageSet) | Sort-Object + $monitorDayField = if ($monitorCoverageDows.Count -ge 7) { '*' } else { ($monitorCoverageDows -join ',') } + $monitorCron = ('{0} * * * {1}' -f $monitorMinuteField, $monitorDayField) + $monitorCoverageLabel = if ($monitorCoverageDows.Count -ge 7) { 'every day' } else { (($monitorCoverageDows | ForEach-Object { $monitorDayNames[$_] }) -join ', ') } + $monitorApplyLabel = ($monitorApplyDows | ForEach-Object { $monitorDayNames[$_] }) -join ', ' + + [void]$md.Add(('Apply firing weekday(s): **{0}**. With {1} trailing day(s) of coverage, the monitor should run on: **{2}**.' -f $monitorApplyLabel, $MonitorTrailingDays, $monitorCoverageLabel)) + [void]$md.Add('') + [void]$md.Add('Recommended Update: 4 monitor cron - paste into the `schedule:` block of `monitor-updates.yml`:') + [void]$md.Add('') + [void]$md.Add('```') + [void]$md.Add($monitorCron) + [void]$md.Add('```') + [void]$md.Add('') + [void]$md.Add('> GitHub Actions supports sub-hour cron (e.g. `*/30`). Azure DevOps YAML `schedules:` is hourly-only - for sub-hour cadence on ADO, trigger `monitor-updates.yml` from an external scheduler via REST. Widen the day list above if your runs routinely exceed the trailing-day coverage.') + [void]$md.Add('') + } + else { + $monitorFallbackCron = ('{0} * * * *' -f $monitorMinuteField) + [void]$md.Add('> No simple weekly apply-window firings were found in the pipeline YAML, so a focused monitor window could not be derived. Run the monitor 24x7 at the configured cadence:') + [void]$md.Add('') + [void]$md.Add('```') + [void]$md.Add($monitorFallbackCron) + [void]$md.Add('```') + [void]$md.Add('') + } + [void]$md.Add('### Reports Available') [void]$md.Add("- ``$AuditCsvFileName`` - one row per (UpdateRing, UpdateStartWindow) pair") [void]$md.Add("- ``$MatrixCsvFileName`` - full inventory + required cron per row") diff --git a/AzLocal.UpdateManagement/Public/Export-AzLocalUpdateRunMonitorReport.ps1 b/AzLocal.UpdateManagement/Public/Export-AzLocalUpdateRunMonitorReport.ps1 index 51aa570d..a0b4f316 100644 --- a/AzLocal.UpdateManagement/Public/Export-AzLocalUpdateRunMonitorReport.ps1 +++ b/AzLocal.UpdateManagement/Public/Export-AzLocalUpdateRunMonitorReport.ps1 @@ -491,6 +491,23 @@ function Export-AzLocalUpdateRunMonitorReport { # ---- JUnit XML via the shared emitter --------------------------------- $testCases = New-Object 'System.Collections.Generic.List[hashtable]' + # v0.8.87: emit per-testcase (ClusterName / ClusterResourceId / + # UpdateName / Status / CurrentStep / portal URLs) so the ITSM connector + # (New-AzLocalIncident) can compute the SHA256 dedupe key and the Mustache + # body template can deep-link into the Azure portal. The Status property is + # what Get-AzLocalItsmTriggerDecision matches against the trigger matrix. + $tcProps = { + param($row, [string]$statusValue) + [ordered]@{ + ClusterName = [string]$row.ClusterName + ClusterResourceId = [string]$row.ClusterResourceId + UpdateName = [string]$row.UpdateName + Status = $statusValue + CurrentStep = [string]$row.CurrentStep + ClusterPortalUrl = [string]$row.ClusterPortalUrl + UpdateRunPortalUrl = [string]$row.UpdateRunPortalUrl + } + } foreach ($r in ($inFlight | Sort-Object @{Expression='SeverityScore';Descending=$true}, ClusterName)) { $safeName = ($r.ClusterName -replace '[^A-Za-z0-9_.-]', '_') $caseName = "$safeName - $($r.UpdateName) - $($r.CurrentStep)" @@ -513,6 +530,7 @@ function Export-AzLocalUpdateRunMonitorReport { ClassName = 'UpdateMonitor' Time = [double]$r.RunDurationSeconds Failure = @{ Message = $msg; Type = 'StepError'; Body = $msg } + Properties = (& $tcProps $r 'StepError') }) | Out-Null } elseif ($r.ExceedsStepThreshold) { @@ -522,6 +540,7 @@ function Export-AzLocalUpdateRunMonitorReport { ClassName = 'UpdateMonitor' Time = [double]$r.RunDurationSeconds Failure = @{ Message = $msg; Type = 'LongRunningStep'; Body = $msg } + Properties = (& $tcProps $r 'LongRunningStep') }) | Out-Null } elseif ($r.ExceedsThreshold) { @@ -531,6 +550,7 @@ function Export-AzLocalUpdateRunMonitorReport { ClassName = 'UpdateMonitor' Time = [double]$r.RunDurationSeconds Failure = @{ Message = $msg; Type = 'LongRunningOverall'; Body = $msg } + Properties = (& $tcProps $r 'LongRunningOverall') }) | Out-Null } else { @@ -538,6 +558,7 @@ function Export-AzLocalUpdateRunMonitorReport { Name = $caseName ClassName = 'UpdateMonitor' Time = [double]$r.RunDurationSeconds + Properties = (& $tcProps $r 'InProgress') }) | Out-Null } } @@ -561,6 +582,7 @@ function Export-AzLocalUpdateRunMonitorReport { ClassName = 'UpdateMonitor' Time = [double]$r.RunDurationSeconds Failure = @{ Message = $msg; Type = 'RecentFailure'; Body = $msg } + Properties = (& $tcProps $r 'Failed') }) | Out-Null } foreach ($gap in ($attemptGaps | Sort-Object @{Expression='AttemptUtc';Descending=$true}, ClusterName)) { @@ -574,6 +596,17 @@ function Export-AzLocalUpdateRunMonitorReport { ClassName = 'UpdateMonitor' Time = 0.0 Failure = @{ Message = $msg; Type = 'AttemptWithoutRun'; Body = $msg } + Properties = [ordered]@{ + ClusterName = [string]$gap.ClusterName + ClusterResourceId = [string]$gap.ClusterResourceId + UpdateName = $updateLabel + Status = 'AttemptWithoutRun' + CurrentStep = '' + ClusterPortalUrl = if ($gap.ClusterResourceId) { 'https://portal.azure.com/#@/resource' + [string]$gap.ClusterResourceId + '/updates' } else { '' } + UpdateRunPortalUrl = '' + Outcome = [string]$gap.Outcome + AttemptUtc = [string]$gap.AttemptUtcText + } }) | Out-Null } $null = New-AzLocalPipelineJUnitXml -TestSuitesName 'Update Run Monitor' -Suites @( diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 45b5dbe4..fb5ee94d 100644 --- a/AzLocal.UpdateManagement/README.md +++ b/AzLocal.UpdateManagement/README.md @@ -88,7 +88,17 @@ If you are new to this module, work through these in order from a regular PowerS ## What's New in v0.8.87 -**Fleet Connectivity Status output clarification.** Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to **"Non-Azure Local and/or Orphan ARB appliances"** and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is *not necessarily orphaned*. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so a listed appliance may be a healthy, in-use resource bridge for a non-Azure Local platform. +**Pipeline display-name rename.** The bundled pipeline display names are renamed into a three-group `Config: N` / `Monitor: N` / `Update: N` scheme (single-digit, e.g. `Monitor: 1 - Fleet Connectivity Status`, `Update: 4 - Monitor In-Flight Updates`), replacing the former `Setup: 0N` / `Fleet: 0N` prefixes so the GitHub Actions and Azure DevOps lists group onboarding, day-2 monitoring, and update-lifecycle workflows logically. Filenames and `AZLOCAL-PIPELINE-ID` values are unchanged. + +**Fleet Connectivity Status output clarification.** Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Monitor: 1 - Fleet Connectivity Status) markdown output to **"Non-Azure Local and/or Orphan ARB appliances"** and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is *not necessarily orphaned*. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so a listed appliance may be a healthy, in-use resource bridge for a non-Azure Local platform. + +**In-flight monitor ITSM auto-ticketing + Config: 3 monitor-cadence recommendation.** The Update: 4 in-flight monitor and the ServiceNow ITSM connector now join up so stuck, failed, and attempt-without-run clusters auto-raise deduped incidents, and the Config: 3 schedule auditor recommends how often to poll the monitor. + +### Added + +- **`Export-AzLocalUpdateRunMonitorReport` (Update: 4)** now writes per-`` `` into `update-monitor.xml` (`ClusterName`, `ClusterResourceId`, `UpdateName`, `Status`, `CurrentStep`, `ClusterPortalUrl`, `UpdateRunPortalUrl`). `Status` is one of `StepError`, `LongRunningStep`, `LongRunningOverall`, `InProgress`, `Failed`, or `AttemptWithoutRun`. This lets `New-AzLocalIncident` dedupe (one incident per affected cluster until it clears) and deep-link into the Azure portal - previously every monitor row was skipped for a missing ClusterResourceId / UpdateName. +- **Opt-in ITSM ticketing in `monitor-updates.yml`** (GitHub Actions and Azure DevOps). A new `raise_itsm_ticket` / `raiseItsmTicket` input (default off) runs `New-AzLocalIncident` against the monitor JUnit after it is published. The monitor stays report-only and always green; ITSM failures never affect its result. The sample trigger matrix `azurelocal-itsm.yml` gains `AttemptWithoutRun` (raise, severity 3), `StepError` (raise, severity 2), and opt-in `LongRunningOverall` / `LongRunningStep` entries. +- **`Export-AzLocalApplyUpdatesScheduleAudit` (Config: 3)** gains an always-on **"Recommended in-flight monitor schedule (Update: 4)"** section that derives a `monitor-updates.yml` poll cron from the apply-window weekday(s), covering the apply day plus a configurable trailing window for multi-day runs. Two new parameters: `-MonitorFiresPerHour` (1-12, default 2 = every 30 min) and `-MonitorTrailingDays` (0-14, default 3). ### Changed @@ -98,7 +108,7 @@ If you are new to this module, work through these in order from a regular PowerS ### Notes -- **Output text only** - no public API, parameter, or behavioural change. +- The Monitor: 1 connectivity changes are **output text only**. The monitor / ITSM / Config: 3 changes add new JUnit properties, two optional parameters (`-MonitorFiresPerHour`, `-MonitorTrailingDays`), and an opt-in pipeline step - all **backward-compatible** with existing defaults. - **No new exports** (count unchanged at 60). - **`GENERATED_AGAINST_MODULE_VERSION`** bumped from `0.8.86` to `0.8.87` across bundled pipeline templates. diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index b8e6b716..6f82f67b 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -19461,3 +19461,84 @@ Describe 'v0.8.82: Export-AzLocalUpdateRunMonitorReport reconciles UpdateLastAtt } #endregion v0.8.82 Item 5: LastUpdated column + UpdateLastAttempt audit tag + +#region v0.8.87: monitor JUnit properties + ITSM wiring + Config:03 monitor cron + +Describe 'v0.8.87: Export-AzLocalUpdateRunMonitorReport emits per-testcase ITSM Properties' { + BeforeAll { + $script:src887mon = Get-Content -LiteralPath "$PSScriptRoot/../Public/Export-AzLocalUpdateRunMonitorReport.ps1" -Raw + } + It 'Defines a $tcProps scriptblock with the ITSM dedupe/deep-link keys' { + $script:src887mon | Should -Match '\$tcProps\s*=\s*\{' + $script:src887mon | Should -Match 'ClusterResourceId\s*=\s*\[string\]\$row\.ClusterResourceId' + $script:src887mon | Should -Match 'UpdateName\s*=\s*\[string\]\$row\.UpdateName' + $script:src887mon | Should -Match 'Status\s*=\s*\$statusValue' + } + It 'Attaches Properties to each in-flight testcase with the correct Status value' { + $script:src887mon | Should -Match "Properties = \(& \`$tcProps \`$r 'StepError'\)" + $script:src887mon | Should -Match "Properties = \(& \`$tcProps \`$r 'LongRunningStep'\)" + $script:src887mon | Should -Match "Properties = \(& \`$tcProps \`$r 'LongRunningOverall'\)" + $script:src887mon | Should -Match "Properties = \(& \`$tcProps \`$r 'InProgress'\)" + $script:src887mon | Should -Match "Properties = \(& \`$tcProps \`$r 'Failed'\)" + } + It 'AttemptWithoutRun gap rows emit a Status=AttemptWithoutRun property with a stable UpdateName fallback' { + $script:src887mon | Should -Match "Status\s+=\s+'AttemptWithoutRun'" + $script:src887mon | Should -Match '\(no update name\)' + } +} + +Describe 'v0.8.87: Export-AzLocalApplyUpdatesScheduleAudit recommends an Update:4 monitor cron' { + BeforeAll { + $script:src887cfg = Get-Content -LiteralPath "$PSScriptRoot/../Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1" -Raw + } + It 'Exposes -MonitorFiresPerHour (1-12, default 2) and -MonitorTrailingDays (0-14, default 3)' { + $script:src887cfg | Should -Match '\[ValidateRange\(1,\s*12\)\]' + $script:src887cfg | Should -Match '\[int\]\$MonitorFiresPerHour\s*=\s*2' + $script:src887cfg | Should -Match '\[ValidateRange\(0,\s*14\)\]' + $script:src887cfg | Should -Match '\[int\]\$MonitorTrailingDays\s*=\s*3' + } + It 'Renders an always-on "Recommended in-flight monitor schedule (Update: 4)" section' { + $script:src887cfg | Should -Match 'Recommended in-flight monitor schedule \(Update: 4\)' + } + It 'Derives the minute field from the cadence (0 for hourly, */N otherwise)' { + $script:src887cfg | Should -Match "if \(\`$monitorFires -le 1\) \{ '0' \} else \{ \('\*/\{0\}' -f \`$monitorIntervalMinutes\) \}" + } + It 'Expands apply weekday(s) across the trailing coverage window' { + $script:src887cfg | Should -Match 'for \(\$k = 0; \$k -le \$MonitorTrailingDays; \$k\+\+\)' + $script:src887cfg | Should -Match '\$monitorCoverageSet\.Add\(\(\(\$applyDow \+ \$k\) % 7\)\)' + } + It 'Falls back to a 24x7 cron when no apply firings are derivable' { + $script:src887cfg | Should -Match "\`$monitorFallbackCron = \('\{0\} \* \* \* \*' -f \`$monitorMinuteField\)" + } +} + +Describe 'v0.8.87: ITSM trigger matrix covers the Update:4 monitor statuses' { + BeforeAll { + $script:itsmCfg887 = Get-Content -LiteralPath "$PSScriptRoot/../Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml" -Raw + } + It 'Raises on AttemptWithoutRun and StepError' { + $script:itsmCfg887 | Should -Match '(?m)^\s*AttemptWithoutRun:' + $script:itsmCfg887 | Should -Match '(?m)^\s*StepError:' + } + It 'Includes opt-in (raiseTicket:false) entries for the long-running monitor statuses' { + $script:itsmCfg887 | Should -Match '(?m)^\s*LongRunningOverall:' + $script:itsmCfg887 | Should -Match '(?m)^\s*LongRunningStep:' + } +} + +Describe 'v0.8.87: monitor-updates.yml gains an opt-in ITSM ticketing step' { + It 'GitHub Actions monitor pipeline wires raise_itsm_ticket + New-AzLocalIncident' { + $gh = Get-Content -LiteralPath "$PSScriptRoot/../Automation-Pipeline-Examples/github-actions/monitor-updates.yml" -Raw + $gh | Should -Match '(?m)^\s*raise_itsm_ticket:' + $gh | Should -Match 'New-AzLocalIncident' + $gh | Should -Match 'update-monitor\.xml' + } + It 'Azure DevOps monitor pipeline wires raiseItsmTicket + New-AzLocalIncident' { + $ado = Get-Content -LiteralPath "$PSScriptRoot/../Automation-Pipeline-Examples/azure-devops/monitor-updates.yml" -Raw + $ado | Should -Match '(?m)^\s*-\s*name:\s*raiseItsmTicket' + $ado | Should -Match 'New-AzLocalIncident' + $ado | Should -Match 'update-monitor\.xml' + } +} + +#endregion v0.8.87: monitor JUnit properties + ITSM wiring + Config:03 monitor cron diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 01d3e5ea..719dea81 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -40,3 +40,6 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-16T12:47:00Z","0.8.87","1305","1266","0","1","116.33","113.88","4.43","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T13:00:02Z","0.8.87","1306","1267","0","1","96.98","95.68","3.11","Invoke-Tests.ps1","5.1.26100.8730" "2026-06-16T13:06:07Z","0.8.87","1309","1270","0","1","76.61","75.29","2.85","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T13:51:12Z","0.8.87","1321","1282","0","1","84","82.59","3.47","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T13:54:16Z","0.8.87","1321","1282","0","1","49.15","47.69","2.78","Invoke-Tests.ps1","5.1.26100.8730" +"2026-06-16T15:05:37Z","0.8.87","1321","1282","0","1","136.51","134.21","10.79","Invoke-Tests.ps1","5.1.26100.8730" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index b63a096c..28b20f35 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -10,7 +10,9 @@ ### What's New in v0.8.87 -Patch release. Renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Fleet: 02 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. The `### Cluster Connectivity (with ARB Status)` table is split into a `### Cluster with Connectivity Issues` table (clusters not Connected with a Running ARB) shown first and expanded, and a `### Cluster without Connectivity Issues` table (Connectivity = Connected AND ARB Status = Running) collapsed behind an `Expand to view clusters` details block; both tables share the same columns. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. +Renames the bundled pipeline display names into a three-group `Config: N` / `Monitor: N` / `Update: N` scheme (single-digit, e.g. `Monitor: 1 - Fleet Connectivity Status`, `Update: 4 - Monitor In-Flight Updates`), replacing the former `Setup: 0N` / `Fleet: 0N` prefixes; filenames and `AZLOCAL-PIPELINE-ID` values are unchanged. Also renames the "Orphan ARBs" section in the `New-AzLocalFleetConnectivityStatusSummary` (Monitor: 1 - Fleet Connectivity Status) markdown output to "Non-Azure Local and/or Orphan ARB appliances" and adds an explicit caveat that an Arc resource bridge with no matching in-scope Azure Local cluster is NOT necessarily orphaned. Azure Arc resource bridge is also used by **VMware vSphere** and **System Center Virtual Machine Manager (SCVMM)** Arc-enabled deployments, so an appliance listed here may be a healthy, in-use resource bridge for a non-Azure Local platform. The section now carries investigate-before-acting guidance (check the resource type, associated custom location, and the platform served; do not delete an ARB until you have confirmed it is genuinely orphaned). The KPI-table note, the "how to interpret" causes list, and the cluster-table cross-reference are updated to match. The `### Cluster Connectivity (with ARB Status)` table is split into a `### Cluster with Connectivity Issues` table (clusters not Connected with a Running ARB) shown first and expanded, and a `### Cluster without Connectivity Issues` table (Connectivity = Connected AND ARB Status = Running) collapsed behind an `Expand to view clusters` details block; both tables share the same columns. Output text only - no public API, parameter, or behavioural change. Export count unchanged (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.86` to `0.8.87` across all bundled pipeline templates. + +Also (additive): `Export-AzLocalUpdateRunMonitorReport` (Update: 4) now writes per-`` `` (`ClusterName` / `ClusterResourceId` / `UpdateName` / `Status` / `CurrentStep` / `ClusterPortalUrl` / `UpdateRunPortalUrl`) into `update-monitor.xml`, with `Status` one of `StepError` / `LongRunningStep` / `LongRunningOverall` / `InProgress` / `Failed` / `AttemptWithoutRun`, so `New-AzLocalIncident` can dedupe and deep-link stuck / failed / attempt-without-run clusters. The bundled `monitor-updates.yml` (GitHub Actions + Azure DevOps) gains an opt-in ITSM ticketing step (`raise_itsm_ticket` / `raiseItsmTicket`, default off), and the sample ITSM matrix `azurelocal-itsm.yml` gains `AttemptWithoutRun` / `StepError` (raise) plus opt-in `LongRunningOverall` / `LongRunningStep` entries. `Export-AzLocalApplyUpdatesScheduleAudit` (Config: 3) gains an always-on "Recommended in-flight monitor schedule (Update: 4)" section plus two new parameters `-MonitorFiresPerHour` (1-12, default 2) and `-MonitorTrailingDays` (0-14, default 3). All backward-compatible with existing defaults. ---