Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/skills/release-readiness/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,21 @@ Three critical gotchas this skill encodes — see [references/methodology.md](re

This skill depends on `.github/scripts/shared/MauiReleaseVersioning.psm1` for canonical milestone/version parsing (e.g. `Get-CurrentMajorVersion`, `ConvertBranchToMilestone`, `Get-MilestoneSortKey`, `Compare-MauiMilestone`). The module is also consumed by `Fix-MilestoneDrift.ps1` to keep milestone classification consistent across all release-related automation.

### `scripts/NightlyFeed.ps1` (nightly dogfood feed staleness banner)

Both engines dot-source [`scripts/NightlyFeed.ps1`](scripts/NightlyFeed.ps1) to surface a one-line **nightly dogfood feed freshness banner** at the top of each tracker (just under **Generated**). The point of the banner is to make it obvious when the dogfood bits people are told to test have stopped flowing — e.g. when the `ci.inflight` pipeline is red, the feed goes stale and the banner turns ❌ so consumers don't waste time validating against builds that never updated.

Key functions (all PURE except the one network call, which is **fail-open** — any feed error returns `$null` / renders a muted "freshness unknown" note and never breaks tracker generation):

| Function | Purpose |
|----------|---------|
| `Get-NightlyFeedFreshness` | Queries an Azure Artifacts NuGet feed (`dotnet10`, `dotnet11`, …) for the newest **published** build matching a version-prefix regex; returns version + publish date. Injectable `-Fetcher` for offline tests. |
| `Resolve-NightlyDogfoodFreshness` | Picks the stream that matters: **`ci.inflight` first** (the "shipping next" dogfood bits), the lane band only as a fallback. Conservatively returns `matched=$false` when *only* `ci.main` exists, so a daily main build never paints a false green. |
| `Format-NightlyFeedLaneLabel` | PURE builder for the `` [`feed`](url) · <typeNote> `` lane label. Centralizes the honest-labeling rule (`inflight`→`ci.inflight`; `band`→caller-formatted band note; unknown→`ci.inflight`) so the SR and Preview lanes can't drift. |
| `Get-NightlyFeedTier` / `Format-NightlyFeedBanner` | Bucket age into ✅ ≤2d · ⚠️ 3–6d · ❌ ≥7d and render the markdown banner. Both take an explicit `-Now`, so they're deterministic and unit-testable offline. |

Determinism / idempotency: the engine captures **one** `UtcNow` per run (`$Data['nightlyFeedNow']`) and reuses it for both the rendered banner and the semantic-hash tier, so a quiet SR tracker still refreshes when the feed crosses a tier boundary, but a same-tier day-count tick does **not** churn the issue. The freshness band is folded into `Get-ReportSemanticHash` (tier|version only — the raw timestamp is never hashed).

## Integration

- **Custom agent**: `.github/agents/release-readiness-agent.agent.md` wraps this skill — handles regression-label confirmation, runs the script, then uses WorkIQ to add context for `rejected-from-sr` PRs.
Expand Down Expand Up @@ -291,3 +306,4 @@ The harness covers:
- **`-AllActiveMajors`** end-to-end across net10 + net11 with the expected tracker counts
- **`Get-ReleaseReadiness`** verdict classification using known-answer data from the SR7 readiness analysis (e.g. #35313 → `in-sr-active`, #35344 → `in-sr-active` via the SafeArea follow-on fix, #35771 → `no-fix-yet`)
- **Idempotent body hash** stability across re-runs — **SR trackers only** (the daily workflow compares the embedded `<!-- release-readiness-hash: sha=... -->` marker against the live issue and skips the edit when the semantic content is unchanged, so re-runs don't churn the tracker). Preview trackers carry no hash marker and are refreshed on every scheduled run.
- **Nightly dogfood feed banner** (`NightlyFeed.ps1`) — offline unit coverage for the lane-label honest-labeling rule (`Format-NightlyFeedLaneLabel`), the `ci.inflight`-first / `ci.main`-false-green resolver, age→tier bucketing, the fail-open feed query (mocked `-Fetcher`), and the banner's fold into `Get-ReportSemanticHash` (tier change refreshes, same-tier day tick does not). All network-free via injected fixtures and explicit `-Now`.
54 changes: 54 additions & 0 deletions .github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ param(
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

# Shared nightly-feed freshness helpers (Get-NightlyFeedFreshness / Format-NightlyFeedBanner).
# Defensive load: the banner is auxiliary signal, not part of the verdict, so a missing
# helper degrades to "no banner" rather than crashing the unattended preview tracker job.
# Loaded above the dot-source guard so the pure renderer is reachable from the test harness.
$Script:NightlyFeedHelperLoaded = $false
$nightlyFeedHelperPath = Join-Path $PSScriptRoot 'NightlyFeed.ps1'
if (Test-Path $nightlyFeedHelperPath) {
. $nightlyFeedHelperPath
$Script:NightlyFeedHelperLoaded = $true
} else {
Write-Warning "NightlyFeed.ps1 helper not found at $nightlyFeedHelperPath — nightly-feed banner disabled." -WarningAction Continue
}

# ===================================================================
# BRANCH PARSING
# ===================================================================
Expand Down Expand Up @@ -1346,6 +1359,43 @@ $report = [PSCustomObject]@{
PriorityIssues = $priorityIssues
KnownBuildErrorIssues = $kbeIssues
CiScanIssues = $ciScanIssues
NightlyFeed = $null
}

# Nightly dogfood feed freshness (preview lane). Tracks the inflight/current dogfood stream
# (ci.inflight builds) on the dotnet<major> feed; falls back to this preview's preview.N
# version band when the feed has no inflight builds yet (the common case while a major is
# still in preview — its newest bits ARE the preview.N builds). Fail-open: any gap (helper
# unloaded, version unreadable, network error) degrades to "no banner".
$nightlyFeedBanner = $null
if ($Script:NightlyFeedHelperLoaded -and
(Get-Command Resolve-NightlyDogfoodFreshness -ErrorAction SilentlyContinue) -and
(Get-Command Format-NightlyFeedBanner -ErrorAction SilentlyContinue)) {
try {
$nfFeed = "dotnet$majorVersion"
$nfFeedUrl = "https://dev.azure.com/dnceng/public/_artifacts/feed/$nfFeed"
$nfIteration = Get-PreReleaseVersionIteration -BranchName $SurveyRef
if ([string]::IsNullOrWhiteSpace($nfIteration)) { $nfIteration = "$previewNumber" }
$nfBand = "$majorVersion.0.0-preview.$nfIteration"
$nfBandPrefix = '^' + [regex]::Escape("$nfBand.")

$nfFresh = Resolve-NightlyDogfoodFreshness -Feed $nfFeed -BandPrefixRegex $nfBandPrefix
if ($null -eq $nfFresh) { $nfFresh = @{ unknown = $true } }

$nfBuildType = [string](Get-NightlyFeedProp $nfFresh 'buildType')
$nfLaneLabel = Format-NightlyFeedLaneLabel -Feed $nfFeed -FeedUrl $nfFeedUrl -BuildType $nfBuildType -BandNote "``$nfBand`` (preview.$nfIteration)"
$nfFresh['laneLabel'] = $nfLaneLabel
$nfFresh['feedUrl'] = $nfFeedUrl
$nfFresh['versionPrefix'] = $nfBandPrefix

$report.NightlyFeed = $nfFresh
$nightlyFeedBanner = Format-NightlyFeedBanner -Freshness $nfFresh -Now ([DateTime]::UtcNow)
} catch {
# -WarningAction Continue: keep this fail-open even under an ambient
# $WarningPreference='Stop', where a bare Write-Warning would be promoted to a
# terminating error inside the catch and escape, crashing the unattended job.
Write-Warning "Nightly-feed freshness check failed (non-fatal): $($_.Exception.Message)" -WarningAction Continue
}
}

$md = [System.Text.StringBuilder]::new()
Expand All @@ -1360,6 +1410,10 @@ if ($Mode -eq 'candidate') {
[void]$md.AppendLine("")
[void]$md.AppendLine("**Overall status:** **$overallStatus**")
[void]$md.AppendLine("")
if ($nightlyFeedBanner) {
[void]$md.AppendLine($nightlyFeedBanner)
[void]$md.AppendLine("")
}

# === HIGH-PRIORITY ITEMS (hoisted to the very top) ===
# Four categories the release captain must see BEFORE anything else:
Expand Down
113 changes: 113 additions & 0 deletions .github/skills/release-readiness/scripts/Get-ReleaseReadiness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,18 @@ param(
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# Shared nightly-feed freshness helpers (Get-NightlyFeedFreshness / Format-NightlyFeedBanner).
# Defensive load: the banner is auxiliary signal, not part of the verdict, so a missing
# helper must degrade to "no banner" rather than crash the unattended nightly tracker job.
$Script:NightlyFeedHelperLoaded = $false
$nightlyFeedHelperPath = Join-Path $PSScriptRoot 'NightlyFeed.ps1'
if (Test-Path $nightlyFeedHelperPath) {
. $nightlyFeedHelperPath
$Script:NightlyFeedHelperLoaded = $true
} else {
Write-Warning "NightlyFeed.ps1 helper not found at $nightlyFeedHelperPath — nightly-feed banner disabled." -WarningAction Continue
}

# DETERMINISTIC RULE — SR branches in dotnet/maui ALWAYS cut from `main`.
# Refuse to operate on any `inflight/*` or `staging/*` ref — those are
# integration branches, not SR sources. This guard exists because conflating
Expand Down Expand Up @@ -2999,6 +3011,27 @@ function Get-ReportSemanticHash {
"$($_.Area):$($_.Status)"
}) -join '|'
} else { '' }
# Nightly dogfood feed banner state. Folded in so a feed going stale (or a
# fresh build landing) refreshes the tracker even on an otherwise-quiet branch
# — the banner is the whole point of the feature and must not be frozen out by
# the idempotent no-op. We hash the non-drifting tier + resolved version (NOT the
# "N days" count) so threshold crossings and new builds flip the hash but a daily
# day-count tick within the same tier does not (no watcher spam). Fail-open: if the
# NightlyFeed helper isn't loaded, contributes '' (hash behaves as before).
nightlyFeed = if ($Data.ContainsKey('nightlyFeed') -and $Data['nightlyFeed'] -and
(Get-Command Get-NightlyFeedTier -ErrorAction SilentlyContinue)) {
$nf = $Data['nightlyFeed']
# Reuse the SAME instant the banner was rendered with (stored by
# Add-SrNightlyFeedFreshness) so the hashed tier can never disagree with
# the displayed banner tier and freeze a stale banner via the no-op gate.
# Fall back to UtcNow when unset (e.g. unit tests that inject nightlyFeed directly).
$nfNow = if ($Data.ContainsKey('nightlyFeedNow') -and $Data['nightlyFeedNow']) {
[datetime]$Data['nightlyFeedNow']
} else { [datetime]::UtcNow }
$tier = Get-NightlyFeedTier -Freshness $nf -Now $nfNow
$ver = [string](Get-NightlyFeedProp $nf 'version')
if ($ver) { "$tier|$ver" } else { $tier }
} else { '' }
}

$json = $semantic | ConvertTo-Json -Depth 5 -Compress
Expand Down Expand Up @@ -3065,6 +3098,15 @@ function Format-MarkdownReport {
$shaLinked = ConvertTo-LinkedSha -Sha $ctx.srHeadSha -RepoUrl $RepoUrl
[void]$sb.AppendLine("**HEAD**: $shaLinked — $($ctx.srHeadSubject)")
[void]$sb.AppendLine("**Generated**: $($ctx.fetchedAt)")
# Nightly dogfood feed freshness — surfaces when the feed testers point at has gone
# stale (no new build), so a captain sees at a glance whether dogfood feedback is being
# collected against current bits. The banner string is rendered upstream in Invoke-Main
# (where "now" is natural), keeping this renderer clock-free and deterministic. Absent in
# phase-scoped runs / when the helper isn't loaded → nothing is appended.
if ($Data.ContainsKey('nightlyFeedBanner') -and $Data['nightlyFeedBanner']) {
[void]$sb.AppendLine()
[void]$sb.AppendLine($Data['nightlyFeedBanner'])
}
# Expected ship date — cadence depends on PatchVersion:
# - x0 patches (80, 90…) + previews → 2nd Tuesday of the month
# - hotfix patches (81, 82…) → ASAP, no cadence
Expand Down Expand Up @@ -3590,6 +3632,70 @@ function Format-MarkdownReport {

# region ────────────────────── 8. ORCHESTRATOR ────────────────────────────

function Add-SrNightlyFeedFreshness {
<#
.SYNOPSIS
Maps this SR lane to its nightly Azure Artifacts dogfood feed + version band,
queries the freshest matching build, and stores both the structured result
($Data['nightlyFeed']) and a pre-rendered banner string ($Data['nightlyFeedBanner']).
.DESCRIPTION
Lane → feed/band mapping (verified against the live feeds):
- feed = dotnet<Major> (e.g. dotnet10, dotnet11)
- signal = the inflight/current dogfood stream (ci.inflight builds) on that feed —
the "shipping next" bits dogfooders validate against. Resolved feed-wide
(not band-pinned) so it auto-follows when inflight/current advances bands
(e.g. 10.0.80 → 10.0.90). Ordinary main CI (ci.main) is deliberately NOT
tracked: it publishes daily and would paint an inflight stall green.
- band = <Major>.0.<Patch> (PatchVersion from eng/Versions.props at srRef)
used only as a FALLBACK when the feed has no inflight builds at all
(e.g. a preview feed not yet in the inflight phase).
Fail-open throughout: any gap (helper not loaded, version unreadable, network error)
degrades to "no banner"/"unknown" rather than disturbing the verdict.
#>
param([hashtable]$Data)

if (-not $Script:NightlyFeedHelperLoaded) { return }
if (-not (Get-Command Resolve-NightlyDogfoodFreshness -ErrorAction SilentlyContinue)) { return }
if (-not (Get-Command Format-NightlyFeedBanner -ErrorAction SilentlyContinue)) { return }

try {
$ctx = $Data.metadata
$surveyRef = $ctx.srRef
$vp = Get-VersionsPropsState -Ref $surveyRef
if (-not $vp) { return } # can't map a band → skip silently (no banner)

$major = [int]$vp.Major
$patch = [int]$vp.Patch
$band = "$major.0.$patch"
$feed = "dotnet$major"
$feedUrl = "https://dev.azure.com/dnceng/public/_artifacts/feed/$feed"
$bandPrefix = '^' + [regex]::Escape($band) + '-'

$fresh = Resolve-NightlyDogfoodFreshness -Feed $feed -BandPrefixRegex $bandPrefix
if ($null -eq $fresh) { $fresh = @{ unknown = $true } }

$buildType = [string](Get-NightlyFeedProp $fresh 'buildType')
$laneLabel = Format-NightlyFeedLaneLabel -Feed $feed -FeedUrl $feedUrl -BuildType $buildType -BandNote "``$band``"
$fresh['laneLabel'] = $laneLabel
$fresh['feedUrl'] = $feedUrl
$fresh['versionPrefix'] = $bandPrefix

# Capture ONE timestamp and reuse it for both the banner render and the semantic-hash
# tier (Get-ReportSemanticHash reads $Data['nightlyFeedNow']) so the two can never
# sample different sides of a tier boundary within a single run.
$nfNow = [DateTime]::UtcNow
$Data['nightlyFeed'] = $fresh
$Data['nightlyFeedNow'] = $nfNow
$banner = Format-NightlyFeedBanner -Freshness $fresh -Now $nfNow
if ($banner) { $Data['nightlyFeedBanner'] = $banner }
} catch {
# -WarningAction Continue: keep this fail-open even under an ambient
# $WarningPreference='Stop', where a bare Write-Warning would be promoted to a
# terminating error inside the catch and escape, crashing the unattended job.
Write-Warning "Nightly-feed freshness check failed (non-fatal): $($_.Exception.Message)" -WarningAction Continue
}
}

function Invoke-Main {
$excludes = $ExcludeBranches -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$ctx = Resolve-Context -SrBranch $SrBranch -Repo $Repo -MainBranch $MainBranch `
Expand Down Expand Up @@ -3624,6 +3730,13 @@ function Invoke-Main {
warnings = @()
}

# Nightly dogfood feed freshness (full runs only). Maps this SR lane to its Azure
# Artifacts feed + version band and records how fresh the newest matching build is, so
# the tracker can flag when dogfooders are testing stale bits. Fail-open inside.
if ($Phase -eq 'all') {
Add-SrNightlyFeedFreshness -Data $data
}

if ($Phase -in 'all', 'commits', 'regressions') {
$srContents = Get-SrCommits -Ctx $ctx
$data['srContents'] = $srContents
Expand Down
Loading
Loading