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