Deploy Documentation #925
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: Deploy Documentation | |
| on: | |
| push: | |
| branches: [main] | |
| schedule: | |
| # Once daily at 02:17 UTC. Was hourly (24 × ~76 min ≈ 1,800 min/day). | |
| # push-to-main covers same-day doc edits; scheduled run picks up addon changes. | |
| - cron: '17 2 * * *' | |
| workflow_dispatch: | |
| concurrency: | |
| group: deploy-docs | |
| cancel-in-progress: false | |
| jobs: | |
| # ── Change detection ──────────────────────────────────────────────────────── | |
| # Checks the docs repo and all addon repos via the GitHub API before cloning | |
| # anything. If nothing has changed since the last successful deploy the build | |
| # job is skipped entirely. push/workflow_dispatch events always build. | |
| check-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| changed: ${{ steps.check.outputs.changed }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Detect changes since last successful deploy | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }} | |
| run: | | |
| # Default: build. Overridden to 'false' only when all checks pass clean. | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| set -euo pipefail | |
| # ── Last successful deploy timestamp ─────────────────────────────── | |
| # status=success excludes the current (in_progress) run, so [0] is | |
| # the most recently completed successful deploy. | |
| LAST_RUN=$(gh api \ | |
| "repos/${{ github.repository }}/actions/workflows/deploy-docs.yml/runs?status=success&per_page=2" \ | |
| --jq '.workflow_runs[0].created_at // empty' 2>/dev/null || true) | |
| if [ -z "$LAST_RUN" ]; then | |
| echo "No previous successful run found — building unconditionally." | |
| exit 0 | |
| fi | |
| echo "Last successful deploy: $LAST_RUN" | |
| # ── Docs repo ────────────────────────────────────────────────────── | |
| DOCS_COMMITS=$(git log --since="$LAST_RUN" --oneline | wc -l | xargs) | |
| echo "Docs repo commits since last deploy: $DOCS_COMMITS" | |
| if [ "$DOCS_COMMITS" -gt 0 ]; then | |
| echo "Docs repo changed — building." | |
| exit 0 | |
| fi | |
| # ── Addon repos (API check — no cloning) ────────────────────────── | |
| ADDON_REPOS=( | |
| ultimate-multisite | |
| ultimate-multisite-woocommerce | |
| multisite-ultimate-site-exporter | |
| multisite-ultimate-affiliatewp | |
| multisite-ultimate-ai-site-builder | |
| ultimate-multisite-analytics | |
| multisite-ultimate-captcha | |
| ultimate-multisite-chuck-norris-facts | |
| ultimate-multisite-content-sync | |
| multisite-ultimate-domain-seller | |
| ultimate-multisite-fluent-forms | |
| multisite-ultimate-gocardless | |
| ultimate-multisite-gravity-forms | |
| multisite-ultimate-language-selector | |
| ultimate-multisite-loco-translate | |
| multisite-ultimate-mailchimp | |
| ultimate-multisite-mailster | |
| multisite-ultimate-metered-plans | |
| ultimate-multisite-multinetwork | |
| ultimate-multisite-multi-tenancy | |
| multisite-ultimate-payfast | |
| multisite-ultimate-plugin-and-theme-manager | |
| multisite-ultimate-support-agents | |
| multisite-ultimate-support-tickets | |
| multisite-ultimate-vat | |
| multisite-ultimate-admin-page-creator | |
| ultimate-multisite-addon-template | |
| ultimate-multisite-emails | |
| multisite-ultimate-metrics-and-onboarding | |
| tutor-multisite-compatibilty | |
| material-wp | |
| ) | |
| for repo in "${ADDON_REPOS[@]}"; do | |
| LATEST=$(gh api "repos/Ultimate-Multisite/$repo/commits?per_page=1" \ | |
| --jq '.[0].commit.committer.date' 2>/dev/null || true) | |
| if [ -z "$LATEST" ]; then | |
| echo " $repo: not accessible — skipping" | |
| continue | |
| fi | |
| # ISO 8601 strings compare correctly as plain strings | |
| if [[ "$LATEST" > "$LAST_RUN" ]]; then | |
| echo " $repo: changed (latest commit: $LATEST) — building." | |
| exit 0 | |
| fi | |
| echo " $repo: unchanged (latest commit: $LATEST)" | |
| done | |
| echo "No changes detected across docs repo or any addon since $LAST_RUN — skipping build." | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| # ── Locale chunk planning ──────────────────────────────────────────────────── | |
| plan-locale-chunks: | |
| needs: check-changes | |
| # Always build on push/manual; skip scheduled runs when nothing changed. | |
| if: needs.check-changes.outputs.changed == 'true' || github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| chunks: ${{ steps.plan.outputs.chunks }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Plan locale chunks | |
| id: plan | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' | |
| import json | |
| import os | |
| import re | |
| from pathlib import Path | |
| chunk_size = 10 | |
| config = Path('docusaurus.config.js').read_text(encoding='utf-8') | |
| match = re.search(r'locales:\s*\[(.*?)\]\s*,\s*localeConfigs', config, re.S) | |
| if not match: | |
| raise SystemExit('Unable to find i18n.locales in docusaurus.config.js') | |
| locales = re.findall(r"'([^']+)'", match.group(1)) | |
| if not locales or locales[0] != 'en': | |
| raise SystemExit('Expected en to be the first Docusaurus locale') | |
| chunks = [{'chunk': '00-en', 'locales': 'en'}] | |
| translated_locales = locales[1:] | |
| for index in range(0, len(translated_locales), chunk_size): | |
| chunk_number = (index // chunk_size) + 1 | |
| chunk_locales = translated_locales[index:index + chunk_size] | |
| chunks.append({ | |
| 'chunk': f'{chunk_number:02d}', | |
| 'locales': ' '.join(chunk_locales), | |
| }) | |
| with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output: | |
| output.write(f'chunks={json.dumps(chunks)}\n') | |
| with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as summary: | |
| summary.write('## Locale build chunks\n\n') | |
| for chunk in chunks: | |
| summary.write(f"- `{chunk['chunk']}`: `{chunk['locales']}`\n") | |
| PY | |
| # ── Generated source preparation ───────────────────────────────────────────── | |
| prepare-docs-source: | |
| needs: check-changes | |
| # Always build on push/manual; skip scheduled runs when nothing changed. | |
| if: needs.check-changes.outputs.changed == 'true' || github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Checkout main plugin | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: Ultimate-Multisite/ultimate-multisite | |
| path: ultimate-multisite | |
| token: ${{ secrets.DOCS_DISPATCH_TOKEN }} | |
| - name: Checkout addons | |
| run: | | |
| mkdir -p addons | |
| # Format: github-repo-name=local-dir-name | |
| declare -A ADDONS=( | |
| [ultimate-multisite-woocommerce]=ultimate-multisite-woocommerce | |
| [multisite-ultimate-site-exporter]=ultimate-multisite-site-exporter | |
| [multisite-ultimate-affiliatewp]=ultimate-multisite-affiliatewp | |
| [multisite-ultimate-ai-site-builder]=ultimate-multisite-ai-site-builder | |
| [ultimate-multisite-analytics]=ultimate-multisite-analytics | |
| [multisite-ultimate-captcha]=ultimate-multisite-captcha | |
| [ultimate-multisite-chuck-norris-facts]=ultimate-multisite-chuck-norris-facts | |
| [ultimate-multisite-content-sync]=ultimate-multisite-content-sync | |
| [multisite-ultimate-domain-seller]=ultimate-multisite-domain-seller | |
| [ultimate-multisite-fluent-forms]=ultimate-multisite-fluent-forms | |
| [multisite-ultimate-gocardless]=ultimate-multisite-gocardless | |
| [ultimate-multisite-gravity-forms]=ultimate-multisite-gravity-forms | |
| [multisite-ultimate-language-selector]=ultimate-multisite-language-selector | |
| [ultimate-multisite-loco-translate]=ultimate-multisite-loco-translate | |
| [multisite-ultimate-mailchimp]=ultimate-multisite-mailchimp | |
| [ultimate-multisite-mailster]=ultimate-multisite-mailster | |
| [multisite-ultimate-metered-plans]=ultimate-multisite-metered-plans | |
| [ultimate-multisite-multinetwork]=ultimate-multisite-multinetwork | |
| [ultimate-multisite-multi-tenancy]=ultimate-multisite-multi-tenancy | |
| [multisite-ultimate-payfast]=ultimate-multisite-payfast | |
| [multisite-ultimate-plugin-and-theme-manager]=ultimate-multisite-plugin-and-theme-manager | |
| [multisite-ultimate-support-agents]=ultimate-multisite-support-agents | |
| [multisite-ultimate-support-tickets]=ultimate-multisite-support-tickets | |
| [multisite-ultimate-vat]=ultimate-multisite-vat | |
| [multisite-ultimate-admin-page-creator]=ultimate-multisite-admin-page-creator | |
| [ultimate-multisite-addon-template]=ultimate-multisite-addon-template | |
| [ultimate-multisite-emails]=ultimate-multisite-emails | |
| [multisite-ultimate-metrics-and-onboarding]=ultimate-multisite-metrics-and-onboarding | |
| [tutor-multisite-compatibilty]=tutor-multisite-compatibility | |
| [material-wp]=material-wp | |
| ) | |
| for repo in "${!ADDONS[@]}"; do | |
| local_dir="${ADDONS[$repo]}" | |
| echo "Cloning $repo -> addons/$local_dir" | |
| git clone --depth 1 "https://x-access-token:${{ secrets.DOCS_DISPATCH_TOKEN }}@github.com/Ultimate-Multisite/$repo.git" "addons/$local_dir" 2>/dev/null || echo " Skipped $repo (not found)" | |
| done | |
| shell: bash | |
| - name: Update addon changelogs | |
| run: python3 scripts/update-changelogs.py | |
| continue-on-error: true | |
| - name: Generate hooks documentation | |
| run: bash scripts/generate-hooks.sh | |
| continue-on-error: true | |
| - name: Upload prepared docs source | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: prepared-docs-source | |
| path: | | |
| docs | |
| i18n | |
| scripts | |
| src | |
| static | |
| docusaurus.config.js | |
| sidebars.js | |
| package.json | |
| package-lock.json | |
| if-no-files-found: error | |
| retention-days: 1 | |
| compression-level: 3 | |
| # ── Parallel locale builds ─────────────────────────────────────────────────── | |
| build-locale-chunk: | |
| name: Build locale chunk ${{ matrix.chunk }} | |
| needs: | |
| - check-changes | |
| - plan-locale-chunks | |
| - prepare-docs-source | |
| # Always build on push/manual; skip scheduled runs when nothing changed. | |
| if: needs.check-changes.outputs.changed == 'true' || github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.plan-locale-chunks.outputs.chunks) }} | |
| steps: | |
| - name: Download prepared docs source | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: prepared-docs-source | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build locale chunk | |
| env: | |
| LOCALES: ${{ matrix.locales }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| locale_args=() | |
| for locale in $LOCALES; do | |
| echo "Building locale: $locale" | |
| locale_args+=(--locale "$locale") | |
| done | |
| rm -rf build .docusaurus | |
| npx docusaurus build "${locale_args[@]}" | |
| - name: Upload locale build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: docs-build-${{ matrix.chunk }} | |
| path: build/ | |
| if-no-files-found: error | |
| retention-days: 1 | |
| compression-level: 3 | |
| # ── Build assembly & deploy ────────────────────────────────────────────────── | |
| assemble-and-deploy: | |
| needs: | |
| - check-changes | |
| - build-locale-chunk | |
| # Always assemble on push/manual; skip scheduled runs when nothing changed. | |
| if: needs.check-changes.outputs.changed == 'true' || github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download locale build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: docs-build-* | |
| path: locale-builds | |
| - name: Assemble documentation site | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p build | |
| for artifact_dir in locale-builds/docs-build-*; do | |
| if [ ! -d "$artifact_dir" ] || [ "$(basename "$artifact_dir")" = "docs-build-00-en" ]; then | |
| continue | |
| fi | |
| rsync -a "$artifact_dir"/ build/ | |
| done | |
| if [ ! -d locale-builds/docs-build-00-en ]; then | |
| echo "Missing default English build artifact" >&2 | |
| exit 1 | |
| fi | |
| rsync -a locale-builds/docs-build-00-en/ build/ | |
| test -f build/index.html | |
| - name: Deploy to server | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 | |
| chmod 600 ~/.ssh/id_ed25519 | |
| ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null | |
| rsync -avz --delete build/ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/srv/www/static-sites/ultimatemultisite.com/docs/ |