-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapply-updates-schedule-audit.yml
More file actions
272 lines (255 loc) · 14.5 KB
/
Copy pathapply-updates-schedule-audit.yml
File metadata and controls
272 lines (255 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# AZLOCAL-PIPELINE-ID: apply-updates-schedule-audit
# Apply-Updates Schedule Coverage Audit
# This workflow 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.
#
# WHY THIS PIPELINE EXISTS
# ------------------------
# apply-updates.yml ships with NO default schedule (workflow_dispatch only) on purpose,
# so customers must consciously choose when updates fire. After you have configured
# UpdateStartWindow tags on your clusters and added cron entries to apply-updates.yml, this
# pipeline is the safety net that catches:
# - a new ring tagged with UpdateStartWindow that no cron in apply-updates.yml will reach
# - an UpdateStartWindow tag value that fails to parse (typo / wrong syntax)
# - apply-updates.yml cron entries that the advisor cannot reason about
# (DayOfMonth restrictions, step values) so you can audit them manually
#
# REPORTS GENERATED
# -----------------
# - Markdown step summary: human-readable Audit + Matrix views at the top of the run
# - JUnit XML: one <testcase time="0"> per (UpdateRing, UpdateStartWindow) pair - uncovered pairs
# become <failure>. dorny/test-reporter renders the tree in the run page.
# - CSV: schedule-coverage-audit.csv with the full Audit view
# - Markdown: schedule-coverage-recommend.md with the ready-to-paste cron block
#
# AUTHENTICATION
# --------------
# Uses OpenID Connect (OIDC) - recommended for secretless authentication
# 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.3 - Apply-Updates Schedule Coverage Audit
on:
# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers
# Edits inside this block are preserved across Update-AzLocalPipelineExample
# module upgrades. Add or modify `schedule:` cron blocks below.
# Weekly on Monday at 05:00 UTC - well before any common Saturday/Sunday or weekday
# evening update window so a coverage drift report lands in the inbox before the
# next scheduled apply-updates run.
schedule:
- cron: '0 5 * * 1' # Mondays at 05:00 UTC
# END-AZLOCAL-CUSTOMIZE:schedule-triggers
workflow_dispatch:
inputs:
pipeline_path:
description: 'Path (file or folder) to apply-updates.yml to audit, repo-relative. Required so the Recommend view can diff its proposed crons against what is already present in apply-updates.yml and only emit a snippet for the truly missing entries. Default `.github/workflows` matches the standard GitHub Actions consumer layout where you paste the bundled `apply-updates.yml` sample.'
required: true
default: '.github/workflows'
schedule_path:
description: 'Path to apply-updates-schedule.yml. When set, the audit also reports RingMissingFromSchedule (fleet ring with no schedule row) and RingOrphanedInSchedule (schedule ring no cluster carries). Leave empty to skip the schedule-file two-way diff.'
required: false
default: 'config/apply-updates-schedule.yml'
lead_time_minutes:
description: 'Minutes before UpdateStartWindow opens that the pipeline should fire (0-60). Default 5.'
required: false
default: '5'
fires_per_window:
description: 'Cron entries the Recommend view should emit per UpdateStartWindow segment. 2 (default, recommended) = belt-and-braces: opening edge + one mid-window retry capped at +60min, so GitHub Actions schedule jitter and transient first-fire failures cannot silently skip a cluster for the day. 1 = opening edge only. The runtime gate + in-flight guard prevent double-triggering. Audit semantics are unchanged either way.'
required: false
default: '2'
type: choice
options:
- '1'
- '2'
include_untagged:
description: 'Surface clusters with no UpdateStartWindow tag as their own row.'
required: false
default: 'true'
type: choice
options:
- 'false'
- 'true'
cluster_csv_path:
description: 'Path to the source-controlled cluster inventory CSV (the same file Step.2 consumes - default `config/ClusterUpdateRings.csv`). When set, the Recommend view emits a NoWindowTag remediation section: for each cluster missing an UpdateStartWindow tag, the advisor proposes a peer-derived value and tells the operator which row of the CSV to edit (or to re-run Step.1 if the cluster is absent from the CSV). Leave empty to skip the CSV-driven remediation.'
required: false
default: 'config/ClusterUpdateRings.csv'
module_version:
description: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".'
required: false
default: ''
debug:
description: 'Enable verbose/debug PowerShell streams for self-service triage. Surfaces $VerbosePreference=Continue, $DebugPreference=Continue, plus a one-shot environment snapshot before the audit runs.'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
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 -Update. See Automation-Pipeline-Examples/README.md section 5.
GENERATED_AGAINST_MODULE_VERSION: '0.8.71'
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:
audit:
name: Audit Apply-Updates Schedule Coverage
runs-on: windows-latest
permissions:
id-token: write
contents: read
checks: write # Required for test reporter
outputs:
# v0.8.5 thin-YAML: keys align byte-for-byte with the lowercase output
# names emitted by Export-AzLocalApplyUpdatesScheduleAudit (via
# Set-AzLocalPipelineOutput). ADO is case-sensitive on task.setvariable
# lookups; GH is case-insensitive but lowercase is the canonical v0.8.5+
# convention.
total_rows: ${{ steps.audit.outputs.total_rows }}
covered: ${{ steps.audit.outputs.covered }}
uncovered: ${{ steps.audit.outputs.uncovered }}
partial: ${{ steps.audit.outputs.partial }}
malformed: ${{ steps.audit.outputs.malformed }}
unparseable: ${{ steps.audit.outputs.unparseable }}
no_window_tag: ${{ steps.audit.outputs.no_window_tag }}
ring_missing: ${{ steps.audit.outputs.ring_missing }}
ring_orphan: ${{ steps.audit.outputs.ring_orphan }}
ring_mixed: ${{ steps.audit.outputs.ring_mixed }}
have_schedule: ${{ steps.audit.outputs.have_schedule }}
schedule_path: ${{ steps.audit.outputs.schedule_path }}
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 }}
# AZURE_SUBSCRIPTION_ID is a repository *Variable* (vars.*), not a Secret. It
# is consumed ONLY here: azure/login@v3 runs `az account set --subscription
# <id>` 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 <subId>
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
# v0.8.5 thin-YAML: the drift-detection + version-banner block is
# produced by Add-AzLocalPipelineVersionBanner (Public cmdlet); the
# cmdlet writes the version banner to GITHUB_STEP_SUMMARY and emits
# the installed_module_version step output consumed by the next step.
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 Schedule Coverage Audit
# v0.8.5 thin-YAML: the inline body (~220-line 'Run Schedule
# Coverage Audit' + ~210-line 'Create Schedule Coverage Summary'
# blocks: 3 calls to Test-AzLocalApplyUpdatesScheduleCoverage,
# inline JUnit StringBuilder, 12-row markdown summary, schedule-row
# detail tables, allow-list coverage v1/v2 sections, cycle calendar
# render, and 12 step outputs) is now the Public cmdlet
# Export-AzLocalApplyUpdatesScheduleAudit. The cmdlet writes the
# 4 ./reports/schedule-coverage-* artifacts, pushes the markdown
# summary via GITHUB_STEP_SUMMARY, and sets the 12 lowercase step
# outputs (total_rows, covered, uncovered, partial, malformed,
# unparseable, no_window_tag, ring_missing, ring_orphan, ring_mixed,
# have_schedule, schedule_path) via Set-AzLocalPipelineOutput.
# **v0.8.5 fix**: the cycle calendar now renders UNCONDITIONALLY
# whenever -SchedulePath is supplied, eliminating the v0.8.4
# ``$hasIssues``-gate regression that silently dropped the calendar
# on clean-fleet runs.
shell: pwsh
id: audit
env:
INPUT_PIPELINE_PATH: ${{ github.event.inputs.pipeline_path || '.github/workflows' }}
INPUT_SCHEDULE_PATH: ${{ github.event.inputs.schedule_path || 'config/apply-updates-schedule.yml' }}
INPUT_LEAD_TIME_MINUTES: ${{ github.event.inputs.lead_time_minutes || '5' }}
INPUT_FIRES_PER_WINDOW: ${{ github.event.inputs.fires_per_window || '2' }}
INPUT_INCLUDE_UNTAGGED: ${{ github.event.inputs.include_untagged || 'false' }}
INPUT_CLUSTER_CSV_PATH: ${{ github.event.inputs.cluster_csv_path || 'config/ClusterUpdateRings.csv' }}
INPUT_DEBUG: ${{ github.event.inputs.debug || 'false' }}
INSTALLED_MODULE_VERSION: ${{ steps.module-version.outputs.installed_module_version }}
# v0.8.7 sideload advisor (opt-in). When SIDELOAD_UPDATES=true the audit
# appends a "Recommended sideload schedule" section. Inert when unset/false.
SIDELOAD_UPDATES: ${{ vars.SIDELOAD_UPDATES || 'false' }}
SIDELOAD_LEAD_DAYS: ${{ vars.SIDELOAD_LEAD_DAYS || '7' }}
run: |
$ErrorActionPreference = 'Stop'
Import-Module AzLocal.UpdateManagement -Force
if ($env:INPUT_DEBUG -eq 'true') {
$VerbosePreference = 'Continue'
$DebugPreference = 'Continue'
Write-Host "::group::Debug environment"
Write-Host "PSVersion : $($PSVersionTable.PSVersion)"
Write-Host "PSEdition : $($PSVersionTable.PSEdition)"
$mod = Get-Module AzLocal.UpdateManagement | Sort-Object Version -Descending | Select-Object -First 1
Write-Host "AzLocal.UpdateManagement : v$($mod.Version) (path: $($mod.Path))"
Write-Host "::endgroup::"
}
$params = @{
PipelineYamlPath = $env:INPUT_PIPELINE_PATH
LeadTimeMinutes = [int]$env:INPUT_LEAD_TIME_MINUTES
RecommendFiresPerWindow = [int]$env:INPUT_FIRES_PER_WINDOW
Platform = 'GitHubActions'
OutputDirectory = './reports'
InstalledModuleVersion = $env:INSTALLED_MODULE_VERSION
}
if ($env:INPUT_SCHEDULE_PATH) { $params['SchedulePath'] = $env:INPUT_SCHEDULE_PATH }
if ($env:INPUT_CLUSTER_CSV_PATH) { $params['ClusterCsvPath'] = $env:INPUT_CLUSTER_CSV_PATH }
if ($env:INPUT_INCLUDE_UNTAGGED -eq 'true') { $params['IncludeUntagged'] = $true }
Export-AzLocalApplyUpdatesScheduleAudit @params
- name: Compute Artifact Timestamp
if: always()
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 Schedule Coverage Reports
if: always()
uses: actions/upload-artifact@v6
with:
name: azlocal-step.3-apply-updates-schedule-audit-report_${{ steps.artifact-stamp.outputs.timestamp }}
path: ./reports/
retention-days: 90
- name: Publish Test Results
uses: dorny/test-reporter@v3
if: always()
with:
name: Apply-Updates Schedule Coverage
path: ./reports/schedule-coverage-audit.xml
reporter: java-junit
fail-on-error: false
list-suites: failed
list-tests: failed