diff --git a/.github/workflows/builds-docs.yml b/.github/workflows/builds-docs.yml deleted file mode 100644 index 0f1d4bbf38..0000000000 --- a/.github/workflows/builds-docs.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: github pages - -on: - push: - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: write - pages: write - -jobs: - deploy: - runs-on: windows-latest - steps: - - - name: Checkout - uses: actions/checkout@v3 - - - name: Install MAUI workload - run: dotnet workload install maui - - - name: Install wasm-tools workload - run: dotnet workload install wasm-tools - - - name: Restore .NET tools - run: dotnet tool restore - - - name: Build the assemblies - run: dotnet build scripts/SkiaSharp.Extended-Pack.slnf -c Release -f net10.0 - - - name: Publish Blazor sample - run: dotnet publish samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj -c Release -o publish/sample - - - name: Build docs - run: dotnet docfx docs/docfx.json - - - name: Copy Blazor sample into docs site - run: | - New-Item -ItemType Directory -Path docs/_site/sample -Force - Get-ChildItem -Path publish/sample/wwwroot -Force | Copy-Item -Destination docs/_site/sample -Recurse -Force - - # Rewrite base href for GitHub Pages deployment - $indexPath = "docs/_site/sample/index.html" - (Get-Content $indexPath -Raw) -replace '', '' | Set-Content $indexPath - shell: pwsh - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: 'docs/_site' - - - name: Deploy - if: github.ref == 'refs/heads/main' - uses: peaceiris/actions-gh-pages@v3.6.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_site diff --git a/.github/workflows/docs-cleanup-staging-pr.yml b/.github/workflows/docs-cleanup-staging-pr.yml new file mode 100644 index 0000000000..3b7ca73cfb --- /dev/null +++ b/.github/workflows/docs-cleanup-staging-pr.yml @@ -0,0 +1,61 @@ +name: "Docs - PR Staging - Cleanup" + +on: + pull_request: + branches: [ main ] + types: [ closed ] + +# Default to no permissions โ€” each job declares only what it needs. +permissions: {} + +# Ensure documentation workflows run sequentially +concurrency: + group: "docs-deployment" + cancel-in-progress: false + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + contents: write # push to gh-pages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Remove staging directory from gh-pages + env: + PR_NUMBER: ${{ github.event.number }} + run: | + # Validate PR_NUMBER is a positive integer before using in paths + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "Invalid PR number: $PR_NUMBER" + exit 1 + fi + + # Switch to gh-pages branch; exit cleanly if it doesn't exist yet + if ! git fetch origin gh-pages:gh-pages 2>/dev/null; then + echo "gh-pages branch does not exist, nothing to clean up" + exit 0 + fi + if ! git checkout gh-pages 2>/dev/null; then + echo "Failed to checkout gh-pages, nothing to clean up" + exit 0 + fi + + if [ -d "staging/$PR_NUMBER" ]; then + rm -rf staging/$PR_NUMBER + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add -A + if ! git diff --cached --quiet; then + git commit -m "Remove staging docs for PR #$PR_NUMBER" + git push origin gh-pages + echo "Removed staging directory for PR #$PR_NUMBER" + else + echo "No changes to commit for PR #$PR_NUMBER" + fi + else + echo "No staging directory found for PR #$PR_NUMBER" + fi diff --git a/.github/workflows/docs-deploy-fork-pr.yml b/.github/workflows/docs-deploy-fork-pr.yml new file mode 100644 index 0000000000..963967bf60 --- /dev/null +++ b/.github/workflows/docs-deploy-fork-pr.yml @@ -0,0 +1,152 @@ +name: "Docs - Fork PR - Deploy" + +# This workflow runs in a TRUSTED context (base repo token with write access). +# It is triggered AFTER "Docs - Deploy" completes for a fork PR. +# The "Docs - Deploy" build job uploads the site artifact; this workflow +# downloads it and deploys to gh-pages/staging/{pr}/, then comments on the PR. +# +# Security: no untrusted code is executed here โ€” we only deploy a pre-built +# artifact produced by our own workflow. The workflow_run trigger always runs +# with the base repository's token, regardless of where the PR originated. + +on: + workflow_run: + workflows: [ "Docs - Deploy" ] + types: [ completed ] + +# Default to no permissions โ€” each job declares only what it needs. +permissions: {} + +# Ensure documentation workflows run sequentially +concurrency: + group: "docs-deployment" + cancel-in-progress: false + +jobs: + deploy-fork-pr-staging: + # Only run for successful fork PR builds + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.head_repository.full_name != github.repository + runs-on: ubuntu-latest + permissions: + contents: write # push to gh-pages + pull-requests: write # comment on PR + steps: + - name: Find staging artifact for this PR + id: artifact + uses: actions/github-script@v7 + env: + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} + with: + script: | + const runId = parseInt(process.env.WORKFLOW_RUN_ID, 10); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + const staging = artifacts.data.artifacts.find(a => a.name.startsWith('staging-site-')); + if (!staging) { + core.setFailed('No staging artifact found for this workflow run'); + return; + } + // Extract and validate PR number (must be a positive integer) + const prNumberStr = staging.name.replace('staging-site-', ''); + const prNumber = parseInt(prNumberStr, 10); + if (!prNumber || prNumber <= 0 || String(prNumber) !== prNumberStr) { + core.setFailed(`Invalid PR number extracted from artifact name: ${staging.name}`); + return; + } + core.setOutput('artifact_name', staging.name); + core.setOutput('pr_number', String(prNumber)); + + - name: Download staging artifact + uses: actions/download-artifact@v4 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + name: ${{ steps.artifact.outputs.artifact_name }} + path: /tmp/site-temp + + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Deploy staging content + env: + PR_NUMBER: ${{ steps.artifact.outputs.pr_number }} + run: | + # Validate PR_NUMBER is a positive integer before using in paths + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "Invalid PR number: $PR_NUMBER" + exit 1 + fi + mkdir -p staging/$PR_NUMBER + rm -rf staging/$PR_NUMBER/* + cp -r /tmp/site-temp/. staging/$PR_NUMBER/ + rm -rf /tmp/site-temp + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add staging/$PR_NUMBER + if ! git diff --cached --quiet; then + git commit -m "Deploy staging docs for PR #$PR_NUMBER" + git push origin gh-pages + else + echo "No changes to commit" + fi + + - name: Comment on PR + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ steps.artifact.outputs.pr_number }} + with: + script: | + const prNumber = parseInt(process.env.PR_NUMBER, 10); + if (!prNumber || prNumber <= 0) { + core.setFailed('Invalid PR number'); + return; + } + const stagingUrl = `https://mono.github.io/SkiaSharp.Extended/staging/${prNumber}/`; + const sampleUrl = `https://mono.github.io/SkiaSharp.Extended/staging/${prNumber}/sample/`; + + // Only comment once; check for an existing bot comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + const botComment = comments.data.find(c => + c.user.type === 'Bot' && + c.body.includes('Documentation Preview') + ); + if (botComment) { + console.log('Bot comment already exists, skipping.'); + return; + } + + const commentBody = [ + '๐Ÿ“– **Documentation Preview**', + '', + 'The documentation for this PR has been deployed and is available at:', + '', + `๐Ÿ”— **[View Staging Documentation](${stagingUrl})**`, + '', + `๐Ÿ”— **[View Staging Blazor Sample](${sampleUrl})**`, + '', + 'This preview will be updated automatically when you push new commits to this PR.', + '', + '---', + '*This comment is automatically updated by the documentation staging workflow.*' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody + }); + diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000000..43f2654ba3 --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,242 @@ +name: "Docs - Deploy" + +on: + push: + branches: [ main ] + paths: + - 'docs/**' + - 'samples/SkiaSharpDemo.Blazor/**' + - 'source/**' + - 'scripts/**' + - '.github/workflows/docs-deploy.yml' + pull_request: + branches: [ main ] + paths: + - 'docs/**' + - 'samples/SkiaSharpDemo.Blazor/**' + - 'source/**' + - 'scripts/**' + - '.github/workflows/docs-deploy.yml' + types: [ opened, synchronize, reopened ] + workflow_dispatch: + +# Default to no permissions โ€” each job declares only what it needs. +permissions: {} + +# Ensure documentation workflows run sequentially +concurrency: + group: "docs-deployment" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read # checkout only + outputs: + artifact_name: ${{ steps.vars.outputs.artifact_name }} + is_staging: ${{ steps.vars.outputs.is_staging }} + pr_number: ${{ steps.vars.outputs.pr_number }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup build variables + id: vars + env: + EVENT_NAME: ${{ github.event_name }} + PR_NUMBER: ${{ github.event.number }} + shell: bash + run: | + if [ "$EVENT_NAME" == "pull_request" ]; then + # Validate PR_NUMBER is a positive integer before using it in artifact names/paths + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "Invalid PR number: $PR_NUMBER" + exit 1 + fi + echo "artifact_name=staging-site-${PR_NUMBER}" >> $GITHUB_OUTPUT + echo "sample_base_href=/SkiaSharp.Extended/staging/${PR_NUMBER}/sample/" >> $GITHUB_OUTPUT + echo "sample_segment_count=4" >> $GITHUB_OUTPUT + echo "is_staging=true" >> $GITHUB_OUTPUT + echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT + echo "source_label=PR #${PR_NUMBER}" >> $GITHUB_OUTPUT + else + echo "artifact_name=main-site" >> $GITHUB_OUTPUT + echo "sample_base_href=/SkiaSharp.Extended/sample/" >> $GITHUB_OUTPUT + echo "sample_segment_count=2" >> $GITHUB_OUTPUT + echo "is_staging=false" >> $GITHUB_OUTPUT + echo "pr_number=" >> $GITHUB_OUTPUT + echo "source_label=main" >> $GITHUB_OUTPUT + fi + + - name: Install MAUI workload + run: dotnet workload install maui-android + + - name: Install wasm-tools workload + run: dotnet workload install wasm-tools + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Build the assemblies + run: dotnet build scripts/SkiaSharp.Extended-Pack.slnf -c Release -f net10.0 + + - name: Publish Blazor sample + env: + SOURCE_LABEL: ${{ steps.vars.outputs.source_label }} + run: | + COMMIT_SHORT="${GITHUB_SHA:0:7}" + BUILD_INFO="${SOURCE_LABEL} ยท ${COMMIT_SHORT}" + dotnet publish samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj \ + -c Release -o publish/sample \ + -p:BuildInfo="$BUILD_INFO" + + - name: Build docs + env: + SOURCE_LABEL: ${{ steps.vars.outputs.source_label }} + run: | + COMMIT_SHORT="${GITHUB_SHA:0:7}" + APP_FOOTER="source: ${SOURCE_LABEL} ยท ${COMMIT_SHORT}" + jq --arg footer "$APP_FOOTER" \ + '.build.globalMetadata._appFooter = $footer' \ + docs/docfx.json > docs/docfx-build.json + dotnet docfx docs/docfx-build.json + + - name: Copy Blazor sample into docs site + env: + BASE_HREF: ${{ steps.vars.outputs.sample_base_href }} + SEGMENT_COUNT: ${{ steps.vars.outputs.sample_segment_count }} + run: | + mkdir -p docs/_site/sample + cp -r publish/sample/wwwroot/. docs/_site/sample/ + + # Rewrite base href for deployment + sed -i "s|||" docs/_site/sample/index.html + + # Update segmentCount in 404.html + if [ -f docs/_site/sample/404.html ]; then + sed -i "s|var segmentCount = [0-9]*;|var segmentCount = $SEGMENT_COUNT;|" docs/_site/sample/404.html + fi + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.vars.outputs.artifact_name }} + path: docs/_site + retention-days: 1 + + deploy: + runs-on: ubuntu-latest + needs: build + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Download site artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.build.outputs.artifact_name }} + path: /tmp/site-temp + + - name: Setup main site content + if: needs.build.outputs.is_staging == 'false' + run: | + find . -maxdepth 1 \ + -not -name 'staging' \ + -not -name '.git' \ + -not -name '.' \ + -not -name '..' \ + -exec rm -rf {} \; + cp -r /tmp/site-temp/. . + rm -rf /tmp/site-temp + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add . + if ! git diff --cached --quiet; then + git commit -m "Deploy main documentation from commit ${{ github.sha }}" + git push origin gh-pages + else + echo "No changes to commit" + fi + + - name: Setup staging site content + if: needs.build.outputs.is_staging == 'true' + env: + PR_NUMBER: ${{ needs.build.outputs.pr_number }} + run: | + # Validate PR_NUMBER is a positive integer before using in paths + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "Invalid PR number: $PR_NUMBER" + exit 1 + fi + mkdir -p staging/$PR_NUMBER + rm -rf staging/$PR_NUMBER/* + cp -r /tmp/site-temp/. staging/$PR_NUMBER/ + rm -rf /tmp/site-temp + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add staging/$PR_NUMBER + if ! git diff --cached --quiet; then + git commit -m "Deploy staging docs for PR #$PR_NUMBER" + git push origin gh-pages + else + echo "No changes to commit" + fi + + comment: + runs-on: ubuntu-latest + needs: [build, deploy] + if: needs.build.outputs.is_staging == 'true' && (github.event.action == 'opened' || github.event.action == 'reopened') + permissions: + pull-requests: write + steps: + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.issue.number; + const stagingUrl = `https://mono.github.io/SkiaSharp.Extended/staging/${prNumber}/`; + const sampleUrl = `https://mono.github.io/SkiaSharp.Extended/staging/${prNumber}/sample/`; + + // Only comment once; check for an existing bot comment to avoid duplicates on reopen + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + const botComment = comments.data.find(c => + c.user.type === 'Bot' && + c.body.includes('Documentation Preview') + ); + if (botComment) { + console.log('Bot comment already exists, skipping.'); + return; + } + + const commentBody = [ + '๐Ÿ“– **Documentation Preview**', + '', + 'The documentation for this PR has been deployed and is available at:', + '', + `๐Ÿ”— **[View Staging Documentation](${stagingUrl})**`, + '', + `๐Ÿ”— **[View Staging Blazor Sample](${sampleUrl})**`, + '', + 'This preview will be updated automatically when you push new commits to this PR.', + '', + '---', + '*This comment is automatically updated by the documentation staging workflow.*' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody + }); diff --git a/.github/workflows/docs-go-live.yml b/.github/workflows/docs-go-live.yml new file mode 100644 index 0000000000..1185c29591 --- /dev/null +++ b/.github/workflows/docs-go-live.yml @@ -0,0 +1,48 @@ +name: "Docs - Go Live!" + +on: + workflow_run: + workflows: + - "Docs - Deploy" + - "Docs - Fork PR - Deploy" + - "Docs - PR Staging - Cleanup" + types: + - completed + workflow_dispatch: + +# Default to no permissions โ€” each job declares only what it needs. +permissions: {} + +# Ensure go-live runs one at a time (latest wins) +concurrency: + group: "docs-go-live" + cancel-in-progress: true + +jobs: + deploy: + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: read + pages: write + id-token: write + runs-on: ubuntu-latest + steps: + - name: Checkout gh-pages + uses: actions/checkout@v4 + with: + ref: gh-pages + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: . + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/docfx.json b/docs/docfx.json index ebced96514..56932bc04c 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -5,7 +5,7 @@ "src": [ { "files": [ - "**/bin/Release/net9.0/**.dll" + "**/bin/Release/net10.0/**.dll" ], "src": "../source/" } diff --git a/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor b/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor index 5773ded5ca..e1527227a8 100644 --- a/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor +++ b/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor @@ -1,4 +1,5 @@ -๏ปฟ@inherits LayoutComponentBase +@inherits LayoutComponentBase +@using System.Reflection
+ +@code { + private string _buildInfo = typeof(App).Assembly + .GetCustomAttributes() + .FirstOrDefault(a => a.Key == "BuildInfo")?.Value ?? "local"; +} diff --git a/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor.css b/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor.css index baef3ee5fc..2375260d01 100644 --- a/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor.css +++ b/samples/SkiaSharpDemo.Blazor/Layout/MainLayout.razor.css @@ -45,6 +45,15 @@ main { margin-left: 0; } } + +.build-info { + text-align: right; + padding: 0.25rem 1.5rem; + font-size: 0.65rem; + font-family: monospace; + color: #999; + border-top: 1px solid #eee; +} @media (min-width: 641px) { .page { diff --git a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj index 4cdae9a4cf..e22a8ae99e 100644 --- a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj +++ b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj @@ -12,8 +12,19 @@ false false + + + local + + + + <_Parameter1>BuildInfo + <_Parameter2>$(BuildInfo) + + + diff --git a/samples/SkiaSharpDemo.Blazor/wwwroot/css/app.css b/samples/SkiaSharpDemo.Blazor/wwwroot/css/app.css index d919e040b1..1b97224f29 100644 --- a/samples/SkiaSharpDemo.Blazor/wwwroot/css/app.css +++ b/samples/SkiaSharpDemo.Blazor/wwwroot/css/app.css @@ -111,4 +111,4 @@ code { .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { text-align: start; -} \ No newline at end of file +}