Prevent indexing of non-stable docs versions and publish stable-only crawl policy #34043
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Build documentation" | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - master | |
| - stable* | |
| permissions: | |
| contents: read | |
| packages: write | |
| concurrency: | |
| group: build-documentation-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ============================================================================ | |
| # BUILD HTML | |
| # ============================================================================ | |
| # Builds the HTML documentation for all manuals. No LaTeX required. | |
| # Starts immediately without waiting for any setup job. | |
| # ============================================================================ | |
| build-html: | |
| name: Building ${{ matrix.manual.name }} HTML | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| manual: | |
| - name: "user_manual" | |
| directory: "user_manual" | |
| make_target: "html" | |
| build_path: "_build/html" | |
| publish: true | |
| - name: "user_manual-en" | |
| directory: "user_manual" | |
| make_target: "html-lang-en" | |
| build_path: "_build/html" | |
| publish: false | |
| - name: "developer_manual" | |
| directory: "developer_manual" | |
| make_target: "html" | |
| build_path: "_build/html/com" | |
| publish: true | |
| - name: "admin_manual" | |
| directory: "admin_manual" | |
| make_target: "html" | |
| build_path: "_build/html/com" | |
| publish: true | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| cache: "pip" | |
| - name: Install pip dependencies | |
| run: python -m pip install -r requirements.txt | |
| - name: Build html documentation | |
| run: cd ${{ matrix.manual.directory }} && make ${{ matrix.manual.make_target }} | |
| - name: Upload static documentation | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: ${{ matrix.manual.publish }} | |
| with: | |
| name: ${{ matrix.manual.name }} | |
| path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_path }} | |
| # ============================================================================ | |
| # PREPARE PDF IMAGE | |
| # ============================================================================ | |
| # Detects whether .devcontainer/Dockerfile changed in this run. | |
| # - If it changed: builds a fresh image, pushes it to GHCR with a SHA tag, | |
| # and outputs that tag so build-pdf uses the new image immediately. | |
| # - If it did not change: outputs the stable ghcr.io/.../sphinx-latex:latest | |
| # tag so build-pdf uses the already-published image. | |
| # ============================================================================ | |
| prepare-pdf-image: | |
| name: Prepare PDF build image | |
| runs-on: ubuntu-latest | |
| outputs: | |
| image: ${{ steps.result.outputs.image }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 2 | |
| - name: Check whether Dockerfile changed | |
| id: changed | |
| run: | | |
| if git diff --name-only HEAD^ HEAD -- .devcontainer/Dockerfile 2>/dev/null | grep -q .; then | |
| echo "dockerfile_changed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "dockerfile_changed=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Set up Docker Buildx | |
| if: steps.changed.outputs.dockerfile_changed == 'true' | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| - name: Login to GitHub Container Registry | |
| if: steps.changed.outputs.dockerfile_changed == 'true' | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push image with SHA tag | |
| if: steps.changed.outputs.dockerfile_changed == 'true' | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: .devcontainer | |
| push: true | |
| tags: ghcr.io/${{ github.repository }}/sphinx-latex:sha-${{ github.sha }} | |
| cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/sphinx-latex:latest | |
| cache-to: type=inline | |
| - name: Output image reference | |
| id: result | |
| run: | | |
| if [ "${{ steps.changed.outputs.dockerfile_changed }}" = "true" ]; then | |
| echo "image=ghcr.io/${{ github.repository }}/sphinx-latex:sha-${{ github.sha }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "image=ghcr.io/${{ github.repository }}/sphinx-latex:latest" >> $GITHUB_OUTPUT | |
| fi | |
| # ============================================================================ | |
| # BUILD PDF | |
| # ============================================================================ | |
| # Builds the PDF documentation using the pre-built sphinx-latex Docker image. | |
| # The image already contains all LaTeX packages, so no apt install is needed. | |
| # Uses the image prepared by the prepare-pdf-image job: either the stable | |
| # ghcr.io/.../sphinx-latex:latest image or a freshly built SHA-tagged image | |
| # if .devcontainer/Dockerfile changed in this run. | |
| # ============================================================================ | |
| build-pdf: | |
| name: Building ${{ matrix.manual.name }} PDF | |
| runs-on: ubuntu-latest | |
| needs: prepare-pdf-image | |
| # Use the image prepared by prepare-pdf-image: either the stable ghcr image | |
| # or a freshly built SHA-tagged image if .devcontainer/Dockerfile changed. | |
| container: ${{ needs.prepare-pdf-image.outputs.image }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| manual: | |
| - name: "user_manual" | |
| directory: "user_manual" | |
| build_pdf_path: "_build/latex/Nextcloud_User_Manual.pdf" | |
| - name: "admin_manual" | |
| directory: "admin_manual" | |
| build_pdf_path: "_build/latex/Nextcloud_Server_Administration_Manual.pdf" | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| # pip cache is not compatible with the Docker container | |
| # cache: "pip" | |
| - name: Install pip dependencies | |
| run: python -m pip install -r requirements.txt | |
| - name: Compute PDF release version | |
| id: pdf_version | |
| run: | | |
| branch="${GITHUB_REF#refs/heads/}" | |
| if [[ "$branch" == stable* ]]; then | |
| echo "release=${branch#stable}" >> $GITHUB_OUTPUT | |
| else | |
| echo "release=latest" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build pdf documentation | |
| env: | |
| DOCS_RELEASE: ${{ steps.pdf_version.outputs.release }} | |
| run: | | |
| set -e | |
| cd ${{ matrix.manual.directory }} | |
| make latexpdf | |
| ls -la ${{ matrix.manual.build_pdf_path }} | |
| - name: Upload PDF documentation | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ matrix.manual.name }}-pdf | |
| path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_pdf_path }} | |
| # ============================================================================ | |
| # STAGE AND VALIDATE | |
| # ============================================================================ | |
| # This job is responsible for: | |
| # 1. Determining deployment target folder names (branch_name/version_name) | |
| # 2. Organizing build artifacts into a clean structure | |
| # 3. Validating the documentation (link checking) | |
| # 4. Uploading a minimal staging artifact for the deploy job | |
| # | |
| # IMPORTANT: This job does NOT modify gh-pages. It only prepares and validates | |
| # the artifacts that will be deployed. The actual deployment happens in the | |
| # deploy job. | |
| # ============================================================================ | |
| stage-and-check: | |
| name: Stage and check documentation | |
| needs: [build-html, build-pdf] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| # branch_name: The primary deployment folder name for this branch | |
| # - master β "latest" | |
| # - stable<N> (if highest) β "stable" | |
| # - stable<N> (if not highest) β "<N>" (numeric version) | |
| branch_name: ${{ steps.branch.outputs.branch_name }} | |
| # additional_deployment: ONLY set if deploying the highest stable branch | |
| # - If this IS the highest stable β "<N>" (numeric version, e.g. "32") | |
| # - Otherwise β "" (empty string) | |
| # | |
| # This allows the highest stable to be deployed to TWO locations: | |
| # server/stable/ (via branch_name) | |
| # server/<N>/ (via additional_deployment) | |
| additional_deployment: ${{ steps.branch.outputs.additional_deployment }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: artifacts/ | |
| # ======================================================================== | |
| # DETERMINE DEPLOYMENT TARGETS (branch_name and version_name) | |
| # ======================================================================== | |
| # Logic: | |
| # 1. Determine current_branch: use GITHUB_REF if push, GITHUB_BASE_REF if PR | |
| # 2. Find the highest numbered stable branch from git remotes | |
| # 3. Map the current branch to deployment folder names: | |
| # | |
| # master β branch_name=latest (no version_name) | |
| # | |
| # stable<N> where N is highest β branch_name=stable, version_name=<N> | |
| # (deployed to both server/stable/ and server/<N>/) | |
| # | |
| # stable<N> where N is not highest β branch_name=<N> (no version_name) | |
| # (deployed only to server/<N>/) | |
| # | |
| # Any other branch β branch_name=<branch> (no version_name) | |
| # ======================================================================== | |
| - name: Determine deployment targets (branch_name and version_name) | |
| id: branch | |
| run: | | |
| # Determine which branch we're building from | |
| current_branch=${GITHUB_REF#refs/heads/} | |
| if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then | |
| current_branch=${GITHUB_BASE_REF} | |
| fi | |
| # Find the highest numbered stable branch from the remote | |
| # e.g., "stable30", "stable31", "stable32" β extract "32" | |
| highest_stable=$(git ls-remote --heads origin | sed -n 's?.*refs/heads/stable\([0-9]\{2\}\)$?\1?p' | sort -n | tail -1) | |
| highest_stable_branch="stable${highest_stable}" | |
| echo "Current branch: $current_branch" | |
| echo "Highest stable branch found: $highest_stable_branch" | |
| # Map branch to deployment folder names | |
| case "$current_branch" in | |
| "master") | |
| # master always deploys to "latest" | |
| echo "branch_name=latest" >> $GITHUB_OUTPUT | |
| ;; | |
| "$highest_stable_branch") | |
| # Highest stable gets TWO locations: both "stable" and "<N>" | |
| echo "branch_name=stable" >> $GITHUB_OUTPUT | |
| echo "additional_deployment=${highest_stable}" >> $GITHUB_OUTPUT | |
| ;; | |
| *) | |
| # Other branches (including older stable branches) get their branch name | |
| # For stable<N> where N is not highest: strip "stable" prefix to get just "<N>" | |
| branch_for_deploy="${current_branch#stable}" | |
| echo "branch_name=$branch_for_deploy" >> $GITHUB_OUTPUT | |
| ;; | |
| esac | |
| - name: Log deployment targets | |
| run: | | |
| echo "Deployment target folder: ${{ steps.branch.outputs.branch_name }}" | |
| echo "Additional deployment folder (if applicable): ${{ steps.branch.outputs.additional_deployment }}" | |
| # ======================================================================== | |
| # ORGANIZE ARTIFACTS FOR DEPLOYMENT | |
| # ======================================================================== | |
| # Create a clean, minimal staging structure: | |
| # - Deploy only the NEW artifacts for this branch | |
| # - No need to include existing versions (we'll merge them during deploy) | |
| # ======================================================================== | |
| - name: Organize artifacts for deployment | |
| id: organize | |
| run: | | |
| branch="${{ steps.branch.outputs.branch_name }}" | |
| # Create the branch folder directly | |
| mkdir -p "stage/${branch}" | |
| # Copy artifacts preserving their manual folder structure | |
| # Each artifact (user_manual, admin_manual, developer_manual) contains | |
| # the build output that should be placed in a folder named after the artifact | |
| for artifact in artifacts/*; do | |
| if [ -d "$artifact" ]; then | |
| manual_name="$(basename "$artifact")" | |
| # Create the manual-specific folder | |
| mkdir -p "stage/${branch}/${manual_name}" | |
| # Copy artifact contents into the manual folder | |
| cp -r "$artifact/"* "stage/${branch}/${manual_name}/" | |
| fi | |
| done | |
| # Move PDF files to the root of the branch folder for cleaner structure | |
| echo "Looking for PDF files to move..." | |
| find "stage/${branch}/" -maxdepth 2 -name "*.pdf" -type f | |
| for pdf in "stage/${branch}"/*/*.pdf; do | |
| if [ -f "$pdf" ]; then | |
| echo "Moving PDF: $pdf" | |
| mv "$pdf" "stage/${branch}/" | |
| fi | |
| done | |
| # Clean up empty directories | |
| find stage -type d -empty -delete | |
| echo "Staged artifacts for ${branch}:" | |
| find stage -type f | head -20 | |
| # ======================================================================== | |
| # UPLOAD STAGING ARTIFACTS | |
| # ======================================================================== | |
| # Upload the staging folder for use in both the deploy and link-check jobs. | |
| # ======================================================================== | |
| - name: Upload staged artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: staged-docs | |
| path: stage/ | |
| retention-days: 1 | |
| # ============================================================================ | |
| # LINK CHECK | |
| # ============================================================================ | |
| # Runs in parallel with deploy. Downloads the staged artifacts, strips | |
| # canonical links, then runs lychee against the new content only. | |
| # ============================================================================ | |
| link-check: | |
| name: Check for broken links | |
| needs: stage-and-check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download staged artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: staged-docs | |
| path: stage/ | |
| - name: Strip canonical links from validation HTML | |
| run: | | |
| find "stage/${{ needs.stage-and-check.outputs.branch_name }}" -name '*.html' -print0 | while IFS= read -r -d '' f; do | |
| perl -0pi -e 's{^\s*<link rel="canonical" href="https://docs\.nextcloud\.com/server/[^"]*" />\n}{}m' "$f" | |
| perl -pi -e 's{<meta name="robots" content="noindex, follow" />\n?}{}g' "$f" | |
| done | |
| ls -la stage/* | |
| # We need to exclude certain links from the check: | |
| # - go.php: This is a special redirect page | |
| # - mailto: links: These are not valid URLs and will always fail | |
| # - 404.html: This is not necessary | |
| # - latest/stable/xx links from the version selector | |
| - name: Check for broken links with lychee | |
| uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 | |
| with: | |
| fail: true | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| jobSummary: true | |
| args: | | |
| --root-dir "$(pwd)/stage" | |
| --offline --no-progress | |
| --remap "https://docs.nextcloud.com/server/latest/ file://$(pwd)/stage/${{ needs.stage-and-check.outputs.branch_name }}/" | |
| --remap "https://docs.nextcloud.com/server/ file://$(pwd)/stage/" | |
| --exclude 'go\.php' --exclude 'mailto:' --exclude-path '.*/404\.html' --exclude-path '.*/_static/.*' | |
| --exclude "/user_manual/" --include "/user_manual/en/" | |
| --exclude '^file://.*/stage/(latest|stable|[0-9]+)/(developer_manual|admin_manual|user_manual)/?$' | |
| 'stage/${{ needs.stage-and-check.outputs.branch_name }}/user_manual/en/**/*.html' | |
| 'stage/${{ needs.stage-and-check.outputs.branch_name }}/admin_manual/**/*.html' | |
| 'stage/${{ needs.stage-and-check.outputs.branch_name }}/developer_manual/**/*.html' | |
| # ============================================================================ | |
| # DEPLOY | |
| # ============================================================================ | |
| # This job is responsible for: | |
| # 1. Downloading the staged artifacts from stage-and-check | |
| # 2. Applying them to the gh-pages branch | |
| # 3. Creating a pull request for the deployment | |
| # | |
| # This job ONLY runs on pushes (not on pull requests), since we only want | |
| # to deploy when code is merged to master or a stable branch. | |
| # ============================================================================ | |
| deploy: | |
| name: Deploy documentation for gh-pages | |
| needs: stage-and-check | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout gh-pages branch | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: gh-pages | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Download staged artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: staged-docs | |
| path: stage/ | |
| # ======================================================================== | |
| # APPLY STAGED ARTIFACTS TO GH-PAGES | |
| # ======================================================================== | |
| # Strategy: | |
| # - Copy from stage/<branch_name>/ to server/<branch_name>/ | |
| # - If version_name is set, ALSO copy to server/<version_name>/ | |
| # - This allows the highest stable to live in both locations | |
| # ======================================================================== | |
| - name: Apply staged artifacts to gh-pages | |
| id: apply | |
| run: | | |
| branch="${{ needs.stage-and-check.outputs.branch_name }}" | |
| additional="${{ needs.stage-and-check.outputs.additional_deployment }}" | |
| # Copy built documentation into server folder | |
| mkdir -p server/${branch} | |
| for artifact in stage/${branch}/*; do | |
| if [ -d "$artifact" ]; then | |
| manual_name="$(basename "$artifact")" | |
| rm -rf "server/${branch}/${manual_name}" | |
| cp -r "$artifact" "server/${branch}/${manual_name}" | |
| fi | |
| done | |
| # Move pdf files to the root of the branch folder | |
| echo "Looking for PDF files to copy..." | |
| find stage/${branch}/ -name "*.pdf" -type f | |
| for pdf in stage/${branch}/*.pdf; do | |
| if [ -f "$pdf" ]; then | |
| echo "Copying PDF: $pdf" | |
| cp "$pdf" server/${branch}/ | |
| fi | |
| done | |
| # If this is the highest stable branch, also deploy to its versioned folder | |
| if [ -n "${additional}" ]; then | |
| rm -rf server/${additional} | |
| cp -r server/${branch} server/${additional} | |
| fi | |
| # Cleanup empty directories | |
| find . -type d -empty -delete | |
| # Check if there are actual changes | |
| if git diff --quiet HEAD; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Strip noindex from stable docs | |
| run: | | |
| # Strip noindex from stable docs β these are the canonical pages we want indexed | |
| if [ -d server/stable ]; then | |
| find server/stable -name '*.html' -print0 | \ | |
| xargs -0 perl -pi -e 's{<meta name="robots" content="noindex, follow" />\n?}{}g' | |
| fi | |
| - name: Write robots.txt | |
| run: | | |
| cat > robots.txt << 'EOF' | |
| User-agent: * | |
| # Only the stable version should be indexed | |
| Allow: /server/stable/ | |
| Disallow: /server/ | |
| EOF | |
| # Remove the stage/ directory BEFORE creating the PR so it doesn't get committed | |
| - name: Clean up staging cache before commit | |
| run: rm -rf stage/ | |
| # ======================================================================== | |
| # ADD REDIRECT FILES | |
| # ======================================================================== | |
| - name: Add various redirects for go.php and user_manual english version | |
| run: | | |
| branch="${{ needs.stage-and-check.outputs.branch_name }}" | |
| additional="${{ needs.stage-and-check.outputs.additional_deployment }}" | |
| git fetch origin ${{ github.event.repository.default_branch }} ${{ github.ref_name }} | |
| # Add go.php redirect from main branch | |
| git checkout origin/${{ github.event.repository.default_branch }} -- go.php/index.html | |
| mkdir -p server/${branch}/go.php | |
| mv go.php/index.html server/${branch}/go.php/index.html | |
| # Add user_manual english redirect | |
| git checkout origin/${{ github.ref_name }} -- user_manual/index.html | |
| mkdir -p server/${branch}/user_manual | |
| mv user_manual/index.html server/${branch}/user_manual/index.html | |
| # Also copy to versioned folder if applicable | |
| if [ -n "${additional}" ]; then | |
| mkdir -p server/${additional}/go.php server/${additional}/user_manual | |
| cp server/${branch}/go.php/index.html server/${additional}/go.php/ | |
| cp server/${branch}/user_manual/index.html server/${additional}/user_manual/ | |
| fi | |
| - name: Create Pull Request for documentation deployment | |
| uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 | |
| id: cpr | |
| if: steps.apply.outputs.has_changes == 'true' | |
| with: | |
| token: ${{ secrets.COMMAND_BOT_PAT }} | |
| commit-message: "chore: update documentation for `${{ needs.stage-and-check.outputs.branch_name }}`" | |
| committer: nextcloud-command <nextcloud-command@users.noreply.github.com> | |
| author: nextcloud-command <nextcloud-command@users.noreply.github.com> | |
| signoff: true | |
| branch: "automated/deploy/documentation-${{ needs.stage-and-check.outputs.branch_name }}" | |
| base: gh-pages | |
| title: "Documentation update for `${{ needs.stage-and-check.outputs.branch_name }}`" | |
| body: | | |
| This PR was automatically generated by the CI workflow and | |
| includes the latest changes for the `${{ needs.stage-and-check.outputs.branch_name }}` branch. | |
| delete-branch: true | |
| labels: "automated, 3. to review" | |
| - name: Enable Pull Request Automerge | |
| run: gh pr merge --merge --auto "${{ steps.cpr.outputs.pull-request-number }}" | |
| if: steps.cpr.outputs.pull-request-number != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} | |
| # ============================================================================ | |
| # NETLIFY PREVIEW | |
| # ============================================================================ | |
| # Runs only on pull requests, in parallel with link-check. | |
| # Downloads the staged-docs artifact (already produced by stage-and-check) | |
| # and deploys a preview to Netlify under a stable per-PR alias. | |
| # | |
| # Required repository secrets: | |
| # NETLIFY_AUTH_TOKEN β personal-access token for the Netlify account | |
| # NETLIFY_SITE_ID β the target Netlify site ID | |
| # | |
| # Failure is non-fatal: the step uses continue-on-error so a missing secret | |
| # or a Netlify outage does not block the pull request. | |
| # ============================================================================ | |
| netlify-preview: | |
| name: Deploy Netlify preview | |
| needs: stage-and-check | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Download staged artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: staged-docs | |
| path: stage/ | |
| - name: Assemble Netlify deploy directory | |
| run: | | |
| branch="${{ needs.stage-and-check.outputs.branch_name }}" | |
| mkdir -p netlify-deploy | |
| # Copy each manual directory into the deploy directory | |
| for manual in admin_manual developer_manual user_manual; do | |
| if [ -d "stage/${branch}/${manual}" ]; then | |
| cp -r "stage/${branch}/${manual}" "netlify-deploy/${manual}" | |
| fi | |
| done | |
| # PDF files (kept at root of deploy dir) | |
| find "stage/${branch}" -maxdepth 1 -name "*.pdf" -exec cp {} netlify-deploy/ \; | |
| # Minimal root index linking to the deployed content | |
| cat > netlify-deploy/index.html <<'EOF' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nextcloud Documentation Preview</title> | |
| <style>body{font-family:sans-serif;max-width:600px;margin:2rem auto}ul{line-height:2}</style> | |
| </head> | |
| <body> | |
| <h1>Nextcloud Documentation Preview</h1> | |
| <ul> | |
| <li><a href="admin_manual/">Administration Manual</a></li> | |
| <li><a href="developer_manual/">Developer Manual</a></li> | |
| <li><a href="user_manual/en/">User Manual (English)</a></li> | |
| <li><a href="Nextcloud_User_Manual.pdf">User Manual PDF</a></li> | |
| <li><a href="Nextcloud_Server_Administration_Manual.pdf">Administration Manual PDF</a></li> | |
| </ul> | |
| </body> | |
| </html> | |
| EOF | |
| - name: List deploy directory tree | |
| run: find netlify-deploy -print | sort | |
| - name: Install Netlify CLI | |
| run: npm install -g netlify-cli | |
| - name: Deploy to Netlify | |
| id: netlify | |
| continue-on-error: true | |
| run: | | |
| set +e | |
| output=$(netlify deploy \ | |
| --dir=netlify-deploy \ | |
| --site="${{ secrets.NETLIFY_SITE_ID }}" \ | |
| --auth="${{ secrets.NETLIFY_AUTH_TOKEN }}" \ | |
| --alias="pr-${{ github.event.pull_request.number }}" \ | |
| --message="Preview for PR #${{ github.event.pull_request.number }}" 2>&1) | |
| exit_code=$? | |
| echo "$output" | |
| # Strip ANSI codes, then extract the URL from "Draft URL: <https://...>" | |
| preview_url=$(printf '%s\n' "$output" \ | |
| | sed 's/\x1b\[[0-9;]*[mGKHFJABCDsuKl]//g' \ | |
| | grep -oP '(?:Draft URL|Website Draft URL|Website URL):\s+<?\Khttps://[^>\s]+' \ | |
| | head -1) | |
| echo "url=${preview_url}" >> "$GITHUB_OUTPUT" | |
| exit $exit_code | |
| - name: Post PR preview comment | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| PREVIEW_URL: ${{ steps.netlify.outputs.url }} | |
| with: | |
| script: | | |
| const previewUrl = process.env.PREVIEW_URL || ''; | |
| const prNumber = context.payload.pull_request.number; | |
| const MAX_LINKS = 20; | |
| const COMMENT_MARKER = '<!-- netlify-preview-comment -->'; | |
| const updatedAt = new Date().toUTCString(); | |
| // Fetch all changed files in the PR (auto-paginated) | |
| const allFiles = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| per_page: 100, | |
| }); | |
| // Keep only RST files that live inside a built manual | |
| const manuals = ['admin_manual/', 'developer_manual/', 'user_manual/']; | |
| const rstFiles = allFiles | |
| .map(f => f.filename) | |
| .filter(f => manuals.some(m => f.startsWith(m)) && f.endsWith('.rst')); | |
| // Build the changed-pages section | |
| let changedSection = ''; | |
| if (previewUrl && rstFiles.length > 0) { | |
| const links = rstFiles.map(f => { | |
| // user_manual HTML is deployed under user_manual/en/; other manuals deploy as-is | |
| const htmlPath = f.startsWith('user_manual/') | |
| ? f.replace(/^user_manual\//, 'user_manual/en/').replace(/\.rst$/, '.html') | |
| : f.replace(/\.rst$/, '.html'); | |
| return `- [${f}](${previewUrl}/${htmlPath})`; | |
| }); | |
| const shown = links.slice(0, MAX_LINKS); | |
| const extra = links.length - shown.length; | |
| const extraLine = extra > 0 | |
| ? `\n_β¦and ${extra} more. [View all changed files](https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}/files)_` | |
| : ''; | |
| changedSection = [ | |
| '', | |
| `<details>`, | |
| `<summary>π ${rstFiles.length} changed documentation ${rstFiles.length === 1 ? 'page' : 'pages'}</summary>`, | |
| '', | |
| shown.join('\n') + extraLine, | |
| '</details>', | |
| ].join('\n'); | |
| } else if (rstFiles.length === 0) { | |
| changedSection = '\n_No RST documentation pages changed in this PR._'; | |
| } | |
| // Compose the full comment body | |
| const previewLine = previewUrl | |
| ? `π **[Open preview β](${previewUrl})**` | |
| : 'β οΈ Preview deployment failed or was skipped.'; | |
| const body = [ | |
| COMMENT_MARKER, | |
| '## π Documentation Preview', | |
| '', | |
| previewLine, | |
| changedSection, | |
| '', | |
| `_Last updated: ${updatedAt}_`, | |
| ].join('\n'); | |
| // Update existing bot comment or create a new one | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find(c => c.body && c.body.includes(COMMENT_MARKER)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| } | |
| summary: | |
| needs: [stage-and-check, link-check, deploy, netlify-preview] | |
| runs-on: ubuntu-latest-low | |
| if: always() | |
| permissions: | |
| contents: read | |
| name: build-deploy-summary | |
| steps: | |
| - name: Summary status | |
| run: | | |
| if ${{ github.event_name == 'pull_request' }} | |
| then | |
| echo "This workflow ran for a pull request. We need stage-and-check and link-check to succeed, deploy must be skipped, and netlify-preview must succeed or fail (non-fatal on forks)" | |
| if ${{ needs.stage-and-check.result != 'success' || needs.link-check.result != 'success' || needs.deploy.result != 'skipped' || (needs.netlify-preview.result != 'success' && needs.netlify-preview.result != 'failure') }}; then exit 1; fi | |
| else | |
| echo "This workflow ran for a push. We need stage-and-check, link-check, and deploy to succeed; netlify-preview must be skipped" | |
| if ${{ needs.stage-and-check.result != 'success' || needs.link-check.result != 'success' || needs.deploy.result != 'success' || needs.netlify-preview.result != 'skipped' }}; then exit 1; fi | |
| fi |