From ebe18e8889d7c3a084ab4a89b6699ec85ead3b84 Mon Sep 17 00:00:00 2001 From: Marvin Gajek Date: Thu, 29 Jan 2026 15:57:42 +0100 Subject: [PATCH] feat(Testing): Add PR preview deployment workflow Add PR preview deployment for fork PRs using pull_request_target event to enable access to repository secrets and write permissions. Use JamesIves/github-pages-deploy-action for cross-repository deployment to gh-pages branch with PR-specific subdirectories. Implement automatic cleanup when PRs close and scheduled cleanup for stale or orphaned previews. Add sticky PR comments with preview URLs that update on each push. Extract build logic to reusable workflow with dynamic baseUrl parameter for flexible deployment paths. Configure Docusaurus baseUrl dynamically via environment variable to support PR preview paths. Preserve PR preview directory during main documentation deployment to prevent accidental deletion of active previews. Modify publish script to backup and restore pr-preview directory across deployments. Closes: #658 --- .github/workflows/_build_docs.yaml | 114 +++++++++++++ .github/workflows/cleanup_stale_previews.yaml | 36 ++++ .github/workflows/deploy_pr_preview.yaml | 159 ++++++++++++++++++ .github/workflows/update_documentation.yml | 101 +---------- publish.sh | 13 ++ website/docusaurus.config.js | 6 +- 6 files changed, 335 insertions(+), 94 deletions(-) create mode 100644 .github/workflows/_build_docs.yaml create mode 100644 .github/workflows/cleanup_stale_previews.yaml create mode 100644 .github/workflows/deploy_pr_preview.yaml diff --git a/.github/workflows/_build_docs.yaml b/.github/workflows/_build_docs.yaml new file mode 100644 index 00000000000..1271484e3cb --- /dev/null +++ b/.github/workflows/_build_docs.yaml @@ -0,0 +1,114 @@ +name: Build Documentation + +on: + workflow_call: + inputs: + base-url: + description: 'Base URL for Docusaurus' + required: false + type: string + default: '/documentation' + pr-ref: + description: 'Git ref to checkout (for PR previews)' + required: false + type: string + default: '' + outputs: + artifact-name: + description: "Name of the uploaded build artifact" + value: ${{ jobs.build.outputs.artifact-name }} + +jobs: + check_markdown_syntax: + name: Check Markdown Syntax + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.pr-ref || github.ref }} + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - run: gem install mdl + - run: ./tools/check-docs.sh + + check_file_names: + name: Check File Names + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.pr-ref || github.ref }} + - run: ./tools/check-file-names.sh + + check_shell_scripts: + name: Check Shell Scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.pr-ref || github.ref }} + - run: sudo apt-get install --yes shellcheck + - run: shellcheck **/*.sh + + check_python_scripts: + name: Check Python Scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.pr-ref || github.ref }} + - run: pip install -r tools/requirements.txt + - run: black --check . + - run: isort --profile black --filter-files --check . + - run: flake8 --config tools/.flake8 + - run: mypy --ignore-missing-imports . + + build: + name: Build + outputs: + artifact-name: documentation-artifacts + needs: [check_markdown_syntax, check_file_names, check_shell_scripts, check_python_scripts] + runs-on: ubuntu-latest + permissions: + packages: read + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.pr-ref || github.ref }} + + - uses: actions/checkout@v6 + with: + repository: rucio/rucio + ref: master + path: tools/run_in_docker/rucio + + - uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Install dependencies and build API docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 -m pip install -U pip setuptools + python3 -m pip install -U -r tools/requirements.txt + docker login https://docker.pkg.github.com -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} + ./tools/build_documentation.sh + docker logout https://docker.pkg.github.com + + - name: Build Docusaurus site + working-directory: website + run: | + yarn install + yarn build + env: + BASE_URL: ${{ inputs.base-url }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: documentation-artifacts + path: website/build + retention-days: 7 diff --git a/.github/workflows/cleanup_stale_previews.yaml b/.github/workflows/cleanup_stale_previews.yaml new file mode 100644 index 00000000000..0786da2858e --- /dev/null +++ b/.github/workflows/cleanup_stale_previews.yaml @@ -0,0 +1,36 @@ +name: Cleanup Stale Previews +on: + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + workflow_dispatch: # Manual trigger + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: gh-pages + token: ${{ secrets.PREVIEW_TOKEN }} + + - name: Remove previews for closed PRs + run: | + # Get all open PR numbers + OPEN_PRS=$(gh pr list --state open --json number --jq '.[].number') + + # Remove preview dirs for closed PRs + for dir in pr-preview/pr-*/; do + PR_NUM=$(echo $dir | grep -oP 'pr-\K[0-9]+') + if ! echo "$OPEN_PRS" | grep -q "^${PR_NUM}$"; then + echo "Removing $dir (PR #$PR_NUM is closed)" + rm -rf "$dir" + fi + done + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "Clean up previews for closed PRs" || echo "Nothing to clean" + git push + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy_pr_preview.yaml b/.github/workflows/deploy_pr_preview.yaml new file mode 100644 index 00000000000..17186d46c97 --- /dev/null +++ b/.github/workflows/deploy_pr_preview.yaml @@ -0,0 +1,159 @@ +name: Deploy PR Preview + +on: + # Uses pull_request_target instead of pull_request to: + # 1. Access repository secrets (PREVIEW_TOKEN) for fork PRs + # 2. Deploy to target repository with write permissions + # Note: pull_request_target runs in base repo context, so we explicitly + # checkout PR head SHA to build the correct code + pull_request_target: + types: [opened, synchronize, reopened, closed] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build: + if: github.event.action != 'closed' + uses: ./.github/workflows/_build_docs.yaml + secrets: inherit + permissions: + packages: read + with: + base-url: ${{ github.event_name == 'pull_request_target' && format('/documentation/pr-preview/pr-{0}', github.event.pull_request.number) || '/documentation' }} + pr-ref: ${{ github.event.pull_request.head.sha }} + + deploy-preview: + name: Deploy Preview + needs: build + if: github.event.action != 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + token: ${{ secrets.PREVIEW_TOKEN }} + persist-credentials: true + + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build.outputs.artifact-name }} + path: website/build + + - uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: website/build + repository-name: ${{ github.repository }} + branch: gh-pages + target-folder: pr-preview/pr-${{ github.event.pull_request.number }} + token: ${{ secrets.PREVIEW_TOKEN }} + clean: false + force: false + + - name: Comment on PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PREVIEW_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const previewUrl = `https://${context.repo.owner}.github.io/${context.repo.repo}/pr-preview/pr-${prNumber}/`; + + const comment = `## ๐Ÿš€ Preview Deployed + + Preview URL: ${previewUrl} + + Built from commit: ${context.payload.pull_request.head.sha.substring(0, 7)}`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Preview Deployed') + ); + + // Update or create comment + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: comment + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: comment + }); + } + + cleanup-preview: + name: Cleanup Preview + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + repository: ${{ github.repository }} + ref: gh-pages + token: ${{ secrets.PREVIEW_TOKEN }} + + - name: Remove preview directory + run: | + rm -rf pr-preview/pr-${{ github.event.pull_request.number }} + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "Remove preview for PR #${{ github.event.pull_request.number }}" || echo "No changes to commit" + git push + + - name: Comment on PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PREVIEW_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + const comment = `## ๐Ÿงน Preview Removed + + Preview has been removed because the PR was closed.`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Preview') + ); + + // Update existing comment + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: comment + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: comment + }); + } diff --git a/.github/workflows/update_documentation.yml b/.github/workflows/update_documentation.yml index 0c5eac5aefb..f5549ecf5e0 100644 --- a/.github/workflows/update_documentation.yml +++ b/.github/workflows/update_documentation.yml @@ -1,110 +1,27 @@ name: Update Documentation + on: push: - pull_request: + branches: [main] schedule: - cron: '0 4 * * 1-5' jobs: - check_markdown_syntax: - name: Check Markdown Syntax - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.4 - - name: Install markdownlint - run: | - gem install mdl - - name: Lint docs - run: | - ./tools/check-docs.sh - check_file_names: - name: Check File Names - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Check file names - run: | - ./tools/check-file-names.sh - check_shell_scripts: - name: Check Shell Scripts - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Install dependencies - run: | - sudo apt-get install --yes shellcheck - - name: Check Shell Scripts - run: | - shellcheck **/*.sh - check_python_scripts: - name: Check Python Scripts - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Install dependencies - run: | - pip install -r tools/requirements.txt - - name: Run black - run: | - black --check . - - name: Run isort - run: | - isort --profile black --filter-files --check . - - name: Run flake8 - run: | - flake8 --config tools/.flake8 - - name: MyPy - run: | - mypy --ignore-missing-imports . build: - name: Build - needs: [check_markdown_syntax, check_file_names, check_shell_scripts, check_python_scripts] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/checkout@v6 - with: - repository: rucio/rucio - ref: master - path: tools/run_in_docker/rucio - - uses: actions/setup-node@v6 - with: - node-version: 24 - - name: Install rucio-api generation dependencies and build markdown sites for the API - run: | - python3 -m pip install -U pip setuptools - python3 -m pip install -U -r tools/requirements.txt - docker login https://docker.pkg.github.com -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - ./tools/build_documentation.sh - docker logout https://docker.pkg.github.com - - name: Install dependencies and static website - run: | - cd website - yarn install - yarn build - - uses: actions/upload-artifact@master - with: - name: documentation-artifacts - path: website/build + uses: ./.github/workflows/_build_docs.yaml + secrets: inherit + deploy: name: Deploy needs: build - if: github.ref == 'refs/heads/main' || github.event_name == 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/download-artifact@master + - uses: actions/download-artifact@v4 with: - name: documentation-artifacts + name: ${{ needs.build.outputs.artifact-name }} path: website/build - - name: Push to Github Pages branch + - name: Push to Github Pages + run: ./publish.sh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./publish.sh diff --git a/publish.sh b/publish.sh index 6133c0ea6dd..a9bd82873d2 100755 --- a/publish.sh +++ b/publish.sh @@ -16,9 +16,22 @@ remote_repo="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSIT rm -rf "${OUTPUT_DIRECTORY}" git clone --branch "${INPUT_BRANCH}" "${remote_repo}" "${OUTPUT_DIRECTORY}" mv "${OUTPUT_DIRECTORY}/.git" output.git + +# Preserve pr-preview directory if it exists +if [ -d "${OUTPUT_DIRECTORY}/pr-preview" ]; then + echo "Preserving PR preview directory..." + mv "${OUTPUT_DIRECTORY}/pr-preview" pr-preview-backup +fi + rm -rf "${OUTPUT_DIRECTORY}" cp -r "${INPUT_DIRECTORY}" "${OUTPUT_DIRECTORY}" +# Restore pr-preview directory +if [ -d "pr-preview-backup" ]; then + echo "Restoring PR preview directory..." + mv pr-preview-backup "${OUTPUT_DIRECTORY}/pr-preview" +fi + mv output.git "${OUTPUT_DIRECTORY}/.git" cd "${OUTPUT_DIRECTORY}" touch .nojekyll diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 1e0d130ae73..b2e0aa70f1f 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -1,14 +1,16 @@ +const baseUrl = process.env.BASE_URL || "/documentation"; + module.exports={ "title": "Rucio Documentation", "url": "https://rucio.github.io", - "baseUrl": "/documentation", + "baseUrl": baseUrl, "organizationName": "rucio", "projectName": "documentation", "scripts": [ "https://buttons.github.io/buttons.js", ], "stylesheets": [ - "/documentation/css/custom.css", + baseUrl + "/css/custom.css", "https://fonts.googleapis.com/css?family=Inter:400,500,700&display=swap", "https://fonts.googleapis.com/css?family=Rubik:400,500,700&display=swap", "https://fonts.googleapis.com/css2?family=Fira+Code&display=swap"