From b543465d148872af328170701b8911a949c25204 Mon Sep 17 00:00:00 2001 From: Marvin Gajek Date: Wed, 3 Dec 2025 13:20:08 +0100 Subject: [PATCH] feat(ci): add PR preview deployment workflow - Adds PR preview deployment for fork PRs using pull_request_target - Uses JamesIves/github-pages-deploy-action for cross-repo deployment - Implements automatic cleanup when PRs close - Adds sticky PR comments with preview URLs - Includes scheduled cleanup for stale/orphaned previews - Extracts build logic to reusable workflow with dynamic baseUrl - Configures Docusaurus baseUrl dynamically for PR preview paths - Preserves PR preview directory during main docs deployment Ref #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..21f7a8d251b --- /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 \ No newline at end of file diff --git a/.github/workflows/cleanup_stale_previews.yaml b/.github/workflows/cleanup_stale_previews.yaml new file mode 100644 index 00000000000..24ad25797da --- /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 }} \ No newline at end of file diff --git a/.github/workflows/deploy_pr_preview.yaml b/.github/workflows/deploy_pr_preview.yaml new file mode 100644 index 00000000000..9c35f565b29 --- /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 + }); + } \ No newline at end of file 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 0783a33f115..ba1e2b8bf6a 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"