Skip to content
Draft

WIP #75672

Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/Preview-Url-Comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Comment Preview URLs

on:
workflow_run:
workflows: ["Doc-Preview"]
types:
- completed

jobs:
comment:
name: Post Preview URLs Comment
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
permissions:
pull-requests: write

steps:
- name: Download artifacts
id: download
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: doc-preview-comment
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Read artifacts
id: artifacts-data
if: steps.download.outcome == 'success'
run: |
PR_NUMBER=$(cat pr_number.txt)
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
COMMENT_BODY=$(cat comment_body.txt)
{
echo 'comment_body<<EOF'
echo "$COMMENT_BODY"
echo EOF
} >> $GITHUB_OUTPUT

- name: Find existing comment
id: fc
if: steps.download.outcome == 'success'
uses: peter-evans/find-comment@v4
with:
issue-number: ${{ steps.artifacts-data.outputs.pr_number }}
comment-author: 'github-actions[bot]'
body-includes: 'Preview documentation links for API changes in this PR'

- name: Create or update comment
if: steps.download.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ steps.artifacts-data.outputs.pr_number }}
body: ${{ steps.artifacts-data.outputs.comment_body }}
edit-mode: replace
39 changes: 37 additions & 2 deletions .github/workflows/_Doc-Preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,53 @@ jobs:
echo "Extracting build.tar.gz"
git config --global --add safe.directory ${work_dir}
tar --use-compress-program="pzstd -1" -xpf build.tar.gz --strip-components=1
api_doc_spec_diff=$(python tools/diff_api.py paddle/fluid/API_DEV.spec.doc paddle/fluid/API_PR.spec.doc)
if [ "$api_doc_spec_diff" == "" ]; then
api_doc_spec_diff=$(python tools/diff_api.py paddle/fluid/API_DEV.spec.doc paddle/fluid/API_PR.spec.doc || true)
if [ -z "$api_doc_spec_diff" ]; then
echo "API documents no change."
exit 0
fi
# Save diff to a file for the next step
echo "$api_doc_spec_diff" > /tmp/api_doc_diff.txt

curl -sS -o /tmp/entrypoint.sh https://paddle-dev-tools-open.bj.bcebos.com/fluiddoc-preview/entrypoint-paddle-docs-review.sh
cd /
source ${{ github.workspace }}/../../../proxy
bash "/tmp/entrypoint.sh"
'

- name: Generate Comment Body
id: generate_comment
run: |
comment_body=$(docker exec -t ${{ env.container_name }} /bin/bash -c '
if [ ! -f "/tmp/api_doc_diff.txt" ]; then
echo ""
exit 0
fi
python /paddle/tools/generate_doc_comment.py /tmp/api_doc_diff.txt ${{ env.PR_ID }}
')
echo "comment_body<<EOF" >> $GITHUB_OUTPUT
echo "$comment_body" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Save comment artifacts
if: steps.generate_comment.outputs.comment_body != ''
run: |
echo "${{ steps.generate_comment.outputs.comment_body }}" > comment_body.txt
echo "${{ env.PR_ID }}" > pr_number.txt
# [TODO] remove it after test.
echo "${{ steps.generate_comment.outputs.comment_body }}"
echo "${{ env.PR_ID }}"

- name: Upload comment artifacts
if: steps.generate_comment.outputs.comment_body != ''
uses: actions/upload-artifact@v4
with:
name: doc-preview-comment
path: |
comment_body.txt
pr_number.txt
retention-days: 1

- name: Terminate and delete the container
if: always()
run: |
Expand Down
114 changes: 114 additions & 0 deletions tools/generate_doc_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib
import inspect
import re
import sys

import paddle # noqa: F401


def resolve_string_to_obj(path: str):
"""
Recursively resolves a string path to a Python object.
Handles modules, functions, classes, and methods.
"""
if not path:
return None

# First, try to import the entire path as a module (e.g., "paddle" or "paddle.autograd").
try:
return importlib.import_module(path)
except ImportError:
# If the import fails, it might be an object within a module.
# If there's no dot, it was a failed top-level import, so we can't proceed.
if "." not in path:
return None

# Split the path into its parent and the final object name.
# e.g., "paddle.Tensor" -> parent="paddle", child="Tensor"
parent_path, child_name = path.rsplit('.', 1)
parent_obj = resolve_string_to_obj(parent_path)

# If the parent object could not be resolved, we can't find the child.
if parent_obj is None:
return None

# Use getattr with a default value to safely get the child object.
return getattr(parent_obj, child_name, None)


def generate_comment_body(doc_diff, pr_id):
if not doc_diff:
return ""

output_lines = []
base_url = f"http://preview-paddle-pr-{pr_id}.paddle-docs-preview.paddlepaddle.org.cn/documentation/docs/en/api"

# Extract API names like 'paddle.autograd.backward' from lines like:
# - paddle.autograd.backward (ArgSpec(...), ('document', ...))
# + paddle.autograd.backward (ArgSpec(...), ('document', ...))
apis = sorted(
set(re.findall(r"^[+]\s*([a-zA-Z0-9_.]+)\s*\(", doc_diff, re.MULTILINE))
)

for api in apis:
api_obj = resolve_string_to_obj(api)

if api_obj is None:
raise ValueError(f"Could not resolve API path: {api}")

api_path = api.replace('.', '/')
url = f"{base_url}/{api_path}_en.html"

if "." in api:
parent_path, child_name = api.rsplit('.', 1)
parent_obj = resolve_string_to_obj(parent_path)
if inspect.isclass(parent_obj) and inspect.isfunction(api_obj):
parent_api_path = parent_path.replace('.', '/')
url = f"{base_url}/{parent_api_path}_en.html#{child_name}"

output_lines.append(f"- [{api}]({url})")

if not output_lines:
return ""

comment_body = """<details>
<summary>📚 Preview documentation links for API changes in this PR (Click to expand)</summary>
The following are preview links for new or modified API documentation in this PR:
{}
</details>""".format("\n".join(output_lines))

return comment_body


if __name__ == "__main__":
if len(sys.argv) < 3:
print(
"Usage: python generate_doc_comment.py <path_to_doc_diff> <pr_id>"
)
sys.exit(1)

doc_diff_path = sys.argv[1]
pr_id = sys.argv[2]

with open(doc_diff_path, 'r') as f:
doc_diff_content = f.read()

comment = generate_comment_body(doc_diff_content, pr_id)
print(comment)
Loading