From fa741fbedb8af8d0c1b0f1055e9c423a1b7f99a2 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Thu, 11 Jun 2026 15:08:44 +0200 Subject: [PATCH] feat: docs-publish worfklow --- .github/workflows/docs-publish.yml | 168 +++++++++++++++++ .github/workflows/docs.yml | 281 ++--------------------------- 2 files changed, 185 insertions(+), 264 deletions(-) create mode 100644 .github/workflows/docs-publish.yml diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml new file mode 100644 index 0000000..09593af --- /dev/null +++ b/.github/workflows/docs-publish.yml @@ -0,0 +1,168 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: Publish Documentation + +# Assuming you have one workflow that builds the documentation artifact (e.g., "PR"), +# this workflow can be used to deploy the documentation to GitHub Pages in a versioned manner. +# It also updates a versions.json file to keep track of available documentation versions and +# comments on the PR with a link to the preview if applicable. +# Note: this can be used with any documentation-building workflow, not only docs-as-code. + +# It will use gh-pages branch internally to mix the new documentation with the existing one. +# Use daily.yml to clean up old documentation versions (merged PRs). + +# Usage: +# on: +# workflow_run: +# workflows: ["PR"] # <-- name of the workflow that builds the documentation artifact +# types: +# - completed +# jobs: +# docs-deploy: +# uses: eclipse-score/cicd-workflows/.github/workflows/copyright.yml@ +# permissions: +# pages: write +# id-token: write +# contents: write +# pull-requests: write + +on: + workflow_call: + +concurrency: + group: cicd-pages-deploy + cancel-in-progress: false + +jobs: + docs-deploy: + name: Deploy documentation to GitHub Pages + if: github.event.workflow_run.conclusion == 'success' + permissions: + pages: write # publish to GitHub Pages + id-token: write # to verify the deployment originates from an appropriate source + contents: write # to update versions.json and commit to gh-pages + pull-requests: write # to comment on PRs with preview link + runs-on: ubuntu-latest + steps: + - name: Resolve build metadata + id: metadata + run: | + EVENT_NAME="${{ github.event.workflow_run.event }}" + PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}" + REF_NAME="${{ github.event.workflow_run.head_branch }}" + + if [[ "$EVENT_NAME" == "merge_group" ]]; then + echo "should_deploy=false" >> "$GITHUB_OUTPUT" + else + echo "should_deploy=true" >> "$GITHUB_OUTPUT" + fi + + echo "event_name=$EVENT_NAME" >> "$GITHUB_OUTPUT" + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "ref_name=$REF_NAME" >> "$GITHUB_OUTPUT" + + - name: Determine target folder + if: steps.metadata.outputs.should_deploy == 'true' + id: target + run: | + EVENT_NAME="${{ steps.metadata.outputs.event_name }}" + if [[ "$EVENT_NAME" == "pull_request" ]]; then + echo "folder=pr-${{ steps.metadata.outputs.pr_number }}" >> "$GITHUB_OUTPUT" + else + echo "folder=${{ steps.metadata.outputs.ref_name }}" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout gh-pages + if: steps.metadata.outputs.should_deploy == 'true' + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: gh-pages + path: gh-pages-content + + - name: Clean up old documentation + if: steps.metadata.outputs.should_deploy == 'true' + run: | + rm -rf "gh-pages-content/${{ steps.target.outputs.folder }}" + + - name: Download documentation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: github-pages + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + path: gh-pages-content/${{ steps.target.outputs.folder }} + + - name: Ensure .nojekyll exists + if: steps.metadata.outputs.should_deploy == 'true' + run: touch gh-pages-content/.nojekyll + + - name: Extend versions.json + if: steps.metadata.outputs.should_deploy == 'true' + run: | + TARGET="${{ steps.target.outputs.folder }}" + + # if versions.json does not exist, create it with an empty array as content + if [ ! -f gh-pages-content/versions.json ]; then + echo '[]' > gh-pages-content/versions.json + fi + + # Add new version entry to versions.json if it doesn't already exist + if jq -e --arg v "$TARGET" 'map(select(.version == $v)) | length > 0' gh-pages-content/versions.json > /dev/null; then + echo "Version '$TARGET' already exists in versions.json" + else + REPO_NAME=$(basename "${{ github.repository }}") + OWNER_NAME="${{ github.repository_owner }}" + PAGES_URL="https://${OWNER_NAME}.github.io/${REPO_NAME}" + jq --arg v "$TARGET" --arg u "${PAGES_URL}/${TARGET}/" \ + '. + [{"version": $v, "url": $u}]' gh-pages-content/versions.json > gh-pages-content/tmp.json && mv gh-pages-content/tmp.json gh-pages-content/versions.json + fi + + - name: Commit and push changes to gh-pages + if: steps.metadata.outputs.should_deploy == 'true' + run: | + cd gh-pages-content + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "Update documentation for ${{ steps.target.outputs.folder }}" || echo "No changes to commit" + git push + + - name: Create artifact from gh-pages + if: steps.metadata.outputs.should_deploy == 'true' + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: gh-pages-content + + - name: Deploy artifact to GitHub Pages + if: steps.metadata.outputs.should_deploy == 'true' + id: deploy-pages + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + + - name: Find existing PR comment + if: steps.metadata.outputs.should_deploy == 'true' && steps.metadata.outputs.event_name == 'pull_request' && steps.metadata.outputs.pr_number != '' + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 + id: fc + with: + issue-number: ${{ steps.metadata.outputs.pr_number }} + comment-author: 'github-actions[bot]' + body-includes: Documentation preview for this pull request + + - name: Comment on PR with preview link + if: steps.metadata.outputs.should_deploy == 'true' && steps.metadata.outputs.event_name == 'pull_request' && steps.metadata.outputs.pr_number != '' && steps.fc.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 + with: + issue-number: ${{ steps.metadata.outputs.pr_number }} + body: | + Documentation preview for this pull request is available at: + **${{ steps.target.outputs.folder }}**: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ steps.target.outputs.folder }}/ diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 493f372..b05140e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -62,9 +62,17 @@ jobs: name: Build Documentation runs-on: ${{ vars.runner_labels_ghub_standard_x64 && fromJSON(vars.runner_labels_ghub_standard_x64) || vars.REPO_RUNNER_LABELS && fromJSON(vars.REPO_RUNNER_LABELS) || 'ubuntu-latest' }} permissions: - pull-requests: write contents: read steps: + - name: Refuse to run on pull_request_target + if: github.event_name == 'pull_request_target' + run: | + echo "This workflow must not be called from pull_request_target." + echo "That event has write access to repo secrets while checking out fork code," + echo "making it unsafe to run bazel on untrusted input." + echo "Use pull_request or schedule instead." + exit 1 + - uses: eclipse-score/cicd-actions/inter-repo-access@51883d9cd772bee5b7d139a2e6ffd8aeabca4224 # main branch as of 2026-04-23 # Hint: if condition cannot access secrets, so we pass them via env env: @@ -76,280 +84,25 @@ jobs: github-app-client-id: ${{ inputs.gh_app_client_id }} github-app-private-key: ${{ secrets.gh_app_private_key }} - - uses: eclipse-score/more-disk-space@6a3b48901846bf7f8cc985925157d71a8973e61f # v1 - with: - level: 3 - - - name: Checkout repository (Handle all events) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.head_ref || github.event.pull_request.head.ref || github.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - # Reuse the token from inter-repo-access action to ensure access to private repos - persist-credentials: false + - uses: eclipse-score/more-disk-space@3fac5780033e636b114cf69e891dc19d640855c2 # v1.1.0 - - name: Checkout action - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - # current repo name - repository: eclipse-score/cicd-workflows - ref: ${{ inputs.workflow-version }} - path: ./cicd-workflows # Reuse the token from inter-repo-access action to ensure access to private repos persist-credentials: false - - name: Compute cache paths (Linux) - id: paths - shell: bash - run: | - BASE="/home/runner/.cache" - echo "bazelisk_home=${BASE}/bazelisk" >> "$GITHUB_OUTPUT" - echo "output_base=${BASE}/bazel/output_base" >> "$GITHUB_OUTPUT" - echo "repo_contents=${BASE}/bazel/repo_contents" >> "$GITHUB_OUTPUT" - echo "repo_cache=${BASE}/bazel/repo" >> "$GITHUB_OUTPUT" - echo "disk_cache=${BASE}/bazel/disk" >> "$GITHUB_OUTPUT" - echo "pip_cache=${BASE}/pip" >> "$GITHUB_OUTPUT" - - - name: Setup Bazel - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 - with: - disk-cache: false - repository-cache: false - bazelisk-cache: true - cache-save: ${{ github.event_name == 'push' }} - - - name: Prepare cache dirs - shell: bash - run: | - mkdir -p \ - "${{ steps.paths.outputs.bazelisk_home }}" \ - "${{ steps.paths.outputs.output_base }}" \ - "${{ steps.paths.outputs.repo_contents }}" \ - "${{ steps.paths.outputs.repo_cache }}" \ - "${{ steps.paths.outputs.disk_cache }}" \ - "${{ steps.paths.outputs.pip_cache }}" - - - name: Restore content cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: | - ${{ steps.paths.outputs.bazelisk_home }} - ${{ steps.paths.outputs.output_base }} - ${{ steps.paths.outputs.repo_contents }} - ${{ steps.paths.outputs.repo_cache }} - ${{ steps.paths.outputs.disk_cache }} - ${{ steps.paths.outputs.pip_cache }} - key: ${{ env.CACHE_KEY_PREFIX }}-bazel-${{ runner.os }}-${{ hashFiles('MODULE.bazel', 'MODULE.bazel.lock', 'WORKSPACE*', '.bazelrc*') }} - restore-keys: | - ${{ env.CACHE_KEY_PREFIX }}-bazel-${{ runner.os }}- - - - name: List cache contents after restore - bazelisk_home - shell: bash - if: runner.debug - run: tree -nf -L 3 "${{ steps.paths.outputs.bazelisk_home }}" || true - - - name: List cache contents after restore - output_base - shell: bash - if: runner.debug - run: tree -nf -L 3 "${{ steps.paths.outputs.output_base }}" || true - - - name: List cache contents after restore - repo_contents - shell: bash - if: runner.debug - run: tree -nf -L 3 "${{ steps.paths.outputs.repo_contents }}" || true - - - name: List cache contents after restore - repo_cache - shell: bash - if: runner.debug - run: tree -nf -L 3 "${{ steps.paths.outputs.repo_cache }}" || true - - - name: List cache contents after restore - disk_cache - shell: bash - if: runner.debug - run: tree -nf -L 3 "${{ steps.paths.outputs.disk_cache }}" || true - - - name: List cache contents after restore - pip_cache - shell: bash - if: runner.debug - run: tree -nf -L 3 "${{ steps.paths.outputs.pip_cache }}" || true - - - name: Create cache snapshot (tgz) - if: runner.debug - continue-on-error: true - shell: bash - run: | - set -uxo pipefail - - ARCHIVE_PATH="$RUNNER_TEMP/cache-snapshot.tgz" - entries=() - for p in \ - "${{ steps.paths.outputs.bazelisk_home }}" \ - "${{ steps.paths.outputs.output_base }}" \ - "${{ steps.paths.outputs.repo_contents }}" \ - "${{ steps.paths.outputs.repo_cache }}" \ - "${{ steps.paths.outputs.disk_cache }}" \ - "${{ steps.paths.outputs.pip_cache }}"; do - if [ -e "$p" ]; then - entries+=("${p#/}") - else - echo "Path does not exist (skipping): $p" - fi - done - - if [ "${#entries[@]}" -eq 0 ]; then - echo "No cache paths found to archive; skipping snapshot creation." - exit 0 - fi - - if ! tar -C / -czf "$ARCHIVE_PATH" "${entries[@]}"; then - echo "Cache snapshot creation failed; removing incomplete archive." - rm -f "$ARCHIVE_PATH" - exit 1 - fi - - - name: Upload cache snapshot artifact - if: runner.debug - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cache-snapshot-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/cache-snapshot.tgz - retention-days: 5 - if-no-files-found: ignore - - - name: Install Graphviz - uses: eclipse-score/apt-install@bd30e2e74a4850389719cb8c3e312bb26aada4e0 # v1.0.0 + - uses: eclipse-score/cicd-actions/setup-bazel-cache@659dcbed63f6b7fbde88c7850125ea0a0a92f939 # setup-bazel-cache/v0.0.0 with: - packages: graphviz - cache: false - - - name: Download test report - if: ${{ inputs.tests-report-artifact != '' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ inputs.tests-report-artifact }} - path: tests-report - - - name: List downloaded test report contents - if: ${{ inputs.tests-report-artifact != '' }} - run: | - echo "Contents of downloaded test report artifact:" - find tests-report -type f -exec ls -la {} \; - echo "" - echo "Directory structure:" - find tests-report -type d | sort + unique-cache-name: ${{ github.workflow }}-${{ github.job }} - name: Build documentation - run: | - bazel --output_base="${{ steps.paths.outputs.output_base }}" run \ - --repo_contents_cache="${{ steps.paths.outputs.repo_contents }}" \ - --repository_cache="${{ steps.paths.outputs.repo_cache }}" \ - --disk_cache="${{ steps.paths.outputs.disk_cache }}" \ - ${{ inputs.bazel-target }} - - tar -cf github-pages.tar _build + run: bazel run //:docs - name: Upload documentation artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: github-pages-${{ github.event.pull_request.head.sha || github.sha }} - path: github-pages.tar + name: github-pages + path: _build retention-days: ${{ inputs.retention-days }} if-no-files-found: error - - - name: Save content cache (branch) - if: success() && github.event_name == 'push' - continue-on-error: true - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: | - ${{ steps.paths.outputs.bazelisk_home }} - ${{ steps.paths.outputs.output_base }} - ${{ steps.paths.outputs.repo_contents }} - ${{ steps.paths.outputs.repo_cache }} - ${{ steps.paths.outputs.disk_cache }} - ${{ steps.paths.outputs.pip_cache }} - key: ${{ env.CACHE_KEY_PREFIX }}-bazel-${{ runner.os }}-${{ hashFiles('MODULE.bazel', 'MODULE.bazel.lock', 'WORKSPACE*', '.bazelrc*') }} - - docs-deploy: - name: Deploy Documentation to GitHub Pages - runs-on: ${{ vars.runner_labels_ghub_standard_x64 && fromJSON(vars.runner_labels_ghub_standard_x64) || vars.REPO_RUNNER_LABELS && fromJSON(vars.REPO_RUNNER_LABELS) || 'ubuntu-latest' }} - needs: docs-build - concurrency: - group: pages-deploy-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: false - permissions: - pages: write - id-token: write - contents: write - pull-requests: write - steps: - - name: Ensure gh-pages branch exists with initial files - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - run: | - set -e - if ! git ls-remote --exit-code --heads "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" gh-pages; then - echo "gh-pages branch does not exist. Creating it..." - git clone --depth=1 "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" repo - cd repo - git fetch origin main --depth=1 - AUTHOR_NAME=$(git log origin/main -1 --pretty=format:'%an') - AUTHOR_EMAIL=$(git log origin/main -1 --pretty=format:'%ae') - git config user.name "$AUTHOR_NAME" - git config user.email "$AUTHOR_EMAIL" - echo "Using commit identity: $AUTHOR_NAME <$AUTHOR_EMAIL>" - - git checkout --orphan gh-pages - git rm -rf . || true - REPO_NAME=$(basename "${REPO}") - OWNER_NAME="${REPO%%/*}" - - touch versions.json - echo "[" > versions.json - echo " {" >> versions.json - echo " \"version\": \"main\"," >> versions.json - echo " \"url\": \"https://${OWNER_NAME}.github.io/${REPO_NAME}/main/\"" >> versions.json - echo " }" >> versions.json - echo "]" >> versions.json - - touch index.html - echo '' > index.html - echo '' >> index.html - echo '' >> index.html - echo ' ' >> index.html - echo ' ' >> index.html - echo ' Redirecting...' >> index.html - echo '' >> index.html - echo '' >> index.html - echo '

If you are not redirected, click here.

' >> index.html - echo '' >> index.html - echo '' >> index.html - - touch .nojekyll - git add versions.json index.html .nojekyll - git commit -m "Initialize gh-pages branch with versions.json and index.html" - git push origin gh-pages - cd .. - rm -rf repo - else - echo "gh-pages branch exists. Skipping creation." - fi - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Download documentation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: github-pages-${{ github.event.pull_request.head.sha || github.sha }} - - - name: Untar documentation artifact - run: mkdir -p extracted_docs && tar -xf github-pages.tar -C extracted_docs - - - name: Deploy 🚀 - id: pages-deployment - uses: eclipse-score/cicd-workflows/.github/actions/deploy-versioned-pages@main - with: - source_folder: extracted_docs/_build - deployment_type: ${{ inputs.deployment_type }}