From 063a743ed365a8e32049bb9cc9d76f221610deb0 Mon Sep 17 00:00:00 2001 From: Neil Bird Date: Tue, 16 Jun 2026 10:50:05 +0100 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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: