Skip to content

Commit cf92c0c

Browse files
authored
[CI] Add Report Preview URLs Workflow (PaddlePaddle#75687)
1 parent 08fe857 commit cf92c0c

File tree

4 files changed

+252
-3
lines changed

4 files changed

+252
-3
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Comment Preview URLs
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Doc-Preview"]
6+
types:
7+
- completed
8+
9+
jobs:
10+
comment:
11+
name: Post Preview URLs Comment
12+
runs-on: ubuntu-latest
13+
if: >
14+
github.event.workflow_run.event == 'pull_request' &&
15+
github.event.workflow_run.conclusion == 'success'
16+
permissions:
17+
pull-requests: write
18+
19+
steps:
20+
- name: Download artifacts
21+
id: download
22+
uses: actions/download-artifact@v4
23+
continue-on-error: true
24+
with:
25+
name: doc-preview-comment
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
run-id: ${{ github.event.workflow_run.id }}
28+
29+
- name: Read artifacts
30+
id: artifacts-data
31+
if: steps.download.outcome == 'success'
32+
run: |
33+
PR_NUMBER=$(cat pr_number.txt)
34+
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
35+
COMMENT_BODY=$(cat comment_body.txt)
36+
{
37+
echo 'comment_body<<EOF'
38+
echo "$COMMENT_BODY"
39+
echo EOF
40+
} >> $GITHUB_OUTPUT
41+
42+
- name: Find existing comment
43+
id: fc
44+
if: steps.download.outcome == 'success'
45+
uses: peter-evans/find-comment@v4
46+
with:
47+
issue-number: ${{ steps.artifacts-data.outputs.pr_number }}
48+
comment-author: 'github-actions[bot]'
49+
body-includes: 'Preview documentation links for API changes in this PR'
50+
51+
- name: Create or update comment
52+
if: steps.download.outcome == 'success'
53+
uses: peter-evans/create-or-update-comment@v4
54+
with:
55+
comment-id: ${{ steps.fc.outputs.comment-id }}
56+
issue-number: ${{ steps.artifacts-data.outputs.pr_number }}
57+
body: ${{ steps.artifacts-data.outputs.comment_body }}
58+
edit-mode: replace

.github/workflows/_Doc-Preview.yml

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,57 @@ jobs:
9494
echo "Extracting build.tar.gz"
9595
git config --global --add safe.directory ${work_dir}
9696
tar --use-compress-program="pzstd -1" -xpf build.tar.gz --strip-components=1
97-
api_doc_spec_diff=$(python tools/diff_api.py paddle/fluid/API_DEV.spec.doc paddle/fluid/API_PR.spec.doc)
98-
if [ "$api_doc_spec_diff" == "" ]; then
97+
api_doc_spec_diff=$(python tools/diff_api.py paddle/fluid/API_DEV.spec.doc paddle/fluid/API_PR.spec.doc || true)
98+
if [ -z "$api_doc_spec_diff" ]; then
9999
echo "API documents no change."
100100
exit 0
101101
fi
102+
# Save diff to a file for the next step
103+
echo "$api_doc_spec_diff" > /tmp/api_doc_diff.txt
102104
103105
curl -sS -o /tmp/entrypoint.sh https://paddle-dev-tools-open.bj.bcebos.com/fluiddoc-preview/entrypoint-paddle-docs-review.sh
104106
cd /
105107
source ${{ github.workspace }}/../../../proxy
106108
bash "/tmp/entrypoint.sh"
107109
'
108110
111+
- name: Generate Comment Body
112+
id: generate_comment
113+
run: |
114+
comment_body=$(docker exec ${{ env.container_name }} /bin/bash -c '
115+
if [ ! -f "/tmp/api_doc_diff.txt" ]; then
116+
exit 0
117+
fi
118+
python /paddle/tools/generate_doc_comment.py /tmp/api_doc_diff.txt ${{ env.PR_ID }}
119+
')
120+
echo "comment_body<<EOF" >> $GITHUB_OUTPUT
121+
echo "$comment_body" >> $GITHUB_OUTPUT
122+
echo "EOF" >> $GITHUB_OUTPUT
123+
124+
if [ -n "$comment_body" ]; then
125+
echo "::group::📝 Generated Comment Preview"
126+
echo "$comment_body"
127+
echo "::endgroup::"
128+
else
129+
echo "::notice::No comment generated"
130+
fi
131+
132+
- name: Save comment artifacts
133+
if: steps.generate_comment.outputs.comment_body != ''
134+
run: |
135+
echo "${{ steps.generate_comment.outputs.comment_body }}" > comment_body.txt
136+
echo "${{ env.PR_ID }}" > pr_number.txt
137+
138+
- name: Upload comment artifacts
139+
if: steps.generate_comment.outputs.comment_body != ''
140+
uses: actions/upload-artifact@v4
141+
with:
142+
name: doc-preview-comment
143+
path: |
144+
comment_body.txt
145+
pr_number.txt
146+
retention-days: 1
147+
109148
- name: Terminate and delete the container
110149
if: always()
111150
run: |

python/paddle/nn/functional/conv.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,12 +361,15 @@ def conv1d(
361361
bias (Tensor, optional): The bias with shape [M,]. Default: None.
362362
stride (int|list|tuple, optional): The stride size. If stride is a list/tuple, it must
363363
contain one integers, (stride_size). Default: 1.
364-
padding (int|str|tuple|list, optional): The padding size. Padding could be in one of the following forms.
364+
padding (int|str|tuple|list, optional): The padding size.
365+
Padding could be in one of the following forms.
366+
365367
1. a string in ['valid', 'same'].
366368
2. an int, which means the feature map is zero paded by size of `padding` on both sides.
367369
3. a list[int] or tuple[int] whose length is 1, which means the feature map is zero paded by size of `padding[0]` on both sides.
368370
4. a list[int] or tuple[int] whose length is 2. It has the form [pad_before, pad_after].
369371
5. a list or tuple of pairs of ints. It has the form [[pad_before, pad_after], [pad_before, pad_after], ...]. Note that, the batch dimension and channel dimension are also included. Each pair of integers correspond to the amount of padding for a dimension of the input. Padding in batch dimension and channel dimension should be [0, 0] or (0, 0).
372+
370373
The default value is 0.
371374
dilation (int|list|tuple, optional): The dilation size. If dilation is a list/tuple, it must
372375
contain one integer, (dilation_size). Default: 1.

tools/generate_doc_comment.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from __future__ import annotations
15+
16+
import argparse
17+
import importlib
18+
import inspect
19+
import re
20+
from typing import TYPE_CHECKING, Any
21+
22+
if TYPE_CHECKING:
23+
from collections.abc import Callable
24+
25+
import paddle # noqa: F401
26+
27+
28+
def load_api_by_name(path: str) -> Callable[..., Any] | None:
29+
"""
30+
Recursively resolves a string path to a Python object.
31+
"""
32+
if not path:
33+
return None
34+
35+
# First, try to import the entire path as a module (e.g., "paddle" or "paddle.autograd").
36+
try:
37+
return importlib.import_module(path)
38+
except ImportError:
39+
# If the import fails, it might be an object within a module.
40+
# If there's no dot, it was a failed top-level import, so we can't proceed.
41+
if "." not in path:
42+
return None
43+
44+
# Split the path into its parent and the final object name.
45+
# e.g., "paddle.Tensor" -> parent="paddle", child="Tensor"
46+
parent_path, child_name = path.rsplit('.', 1)
47+
parent_obj = load_api_by_name(parent_path)
48+
49+
# If the parent object could not be resolved, we can't find the child.
50+
if parent_obj is None:
51+
return None
52+
53+
# Use getattr with a default value to safely get the child object.
54+
return getattr(parent_obj, child_name, None)
55+
56+
57+
def generate_comment_body(doc_diff: str, pr_id: int) -> str:
58+
if not doc_diff:
59+
return ""
60+
61+
output_lines: list[str] = []
62+
base_url = f"http://preview-paddle-pr-{pr_id}.paddle-docs-preview.paddlepaddle.org.cn/documentation/docs/en/api"
63+
64+
# Extract API names like 'paddle.autograd.backward' from lines like:
65+
# - paddle.autograd.backward (ArgSpec(...), ('document', ...))
66+
# + paddle.autograd.backward (ArgSpec(...), ('document', ...))
67+
apis: list[str] = sorted(
68+
set(re.findall(r"^[+]\s*([a-zA-Z0-9_.]+)\s*\(", doc_diff, re.MULTILINE))
69+
)
70+
# All apis should be loaded, this seems a explicitly check.
71+
unload_apis: list[str] = []
72+
73+
if not apis:
74+
return ""
75+
76+
for api in apis:
77+
api_obj = load_api_by_name(api)
78+
79+
if api_obj is None:
80+
unload_apis.append(api)
81+
continue
82+
83+
api_path = api.replace('.', '/')
84+
url = f"{base_url}/{api_path}_en.html"
85+
86+
if "." in api:
87+
parent_path, child_name = api.rsplit('.', 1)
88+
parent_obj = load_api_by_name(parent_path)
89+
if inspect.isclass(parent_obj) and not inspect.isclass(api_obj):
90+
parent_api_path = parent_path.replace('.', '/')
91+
url = f"{base_url}/{parent_api_path}_en.html#{child_name}"
92+
93+
output_lines.append(f"- **{api}**: [Preview]({url})")
94+
unload_error_msg = (
95+
f"@ooooo-create, following apis cannot be loaded, please check it: {', '.join(unload_apis)}"
96+
if unload_apis
97+
else ""
98+
)
99+
100+
if not output_lines:
101+
return unload_error_msg
102+
103+
api_links = "\n".join(output_lines)
104+
comment_body = f"""<details>
105+
<summary>📚 Preview documentation links for API changes in this PR (Click to expand)</summary>
106+
107+
{unload_error_msg}
108+
109+
<table>
110+
<tr>
111+
<td>
112+
ℹ️ <b>Preview Notice</b><br>
113+
Please wait for the <code>Doc-Preview</code> workflow to complete before clicking the preview links below, otherwise you may see outdated content.
114+
</td>
115+
</tr>
116+
</table>
117+
118+
The following are preview links for new or modified API documentation in this PR:
119+
120+
{api_links}
121+
122+
</details>"""
123+
124+
return comment_body
125+
126+
127+
def cli():
128+
parser = argparse.ArgumentParser(
129+
description="Generate documentation comment for PR with API changes"
130+
)
131+
parser.add_argument(
132+
"doc_diff_path", help="Path to the documentation diff file", type=str
133+
)
134+
parser.add_argument("pr_id", help="Pull request ID", type=int)
135+
return parser.parse_args()
136+
137+
138+
def main():
139+
args = cli()
140+
141+
with open(args.doc_diff_path, 'r') as f:
142+
doc_diff_content = f.read()
143+
144+
comment = generate_comment_body(doc_diff_content, args.pr_id)
145+
print(comment)
146+
147+
148+
if __name__ == "__main__":
149+
main()

0 commit comments

Comments
 (0)