Skip to content
Merged
Changes from all 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
153 changes: 138 additions & 15 deletions .github/workflows/docfx.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ on:
required: false
type: boolean
default: true
overlay_canonical_docs_assets:
description: 'Before docfx build, overlay docfx_project/{public,versions.json,logo.svg,docfx.json} from origin/main onto the checked-out ref. Lets you backfill the canonical version-picker UI onto older tags whose original docfx_project predated it. Leave on for old-tag rebuilds; harmless no-op when running from main.'
required: false
type: boolean
default: true
# Manual trigger for ad-hoc builds or dry-runs.
# Leave 'version' blank to use the selected branch or tag name as the destination.
workflow_dispatch:
Expand All @@ -37,6 +42,10 @@ on:
description: 'Also deploy to the site root (/) and versions/latest/ (uncheck when rebuilding older versions)'
type: boolean
default: true
overlay_canonical_docs_assets:
description: 'Before docfx build, overlay docfx_project/{public,versions.json,logo.svg,docfx.json} from origin/main onto the checked-out ref. Lets you backfill the canonical version-picker UI onto older tags whose original docfx_project predated it. Leave on for old-tag rebuilds; harmless no-op when running from main.'
type: boolean
default: true

permissions:
contents: read # Default to read-only; the build-and-deploy job overrides with write
Expand All @@ -56,6 +65,67 @@ jobs:
fetch-depth: 0 # Full history needed to enumerate all v* tags
persist-credentials: false

# When backfilling docs for a tag that predates the canonical
# version-picker assets, the checked-out tag has no
# docfx_project/public/version-picker.js and its docfx.json
# has no picker-bootstrap <script> in globalMetadata._appFooter
# — so the rebuilt docs would deploy without the dropdown.
#
# Overlay the canonical assets from origin/main onto whatever ref
# we just checked out. When triggered from main (release.yaml's
# workflow_call, or workflow_dispatch from main), this is a no-op
# because the files are already at main's content. When triggered
# from an older tag, this swaps just the docs-tooling files so
# the picker appears on the rebuilt versioned docs.
#
# Scope is deliberately narrow: only docs tooling files, never
# source/csproj/tests/scripts. Set
# overlay_canonical_docs_assets=false to skip (e.g. when
# intentionally rebuilding a tag's original docs config).
- name: Overlay canonical docs assets from origin/main
if: inputs.overlay_canonical_docs_assets != false
shell: pwsh
run: |
# The checkout above did fetch-depth: 0, so origin/main is
# already in the local refs — no fresh fetch needed.
$assets = @(
'docfx_project/public',
'docfx_project/versions.json',
'docfx_project/logo.svg',
'docfx_project/docfx.json'
)

# Detect whether the current HEAD already matches origin/main.
# When it does (push to main, workflow_dispatch from main),
# the overlay is a no-op and skipping it keeps the run log
# clean.
$headSha = (git rev-parse HEAD).Trim()
$mainSha = (git rev-parse origin/main).Trim()
if ($headSha -eq $mainSha) {
Write-Host "HEAD == origin/main — overlay is a no-op, skipping."
exit 0
}

Write-Host "Overlaying canonical docs assets from origin/main"
Write-Host " HEAD = $headSha"
Write-Host " main = $mainSha"
foreach ($path in $assets) {
# Use git ls-tree to check whether the path exists on main
# before trying to checkout — avoids spurious errors for
# repos that don't carry every asset yet.
$existsOnMain = (git ls-tree -r --name-only origin/main -- $path)
if (-not $existsOnMain) {
Write-Host " skip: $path (not present on main)"
continue
}
git checkout origin/main -- $path
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to overlay $path from origin/main"
exit $LASTEXITCODE
}
Write-Host " overlaid: $path"
}

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
Expand Down Expand Up @@ -127,8 +197,30 @@ jobs:
$reports = ($coverageFiles | ForEach-Object { $_.FullName }) -join ';'
$outDir = "docfx_project/_site/coverage"
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
reportgenerator "-reports:$reports" "-targetdir:$outDir" "-reporttypes:Html;TextSummary"
Write-Host "Coverage report written to $outDir"

# Coverage TREND (T1, #65): ReportGenerator renders a historical line
# chart when given a -historydir containing prior snapshots. We persist
# that history under _site/coverage/history so it deploys to gh-pages,
# and restore the previously-published snapshots from gh-pages before
# generating, so the trend accumulates across releases instead of
# resetting each deploy. Best-effort: any failure here just yields a
# point-in-time report (the step is continue-on-error regardless).
$historyDir = "$outDir/history"
New-Item -ItemType Directory -Force -Path $historyDir | Out-Null
try {
git fetch --no-tags --depth=1 origin gh-pages 2>&1 | Out-Host
$prior = @(git ls-tree -r --name-only FETCH_HEAD 2>$null | Where-Object { $_ -like 'coverage/history/*' })
foreach ($path in $prior) {
$leaf = Split-Path $path -Leaf
git show "FETCH_HEAD:$path" 2>$null | Set-Content -Path (Join-Path $historyDir $leaf) -Encoding utf8NoBOM
}
Write-Host "Restored $($prior.Count) prior coverage-history snapshot(s)."
} catch {
Write-Host "::notice::Could not restore prior coverage history ($($_.Exception.Message)) - starting fresh."
}

reportgenerator "-reports:$reports" "-targetdir:$outDir" "-reporttypes:Html;TextSummary" "-historydir:$historyDir"
Write-Host "Coverage report (with trend) written to $outDir"

- name: Generate versions.json
# Produces versions.json consumed by the DocFX version-switcher dropdown.
Expand Down Expand Up @@ -272,6 +364,11 @@ jobs:
# so there's nothing for the preservation guard to protect — a
# transient Pages-fetch failure would block a legitimate rebuild)
if: inputs.deploy_to_pages != false && inputs.deploy_as_latest != false
env:
# github.event.repository is missing under workflow_call, so derive the
# repo name from github.repository ("owner/repo") instead — works under
# workflow_call, workflow_dispatch, and release triggers.
GITHUB_REPOSITORY: ${{ github.repository }}
shell: pwsh
run: |
$newPath = 'docfx_project/_site/versions.json'
Expand All @@ -285,30 +382,56 @@ jobs:
Write-Host "::error::Newly-generated docfx_project/_site/versions.json is missing — docfx generation is broken. Refusing to deploy without a verified version manifest."
exit 1
}
$existingUrl = "https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/versions.json"
$repoName = ($env:GITHUB_REPOSITORY -split '/')[-1]
$existingUrl = "https://${{ github.repository_owner }}.github.io/$repoName/versions.json"
try {
# -UseBasicParsing is a Windows-PowerShell-5.1 flag; pwsh (PowerShell 7+)
# treats it as unsupported and would error. Omit it.
$existingRaw = (Invoke-WebRequest -Uri $existingUrl -ErrorAction Stop).Content
} catch {
# Only treat a true 404 as "first deploy". Other errors (network,
# DNS, Pages outage, auth/redirect) must NOT silently bypass the
# preservation check — they could let a deploy that drops version
# entries from the picker slip through.
$status = $null
if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
if ($status -eq 404) {
Write-Host "::notice::No existing versions.json at $existingUrl (404) - first deploy, skipping preservation check."
# Distinguish 404 (legitimate "first deploy" - the published site
# has no versions.json yet) from any other failure (transient
# network, DNS, 5xx, rate-limit, GitHub Pages outage). Silently
# treating those as "first deploy" would allow a degraded run to
# wipe the published version selector with whatever this deploy
# produced. Only 404 is a safe signal to skip the preservation
# check; everything else aborts the deploy.
$statusCode = 0
if ($_.Exception.PSObject.Properties.Match('Response').Count -gt 0 -and $_.Exception.Response) {
try { $statusCode = [int]$_.Exception.Response.StatusCode } catch { $statusCode = 0 }
}
if ($statusCode -eq 404) {
Write-Host "::notice::No existing versions.json at $existingUrl (HTTP 404) - first deploy, skipping preservation check."
exit 0
}
Write-Error "Failed to fetch existing versions.json from $existingUrl (status=$status): $($_.Exception.Message). Aborting deploy to avoid masking a transient error."
Write-Error "Failed to fetch existing versions.json at $existingUrl (status=$statusCode): $($_.Exception.Message). Refusing to deploy - a transient fetch failure must not be treated as a first deploy because that path can wipe previously-published version entries."
exit 1
}
# Parse the newly-generated local manifest first. A failure here is
# fatal — it means docfx generation is broken.
try {
$existing = $existingRaw | ConvertFrom-Json
$new = Get-Content $newPath -Raw | ConvertFrom-Json
$new = Get-Content $newPath -Raw | ConvertFrom-Json -ErrorAction Stop
} catch {
Write-Error "Failed to parse versions.json: $($_.Exception.Message)"
Write-Error "Failed to parse newly-generated ${newPath}: $($_.Exception.Message). docfx generation is broken — refusing to deploy."
exit 1
}
# The fetch returned HTTP 200, but a freshly-created gh-pages branch
# (or a site still propagating its first deploy) can serve a generic
# GitHub 404 HTML page with status 200, or otherwise non-JSON content.
# Treat an existing body that isn't a parseable versions array the
# same as a 404 first deploy — there is no prior manifest to preserve
# — rather than aborting the very first deploy that would create
# versions.json.
try {
$existing = $existingRaw | ConvertFrom-Json -ErrorAction Stop
} catch {
Write-Host "::notice::Existing content at $existingUrl is not valid JSON (likely a 200 placeholder served on first deploy) — treating as first deploy, skipping preservation check."
exit 0
}
if (@($existing | Where-Object { $_.PSObject.Properties.Name -contains 'version' }).Count -eq 0) {
Write-Host "::notice::Existing versions.json at $existingUrl has no version entries (not a versions manifest) — treating as first deploy, skipping preservation check."
exit 0
}
$existingCount = @($existing).Count
$newCount = @($new).Count
if ($newCount -lt $existingCount) {
Expand Down
Loading