diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml
index bad051f4642..a76439cbc3d 100644
--- a/.github/actionlint.yaml
+++ b/.github/actionlint.yaml
@@ -12,6 +12,12 @@ self-hosted-runner:
# GitHub supports queued concurrency runs, but the latest actionlint release
# does not yet recognize the concurrency.queue key.
paths:
+ ".github/workflows/build-docs.yml":
+ ignore:
+ - 'unexpected key "queue" for "concurrency" section'
+ ".github/workflows/cleanup-pr-previews.yml":
+ ignore:
+ - 'unexpected key "queue" for "concurrency" section'
".github/workflows/ci-workflow-health.yml":
ignore:
- 'unexpected key "queue" for "concurrency" section'
diff --git a/.github/actions/doc_preview/action.yml b/.github/actions/doc_preview/action.yml
index 0c60b899fbf..a0c7f212297 100644
--- a/.github/actions/doc_preview/action.yml
+++ b/.github/actions/doc_preview/action.yml
@@ -1,9 +1,9 @@
-# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
name: Docs in PRs
-description: Preview or clean up docs built from PRs
+description: Preview docs built from PRs
# A re-implementation based on the logic of https://github.com/rossjrw/pr-preview-action/blob/41a957c44a456a34718e9bcf825363194db5e6d5/README.md, due to limitations illustrated in NVIDIA/cuda-python#380.
@@ -18,10 +18,8 @@ inputs:
runs:
using: composite
steps:
- # The steps below are executed only when testing in a PR.
- # Note: the PR previews will be removed once merged to main or release/* (see below)
- name: Deploy doc preview
- if: ${{ github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ if: ${{ startsWith(github.ref_name, 'pull-request/') }}
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
with:
git-config-name: cuda-python-bot
@@ -29,9 +27,14 @@ runs:
folder: ${{ inputs.source-folder }}
target-folder: docs/pr-preview/pr-${{ inputs.pr-number }}/
commit-message: "Deploy doc preview for PR ${{ inputs.pr-number }} (${{ github.sha }})"
+ # Every PR preview, the main docs deploy, and the coverage deploy commit to
+ # the shared gh-pages branch. Rebase onto a deployment that landed in the
+ # meantime instead of force-pushing over it, which the branch ruleset
+ # rejects (and which used to discard the other deployment).
+ force: false
- name: Leave a comment after deployment
- if: ${{ github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ if: ${{ startsWith(github.ref_name, 'pull-request/') }}
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
with:
header: pr-preview
@@ -45,27 +48,3 @@ runs:
|
https://nvidia.github.io/cuda-python/pr-preview/pr-${{ inputs.pr-number }}/cuda-bindings/
|
https://nvidia.github.io/cuda-python/pr-preview/pr-${{ inputs.pr-number }}/cuda-pathfinder/
|
Preview will be ready when the GitHub Pages deployment is complete.
-
- # The steps below are executed only when building on main or release/*.
- - name: Remove doc preview
- if: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') }}
- uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
- with:
- git-config-name: cuda-python-bot
- git-config-email: cuda-python-bot@users.noreply.github.com
- folder: ${{ inputs.source-folder }}
- target-folder: docs/pr-preview/pr-${{ inputs.pr-number }}/
- commit-message: "Clean up doc preview for PR ${{ inputs.pr-number }} (${{ github.sha }})"
-
- - name: Leave a comment after removal
- if: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') }}
- uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3
- with:
- header: pr-preview
- number: ${{ inputs.pr-number }}
- hide_and_recreate: true
- hide_classify: "OUTDATED"
- message: |
- Doc Preview CI
- :---:
- Preview removed because the pull request was closed or merged.
diff --git a/.github/actions/get_pr_number/action.yml b/.github/actions/get_pr_number/action.yml
index 1641f806849..4c4288204e0 100644
--- a/.github/actions/get_pr_number/action.yml
+++ b/.github/actions/get_pr_number/action.yml
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
@@ -9,13 +9,11 @@ description: Get the PR number without relying on the pull_request* event trigge
runs:
using: composite
steps:
- - name: Get PR info (non-main, non-release branch)
- if: ${{ github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ - name: Get PR info
uses: nv-gha-runners/get-pr-info@main
id: get-pr-info
- - name: Extract PR number (non-main, non-release branch)
- if: ${{ github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ - name: Extract PR number
shell: bash --noprofile --norc -xeuo pipefail {0}
run: |
trap 'echo "Error at line $LINENO"; exit 1' ERR
@@ -26,33 +24,3 @@ runs:
fi
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
echo "BUILD_PREVIEW=1" >> $GITHUB_ENV
-
- - name: Get PR data (main or release/* branch)
- if: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') }}
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
- id: get-pr-data
- with:
- script: |
- const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({
- commit_sha: context.sha,
- owner: context.repo.owner,
- repo: context.repo.repo,
- });
- if (!prs.data.length) {
- core.setFailed("No PR associated with this commit on 'main' or 'release/*'.");
- } else {
- return prs.data[0];
- }
-
- - name: Extract PR number (main or release/* branch)
- if: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') }}
- shell: bash --noprofile --norc -xeuo pipefail {0}
- run: |
- trap 'echo "Error at line $LINENO"; exit 1' ERR
- PR_NUMBER="${{ fromJSON(steps.get-pr-data.outputs.result).number }}"
- if [[ -z "$PR_NUMBER" ]]; then
- echo "No associated PR found for the commit in 'main' or 'release/*'."
- exit 1
- fi
- echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
- echo "BUILD_LATEST=1" >> $GITHUB_ENV
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index bef2a7e84c0..6e770e0a033 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -50,6 +50,10 @@ jobs:
# The build stage could fail but we want the CI to keep moving.
if: ${{ github.repository_owner == 'nvidia' && !cancelled() }}
runs-on: ubuntu-latest
+ outputs:
+ deployment-artifact-name: ${{ steps.deployment-metadata.outputs.deployment-artifact-name }}
+ docs-github-ref: ${{ steps.deployment-metadata.outputs.docs-github-ref }}
+ pr-number: ${{ steps.deployment-metadata.outputs.pr-number }}
defaults:
run:
shell: bash -el {0}
@@ -216,16 +220,24 @@ jobs:
# --no-deps avoids re-resolving cuda-core from PyPI during tag releases.
pip install --no-deps cuda_python*.whl
- # This step sets the PR_NUMBER/BUILD_LATEST/BUILD_PREVIEW env vars.
+ # PR CI uses pull-request/ refs regardless of the target branch.
- name: Get PR number
- if: ${{ !inputs.is-release }}
+ if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }}
uses: ./.github/actions/get_pr_number
- - name: Set up artifact directories
+ - name: Mark latest docs build
+ if: ${{ !inputs.is-release && !startsWith(github.ref_name, 'pull-request/') }}
+ run: echo "BUILD_LATEST=1" >> "$GITHUB_ENV"
+
+ - name: Record deployment metadata
+ id: deployment-metadata
run: |
- mkdir -p artifacts/docs
- # create an empty folder for removal use
- mkdir -p artifacts/empty_docs
+ echo "deployment-artifact-name=docs-deployment-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
+ echo "docs-github-ref=${CUDA_PYTHON_DOCS_GITHUB_REF}" >> "$GITHUB_OUTPUT"
+ echo "pr-number=${PR_NUMBER:-}" >> "$GITHUB_OUTPUT"
+
+ - name: Set up artifact directories
+ run: mkdir -p artifacts/docs
- name: Build all docs
if: ${{ inputs.component == 'all' }}
@@ -265,7 +277,7 @@ jobs:
mv ${COMPONENT}/docs/build/html/* artifacts/docs/${TARGET}
- name: Write rendered docs file list
- if: ${{ !inputs.is-release && github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }}
run: |
find "${GITHUB_WORKSPACE}/artifacts/docs" -type f -name '*.html' ! -path '*/_static/*' \
| LC_ALL=C sort > lychee-rendered-html-files.txt
@@ -276,7 +288,7 @@ jobs:
wc -l lychee-rendered-html-files.txt
- name: Restore lychee cache
- if: ${{ !inputs.is-release && github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }}
id: restore-lychee-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
@@ -286,7 +298,7 @@ jobs:
docs-rendered-lychee-${{ env.PR_NUMBER }}-
- name: Check rendered docs links
- if: ${{ !inputs.is-release && github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }}
+ if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }}
uses: lycheeverse/lychee-action@6da1d14f3a43098a294b7696d93d938aa8d20fc0 # unreleased: supports v0.24.x archive layout
with:
# PR-preview canonical URLs are checked by the preview deployment workflow.
@@ -317,7 +329,7 @@ jobs:
token: ${{ github.token }}
- name: Save lychee cache
- if: ${{ always() && !inputs.is-release && github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') && steps.restore-lychee-cache.outputs.cache-hit != 'true' && steps.restore-lychee-cache.outputs.cache-primary-key != '' }}
+ if: ${{ always() && !inputs.is-release && startsWith(github.ref_name, 'pull-request/') && steps.restore-lychee-cache.outputs.cache-hit != 'true' && steps.restore-lychee-cache.outputs.cache-primary-key != '' }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .lycheecache
@@ -330,6 +342,16 @@ jobs:
path: artifacts/
retention-days: 3
+ - name: Upload docs deployment payload
+ if: ${{ inputs.deploy-docs }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: ${{ steps.deployment-metadata.outputs.deployment-artifact-name }}
+ path: artifacts/docs/
+ retention-days: 3
+ include-hidden-files: true
+ if-no-files-found: error
+
- name: Upload dry-run docs artifact
if: ${{ !inputs.deploy-docs || (inputs.is-release && inputs.docs-branch != 'gh-pages') }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -338,13 +360,40 @@ jobs:
path: artifacts/docs/
retention-days: 3
- - name: Deploy or clean up doc preview
- if: ${{ inputs.deploy-docs && !inputs.is-release }}
+ deploy:
+ name: Deploy docs
+ # GitHub discards prerequisite artifacts on a new run attempt. If only this
+ # job fails, use "Re-run all jobs" so the build recreates its payload.
+ needs: build
+ if: ${{ inputs.deploy-docs && needs.build.result == 'success' }}
+ runs-on: ubuntu-latest
+ # Must match every job that writes to gh-pages.
+ concurrency:
+ group: cuda-python-gh-pages-publish
+ queue: max
+ cancel-in-progress: false
+ defaults:
+ run:
+ shell: bash -el {0}
+ steps:
+ - name: Checkout ${{ github.event.repository.name }}
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 1
+ ref: ${{ inputs.git-tag }}
+
+ - name: Download docs deployment payload
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: ${{ needs.build.outputs.deployment-artifact-name }}
+ path: artifacts/docs/
+
+ - name: Deploy doc preview
+ if: ${{ inputs.deploy-docs && !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }}
uses: ./.github/actions/doc_preview
with:
- source-folder: ${{ (github.ref_name != 'main' && 'artifacts/docs') ||
- 'artifacts/empty_docs' }}
- pr-number: ${{ env.PR_NUMBER }}
+ source-folder: artifacts/docs
+ pr-number: ${{ needs.build.outputs.pr-number }}
- name: Deploy doc update
if: ${{ inputs.deploy-docs && (github.ref_name == 'main' || inputs.is-release) }}
@@ -355,5 +404,8 @@ jobs:
branch: ${{ inputs.docs-branch }}
folder: artifacts/docs/
target-folder: docs/
- commit-message: "Deploy ${{ (inputs.is-release && 'release') || 'latest' }} docs: ${{ env.CUDA_PYTHON_DOCS_GITHUB_REF }}"
+ commit-message: "Deploy ${{ (inputs.is-release && 'release') || 'latest' }} docs: ${{ needs.build.outputs.docs-github-ref }}"
clean: false
+ # Rebase onto concurrent PR-preview or coverage deployments instead of
+ # force-pushing, which the gh-pages ruleset rejects.
+ force: false
diff --git a/.github/workflows/cleanup-pr-previews.yml b/.github/workflows/cleanup-pr-previews.yml
index 4c367f415c3..11cd458dbf9 100644
--- a/.github/workflows/cleanup-pr-previews.yml
+++ b/.github/workflows/cleanup-pr-previews.yml
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
@@ -19,11 +19,17 @@ on:
permissions:
contents: write # Required to push changes to gh-pages branch
+ pull-requests: write # Required to retire sticky preview comments
jobs:
cleanup:
name: Clean up stale PR preview folders
runs-on: ubuntu-latest
+ # Must match every job that writes to gh-pages.
+ concurrency:
+ group: cuda-python-gh-pages-publish
+ queue: max
+ cancel-in-progress: false
# Only run for NVIDIA org to prevent forks from running this
if: github.repository_owner == 'NVIDIA'
steps:
@@ -41,10 +47,11 @@ jobs:
git config --global user.email "cuda-python-bot@users.noreply.github.com"
- name: Run PR preview cleanup script
+ id: cleanup
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_PAGES_PUBLISH_LOCK: cuda-python-gh-pages-publish
run: |
- # Determine if we should run in dry-run mode
if [[ "${{ inputs.dry-run }}" == "true" ]]; then
echo "Running in dry-run mode (preview only)"
./ci/cleanup-pr-previews --dry-run
@@ -58,11 +65,13 @@ jobs:
run: |
echo "### PR Preview Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
- if [[ "${{ inputs.dry-run }}" == "true" ]]; then
+ if [[ "${{ steps.cleanup.outcome }}" != "success" ]]; then
+ echo "❌ Cleanup failed; no successful update was confirmed" >> $GITHUB_STEP_SUMMARY
+ elif [[ "${{ inputs.dry-run }}" == "true" ]]; then
echo "✅ Dry-run completed successfully" >> $GITHUB_STEP_SUMMARY
- echo "No changes were made to the gh-pages branch" >> $GITHUB_STEP_SUMMARY
+ echo "No changes were made to the gh-pages branch or PR comments" >> $GITHUB_STEP_SUMMARY
else
- echo "✅ Cleanup completed and changes pushed to gh-pages" >> $GITHUB_STEP_SUMMARY
+ echo "✅ Cleanup completed; gh-pages is up to date" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 6015db55c9d..2f66aa4e156 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -465,7 +465,7 @@ jobs:
if-no-files-found: error
combine-and-deploy:
- name: Combine Coverage and Deploy
+ name: Combine Coverage
needs: [coverage-linux, coverage-windows]
runs-on: ubuntu-latest
if: ${{ always() && github.repository_owner == 'nvidia' }}
@@ -566,6 +566,30 @@ jobs:
retention-days: 7
include-hidden-files: true
+ deploy:
+ name: Deploy Coverage
+ # GitHub discards prerequisite artifacts on a new run attempt. If only this
+ # job fails, use "Re-run all jobs" so coverage recreates its payload.
+ needs: combine-and-deploy
+ if: ${{ needs.combine-and-deploy.result == 'success' && github.repository_owner == 'nvidia' }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ # Must match every job that writes to gh-pages.
+ concurrency:
+ group: cuda-python-gh-pages-publish
+ queue: max
+ cancel-in-progress: false
+ steps:
+ - name: Checkout ${{ github.event.repository.name }}
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Download combined coverage results
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: coverage-combined
+ path: docs/coverage/
+
- name: Deploy to gh-pages
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0
with:
@@ -575,3 +599,6 @@ jobs:
target-folder: docs/
commit-message: "Deploy combined coverage (Linux + Windows): ${{ github.sha }}"
clean: false
+ # Rebase onto concurrent docs deployments instead of force-pushing,
+ # which the gh-pages ruleset rejects.
+ force: false
diff --git a/ci/cleanup-pr-previews b/ci/cleanup-pr-previews
index 17fd89ab6c0..414e8507ad3 100755
--- a/ci/cleanup-pr-previews
+++ b/ci/cleanup-pr-previews
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
-# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
@@ -29,14 +29,13 @@ USAGE: $0 [OPTIONS]
OPTIONS:
-n, --dry-run Preview what would be deleted without actually deleting
- --push Commit and push changes to gh-pages (default: false, requires manual push)
+ --push Commit and push from the cleanup GitHub Actions workflow
-h, --help Show this help message
EXAMPLES:
$0 -n # Preview what would be cleaned up (RECOMMENDED FIRST)
- $0 # Clean up folders locally (no push)
- $0 --push # Clean up folders and push to gh-pages branch
- $0 --dry-run --push # Invalid combination (dry-run takes precedence)
+ $0 # Clean up folders locally for inspection (no push)
+ $0 --dry-run --push # Dry run; --push is ignored
REQUIREMENTS:
- GH_TOKEN environment variable must be set with appropriate permissions
@@ -45,8 +44,11 @@ REQUIREMENTS:
SAFETY:
Always run with --dry-run first to verify expected behavior before actual cleanup.
-The script will show a summary of what would be removed. Use --push to automatically
-commit and push changes, otherwise manual git operations are required.
+Without --push, changes are committed locally for inspection only.
+
+Remote updates are owned by the scheduled or manually dispatched "Cleanup: PR
+Preview Documentation" workflow so they share its deployment lock. Other push
+contexts are rejected.
This script is specifically designed for the NVIDIA/cuda-python repository structure.
EOF
@@ -80,6 +82,17 @@ while [[ $# -gt 0 ]]; do
esac
done
+# Remote cleanup is restricted to the cleanup workflow so all preview deletions
+# participate in the shared gh-pages concurrency queue.
+if [[ "$PUSH_CHANGES" == "true" && "$DRY_RUN" != "true" ]] &&
+ [[ "${GITHUB_ACTIONS:-}" != "true" ||
+ ("${GITHUB_EVENT_NAME:-}" != "schedule" &&
+ "${GITHUB_EVENT_NAME:-}" != "workflow_dispatch") ||
+ "${GH_PAGES_PUBLISH_LOCK:-}" != "cuda-python-gh-pages-publish" ]]; then
+ echo -e "${RED}[ERROR]${NC} --push is reserved for the scheduled or manually dispatched PR preview cleanup workflow" >&2
+ exit 1
+fi
+
# Validate required tools and environment
echo -e "${YELLOW}[INFO]${NC} Checking prerequisites..."
@@ -100,6 +113,31 @@ fi
echo -e "${GREEN}[INFO]${NC} All prerequisites satisfied"
+update_preview_comment() {
+ local pr_number="$1"
+ local status_message="$2"
+ local comment_body
+ local comment_id
+
+ printf -v comment_body 'Doc Preview CI\n:---:\n%s\n' "$status_message"
+ if ! comment_id=$(gh api --paginate repos/"${REPOSITORY}"/issues/"${pr_number}"/comments \
+ --header "Accept: application/vnd.github+json" \
+ --jq '.[] | select(.body | contains("")) | .id' \
+ | tail -n 1); then
+ return 1
+ fi
+
+ if [[ -n "$comment_id" ]]; then
+ gh api --method PATCH repos/"${REPOSITORY}"/issues/comments/"${comment_id}" \
+ --header "Accept: application/vnd.github+json" \
+ -f body="$comment_body" >/dev/null
+ else
+ gh api --method POST repos/"${REPOSITORY}"/issues/"${pr_number}"/comments \
+ --header "Accept: application/vnd.github+json" \
+ -f body="$comment_body" >/dev/null
+ fi
+}
+
# Fetch PR preview folders from gh-pages branch
echo -e "${YELLOW}[INFO]${NC} Fetching PR preview folders from gh-pages branch..."
@@ -118,6 +156,7 @@ echo -e "${GREEN}[INFO]${NC} Found $(echo "$PR_FOLDERS" | wc -l) PR preview fold
# Check each PR folder
FOLDERS_TO_REMOVE=()
+CLOSED_PRS=()
TOTAL_FOLDERS=0
OPEN_PRS=0
@@ -146,6 +185,7 @@ while IFS= read -r folder; do
"closed")
echo -e "${RED}[REMOVE]${NC} PR #${PR_NUMBER} is closed"
FOLDERS_TO_REMOVE+=("$folder")
+ CLOSED_PRS+=("$PR_NUMBER")
;;
"not_found")
echo -e "${RED}[REMOVE]${NC} PR #${PR_NUMBER} not found (may have been deleted)"
@@ -212,11 +252,12 @@ else
}
trap cleanup_worktree EXIT
- # Ensure the local gh-pages branch is up-to-date
- git fetch origin gh-pages:gh-pages
+ # Track the remote branch without overwriting a user's local gh-pages branch.
+ git fetch origin +refs/heads/gh-pages:refs/remotes/origin/gh-pages
+ GH_PAGES_SHA=$(git rev-parse refs/remotes/origin/gh-pages)
echo -e "${YELLOW}[INFO]${NC} Creating git worktree for gh-pages branch..."
- if ! git worktree add "$TEMP_DIR" gh-pages >/dev/null 2>&1; then
+ if ! git worktree add --detach "$TEMP_DIR" "$GH_PAGES_SHA" >/dev/null 2>&1; then
echo -e "${RED}[ERROR]${NC} Failed to create git worktree for gh-pages branch" >&2
# Check if the issue might be a leftover worktree registration
@@ -226,7 +267,7 @@ else
rm -rf "$TEMP_DIR" >/dev/null 2>&1 || true
# Try again
- if ! git worktree add "$TEMP_DIR" gh-pages >/dev/null 2>&1; then
+ if ! git worktree add --detach "$TEMP_DIR" "$GH_PAGES_SHA" >/dev/null 2>&1; then
echo -e "${RED}[ERROR]${NC} Still unable to create worktree after cleanup" >&2
exit 1
fi
@@ -267,13 +308,32 @@ $(if [[ ${#FOLDERS_TO_REMOVE[@]} -gt 20 ]]; then echo "... and $((${#FOLDERS_TO_
if [[ "$PUSH_CHANGES" == "true" ]]; then
echo -e "${YELLOW}[INFO]${NC} Pushing to gh-pages branch..."
- git push origin gh-pages
- CHANGES_PUSHED="true"
+ CHANGES_PUSHED="false"
+ for attempt in 1 2 3; do
+ if git push origin HEAD:refs/heads/gh-pages; then
+ CHANGES_PUSHED="true"
+ break
+ fi
+ if [[ "$attempt" -eq 3 ]]; then
+ echo -e "${RED}[ERROR]${NC} Push to gh-pages rejected after ${attempt} attempts" >&2
+ exit 1
+ fi
+ echo -e "${YELLOW}[INFO]${NC} gh-pages advanced; rebasing before retry $((attempt + 1))/3..."
+ git fetch origin +refs/heads/gh-pages:refs/remotes/origin/gh-pages
+ git rebase refs/remotes/origin/gh-pages
+ done
echo -e "${GREEN}[SUCCESS]${NC} Cleanup completed! Removed ${REMOVED_COUNT} PR preview folders and pushed changes"
+ for pr_number in "${CLOSED_PRS[@]}"; do
+ if ! update_preview_comment "$pr_number" \
+ "Preview removed because the pull request was closed or merged."; then
+ echo -e "${YELLOW}[WARNING]${NC} Could not update the preview comment for PR #${pr_number}; continuing" >&2
+ fi
+ done
else
CHANGES_PUSHED="false"
echo -e "${GREEN}[SUCCESS]${NC} Cleanup completed! Removed ${REMOVED_COUNT} PR preview folders"
- echo -e "${YELLOW}[INFO]${NC} Changes have been committed locally but not pushed. Use 'git push origin gh-pages' to push manually."
+ echo -e "${YELLOW}[INFO]${NC} Changes have been committed locally for inspection but not pushed."
+ echo -e "${YELLOW}[INFO]${NC} The PR preview cleanup workflow owns remote deletions."
echo -e "${YELLOW}[WARNING]${NC} Worktree will be preserved for manual verification."
fi
else