Preview Bazel docs PRs #4049
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: Preview Bazel docs PRs | |
| on: | |
| # Since PRs to Bazel repo may come from a fork, and those cannot see GHA Secrets, | |
| # we fall back to polling the Bazel repo for new PRs. | |
| schedule: | |
| # Runs every 30 minutes. | |
| - cron: '*/30 * * * *' | |
| # Also allow manual triggering in the GH UI. | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'Specific bazelbuild/bazel PR number to preview (skips polling, useful for testing)' | |
| required: false | |
| type: string | |
| default: '' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| list-prs: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| matrix: ${{ steps.fetch-prs.outputs.matrix }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch recent PRs | |
| id: fetch-prs | |
| env: | |
| GH_TOKEN: ${{ secrets.BAZELBUILD_BAZEL_PAT || github.token }} | |
| PR_NUMBER_OVERRIDE: ${{ inputs.pr_number || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${PR_NUMBER_OVERRIDE}" ]]; then | |
| # Manual dispatch with a specific PR number — fetch just that PR | |
| pr="$(gh api \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "/repos/bazelbuild/bazel/pulls/${PR_NUMBER_OVERRIDE}")" | |
| matrix="$(echo "$pr" | jq -c '[{ | |
| number: .number, | |
| head_sha: .head.sha, | |
| base_sha: .base.sha, | |
| head_ref: .head.ref, | |
| preview_branch: ("pr-" + (.number|tostring)) | |
| }]')" | |
| else | |
| # Scheduled run — poll for PRs updated in the last 45 minutes | |
| since="$(date -u -d '45 minutes ago' '+%Y-%m-%dT%H:%M:%SZ')" | |
| prs="$(gh api \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "/repos/bazelbuild/bazel/pulls?state=open&sort=updated&direction=desc&per_page=100")" | |
| matrix="$(echo "$prs" | jq -c --arg since "$since" ' | |
| [ | |
| .[] | select(.updated_at >= $since) | | |
| { | |
| number: .number, | |
| head_sha: .head.sha, | |
| base_sha: .base.sha, | |
| head_ref: .head.ref, | |
| preview_branch: ("pr-" + (.number|tostring)) | |
| } | |
| ] | |
| ')" | |
| fi | |
| echo "Matrix entries: $(echo "$matrix" | jq -r 'length')" | |
| echo "matrix=$matrix" >> "$GITHUB_OUTPUT" | |
| build-previews: | |
| needs: list-prs | |
| if: ${{ needs.list-prs.outputs.matrix != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.list-prs.outputs.matrix) }} | |
| uses: ./.github/workflows/pull-from-bazel-build.yml | |
| secrets: inherit | |
| with: | |
| bazelCommitHash: ${{ matrix.head_sha }} | |
| bazelBaseCommitHash: ${{ matrix.base_sha }} | |
| bazelPullRequestNumber: ${{ matrix.number }} | |
| detect_upstream_docs_changes: true | |
| is_internal_pr: true | |
| target_branch: ${{ matrix.preview_branch }} | |
| comment: | |
| needs: [list-prs, build-previews] | |
| if: ${{ always() && needs.list-prs.outputs.matrix != '[]' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.list-prs.outputs.matrix || '[]') }} | |
| steps: | |
| - name: Wait for Mintlify deployment | |
| id: mintlify | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PREVIEW_BRANCH: ${{ matrix.preview_branch }} | |
| PR_NUMBER: ${{ matrix.number }} | |
| run: | | |
| set -euo pipefail | |
| # If the preview branch doesn't exist, this PR has no doc changes — skip. | |
| if ! gh api "/repos/${{ github.repository }}/branches/${PREVIEW_BRANCH}" --silent 2>/dev/null; then | |
| echo "No preview branch for PR #${PR_NUMBER} (no doc changes detected). Skipping comment." | |
| echo "conclusion=skipped" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| sha=$(gh api "/repos/${{ github.repository }}/branches/${PREVIEW_BRANCH}" --jq '.commit.sha') | |
| echo "Waiting for Mintlify deployment on ${PREVIEW_BRANCH} @ ${sha}..." | |
| for i in $(seq 1 30); do | |
| check=$(gh api "/repos/${{ github.repository }}/commits/${sha}/check-runs" \ | |
| --jq '[.check_runs[] | select(.name == "Mintlify Deployment")] | .[0] // empty') | |
| if [[ -z "$check" ]]; then | |
| echo "Attempt $i: Mintlify check not yet registered, waiting 20s..." | |
| sleep 20 | |
| continue | |
| fi | |
| status=$(echo "$check" | jq -r '.status') | |
| conclusion=$(echo "$check" | jq -r '.conclusion // ""') | |
| if [[ "$status" == "completed" ]]; then | |
| echo "Mintlify deployment completed: $conclusion" | |
| echo "conclusion=$conclusion" >> "$GITHUB_OUTPUT" | |
| # Extract parse errors only for files changed in the upstream PR | |
| all_parse_errors=$(echo "$check" | jq -r '.output.text // ""' | grep "^Failed to parse" || true) | |
| changed_docs=$(gh api "/repos/bazelbuild/bazel/pulls/${PR_NUMBER}/files" \ | |
| --jq '[.[] | .filename | select(startswith("docs/")) | ltrimstr("docs/")] | .[]' 2>/dev/null || true) | |
| relevant_errors="" | |
| while IFS= read -r file; do | |
| [[ -z "$file" ]] && continue | |
| match=$(echo "$all_parse_errors" | grep "path ${file}:" || true) | |
| [[ -n "$match" ]] && relevant_errors="${relevant_errors}${match}"$'\n' | |
| done <<< "$changed_docs" | |
| { | |
| echo "parse_errors<<PARSE_EOF" | |
| printf '%s' "$relevant_errors" | |
| echo "PARSE_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Attempt $i: Mintlify status=$status, waiting 20s..." | |
| sleep 20 | |
| done | |
| echo "Timed out waiting for Mintlify deployment" | |
| echo "conclusion=timed_out" >> "$GITHUB_OUTPUT" | |
| - name: Post or update preview comment | |
| if: ${{ steps.mintlify.outputs.conclusion != 'skipped' }} | |
| env: | |
| # BAZELBUILD_BAZEL_PAT is required to post comments on bazelbuild/bazel PRs. | |
| # github.token is used as a fallback for read-only access to this repo. | |
| GH_TOKEN: ${{ secrets.BAZELBUILD_BAZEL_PAT }} | |
| PREVIEW_URL: https://bazel-${{ matrix.preview_branch }}.mintlify.app/ | |
| PR_NUMBER: ${{ matrix.number }} | |
| HEAD_SHA: ${{ matrix.head_sha }} | |
| MINTLIFY_RESULT: ${{ steps.mintlify.outputs.conclusion }} | |
| PARSE_ERRORS: ${{ steps.mintlify.outputs.parse_errors }} | |
| run: | | |
| set -euo pipefail | |
| marker="<!-- bazel-docs-preview -->" | |
| if [[ -z "${GH_TOKEN}" ]]; then | |
| echo "BAZELBUILD_BAZEL_PAT secret is not configured — skipping comment on upstream PR." | |
| echo "Preview URL that would have been posted: ${PREVIEW_URL}" | |
| if [[ -n "${PARSE_ERRORS}" ]]; then | |
| echo "Parse errors in changed files that would have been reported:" | |
| echo "${PARSE_ERRORS}" | |
| fi | |
| exit 0 | |
| fi | |
| # Build optional parse-error section | |
| if [[ -n "${PARSE_ERRORS}" ]]; then | |
| parse_section=$(cat <<EOF | |
| <details> | |
| <summary>⚠️ Some changed doc pages have MDX parse errors and will not render</summary> | |
| \`\`\` | |
| ${PARSE_ERRORS} | |
| \`\`\` | |
| </details> | |
| EOF | |
| ) | |
| else | |
| parse_section="" | |
| fi | |
| if [[ "${MINTLIFY_RESULT}" == "success" ]]; then | |
| body=$(cat <<EOF | |
| ${marker} | |
| :white_check_mark: Bazel docs preview is ready! | |
| **Preview URL:** ${PREVIEW_URL} | |
| ${parse_section} | |
| *Updated for \`${HEAD_SHA}\`* | |
| EOF | |
| ) | |
| else | |
| body=$(cat <<EOF | |
| ${marker} | |
| :x: Bazel docs preview deployment failed (Mintlify result: ${MINTLIFY_RESULT}). | |
| Please check the [GitHub Actions logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. | |
| ${parse_section} | |
| *Updated for \`${HEAD_SHA}\`* | |
| EOF | |
| ) | |
| fi | |
| existing_id="$(gh api "/repos/bazelbuild/bazel/issues/${PR_NUMBER}/comments" \ | |
| --jq "map(select(.body | contains(\"${marker}\")))[0].id // empty")" | |
| if [[ -n "${existing_id}" ]]; then | |
| gh api -X PATCH "/repos/bazelbuild/bazel/issues/comments/${existing_id}" -f body="${body}" | |
| else | |
| gh api -X POST "/repos/bazelbuild/bazel/issues/${PR_NUMBER}/comments" -f body="${body}" | |
| fi | |
| cleanup: | |
| needs: [list-prs, build-previews, comment] | |
| # Only run on scheduled cron — not on manual workflow_dispatch triggers. | |
| # Runs after build-previews and comment so it never races with an in-progress preview build. | |
| if: ${{ always() && github.event_name == 'schedule' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Close draft PRs and delete branches for merged/closed upstream PRs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| BAZELBUILD_TOKEN: ${{ secrets.BAZELBUILD_BAZEL_PAT || github.token }} | |
| run: | | |
| set -euo pipefail | |
| # List all pr-* branches in this repo | |
| branches=$(gh api "repos/${{ github.repository }}/branches?per_page=100" --paginate \ | |
| --jq '[.[].name | select(startswith("pr-"))]') | |
| echo "Found $(echo "$branches" | jq -r 'length') preview branch(es)" | |
| for pr_number in $(echo "$branches" | jq -r '.[] | ltrimstr("pr-")'); do | |
| branch="pr-${pr_number}" | |
| # Check if the upstream bazelbuild/bazel PR is still open | |
| state=$(GH_TOKEN="${BAZELBUILD_TOKEN}" gh api "repos/bazelbuild/bazel/pulls/${pr_number}" \ | |
| --jq '.state' 2>/dev/null || echo "not_found") | |
| if [[ "$state" == "open" ]]; then | |
| continue | |
| fi | |
| echo "Upstream PR #${pr_number} is '${state}'. Cleaning up ${branch}..." | |
| # Close the draft PR in this repo if one exists | |
| pr_id=$(gh pr list --repo "${{ github.repository }}" --head "${branch}" \ | |
| --json number --jq '.[0].number // empty') | |
| if [[ -n "${pr_id}" ]]; then | |
| gh pr close "${pr_id}" --repo "${{ github.repository }}" | |
| echo "Closed draft PR #${pr_id}" | |
| fi | |
| # Delete the preview branch | |
| gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/${branch}" 2>/dev/null \ | |
| && echo "Deleted branch ${branch}" \ | |
| || echo "Branch ${branch} already deleted or not found" | |
| done |