diff --git a/.github/workflows/powerforge-website-ci.yml b/.github/workflows/powerforge-website-ci.yml new file mode 100644 index 00000000..9c3c050b --- /dev/null +++ b/.github/workflows/powerforge-website-ci.yml @@ -0,0 +1,77 @@ +name: PowerForge Website CI + +on: + workflow_call: + inputs: + website_root: + required: true + type: string + pipeline_config: + required: true + type: string + runner_labels_json: + required: false + type: string + default: '["ubuntu-latest"]' + timeout_minutes: + required: false + type: number + default: 20 + dotnet_version: + required: false + type: string + default: "10.0.x" + report_artifact_name: + required: false + type: string + default: "powerforge-website-reports" + powerforge_lock_path: + required: false + type: string + default: "" + powerforge_repository: + required: false + type: string + default: "" + powerforge_ref: + required: false + type: string + default: "" + powerforge_repository_override: + required: false + type: string + default: "" + powerforge_ref_override: + required: false + type: string + default: "" + secrets: + INDEXNOW_KEY: + required: false + +permissions: + contents: read + +concurrency: + group: powerforge-website-ci-${{ github.repository }}-${{ github.ref }}-${{ inputs.website_root }} + cancel-in-progress: true + +jobs: + website-ci: + uses: ./.github/workflows/powerforge-website-run.yml + with: + website_root: ${{ inputs.website_root }} + pipeline_config: ${{ inputs.pipeline_config }} + runner_labels_json: ${{ inputs.runner_labels_json }} + timeout_minutes: ${{ inputs.timeout_minutes }} + dotnet_version: ${{ inputs.dotnet_version }} + report_artifact_name: ${{ inputs.report_artifact_name }} + powerforge_lock_path: ${{ inputs.powerforge_lock_path }} + powerforge_repository: ${{ inputs.powerforge_repository }} + powerforge_ref: ${{ inputs.powerforge_ref }} + powerforge_repository_override: ${{ inputs.powerforge_repository_override }} + powerforge_ref_override: ${{ inputs.powerforge_ref_override }} + pipeline_mode: ci + use_playwright_cache: true + secrets: + indexnow_key: ${{ secrets.INDEXNOW_KEY }} diff --git a/.github/workflows/powerforge-website-maintenance.yml b/.github/workflows/powerforge-website-maintenance.yml new file mode 100644 index 00000000..38f2811f --- /dev/null +++ b/.github/workflows/powerforge-website-maintenance.yml @@ -0,0 +1,77 @@ +name: PowerForge Website Maintenance + +on: + workflow_call: + inputs: + website_root: + required: true + type: string + pipeline_config: + required: true + type: string + runner_labels_json: + required: false + type: string + default: '["ubuntu-latest"]' + timeout_minutes: + required: false + type: number + default: 20 + dotnet_version: + required: false + type: string + default: "10.0.x" + report_artifact_name: + required: false + type: string + default: "powerforge-website-maintenance-reports" + powerforge_lock_path: + required: false + type: string + default: "" + powerforge_repository: + required: false + type: string + default: "" + powerforge_ref: + required: false + type: string + default: "" + powerforge_repository_override: + required: false + type: string + default: "" + powerforge_ref_override: + required: false + type: string + default: "" + +permissions: + contents: read + # Required for maintenance tasks such as GitHub artifact pruning. + actions: write + +concurrency: + group: powerforge-website-maintenance-${{ github.repository }}-${{ github.ref }}-${{ inputs.website_root }} + # Let scheduled/manual maintenance finish once started. + cancel-in-progress: false + +jobs: + website-maintenance: + uses: ./.github/workflows/powerforge-website-run.yml + with: + website_root: ${{ inputs.website_root }} + pipeline_config: ${{ inputs.pipeline_config }} + runner_labels_json: ${{ inputs.runner_labels_json }} + timeout_minutes: ${{ inputs.timeout_minutes }} + dotnet_version: ${{ inputs.dotnet_version }} + report_artifact_name: ${{ inputs.report_artifact_name }} + powerforge_lock_path: ${{ inputs.powerforge_lock_path }} + powerforge_repository: ${{ inputs.powerforge_repository }} + powerforge_ref: ${{ inputs.powerforge_ref }} + powerforge_repository_override: ${{ inputs.powerforge_repository_override }} + powerforge_ref_override: ${{ inputs.powerforge_ref_override }} + # Maintenance stays on strict CI mode; the selected pipeline config decides which tasks run. + pipeline_mode: ci + use_playwright_cache: true + maintenance_mode_note: true diff --git a/.github/workflows/powerforge-website-run.yml b/.github/workflows/powerforge-website-run.yml new file mode 100644 index 00000000..6ecb57a7 --- /dev/null +++ b/.github/workflows/powerforge-website-run.yml @@ -0,0 +1,184 @@ +name: PowerForge Website Run + +on: + workflow_call: + inputs: + website_root: + required: true + type: string + pipeline_config: + required: true + type: string + runner_labels_json: + required: false + type: string + default: '["ubuntu-latest"]' + timeout_minutes: + required: false + type: number + default: 20 + dotnet_version: + required: false + type: string + default: "10.0.x" + report_artifact_name: + required: false + type: string + default: "powerforge-website-reports" + powerforge_lock_path: + required: false + type: string + default: "" + powerforge_repository: + required: false + type: string + default: "" + powerforge_ref: + required: false + type: string + default: "" + powerforge_repository_override: + required: false + type: string + default: "" + powerforge_ref_override: + required: false + type: string + default: "" + pipeline_mode: + required: false + type: string + default: "ci" + use_playwright_cache: + required: false + type: boolean + default: true + maintenance_mode_note: + required: false + type: boolean + default: false + secrets: + indexnow_key: + required: false + +jobs: + website-run: + runs-on: ${{ fromJson(inputs.runner_labels_json) }} + timeout-minutes: ${{ inputs.timeout_minutes }} + env: + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + steps: + - name: Checkout website + uses: actions/checkout@v4 + with: + token: ${{ github.token }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ inputs.dotnet_version }} + + - name: Resolve PowerForge engine source + id: powerforge + shell: pwsh + run: | + $websiteRoot = [string]'${{ inputs.website_root }}' + $lockPath = [string]'${{ inputs.powerforge_lock_path }}' + if ([string]::IsNullOrWhiteSpace($lockPath)) { + $lockPath = Join-Path $websiteRoot '.powerforge/engine-lock.json' + } + + $resolvedRepository = [string]'${{ inputs.powerforge_repository }}' + $resolvedRef = [string]'${{ inputs.powerforge_ref }}' + + if (([string]::IsNullOrWhiteSpace($resolvedRepository) -or [string]::IsNullOrWhiteSpace($resolvedRef)) -and (Test-Path -LiteralPath $lockPath)) { + $lock = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json + if ($null -eq $lock) { + throw "Invalid engine lock JSON: $lockPath" + } + + if ([string]::IsNullOrWhiteSpace($resolvedRepository)) { + $resolvedRepository = [string]$lock.repository + } + + if ([string]::IsNullOrWhiteSpace($resolvedRef)) { + $resolvedRef = [string]$lock.ref + } + } + + if ([string]::IsNullOrWhiteSpace($resolvedRepository) -or [string]::IsNullOrWhiteSpace($resolvedRef)) { + throw "Provide either powerforge_repository + powerforge_ref or a valid engine lock file at '$lockPath'." + } + + $repoOverride = [string]'${{ inputs.powerforge_repository_override }}' + $refOverride = [string]'${{ inputs.powerforge_ref_override }}' + + $finalRepository = if ([string]::IsNullOrWhiteSpace($repoOverride)) { $resolvedRepository } else { $repoOverride } + $finalRef = if ([string]::IsNullOrWhiteSpace($refOverride)) { $resolvedRef } else { $refOverride } + + if ($finalRepository -ne $resolvedRepository -or $finalRef -ne $resolvedRef) { + Write-Warning "Using PowerForge override instead of resolved source (${resolvedRepository}@${resolvedRef})." + } + + if ($finalRef -notmatch '^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$') { + throw "PowerForge ref must be an immutable commit SHA (40/64 hex): '$finalRef'." + } + + "repository=$finalRepository" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "ref=$finalRef" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Checkout PowerForge engine + uses: actions/checkout@v4 + with: + repository: ${{ steps.powerforge.outputs.repository }} + ref: ${{ steps.powerforge.outputs.ref }} + path: ./.powerforge-engine + token: ${{ github.token }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Initialize Playwright cache directory + if: ${{ inputs.use_playwright_cache }} + shell: bash + run: | + set -euo pipefail + mkdir -p "${PLAYWRIGHT_BROWSERS_PATH}" + + - name: Run website pipeline + shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} + INDEXNOW_KEY: ${{ secrets.indexnow_key }} + run: | + $pipelineConfig = [string]'${{ inputs.pipeline_config }}' + $pipelineMode = [string]'${{ inputs.pipeline_mode }}' + + if ('${{ inputs.maintenance_mode_note }}' -eq 'true') { + Write-Host 'Maintenance uses the same strict CI mode; the selected pipeline config controls which tasks run.' + } + + dotnet run --project ./.powerforge-engine/PowerForge.Web.Cli -- pipeline --config "./$pipelineConfig" --mode $pipelineMode + + - name: Upload website reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.report_artifact_name }} + path: | + ./${{ inputs.website_root }}/_reports/** + ./${{ inputs.website_root }}/_site/_reports/** + if-no-files-found: ignore + + - name: Cleanup Playwright cache + if: ${{ always() && inputs.use_playwright_cache }} + shell: bash + run: | + if [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ] && [ -d "${PLAYWRIGHT_BROWSERS_PATH}" ]; then + rm -rf "${PLAYWRIGHT_BROWSERS_PATH}" + fi diff --git a/Docs/PowerForge.Web.ApiDocs.md b/Docs/PowerForge.Web.ApiDocs.md index 2a3b59e6..c0699e66 100644 --- a/Docs/PowerForge.Web.ApiDocs.md +++ b/Docs/PowerForge.Web.ApiDocs.md @@ -43,6 +43,11 @@ Overview identity and quick start: - `short` keeps short names only - `namespace-suffix` (default) disambiguates duplicates as `Type (Namespace.Part)` - `full` uses full type names +- `generateGitFreshness` (pipeline) / `--git-freshness` (CLI) enables opt-in git freshness metadata: + - `gitFreshnessNewDays` / `--git-freshness-new-days` controls the `new` window (default `14`) + - `gitFreshnessUpdatedDays` / `--git-freshness-updated-days` controls the `updated` window (default `90`) + - emitted JSON adds `freshness.status`, `freshness.lastModifiedUtc`, `freshness.commitSha`, `freshness.ageDays`, and `freshness.sourcePath` + - HTML badges render only for `new` / `updated`; `stable` remains unbadged to avoid visual noise If your site uses `Navigation.Profiles` (route/layout specific menus), set: - `navContextPath` (defaults to `/`) @@ -150,6 +155,13 @@ Overview chips: - `.type-chip` – type chip link - `.type-chip.` – kind class (`class`, `struct`, `enum`, `interface`, `delegate`) - `.type-chip` includes `data-kind` and `data-namespace` for filtering +- `.freshness-badge` – freshness badge shell +- `.freshness-badge.new` – recent/new state +- `.freshness-badge.updated` – recently updated state +- `.type-freshness-badge` – type detail header placement +- `.type-list-freshness` – sidebar row placement +- `.quick-card-freshness` – quick-start card placement +- `.type-chip-freshness` – namespace chip placement Member layout: - `.member-toolbar` – member filter toolbar @@ -203,6 +215,11 @@ Member layout: - `.derived-list` – derived types list - `.type-parameters` – type parameter section - `.type-examples` – example section +- `.example-media` – example media figure wrapper +- `.example-media-frame` – media surface (image/video/link frame) +- `.example-media-link` – terminal/download link for non-inline example media +- `.example-media-caption` – media caption +- `.example-media-meta` – capture recency / freshness metadata line - `.type-see-also` – see also section ## JavaScript expectations @@ -261,6 +278,24 @@ in the pipeline or pass `--documented-only` to the CLI. `` and `` tags are converted into links when the referenced type exists in the generated API docs. +XML-doc `` blocks can also include optional media nodes for richer API examples: + +```xml + + Sample.Run(); + Rendered output + + +``` + +Supported media nodes: +- `` / `` / `` +- `