Introduced in v0.8.7. Opt-in, off by default. This pipeline is inert unless the repository variable
SIDELOAD_UPDATESis the literal string'true'.
The Sideload Updates pipeline (sideload-updates.yml, logical pipeline id
sideload-updates, displayed as Step.6) pre-stages Azure Local solution-update
media onto clusters that cannot pull updates from Azure directly - dark,
air-gapped, or restricted-egress fabrics. Once the media is staged, verified, and
imported, the pipeline flips the UpdateSideloaded=True gate so the downstream
Step.7 - Apply Updates pipeline can proceed exactly as it does for
internet-connected clusters.
For the per-pipeline reference card (inputs, artefacts, RBAC, exit conditions) see appendix-pipelines.md - Step 6. For robocopy throttling guidance see sideload-robocopy.md.
The runner (GitHub Actions) or agent (Azure DevOps) must sit on the same network as the target clusters so it can:
- Robocopy the
CombinedSolutionBundle(or OEM SBE package) to each cluster's infrastructureimportSMB share. - PowerShell-remote (WinRM) into a cluster node to verify the SHA256 and run
Add-SolutionUpdate.
Microsoft-hosted / cloud-hosted runners cannot reach the on-prem fabric, so this pipeline targets:
- GitHub Actions - a self-hosted runner labelled
azlocal-sideload(runs-on: [self-hosted, azlocal-sideload]). - Azure DevOps - a self-hosted agent in a pool that satisfies the
azlocal-sideloaddemand (pool: { name: <pool>, demands: azlocal-sideload }).
Terminology: GitHub uses runners (in runner groups); Azure DevOps uses agents (in agent pools). This is never an AKS "node pool".
A solution-bundle copy can take hours over a constrained on-prem link. A pipeline run that blocked for that long would burn agent time, hit job timeouts, and lose all progress if the agent restarted. Instead, the pipeline is a re-entrant state machine:
- Each pipeline run advances every in-scope cluster by exactly one transition, then exits. No run is ever long-lived.
- The multi-hour copy itself runs in a detached Windows Scheduled Task (driven by
the bundled
Tools/Invoke-AzLocalSideloadCopyTask.ps1worker) that survives the pipeline run ending and the agent process recycling. - Progress is persisted in shared-UNC state JSON, so any runner/agent can read the state and advance/report without cross-agent remoting.
Drive the pipeline on a frequent CRON (every 30 minutes) so successive short runs walk each cluster through the states:
Planned -> Copying -> Copied -> Verified -> Imported -> SideloadFlagged
Per cluster, the current shared state determines the action taken by
Invoke-AzLocalSideloadUpdate:
| Current state | Action |
|---|---|
| (no state) + due now | Set UpdateSideloaded=False, ensure the verified bundle is in the shared cache, register + start the detached copy Scheduled Task, write Copying state. |
Copying + fresh heartbeat |
Report progress, leave the task running. |
Copying + stale heartbeat (> SIDELOAD_HEARTBEAT_STALE_MINUTES) |
Treat the host as dead and re-drive on the current (live) host, up to MaxRetries. |
Copied |
Open a WinRM session, verify the remote SHA256, run Add-SolutionUpdate + discovery, flip UpdateSideloaded=True + stamp UpdateVersionInProgress, mark Imported, remove the task. |
Failed |
Bounded retry, else surface the error as a JUnit <failure>. |
Imported |
Done - nothing to do. |
Re-running is always safe - the state machine is idempotent.
SIDELOAD_STATE_ROOT must be a UNC path that every runner/agent can read and write.
It holds three subfolders:
state\- one JSON document per cluster tracking the current transition + heartbeat.logs\- per-run copy / verify / import logs.cache\- the verified media cache (overridable viaSIDELOAD_CACHE_ROOT; defaults to<state-root>\cache).
Because a bundle is downloaded and hashed once into the shared cache and then reused
across every cluster that needs that version, only the first cluster pays the download +
hash cost. The pipeline's concurrency / queueing settings serialize overlapping runs so
they do not race on the same shared state.
Two distinct identities are used - do not conflate them:
- Pipeline identity (Azure plane) - reads the fleet via Azure Resource Graph, reads
the Key Vault secrets, and writes the
UpdateSideloaded/UpdateVersionInProgresstags. Defaults toazure/loginOIDC;enable-AzPSSession=trueis required because the Key Vault secrets are read viaGet-AzKeyVaultSecret(Az PowerShell). For on-prem runners where OIDC is not viable, switch via theSIDELOAD_KV_AUTHvariable (oidc | managedidentity | serviceprincipal). Tag writes need only Tag Contributor (they reuseSet-AzLocalClusterTagsMerge). - Cluster WinRM credential (fabric plane) - an Active Directory
[pscredential]built at run time from two Key Vault secrets named in the matching sideload auth-map row (a username secret + a password secret). This credential - not the pipeline identity - is used for the WinRM session andAdd-SolutionUpdate. The detached copy Scheduled Task runs as the runner service account, which needs UNC rights to the shared cache and the cluster import share.
Maps the numeric UpdateAuthAccountId tag (written onto clusters by Step.2) to the Key
Vault + secret names that hold the AD credential:
UpdateAuthAccountId,KeyVaultName,UsernameSecretName,PasswordSecretName
1,kv-fabric-east,sideload-user,sideload-pass
2,kv-fabric-west,sideload-user,sideload-passUpdateAuthAccountIdmust match^\d{1,3}$(numeric, 1-3 digits) and be unique (a duplicate is a hard error).- All four columns are required.
A source-controlled YAML describing the media available to the automation. Two package
classes are supported via packageType:
- Solution - a Microsoft
CombinedSolutionBundle.<build>.zipdownloadable from a directdownloadUri(published in the Microsoft Learn "import and discover updates offline" table).sha256is required so the download / pre-staged copy can be verified.Update-AzLocalSideloadCatalogcan auto-populate these rows by parsing the Learn table. - SBE - an OEM Solution Builder Extension package that Microsoft does not host.
The operator stages the OEM files manually and records a
sourceFolder(local or UNC path).downloadUriis not applicable;sha256is optional (verified only when supplied). These rows are added manually.
schemaVersion: 1
packages:
- version: '12.2605.1003.210'
packageType: Solution
buildNumber: '12.2605.1003.210'
osBuild: '26100.4061'
downloadUri: 'https://.../CombinedSolutionBundle.12.2605.1003.210.zip'
sha256: 'ABCD...' # required for Solution; ^[0-9A-Fa-f]{64}$
availabilityDate: '2026-05-13'
localPath: ''
- version: 'DellSBE-4.1.2412.1'
packageType: SBE
sourceFolder: '\\fileserver\sbe\Dell\4.1.2412.1'
sha256: '' # optional for SBE
availabilityDate: '2026-05-20'
notes: 'Dell OEM SBE package, staged manually'| Variable | Default | Purpose |
|---|---|---|
SIDELOAD_UPDATES |
false |
Master gate. The job is skipped unless this is the literal 'true'. |
SIDELOAD_STATE_ROOT |
(none) | Shared UNC root holding state\, logs\, cache\. Required when enabled. |
SIDELOAD_CACHE_ROOT |
<state-root>\cache |
Shared verified media cache. |
SIDELOAD_AUTH_MAP_PATH |
./config/sideload-auth-map.csv |
Auth-map CSV (see 4.1). Copy-AzLocalPipelineExample drops a header-only starter here (same config/ folder on GitHub and Azure DevOps). |
SIDELOAD_CATALOG_PATH |
./config/sideload-catalog.yml |
Catalog YAML (see 5). Copy-AzLocalPipelineExample drops an empty skeleton starter here. |
SIDELOAD_LEAD_DAYS |
7 |
Days before a cluster's next apply window that media should be sideloaded. |
SIDELOAD_ROBOCOPY_SWITCHES |
/R:5 /W:30 |
Extra robocopy switches for the detached worker (see sideload-robocopy.md). |
SIDELOAD_HEARTBEAT_STALE_MINUTES |
60 |
Minutes after which a Copying heartbeat is considered stale and re-driven. |
SIDELOAD_REMOTING_FQDN_SUFFIX |
(empty) | Global FQDN suffix appended to a cluster name to form the WinRM host when the auth-map row does not override it. |
SIDELOAD_KV_AUTH |
oidc |
Key Vault auth mode for the on-prem runner. |
APPLY_UPDATES_SCHEDULE_PATH |
./config/apply-updates-schedule.yml |
Ring-aware apply-updates schedule; the planner reads it to find each cluster's next apply window. |
| Cmdlet | Role |
|---|---|
Resolve-AzLocalSideloadPlan |
Read-only planner. Reads the fleet (ARG), apply schedule, auth-map, and catalog; emits one plan row per UpdateAuthAccountId-tagged cluster (plus error rows for misconfigurations) and marks which clusters are due within LeadDays. Reuses the same "next update" selection as the apply path. |
Invoke-AzLocalSideloadUpdate |
The re-entrant state machine (section 2). SupportsShouldProcess - -WhatIf previews transitions with no staging / task / tag changes. |
Export-AzLocalSideloadStatusReport / Add-AzLocalSideloadStepSummary |
JUnit XML + Markdown step-summary emitters. |
Update-AzLocalSideloadCatalog |
Auto-populates Solution rows by parsing the Microsoft Learn offline-updates table. SBE rows are added manually. |
Reset-AzLocalSideloadedTag |
Operator escape hatch to clear a stuck UpdateSideloaded tag. |
- Stand up the runner/agent on the cluster fabric network and label it
azlocal-sideload(GH) / give the pool theazlocal-sideloaddemand (ADO). - Create the shared UNC root (
SIDELOAD_STATE_ROOT) readable + writable by the runner service account, and grant that account rights to each cluster's import share. - Populate Key Vault with the per-fabric AD username/password secrets and author
sideload-auth-map.csv. - Author
sideload-catalog.yml- runUpdate-AzLocalSideloadCatalogto fill the Microsoft Solution rows, then add any OEM SBE rows manually. - Tag the fleet (Step.1 / Step.2): set
UpdateRing,UpdateStartWindow, andUpdateAuthAccountIdon each sideloaded cluster. - Set the repository variables (section 6), starting with
SIDELOAD_UPDATES=true. - Dry run: trigger the pipeline manually with
dry_run=trueand review the planned transitions + thesideload-statusartefacts. - Enable the CRON: uncomment the bundled
*/30 * * * *schedule inside theBEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggersblock (preserved acrossUpdate-AzLocalPipelineExampleupgrades). The Step.3 schedule-coverage audit can recommend a lead-time-aware cron (apply window minusSIDELOAD_LEAD_DAYS). - The state machine advances each cluster to
Imported/SideloadFlagged; the downstream Step.7 - Apply Updates wave then applies the staged update during the cluster'sUpdateStartWindow.