diff --git a/.github/version-picker-template.html b/.github/version-picker-template.html
new file mode 100644
index 0000000..39a2594
--- /dev/null
+++ b/.github/version-picker-template.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+ {{TITLE}}
+
+
+
+ {{TITLE}}
+ Select a documentation version:
+
+
+
diff --git a/.github/workflows/build-all-versions.yaml b/.github/workflows/build-all-versions.yaml
new file mode 100644
index 0000000..a7430ad
--- /dev/null
+++ b/.github/workflows/build-all-versions.yaml
@@ -0,0 +1,347 @@
+name: Build All Versioned Docs
+
+# One-shot (or on-demand) workflow that builds DocFX documentation for every
+# v*.*.* git tag and deploys the output to gh-pages under versions//.
+# The version list is discovered automatically from git tags at runtime – no
+# manual updates are required when new releases are published.
+#
+# Versions without a docfx_project at their tag (e.g. v0.1.x) use the
+# current docfx_project configuration so that the documentation structure
+# is consistent across all versions.
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read # Default to read-only; the build-and-deploy job overrides with write
+
+jobs:
+ build-and-deploy:
+ name: Build & Deploy All Versioned Docs
+ runs-on: windows-latest
+
+ permissions:
+ contents: write # Required to push to gh-pages branch
+
+ steps:
+ - name: Checkout repository (full history + all tags)
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Full history so all tags are reachable
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 5.0.x
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ - name: Install DocFX
+ run: |
+ dotnet tool update docfx --global || dotnet tool install docfx --global
+ shell: pwsh
+
+ - name: Build docs for each version
+ id: build
+ shell: pwsh
+ run: |
+ $outDir = Join-Path $env:RUNNER_TEMP 'all_version_docs'
+ New-Item -ItemType Directory -Force -Path $outDir | Out-Null
+
+ # Versions to build – discovered automatically from all v*.*.* git tags so
+ # no manual list updates are needed when new releases are published.
+ # 'latest' is handled separately below.
+ $semverRe = '^v(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?[0-9A-Za-z.-]+))?$'
+ $versions = @(
+ git tag -l 'v*' |
+ Where-Object { $_ -match $semverRe } |
+ ForEach-Object {
+ $_ -match $semverRe | Out-Null
+ $stable = if ([string]::IsNullOrEmpty($Matches['prerelease'])) { 1 } else { 0 }
+ [PSCustomObject]@{
+ Tag = $_
+ Major = [int]$Matches['major']
+ Minor = [int]$Matches['minor']
+ Patch = [int]$Matches['patch']
+ Stable = $stable
+ }
+ } |
+ Sort-Object -Property Major, Minor, Patch, Stable -Descending |
+ Select-Object -ExpandProperty Tag
+ )
+ Write-Host "Discovered $($versions.Count) version tag(s): $($versions -join ', ')"
+
+ foreach ($version in $versions) {
+ Write-Host "========================================" -ForegroundColor Cyan
+ Write-Host "Building docs for $version" -ForegroundColor Cyan
+ Write-Host "========================================" -ForegroundColor Cyan
+
+ $workDir = Join-Path $env:RUNNER_TEMP "workdir-$version"
+
+ # Clean up any leftover worktree from a previous run
+ $removeOutput = git worktree remove $workDir --force 2>&1
+ if ($LASTEXITCODE -ne 0 -and (Test-Path $workDir)) {
+ Write-Warning "⚠️ Could not remove worktree for $version via git; attempting manual cleanup."
+ Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue
+ }
+
+ # Create a worktree checked out at the version tag
+ git worktree add $workDir $version
+ if ($LASTEXITCODE -ne 0) {
+ Write-Warning "⚠️ Failed to create worktree for $version – skipping."
+ continue
+ }
+
+ # Overlay the current docfx_project config so that older tags
+ # (which may lack docfx_project/) still produce consistent docs.
+ if (Test-Path 'docfx_project') {
+ if (Test-Path "$workDir/docfx_project") {
+ Remove-Item "$workDir/docfx_project" -Recurse -Force
+ }
+ Copy-Item 'docfx_project' "$workDir/docfx_project" -Recurse -Force
+ }
+
+ # Copy build-support files that were added in later versions to help
+ # older code compile cleanly. These files contain global settings
+ # (analyzer rules, banned symbols) that are version-agnostic; copying
+ # them into older worktrees is safe and avoids restore/build warnings.
+ foreach ($f in @('Directory.Build.props', '.globalconfig', 'BannedSymbols.txt')) {
+ if ((Test-Path $f) -and -not (Test-Path "$workDir/$f")) {
+ Copy-Item $f "$workDir/$f" -Force -ErrorAction Stop
+ }
+ }
+ Push-Location $workDir
+ try {
+ # Attempt dotnet restore + build; failures are non-fatal because
+ # DocFX can still extract metadata from source files.
+ $slnFile = Get-ChildItem -Filter '*.sln' -ErrorAction SilentlyContinue |
+ Select-Object -First 1
+ if ($slnFile) {
+ Write-Host "Restoring $($slnFile.Name)..."
+ dotnet restore $slnFile.FullName 2>&1 | Write-Host
+ Write-Host "Building $($slnFile.Name)..."
+ dotnet build $slnFile.FullName --configuration Release --no-restore 2>&1 | Write-Host
+ }
+
+ Write-Host "Running docfx metadata..."
+ docfx metadata docfx_project/docfx.json 2>&1 | Write-Host
+
+ Write-Host "Running docfx build..."
+ docfx build docfx_project/docfx.json 2>&1 | Write-Host
+
+ if (Test-Path 'docfx_project/_site') {
+ $dest = Join-Path $outDir 'versions' $version
+ New-Item -ItemType Directory -Force -Path $dest | Out-Null
+ Copy-Item 'docfx_project/_site/*' $dest -Recurse -Force -ErrorAction Stop
+ Write-Host "✅ Docs built for $version" -ForegroundColor Green
+ } else {
+ Write-Warning "⚠️ No _site output produced for $version."
+ }
+ } finally {
+ Pop-Location
+ $removeOutput = git worktree remove $workDir --force 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Warning "⚠️ git worktree remove failed for $version (exit $LASTEXITCODE): $removeOutput"
+ }
+ }
+ }
+
+ # Build 'latest' from the most recent stable v*.*.* tag so that the
+ # 'latest' docs always match the last published release rather than an
+ # arbitrary HEAD commit.
+ Write-Host "========================================" -ForegroundColor Cyan
+ Write-Host "Building docs for latest" -ForegroundColor Cyan
+ Write-Host "========================================" -ForegroundColor Cyan
+
+ $semverRe = '^v(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)$'
+ $latestTag = git tag -l 'v*' |
+ Where-Object { $_ -match $semverRe } |
+ ForEach-Object {
+ $_ -match $semverRe | Out-Null
+ [PSCustomObject]@{
+ Tag = $_
+ Major = [int]$Matches['major']
+ Minor = [int]$Matches['minor']
+ Patch = [int]$Matches['patch']
+ }
+ } |
+ Sort-Object -Property Major, Minor, Patch -Descending |
+ Select-Object -First 1 -ExpandProperty Tag
+
+ if (-not $latestTag) {
+ Write-Error "❌ No stable v*.*.* tag found – cannot build 'latest'."
+ exit 1
+ }
+
+ Write-Host "Latest stable tag: $latestTag" -ForegroundColor Cyan
+
+ $latestWorkDir = Join-Path $env:RUNNER_TEMP 'workdir-latest'
+ $removeOutput = git worktree remove $latestWorkDir --force 2>&1
+ if ($LASTEXITCODE -ne 0 -and (Test-Path $latestWorkDir)) {
+ Remove-Item $latestWorkDir -Recurse -Force -ErrorAction SilentlyContinue
+ }
+
+ git worktree add $latestWorkDir $latestTag
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "❌ Failed to create worktree for $latestTag (latest)."
+ exit 1
+ }
+
+ # Overlay current docfx_project and build-support files (same as versioned builds)
+ if (Test-Path 'docfx_project') {
+ if (Test-Path "$latestWorkDir/docfx_project") {
+ Remove-Item "$latestWorkDir/docfx_project" -Recurse -Force
+ }
+ Copy-Item 'docfx_project' "$latestWorkDir/docfx_project" -Recurse -Force
+ }
+ foreach ($f in @('Directory.Build.props', '.globalconfig', 'BannedSymbols.txt')) {
+ if ((Test-Path $f) -and -not (Test-Path "$latestWorkDir/$f")) {
+ Copy-Item $f "$latestWorkDir/$f" -Force -ErrorAction Stop
+ }
+ }
+
+ Push-Location $latestWorkDir
+ try {
+ $slnFile = Get-ChildItem -Filter '*.sln' -ErrorAction SilentlyContinue |
+ Select-Object -First 1
+ if ($slnFile) {
+ Write-Host "Restoring $($slnFile.Name)..."
+ dotnet restore $slnFile.FullName
+ if ($LASTEXITCODE -ne 0) {
+ Write-Warning "⚠️ dotnet restore failed for latest – continuing to docfx."
+ }
+ Write-Host "Building $($slnFile.Name)..."
+ dotnet build $slnFile.FullName --configuration Release --no-restore
+ if ($LASTEXITCODE -ne 0) {
+ Write-Warning "⚠️ dotnet build failed for latest – continuing to docfx."
+ }
+ }
+
+ Write-Host "Running docfx metadata..."
+ docfx metadata docfx_project/docfx.json
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "❌ docfx metadata failed for latest (exit $LASTEXITCODE)."
+ exit $LASTEXITCODE
+ }
+
+ Write-Host "Running docfx build..."
+ docfx build docfx_project/docfx.json
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "❌ docfx build failed for latest (exit $LASTEXITCODE)."
+ exit $LASTEXITCODE
+ }
+
+ if (Test-Path 'docfx_project/_site') {
+ $dest = Join-Path $outDir 'versions' 'latest'
+ New-Item -ItemType Directory -Force -Path $dest | Out-Null
+ Copy-Item 'docfx_project/_site/*' $dest -Recurse -Force -ErrorAction Stop
+ Write-Host "✅ Docs built for latest ($latestTag)" -ForegroundColor Green
+ } else {
+ Write-Error "❌ No _site output for latest build."
+ exit 1
+ }
+ } finally {
+ Pop-Location
+ $removeOutput = git worktree remove $latestWorkDir --force 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Warning "⚠️ git worktree remove failed for latest (exit $LASTEXITCODE): $removeOutput"
+ }
+ }
+
+ - name: Generate versions.json for every version
+ # Writes a versions.json (consumed by the version-switcher dropdown) into
+ # each version's output directory. All URLs are rooted under versions/.
+ shell: pwsh
+ env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ run: |
+ $outDir = Join-Path $env:RUNNER_TEMP 'all_version_docs'
+
+ $repoName = ($env:GITHUB_REPOSITORY -split '/')[-1]
+ $base = if ($repoName) { "/$repoName/" } else { "/" }
+
+ $semverRe = '^v(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?[0-9A-Za-z.-]+))?$'
+ $tags = git tag -l 'v*' | Where-Object { $_ -ne '' }
+
+ $taggedVersions = foreach ($t in $tags) {
+ if ($t -match $semverRe) {
+ $stable = if ([string]::IsNullOrEmpty($Matches['prerelease'])) { 1 } else { 0 }
+ [PSCustomObject]@{
+ Tag = $t
+ Major = [int]$Matches['major']
+ Minor = [int]$Matches['minor']
+ Patch = [int]$Matches['patch']
+ Stable = $stable
+ }
+ }
+ }
+
+ $orderedTags = $taggedVersions |
+ Sort-Object -Property Major, Minor, Patch, Stable -Descending |
+ Select-Object -ExpandProperty Tag
+
+ [array]$versions = @([PSCustomObject]@{ version = 'latest'; url = "${base}versions/latest/" })
+ foreach ($t in $orderedTags) {
+ $versions += [PSCustomObject]@{ version = $t; url = "${base}versions/$t/" }
+ }
+
+ $versionsJson = ConvertTo-Json -InputObject $versions -Depth 3
+
+ Get-ChildItem -Path (Join-Path $outDir 'versions') -Directory | ForEach-Object {
+ $versionsJson | Set-Content -Path (Join-Path $_.FullName 'versions.json') -Encoding utf8NoBOM
+ Write-Host "Wrote versions.json to $($_.Name)/"
+ }
+
+ Write-Host "Generated versions.json with $($versions.Count) entries: $($versions | ForEach-Object { $_.version })"
+
+ - name: Generate root index.html
+ # Builds index.html from the shared template (.github/version-picker-template.html)
+ # so the page layout is maintained in one place and both this workflow and
+ # docfx.yaml produce identical markup.
+ shell: pwsh
+ env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ run: |
+ $outDir = Join-Path $env:RUNNER_TEMP 'all_version_docs'
+
+ $repoName = ($env:GITHUB_REPOSITORY -split '/')[-1]
+ $title = if ($repoName) { "$repoName Documentation" } else { "Documentation" }
+
+ $versionsJsonPath = Join-Path $outDir 'versions' 'latest' 'versions.json'
+ $versions = Get-Content $versionsJsonPath -Raw | ConvertFrom-Json
+
+ $listItems = foreach ($v in $versions) {
+ $liClass = if ($v.version -eq 'latest') { ' class="latest"' } else { '' }
+ $label = if ($v.version -eq 'latest') { 'latest (stable)' } else { $v.version }
+ " $label"
+ }
+ $listHtml = $listItems -join "`n"
+
+ $templatePath = '.github/version-picker-template.html'
+ if (-not (Test-Path $templatePath)) {
+ Write-Error "Template file '$templatePath' not found. This file is required."
+ exit 1
+ }
+ $template = Get-Content $templatePath -Raw
+ $html = $template -replace '\{\{TITLE\}\}', $title `
+ -replace '\{\{VERSION_LIST\}\}', $listHtml
+
+ $html | Set-Content -Path (Join-Path $outDir 'index.html') -Encoding utf8NoBOM
+ Write-Host "Generated index.html with $($versions.Count) version link(s)."
+
+ - name: Deploy all versioned docs to gh-pages at root
+ # Publishes the entire all_version_docs/ output directory to gh-pages
+ # at the site root. keep_files: true preserves any other content
+ # already present on the branch (CNAME, root assets, etc.).
+ uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ${{ runner.temp }}/all_version_docs
+ destination_dir: .
+ keep_files: true
+ force_orphan: false
diff --git a/.github/workflows/docfx.yaml b/.github/workflows/docfx.yaml
index f821240..22504c1 100644
--- a/.github/workflows/docfx.yaml
+++ b/.github/workflows/docfx.yaml
@@ -1,63 +1,292 @@
name: Deploy DocFX Pages
on:
- push:
- branches:
- - main # Your primary branch
+ # Called by release.yaml after a GitHub Release is published.
+ # Callers may pass an explicit 'version' string (e.g. v1.2.3); when omitted,
+ # the destination directory is derived from github.ref_name automatically.
+ workflow_call:
+ inputs:
+ version:
+ description: 'Version tag for documentation (e.g., v1.0.0). Defaults to the triggering ref name.'
+ required: false
+ default: ''
+ type: string
+ deploy_to_pages:
+ description: 'Deploy to GitHub Pages'
+ required: false
+ type: boolean
+ default: true
+ deploy_as_latest:
+ description: 'Also deploy to the site root (/) and versions/latest/ as the current latest version'
+ 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:
+ inputs:
+ version:
+ description: 'Version tag for documentation (e.g., v1.0.0). Leave blank to use the ref name.'
+ required: false
+ default: ''
+ deploy_to_pages:
+ description: 'Deploy to GitHub Pages (uncheck for dry-run)'
+ type: boolean
+ default: true
+ deploy_as_latest:
+ description: 'Also deploy to the site root (/) and versions/latest/ (uncheck when rebuilding older versions)'
+ type: boolean
+ default: true
+
+permissions:
+ contents: read # Default to read-only; the build-and-deploy job overrides with write
jobs:
- build:
- runs-on: ubuntu-latest
+ build-and-deploy:
+ name: Build & Deploy Documentation
+ runs-on: windows-latest
permissions:
- contents: read # Allow read access for checkout
- pages: write # Allow write access for Pages deployment
- id-token: write # Allow writing of ID tokens for deployment
+ contents: write # Allow write access for gh-pages branch
steps:
- name: Checkout repository
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Full history needed to enumerate all v* tags
+ persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '10.0.x'
+ dotnet-version: |
+ 5.0.x
+ 6.0.x
+ 7.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore
+ shell: pwsh
+
+ - name: Build solution
+ run: dotnet build --configuration Release --no-restore
+ shell: pwsh
- name: Install DocFX
- run: dotnet tool update docfx --global
+ run: dotnet tool update docfx --global || dotnet tool install docfx --global
+ shell: pwsh
- - name: Build DocFx Metadata
- run: docfx metadata
+ - name: Build DocFX Metadata
+ run: docfx metadata
working-directory: docfx_project
+ shell: pwsh
- name: Build Docs
- run: docfx build
+ run: docfx build
working-directory: docfx_project
+ shell: pwsh
- name: Verify build output
run: |
- if [ ! -d "docfx_project/_site" ]; then
- echo "Error: _site directory not found!"
- exit 1
- fi
- echo "Build successful. Contents of _site:"
- ls -la docfx_project/_site
-
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: docfx_project/_site # The path to the folder to upload
+ if (-Not (Test-Path "docfx_project/_site")) {
+ Write-Host "Error: docfx_project/_site directory not found!"
+ exit 1
+ }
+ Write-Host "Build successful. Contents of _site:"
+ Get-ChildItem "docfx_project/_site"
+ Write-Host "API documentation:"
+ Get-ChildItem "docfx_project/_site/api"
+ shell: pwsh
- deploy:
- needs: build
- permissions:
- pages: write
- id-token: write
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ - name: Generate versions.json
+ # Produces versions.json consumed by the DocFX version-switcher dropdown.
+ # Site layout:
+ # // ← version-picker index.html (deployed to root)
+ # //versions/latest/ ← latest docs (alias under versions/)
+ # //versions/v1.2.3/ ← versioned docs (under versions/)
+ # versions.json format expected by the dropdown:
+ # [{ "version": "latest", "url": "//versions/latest/" }, { "version": "v1.2.3", "url": "//versions/v1.2.3/" }]
+ # URLs must include the repo path segment because this is a GitHub Pages project site
+ # (https://.github.io//), not a root user/org site.
+ env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ shell: pwsh
+ run: |
+ $tags = git tag -l 'v*' | Where-Object { $_ -ne '' }
+
+ # Strict SemVer pattern: vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-PRERELEASE
+ $semverRe = '^v(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?[0-9A-Za-z.-]+))?$'
+
+ $taggedVersions = foreach ($t in $tags) {
+ if ($t -match $semverRe) {
+ # Stable releases (no prerelease) have Stable=1; prerelease builds have Stable=0.
+ # Sorting descending by Stable places stable (1) before prerelease (0) of the same version.
+ $stable = if ([string]::IsNullOrEmpty($Matches['prerelease'])) { 1 } else { 0 }
+ [PSCustomObject]@{
+ Tag = $t
+ Major = [int]$Matches['major']
+ Minor = [int]$Matches['minor']
+ Patch = [int]$Matches['patch']
+ Stable = $stable
+ }
+ }
+ }
+
+ # Sort descending by SemVer components so newest stable version comes first
+ $orderedTags = $taggedVersions |
+ Sort-Object -Property Major, Minor, Patch, Stable -Descending |
+ Select-Object -ExpandProperty Tag
+
+ # Build the base path for this GitHub Pages project site: //
+ # GITHUB_REPOSITORY is "owner/repo"; we need just the repo name.
+ $repoName = ($env:GITHUB_REPOSITORY -split '/')[-1]
+ $base = if ($repoName) { "/$repoName/" } else { "/" }
+
+ # "latest" points to the versions/latest/ folder; each version points to versions//.
+ [array]$versions = @([PSCustomObject]@{ version = 'latest'; url = "${base}versions/latest/" })
+ foreach ($t in $orderedTags) {
+ $versions += [PSCustomObject]@{ version = $t; url = "${base}versions/$t/" }
+ }
+
+ ConvertTo-Json -InputObject $versions -Depth 3 |
+ Set-Content -Path 'docfx_project/_site/versions.json' -Encoding utf8NoBOM
+ Write-Host "Generated versions.json with $($versions.Count) version(s): $($versions | ForEach-Object { $_.version })"
+
+ - name: Compute destination directory
+ # Determines the versioned subfolder name for the docs deployment (e.g. /v1.2.3/).
+ # Uses the explicit 'version' input when provided; otherwise falls back to
+ # github.ref_name so the workflow works without callers passing a version.
+ # Sanitization steps (applied in order):
+ # 1. Replace forward slashes with hyphens (prevents nested paths).
+ # 2. Replace any character outside [A-Za-z0-9._-] with a hyphen.
+ # 3. Collapse consecutive hyphens into one.
+ # 4. Strip leading dots and hyphens (avoids hidden/awkward directory names).
+ # 5. Fall back to "latest" if the result is empty, ".", or "..".
+ id: dest
+ env:
+ INPUT_VERSION: ${{ inputs.version }}
+ REF_NAME: ${{ github.ref_name }}
+ shell: pwsh
+ run: |
+ $raw = if ($env:INPUT_VERSION -ne '') { $env:INPUT_VERSION } else { $env:REF_NAME }
+ $sanitized = $raw -replace '/', '-'
+ $sanitized = $sanitized -replace '[^A-Za-z0-9._\-]', '-'
+ $sanitized = $sanitized -replace '-{2,}', '-'
+ $sanitized = $sanitized -replace '^[.\-]+', ''
+ if ([string]::IsNullOrEmpty($sanitized) -or $sanitized -eq '.' -or $sanitized -eq '..') {
+ $sanitized = 'latest'
+ }
+ Add-Content -Path $env:GITHUB_OUTPUT -Value "dir=$sanitized"
+
+ - name: Deploy docs to GitHub Pages
+ # Assembles the full gh-pages state and pushes a single commit, avoiding the
+ # multiple sequential pushes that would trigger pages-build-deployment repeatedly.
+ #
+ # Layout written to gh-pages in one commit:
+ # versions// – versioned docs (real DocFX index.html)
+ # versions/latest/ – latest alias (real DocFX index.html) [deploy_as_latest only]
+ # / – version-picker index.html + shared assets [deploy_as_latest only]
+ #
+ # Stale root files from the previous build are removed before copying new content,
+ # so outdated DocFX assets do not linger. The versions/ folder is always preserved
+ # so that all prior versioned docs remain accessible.
+ if: inputs.deploy_to_pages != false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ VERSION_DIR: ${{ steps.dest.outputs.dir }}
+ DEPLOY_AS_LATEST: ${{ inputs.deploy_as_latest != false }}
+ shell: pwsh
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git remote set-url origin "https://x-access-token:$($env:GITHUB_TOKEN)@github.com/$($env:GITHUB_REPOSITORY).git"
+
+ $WORK_DIR = Join-Path $env:RUNNER_TEMP 'gh-pages-deploy'
+ $siteDir = Resolve-Path 'docfx_project/_site'
+
+ # Set up gh-pages worktree (or start fresh if the branch does not exist yet)
+ $branchExists = git ls-remote --heads origin gh-pages
+ if ($branchExists) {
+ git fetch origin gh-pages
+ git show-ref --verify --quiet refs/heads/gh-pages
+ if ($LASTEXITCODE -ne 0) { git branch gh-pages origin/gh-pages }
+ git worktree remove $WORK_DIR --force 2>&1 | Out-Null
+ if (Test-Path $WORK_DIR) { Remove-Item $WORK_DIR -Recurse -Force }
+ git worktree add $WORK_DIR gh-pages
+ } else {
+ Write-Host "ℹ️ gh-pages does not exist yet — starting fresh."
+ New-Item -ItemType Directory -Force -Path $WORK_DIR | Out-Null
+ }
+
+ # Remove stale root files; preserve versions/, .git, .nojekyll, CNAME
+ Get-ChildItem -Path $WORK_DIR -Force | Where-Object {
+ $_.Name -notin @('.git', 'CNAME', '.nojekyll', 'versions')
+ } | Remove-Item -Recurse -Force
+
+ # Ensure .nojekyll exists so GitHub Pages does not run Jekyll
+ New-Item -ItemType File -Path (Join-Path $WORK_DIR '.nojekyll') -Force | Out-Null
+
+ # Deploy versioned docs (real DocFX index.html — before version picker overwrites it)
+ $versionedDir = Join-Path $WORK_DIR "versions/$($env:VERSION_DIR)"
+ New-Item -ItemType Directory -Force -Path $versionedDir | Out-Null
+ Copy-Item -Path "$siteDir/*" -Destination $versionedDir -Recurse -Force
+ Write-Host "✅ Copied docs to versions/$($env:VERSION_DIR)/"
+
+ if ($env:DEPLOY_AS_LATEST -eq 'true') {
+ # Deploy to versions/latest/ (real DocFX index.html)
+ $latestDir = Join-Path $WORK_DIR 'versions/latest'
+ New-Item -ItemType Directory -Force -Path $latestDir | Out-Null
+ Copy-Item -Path "$siteDir/*" -Destination $latestDir -Recurse -Force
+ Write-Host "✅ Copied docs to versions/latest/"
+
+ # Generate version-picker index.html and overwrite _site/index.html.
+ # This happens AFTER the versioned copies above, so those directories
+ # retain the real DocFX landing page while the root gets the picker.
+ $repoName = ($env:GITHUB_REPOSITORY -split '/')[-1]
+ $title = if ($repoName) { "$repoName Documentation" } else { "Documentation" }
+ $versions = Get-Content "$siteDir/versions.json" -Raw | ConvertFrom-Json
+
+ $listItems = foreach ($v in $versions) {
+ $liClass = if ($v.version -eq 'latest') { ' class="latest"' } else { '' }
+ $label = if ($v.version -eq 'latest') { 'latest (stable)' } else { $v.version }
+ " $label"
+ }
+ $listHtml = $listItems -join "`n"
+
+ if (-not (Test-Path '.github/version-picker-template.html')) {
+ Write-Error "Error: .github/version-picker-template.html not found; cannot generate root index.html."
+ exit 1
+ }
+
+ $template = Get-Content '.github/version-picker-template.html' -Raw
+ $html = $template -replace '\{\{TITLE\}\}', $title `
+ -replace '\{\{VERSION_LIST\}\}', $listHtml
+ $html | Set-Content -Path "$siteDir/index.html" -Encoding utf8NoBOM
+ Write-Host "Generated version-picker index.html with $($versions.Count) version link(s)."
+
+ # Deploy version picker + shared assets to site root
+ Copy-Item -Path "$siteDir/*" -Destination $WORK_DIR -Recurse -Force
+ Write-Host "✅ Copied version picker to site root"
+ }
+
+ # Single commit and push
+ git -C $WORK_DIR add -A
+ git -C $WORK_DIR diff --cached --quiet
+ if ($LASTEXITCODE -ne 0) {
+ $msg = if ($env:DEPLOY_AS_LATEST -eq 'true') {
+ "docs: deploy $($env:VERSION_DIR) and update latest"
+ } else {
+ "docs: deploy $($env:VERSION_DIR)"
+ }
+ git -C $WORK_DIR commit -m $msg
+ git -C $WORK_DIR push origin HEAD:gh-pages
+ Write-Host "✅ Documentation deployed in a single commit."
+ } else {
+ Write-Host "ℹ️ No documentation changes to deploy."
+ }
+
+ git worktree remove $WORK_DIR --force 2>&1 | Out-Null