diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 00000000000..939ae096a7f --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,482 @@ +# Reusable workflow: build + stage the documentation for a single branch. +# +# Called by: +# - sphinxbuild.yml (PR / push CI — builds the current ref) +# - deploy-docs.yml (scheduled deploy — builds every version via a matrix) +# +# It builds HTML / ePub / PDF for all manuals, stages them into a per-version +# folder and uploads a single `staged-docs-` artifact. It does NOT +# touch gh-pages — deployment lives in deploy-docs.yml. +name: "Build documentation (reusable)" + +on: + workflow_call: + inputs: + branch: + description: "Logical branch used for version detection and deploy folder mapping (e.g. master, stable34)." + required: true + type: string + ref: + description: "Git ref to check out and build. Empty = default checkout (the caller's event ref, e.g. a PR merge)." + required: false + type: string + default: "" + outputs: + branch_name: + description: "Primary deploy folder name (latest / stable / )." + value: ${{ jobs.stage-and-check.outputs.branch_name }} + additional_deployment: + description: "Extra deploy folder for the highest stable (), else empty." + value: ${{ jobs.stage-and-check.outputs.additional_deployment }} + artifact_name: + description: "Name of the uploaded staged-docs artifact." + value: ${{ jobs.stage-and-check.outputs.artifact_name }} + +permissions: + contents: read + packages: write + +jobs: + # ============================================================================ + # BUILD HTML + # ============================================================================ + 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + + - name: Get stable branches + id: stable_branches + run: | + branches=$(git ls-remote --heads origin "heads/stable[0-9][0-9]" \ + | awk '{gsub(/^refs\/heads\/stable/, "", $2); print $2}' \ + | sort -n -r | tr '\n' ' ') + echo "branches=$branches" >> $GITHUB_OUTPUT + + - name: Setup PHP for version detection + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 + + - name: Detect and export version constants + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Detect current highest released stable and lowest supported stable via + # GitHub API (same logic as build/build-index.php and generate-top-index.yml). + # Exports DOCS_VERSION_STABLE and DOCS_VERSION_START so conf.py always has + # accurate values without manual updates to any branch. + eval $(php build/detect-versions.php ${{ steps.stable_branches.outputs.branches }}) + echo "DOCS_VERSION_STABLE=$highest_stable" >> $GITHUB_ENV + echo "DOCS_VERSION_START=$lowest_stable" >> $GITHUB_ENV + + # Compute display version: branch number for stableNN builds, dev version for master. + branch="${{ inputs.branch }}" + if [[ "$branch" =~ ^stable([0-9]+)$ ]]; then + echo "DOCS_DISPLAY_VERSION=${BASH_REMATCH[1]}" >> $GITHUB_ENV + else + echo "DOCS_DISPLAY_VERSION=$((highest_stable + 1))" >> $GITHUB_ENV + fi + + - name: Validate conf.py fallbacks are current + if: inputs.branch == 'master' + run: | + conf_stable=$(grep -m1 "os.environ.get('DOCS_VERSION_STABLE'" conf.py | grep -o '[0-9]\+') + conf_start=$(grep -m1 "os.environ.get('DOCS_VERSION_START'" conf.py | grep -o '[0-9]\+') + err=0 + if [ "$DOCS_VERSION_STABLE" != "$conf_stable" ]; then + echo "::error::conf.py fallback version_stable ($conf_stable) != current highest stable ($DOCS_VERSION_STABLE). Update the fallback in conf.py." + err=1 + fi + if [ "$DOCS_VERSION_START" != "$conf_start" ]; then + echo "::error::conf.py fallback version_start ($conf_start) != current lowest stable ($DOCS_VERSION_START). Update the fallback in conf.py." + err=1 + fi + exit $err + + - 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: + # Namespaced by branch so several versions can build in one run (the + # deploy matrix) without artifact-name collisions. Prefix is stripped + # again in the stage-and-check "organize" step. + name: ${{ inputs.branch }}__${{ matrix.manual.name }} + path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_path }} + + # ============================================================================ + # BUILD EPUB + # ============================================================================ + build-epub: + name: Building ${{ matrix.manual.name }} ePub + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + manual: + - name: "user_manual" + directory: "user_manual" + build_epub_path: "_build/epub/Nextcloud_User_Manual.epub" + + - name: "admin_manual" + directory: "admin_manual" + build_epub_path: "_build/epub/Nextcloud_Server_Administration_Manual.epub" + + - name: "developer_manual" + directory: "developer_manual" + build_epub_path: "_build/epub/Nextcloud_Developer_Manual.epub" + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + + - 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 epub documentation + run: | + set -e + cd ${{ matrix.manual.directory }} + make epub + ls -la ${{ matrix.manual.build_epub_path }} + + - name: Upload ePub documentation + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.branch }}__${{ matrix.manual.name }}-epub + path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_epub_path }} + + # ============================================================================ + # PREPARE PDF 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + 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@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Login to GitHub Container Registry + if: steps.changed.outputs.dockerfile_changed == 'true' + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.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 + # ============================================================================ + build-pdf: + name: Building ${{ matrix.manual.name }} PDF + runs-on: ubuntu-latest + needs: prepare-pdf-image + 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + + - 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 + shell: bash + run: | + branch="${{ inputs.branch }}" + if [[ "$branch" =~ ^stable([0-9]+)$ ]]; then + echo "release=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT + else + # master: derive the dev version from conf.py. + version_stable=$(grep -m1 '^\s*version_stable\s*=' conf.py | grep -o '[0-9]\+') + echo "release=$((version_stable + 1))" >> $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: ${{ inputs.branch }}__${{ matrix.manual.name }}-pdf + path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_pdf_path }} + + # ============================================================================ + # STAGE AND VALIDATE + # ============================================================================ + # Organizes build artifacts into a per-version folder and uploads a single + # staged-docs- artifact for the deploy job. Does NOT modify gh-pages. + # ============================================================================ + stage-and-check: + name: Stage and check documentation + needs: [build-html, build-pdf, build-epub] + runs-on: ubuntu-latest + + outputs: + branch_name: ${{ steps.branch.outputs.branch_name }} + additional_deployment: ${{ steps.branch.outputs.additional_deployment }} + artifact_name: ${{ steps.branch.outputs.artifact_name }} + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + + - name: Download this branch's build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + # Only this version's artifacts — other matrix legs use a different prefix. + pattern: ${{ inputs.branch }}__* + path: artifacts/ + + # ======================================================================== + # DETERMINE DEPLOYMENT TARGETS (branch_name and version_name) + # ======================================================================== + # master → branch_name=latest (no version_name) + # stable where N is highest → branch_name=stable, additional= + # stable where N is not highest → branch_name= + # Any other branch → branch_name= + # ======================================================================== + - name: Determine deployment targets (branch_name and version_name) + id: branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Logical branch to deploy is passed in explicitly by the caller. + current_branch="${{ inputs.branch }}" + + # Find the highest numbered stable branch that has a v{N}.0.0 release. + # Mirrors the is_version_released() check in build/build-index.php: + # a branch whose first release doesn't exist yet (e.g. RC-only) is skipped. + highest_stable="" + for n in $(git ls-remote --heads origin | sed -n 's?.*refs/heads/stable\([0-9]\{2\}\)$?\1?p' | sort -n -r); do + # Nextcloud moved to nextcloud-releases/server starting with v32 + if [ "$n" -ge 32 ]; then + repo="nextcloud-releases/server" + else + repo="nextcloud/server" + fi + http_status=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${repo}/releases/tags/v${n}.0.0") + if [ "$http_status" = "200" ]; then + highest_stable="$n" + echo "stable${n}: v${n}.0.0 released — using as highest stable" + break + else + echo "stable${n}: v${n}.0.0 not released yet (HTTP ${http_status}) — skipping" + fi + done + + if [ -z "$highest_stable" ]; then + echo "ERROR: No released stable branch found" + exit 1 + fi + highest_stable_branch="stable${highest_stable}" + + echo "Current branch: $current_branch" + echo "Highest released stable branch: $highest_stable_branch" + + # Map branch to deployment folder names + case "$current_branch" in + "master") + echo "branch_name=latest" >> $GITHUB_OUTPUT + ;; + "$highest_stable_branch") + echo "branch_name=stable" >> $GITHUB_OUTPUT + echo "additional_deployment=${highest_stable}" >> $GITHUB_OUTPUT + ;; + *) + branch_for_deploy="${current_branch#stable}" + echo "branch_name=$branch_for_deploy" >> $GITHUB_OUTPUT + ;; + esac + + # Artifact name is unique per version so the deploy job can download + # every version's staged output together (pattern staged-docs-*). + echo "artifact_name=staged-docs-${current_branch}" >> $GITHUB_OUTPUT + + - 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 }}" + echo "Artifact name: ${{ steps.branch.outputs.artifact_name }}" + + # ======================================================================== + # ORGANIZE ARTIFACTS FOR DEPLOYMENT + # ======================================================================== + # Stage into stage//. When this is the highest stable branch, + # ALSO stage into stage// so a single artifact carries both the + # server/stable/ and server// deployments and the deploy job can stay + # version-agnostic (it just applies every folder it finds). + # ======================================================================== + - name: Organize artifacts for deployment + id: organize + run: | + branch="${{ steps.branch.outputs.branch_name }}" + additional="${{ steps.branch.outputs.additional_deployment }}" + + mkdir -p "stage/${branch}" + + # Artifacts are named "__[-epub|-pdf]"; strip that + # prefix so the deployed folders are just user_manual/admin_manual/etc. + prefix="${{ inputs.branch }}__" + + # Copy artifacts preserving their manual folder structure + for artifact in artifacts/*; do + if [ -d "$artifact" ]; then + manual_name="$(basename "$artifact")" + manual_name="${manual_name#$prefix}" + mkdir -p "stage/${branch}/${manual_name}" + cp -r "$artifact/"* "stage/${branch}/${manual_name}/" + fi + done + + # Move PDF and ePub files to the root of the branch folder + echo "Looking for PDF and ePub files to move..." + find "stage/${branch}/" -maxdepth 2 \( -name "*.pdf" -o -name "*.epub" \) -type f + for f in "stage/${branch}"/*/*.pdf "stage/${branch}"/*/*.epub; do + if [ -f "$f" ]; then + echo "Moving: $f" + mv "$f" "stage/${branch}/" + fi + done + + # Duplicate into the additional versioned folder for the highest stable. + if [ -n "${additional}" ]; then + rm -rf "stage/${additional}" + cp -r "stage/${branch}" "stage/${additional}" + fi + + # Clean up empty directories + find stage -type d -empty -delete + + echo "Staged artifacts:" + find stage -type f | head -20 + + - name: Upload staged artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.branch.outputs.artifact_name }} + path: stage/ + retention-days: 1 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000000..e50bafda54f --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,241 @@ +# Scheduled deployment of the documentation to gh-pages. +# +# Replaces the old schedule-builds.yml fan-out (which dispatched one +# sphinxbuild.yml run per branch and therefore opened one deploy PR per +# version). Here every version is built in a single run via a matrix over the +# reusable build-docs.yml workflow, then a single deploy job applies all +# versions to gh-pages and opens ONE pull request. +# +# fail-fast is disabled on the build matrix and the deploy job runs as long as +# the run was not cancelled: a version that fails to build simply is not +# deployed that night, without blocking the others. +name: "Deploy documentation" + +on: + schedule: + - cron: '0 2 * * *' # 02:00 UTC daily + workflow_dispatch: + +permissions: + contents: read + packages: write # the reusable build may push the PDF build image to GHCR + +jobs: + # ============================================================================ + # PREPARE — compute the branch matrix (master + all stable branches) + # ============================================================================ + prepare: + name: Compute branch matrix + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.set.outputs.branches }} + steps: + - name: Collect master and stable branches + id: set + env: + GH_TOKEN: ${{ github.token }} + run: | + stable_branches=$(gh api "repos/${{ github.repository }}/branches" --paginate \ + --jq '.[].name | select(startswith("stable"))' | sort -Vr) + # Build a JSON array: ["master", "stable34", "stable33", ...] + branches_json=$(printf '%s\n' master ${stable_branches} | jq -R . | jq -cs .) + echo "Matrix branches: ${branches_json}" + echo "branches=${branches_json}" >> $GITHUB_OUTPUT + + # ============================================================================ + # BUILD — build + stage every version through the reusable workflow + # ============================================================================ + build: + name: Build ${{ matrix.branch }} + needs: prepare + strategy: + fail-fast: false + matrix: + branch: ${{ fromJson(needs.prepare.outputs.branches) }} + uses: ./.github/workflows/build-docs.yml + with: + branch: ${{ matrix.branch }} + ref: ${{ matrix.branch }} + secrets: inherit + + # ============================================================================ + # DEPLOY — apply every staged version to gh-pages, open a single PR + # ============================================================================ + deploy: + name: Deploy documentation for gh-pages + needs: build + # Deploy whatever built successfully, even if some matrix legs failed. + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: gh-pages + fetch-depth: 1 + persist-credentials: false + + - name: Download all staged version artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: staged-docs-* + merge-multiple: true + path: stage/ + + - name: Fail if nothing was staged + id: staged + run: | + if [ -z "$(ls -A stage/ 2>/dev/null)" ]; then + echo "::warning::No staged artifacts found — all builds failed. Nothing to deploy." + echo "has_stage=false" >> $GITHUB_OUTPUT + else + echo "Staged version folders:" + ls -1 stage/ + echo "has_stage=true" >> $GITHUB_OUTPUT + fi + + # ======================================================================== + # APPLY STAGED ARTIFACTS TO GH-PAGES + # ======================================================================== + # Each staged-docs- artifact contributes one or more top-level + # version folders (latest / stable / ). Apply every folder found. + # ======================================================================== + - name: Apply staged artifacts to gh-pages + id: apply + if: steps.staged.outputs.has_stage == 'true' + run: | + for d in stage/*/; do + [ -d "$d" ] || continue + branch="$(basename "$d")" + echo "Applying version folder: ${branch}" + + 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 + + # Copy PDF and ePub files to the root of the branch folder + for f in "stage/${branch}"/*.pdf "stage/${branch}"/*.epub; do + if [ -f "$f" ]; then + echo "Copying: $f" + cp "$f" "server/${branch}/" + fi + done + done + + # Cleanup empty directories + find . -type d -empty -delete + + # Check for meaningful changes, ignoring: + # - lastupdated date lines in HTML (Sphinx build-time timestamps) + # - epub/pdf binaries (they regenerate automatically alongside HTML) + meaningful_html=$(git diff HEAD -- '*.html' | grep -E '^[+-]' | grep -v '^[+-]{3}' | grep -ivE 'lastupdated|Last updated on' | wc -l) + other_changes=$(git diff --name-only HEAD | grep -cvE '\.(html|epub|pdf)$' || true) + + if [ "$meaningful_html" -gt 0 ] || [ "$other_changes" -gt 0 ]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "::notice::Skipping deploy PR: only lastupdated timestamps or epub/pdf binaries changed" + fi + + - name: Strip noindex from stable docs + if: steps.apply.outputs.has_changes == 'true' + 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{\n?}{}g' + fi + + - name: Write robots.txt + if: steps.apply.outputs.has_changes == 'true' + 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 + if: steps.apply.outputs.has_changes == 'true' + run: rm -rf stage/ + + # ======================================================================== + # ADD REDIRECT FILES + # ======================================================================== + # go.php and the user_manual language redirect are branch-agnostic stubs; + # take them from the default branch and apply to every deployed version. + # ======================================================================== + - name: Add various redirects for go.php and user_manual english version + if: steps.apply.outputs.has_changes == 'true' + run: | + default_branch="${{ github.event.repository.default_branch }}" + git fetch origin "${default_branch}" + git checkout "origin/${default_branch}" -- go.php/index.html user_manual/index.html + + for d in server/*/; do + [ -d "$d" ] || continue + branch="$(basename "$d")" + mkdir -p "server/${branch}/go.php" "server/${branch}/user_manual" + cp go.php/index.html "server/${branch}/go.php/index.html" + cp user_manual/index.html "server/${branch}/user_manual/index.html" + done + + - 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" + committer: nextcloud-command + author: nextcloud-command + signoff: true + branch: "automated/deploy/documentation" + base: gh-pages + title: "Documentation update" + body: | + This PR was automatically generated by the scheduled deploy workflow + and includes the latest documentation for all built versions. + 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 }} + + summary: + needs: [prepare, build, deploy] + runs-on: ubuntu-latest-low + if: always() + + permissions: + contents: read + + name: deploy-summary + + steps: + - name: Summary status + run: | + echo "prepare: ${{ needs.prepare.result }}" + echo "build: ${{ needs.build.result }}" + echo "deploy: ${{ needs.deploy.result }}" + # prepare must succeed and deploy must not fail. Individual build legs + # may fail (fail-fast disabled); those versions are simply not deployed. + if [ "${{ needs.prepare.result }}" != "success" ] || [ "${{ needs.deploy.result }}" = "failure" ]; then + exit 1 + fi diff --git a/.github/workflows/schedule-builds.yml b/.github/workflows/schedule-builds.yml deleted file mode 100644 index 665ed665d4a..00000000000 --- a/.github/workflows/schedule-builds.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: "Schedule documentation builds" - -on: - schedule: - - cron: '0 2 * * *' # 02:00 UTC daily — triggers sphinxbuild on master + all stable* branches - workflow_dispatch: - -permissions: - actions: write - -jobs: - dispatch: - name: Dispatch sphinxbuild on all branches - runs-on: ubuntu-latest - - steps: - - name: Dispatch on master and stable branches - env: - GH_TOKEN: ${{ github.token }} - run: | - stable_branches=$(gh api "repos/${{ github.repository }}/branches" --paginate \ - --jq '.[].name | select(startswith("stable"))' | sort -Vr) - for branch in master ${stable_branches}; do - echo "Dispatching sphinxbuild.yml on ${branch}" - gh workflow run sphinxbuild.yml --repo "${{ github.repository }}" --ref "${branch}" \ - || echo "::warning::Failed to dispatch sphinxbuild.yml for ${branch}" - done diff --git a/.github/workflows/sphinxbuild.yml b/.github/workflows/sphinxbuild.yml index 3190a980163..dfb708795d4 100644 --- a/.github/workflows/sphinxbuild.yml +++ b/.github/workflows/sphinxbuild.yml @@ -6,517 +6,49 @@ on: branches: - master - stable* - workflow_dispatch: - -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Get stable branches - id: stable_branches - run: | - branches=$(git ls-remote --heads origin "heads/stable[0-9][0-9]" \ - | awk '{gsub(/^refs\/heads\/stable/, "", $2); print $2}' \ - | sort -n -r | tr '\n' ' ') - echo "branches=$branches" >> $GITHUB_OUTPUT - - - name: Setup PHP for version detection - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 - - - name: Detect and export version constants - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Detect current highest released stable and lowest supported stable via - # GitHub API (same logic as build/build-index.php and generate-top-index.yml). - # Exports DOCS_VERSION_STABLE and DOCS_VERSION_START so conf.py always has - # accurate values without manual updates to any branch. - eval $(php build/detect-versions.php ${{ steps.stable_branches.outputs.branches }}) - echo "DOCS_VERSION_STABLE=$highest_stable" >> $GITHUB_ENV - echo "DOCS_VERSION_START=$lowest_stable" >> $GITHUB_ENV - - # Compute display version: branch number for stableNN builds, dev version for master. - branch="${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}" - if [[ "$branch" =~ ^stable([0-9]+)$ ]]; then - echo "DOCS_DISPLAY_VERSION=${BASH_REMATCH[1]}" >> $GITHUB_ENV - else - echo "DOCS_DISPLAY_VERSION=$((highest_stable + 1))" >> $GITHUB_ENV - fi - - - name: Validate conf.py fallbacks are current - if: github.ref == 'refs/heads/master' || github.base_ref == 'master' - run: | - conf_stable=$(grep -m1 "os.environ.get('DOCS_VERSION_STABLE'" conf.py | grep -o '[0-9]\+') - conf_start=$(grep -m1 "os.environ.get('DOCS_VERSION_START'" conf.py | grep -o '[0-9]\+') - err=0 - if [ "$DOCS_VERSION_STABLE" != "$conf_stable" ]; then - echo "::error::conf.py fallback version_stable ($conf_stable) != current highest stable ($DOCS_VERSION_STABLE). Update the fallback in conf.py." - err=1 - fi - if [ "$DOCS_VERSION_START" != "$conf_start" ]; then - echo "::error::conf.py fallback version_start ($conf_start) != current lowest stable ($DOCS_VERSION_START). Update the fallback in conf.py." - err=1 - fi - exit $err - - - 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 }} - - # ============================================================================ - # BUILD EPUB - # ============================================================================ - # Builds epub documentation for all three manuals. No LaTeX required — - # runs in the same plain Python environment as build-html. - # ============================================================================ - build-epub: - name: Building ${{ matrix.manual.name }} ePub - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - manual: - - name: "user_manual" - directory: "user_manual" - build_epub_path: "_build/epub/Nextcloud_User_Manual.epub" - - - name: "admin_manual" - directory: "admin_manual" - build_epub_path: "_build/epub/Nextcloud_Server_Administration_Manual.epub" - - - name: "developer_manual" - directory: "developer_manual" - build_epub_path: "_build/epub/Nextcloud_Developer_Manual.epub" - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - 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 epub documentation - run: | - set -e - cd ${{ matrix.manual.directory }} - make epub - ls -la ${{ matrix.manual.build_epub_path }} - - - name: Upload ePub documentation - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ matrix.manual.name }}-epub - path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_epub_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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - 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@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - - name: Login to GitHub Container Registry - if: steps.changed.outputs.dockerfile_changed == 'true' - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - 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 - shell: bash - run: | - # For PRs use the target branch; for pushes use the current branch. - # This handles both stable34 direct pushes and backport/*/stable34 PRs. - branch="${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}" - if [[ "$branch" =~ ^stable([0-9]+)$ ]]; then - echo "release=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT - else - # master: derive the dev version from conf.py. - version_stable=$(grep -m1 '^\s*version_stable\s*=' conf.py | grep -o '[0-9]\+') - echo "release=$((version_stable + 1))" >> $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 }} +permissions: + contents: read + packages: write +jobs: # ============================================================================ - # STAGE AND VALIDATE + # BUILD + STAGE # ============================================================================ - # 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. + # All build logic lives in the reusable build-docs.yml workflow. For a PR we + # build against the target (base) branch mapping; for a push we use the pushed + # branch. The checkout ref is left empty so the default event ref (PR merge or + # pushed commit) is built. # ============================================================================ - stage-and-check: - name: Stage and check documentation - needs: [build-html, build-pdf, build-epub] - runs-on: ubuntu-latest - - outputs: - # branch_name: The primary deployment folder name for this branch - # - master → "latest" - # - stable (if highest) → "stable" - # - stable (if not highest) → "" (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 → "" (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// (via additional_deployment) - additional_deployment: ${{ steps.branch.outputs.additional_deployment }} - - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - 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 where N is highest → branch_name=stable, version_name= - # (deployed to both server/stable/ and server//) - # - # stable where N is not highest → branch_name= (no version_name) - # (deployed only to server//) - # - # Any other branch → branch_name= (no version_name) - # ======================================================================== - - name: Determine deployment targets (branch_name and version_name) - id: branch - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - 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 that has a v{N}.0.0 release. - # Mirrors the is_version_released() check in build/build-index.php: - # a branch whose first release doesn't exist yet (e.g. RC-only) is skipped. - highest_stable="" - for n in $(git ls-remote --heads origin | sed -n 's?.*refs/heads/stable\([0-9]\{2\}\)$?\1?p' | sort -n -r); do - # Nextcloud moved to nextcloud-releases/server starting with v32 - if [ "$n" -ge 32 ]; then - repo="nextcloud-releases/server" - else - repo="nextcloud/server" - fi - http_status=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://api.github.com/repos/${repo}/releases/tags/v${n}.0.0") - if [ "$http_status" = "200" ]; then - highest_stable="$n" - echo "stable${n}: v${n}.0.0 released — using as highest stable" - break - else - echo "stable${n}: v${n}.0.0 not released yet (HTTP ${http_status}) — skipping" - fi - done - - if [ -z "$highest_stable" ]; then - echo "ERROR: No released stable branch found" - exit 1 - fi - highest_stable_branch="stable${highest_stable}" - - echo "Current branch: $current_branch" - echo "Highest released stable branch: $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 "" - 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 where N is not highest: strip "stable" prefix to get just "" - 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 and ePub files to the root of the branch folder for cleaner structure - echo "Looking for PDF and ePub files to move..." - find "stage/${branch}/" -maxdepth 2 \( -name "*.pdf" -o -name "*.epub" \) -type f - for f in "stage/${branch}"/*/*.pdf "stage/${branch}"/*/*.epub; do - if [ -f "$f" ]; then - echo "Moving: $f" - mv "$f" "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 + build: + name: Build and stage + uses: ./.github/workflows/build-docs.yml + with: + branch: ${{ github.base_ref || github.ref_name }} + secrets: inherit # ============================================================================ # 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 + needs: build runs-on: ubuntu-latest steps: - name: Download staged artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: staged-docs + name: ${{ needs.build.outputs.artifact_name }} 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 + find "stage/${{ needs.build.outputs.branch_name }}" -name '*.html' -print0 | while IFS= read -r -d '' f; do perl -0pi -e 's{^\s*\n}{}m' "$f" done ls -la stage/* @@ -535,186 +67,19 @@ jobs: 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/latest/ file://$(pwd)/stage/${{ needs.build.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 == 'schedule' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout gh-pages branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - 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// to server// - # - If version_name is set, ALSO copy to server// - # - 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 - - # Copy PDF and ePub files to the root of the branch folder - echo "Looking for PDF and ePub files to copy..." - find stage/${branch}/ -maxdepth 1 \( -name "*.pdf" -o -name "*.epub" \) -type f - for f in stage/${branch}/*.pdf stage/${branch}/*.epub; do - if [ -f "$f" ]; then - echo "Copying: $f" - cp "$f" 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 for meaningful changes, ignoring: - # - lastupdated date lines in HTML (Sphinx build-time timestamps) - # - epub/pdf binaries (they regenerate automatically alongside HTML) - meaningful_html=$(git diff HEAD -- '*.html' | grep -E '^[+-]' | grep -v '^[+-]{3}' | grep -ivE 'lastupdated|Last updated on' | wc -l) - other_changes=$(git diff --name-only HEAD | grep -cvE '\.(html|epub|pdf)$' || true) - - if [ "$meaningful_html" -gt 0 ] || [ "$other_changes" -gt 0 ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - else - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "::notice::Skipping deploy PR: only lastupdated timestamps or epub/pdf binaries changed" - 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{\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 - author: nextcloud-command - 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 }} + 'stage/${{ needs.build.outputs.branch_name }}/user_manual/en/**/*.html' + 'stage/${{ needs.build.outputs.branch_name }}/admin_manual/**/*.html' + 'stage/${{ needs.build.outputs.branch_name }}/developer_manual/**/*.html' # ============================================================================ # 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 @@ -725,7 +90,7 @@ jobs: # ============================================================================ netlify-preview: name: Deploy Netlify preview - needs: stage-and-check + needs: build if: github.event_name == 'pull_request' runs-on: ubuntu-latest @@ -737,12 +102,12 @@ jobs: - name: Download staged artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: staged-docs + name: ${{ needs.build.outputs.artifact_name }} path: stage/ - name: Assemble Netlify deploy directory run: | - branch="${{ needs.stage-and-check.outputs.branch_name }}" + branch="${{ needs.build.outputs.branch_name }}" mkdir -p netlify-deploy # Copy each manual directory into the deploy directory @@ -898,7 +263,7 @@ jobs: } summary: - needs: [stage-and-check, link-check, deploy, netlify-preview] + needs: [build, link-check, netlify-preview] runs-on: ubuntu-latest-low if: always() @@ -912,13 +277,9 @@ jobs: 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 - elif ${{ github.event_name == 'push' }} - then - echo "This workflow ran for a push. We need stage-and-check and link-check to succeed; deploy and netlify-preview must be skipped" - if ${{ needs.stage-and-check.result != 'success' || needs.link-check.result != 'success' || needs.deploy.result != 'skipped' || needs.netlify-preview.result != 'skipped' }}; then exit 1; fi + echo "Pull request: build and link-check must succeed; netlify-preview must succeed or fail (non-fatal on forks)." + if ${{ needs.build.result != 'success' || needs.link-check.result != 'success' || (needs.netlify-preview.result != 'success' && needs.netlify-preview.result != 'failure') }}; then exit 1; fi else - echo "This workflow ran on schedule or workflow_dispatch. 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 + echo "Push: build and link-check must succeed; netlify-preview must be skipped." + if ${{ needs.build.result != 'success' || needs.link-check.result != 'success' || needs.netlify-preview.result != 'skipped' }}; then exit 1; fi fi