Skip to content

Merge pull request #14560 from nextcloud/docs/admin-occ-encryption-co… #33574

Merge pull request #14560 from nextcloud/docs/admin-occ-encryption-co…

Merge pull request #14560 from nextcloud/docs/admin-occ-encryption-co… #33574

Workflow file for this run

name: "Build documentation"
on:
pull_request:
push:
branches:
- master
- stable*
permissions:
contents: read
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 }}
# ============================================================================
# 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.
# Starts immediately without waiting for any setup job.
# ============================================================================
build-pdf:
name: Building ${{ matrix.manual.name }} PDF
runs-on: ubuntu-latest
# Use the pre-built sphinx-latex image which has all LaTeX packages pre-installed.
# The image is built from .docker/sphinx-latex/Dockerfile by the docker-build.yml workflow
# and published to GHCR. Using latest ensures we always use the current image for this repo.
container: ghcr.io/nextcloud/documentation/sphinx-latex:latest
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"
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/ 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
# 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 }}
summary:
needs: [stage-and-check, link-check, deploy]
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, but deploy will be skipped"
if ${{ needs.stage-and-check.result != 'success' || needs.link-check.result != 'success' || needs.deploy.result != 'skipped' }}; then exit 1; fi
else
echo "This workflow ran for a push. We need all jobs to succeed, including deploy"
if ${{ needs.stage-and-check.result != 'success' || needs.link-check.result != 'success' || needs.deploy.result != 'success' }}; then exit 1; fi
fi