Skip to content

Commit 76ac2b6

Browse files
authored
feat: add ability to generate comment containing reStructured Text from Markdown (#247)
* feat: add ability to generate comment containing reStructured Text from Markdown * fix: resolve Python linting issues
1 parent 9581f7b commit 76ac2b6

File tree

5 files changed

+515
-191
lines changed

5 files changed

+515
-191
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: RST Preview Comment
2+
3+
on:
4+
issues:
5+
types: [opened, edited]
6+
7+
jobs:
8+
post-rst-preview:
9+
# Only run for issues created from the Coding Guideline template
10+
# The template automatically adds the "coding guideline" label
11+
if: contains(github.event.issue.labels.*.name, 'coding guideline')
12+
runs-on: ubuntu-latest
13+
permissions:
14+
issues: write
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@v6
22+
23+
- name: Install Pandoc
24+
uses: pandoc/actions/setup@v1
25+
26+
- name: Generate RST preview comment
27+
id: generate-comment
28+
run: |
29+
# Generate the comment content
30+
COMMENT=$(cat <<'EOF' | uv run python scripts/generate-rst-comment.py
31+
${{ toJson(github.event.issue) }}
32+
EOF
33+
)
34+
35+
# Write to file to preserve formatting
36+
echo "$COMMENT" > /tmp/comment-body.md
37+
38+
- name: Post or update comment
39+
uses: peter-evans/create-or-update-comment@v4
40+
with:
41+
issue-number: ${{ github.event.issue.number }}
42+
body-path: /tmp/comment-body.md
43+
comment-author: 'github-actions[bot]'
44+
# Use a hidden marker to identify and update the same comment
45+
body-includes: '<!-- rst-preview-comment -->'
46+
edit-mode: replace

scripts/README.md

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,115 @@
1-
### `auto-pr-helper.py`
1+
# Scripts
22

3-
This script is a utility for automating the generation of guidelines. It takes a GitHub issue's JSON data from standard input, parses its body (which is expected to follow a specific issue template), and converts it into a formatted reStructuredText (`.rst`) guideline.
3+
This directory contains utility scripts for managing coding guidelines.
4+
5+
**Location: scripts/README.md (replaces existing file)**
6+
7+
## Scripts Overview
8+
9+
| Script | Purpose |
10+
|--------|---------|
11+
| `auto-pr-helper.py` | Transforms issue JSON to RST format (used by auto-PR workflow) |
12+
| `generate-rst-comment.py` | Generates GitHub comment with RST preview |
13+
| `guideline_utils.py` | Shared utility functions for guideline processing |
14+
15+
---
16+
17+
## `guideline_utils.py`
18+
19+
A shared module containing common functions used by other scripts:
20+
21+
- `md_to_rst()` - Convert Markdown to reStructuredText using Pandoc
22+
- `normalize_md()` - Fix Markdown formatting issues
23+
- `normalize_list_separation()` - Ensure proper list formatting for Pandoc
24+
- `extract_form_fields()` - Parse issue body into field dictionary
25+
- `guideline_template()` - Generate RST from fields dictionary
26+
- `chapter_to_filename()` - Convert chapter name to filename slug
27+
- `save_guideline_file()` - Append guideline to chapter file
428

529
---
630

7-
### How to Use
31+
## `auto-pr-helper.py`
32+
33+
This script transforms a GitHub issue's JSON data into reStructuredText format for coding guidelines.
34+
35+
### Usage
36+
37+
```bash
38+
# From a local JSON file
39+
cat path/to/issue.json | uv run python scripts/auto-pr-helper.py
40+
41+
# From GitHub API directly
42+
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/123 | uv run python scripts/auto-pr-helper.py
43+
44+
# Save the output to the appropriate chapter file
45+
cat path/to/issue.json | uv run python scripts/auto-pr-helper.py --save
46+
```
47+
48+
### Options
49+
50+
- `--save`: Save the generated RST content to the appropriate chapter file in `src/coding-guidelines/`
51+
52+
---
853

9-
The script reads a JSON payload from **standard input**. The most common way to provide this input is by using a pipe (`|`) to feed the output of another command into the script.
54+
## `generate-rst-comment.py`
1055

11-
#### 1. Using a Local JSON File
56+
This script generates a formatted GitHub comment containing an RST preview of a coding guideline. It's used by the RST Preview Comment workflow to post helpful comments on coding guideline issues.
1257

13-
For local testing, you can use `cat` to pipe the contents of a saved GitHub issue JSON file into the script.
58+
### Usage
1459

1560
```bash
16-
cat path/to/your_issue.json | uv run scripts/auto-pr-helper.py
61+
# From a local JSON file
62+
cat path/to/issue.json | uv run python scripts/generate-rst-comment.py
63+
64+
# From GitHub API directly
65+
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/123 | uv run python scripts/generate-rst-comment.py
1766
```
1867

19-
#### 2. Fetching from the GitHub API directly
20-
You can fetch the data for a live issue directly from the GitHub API using curl and pipe it to the script. This is useful for getting the most up-to-date content.
68+
### Output
69+
70+
The script outputs a Markdown-formatted comment that includes:
71+
72+
1. **Instructions** on how to use the RST content
73+
2. **Target file path** indicating which chapter file to add the guideline to
74+
3. **Collapsible RST content** that can be copied and pasted
75+
76+
### Example Output
77+
78+
```markdown
79+
## 📋 RST Preview for Coding Guideline
80+
81+
This is an automatically generated preview...
82+
83+
### 📁 Target File
84+
Add this guideline to: `src/coding-guidelines/concurrency.rst`
85+
86+
### 📝 How to Use This
87+
1. Fork the repository...
88+
...
89+
90+
<details>
91+
<summary>📄 Click to expand RST content</summary>
92+
93+
\`\`\`rst
94+
.. guideline:: My Guideline Title
95+
:id: gui_ABC123...
96+
\`\`\`
97+
98+
</details>
99+
```
100+
101+
---
102+
103+
## How to Get Issue JSON from GitHub API
104+
105+
To work with these scripts locally, you can fetch issue data from the GitHub API:
21106

22107
```bash
23-
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 | uv run ./scripts/auto-pr-helper.py
108+
curl https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER > issue.json
24109
```
110+
111+
For example:
112+
113+
```bash
114+
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 > issue.json
25115
```

scripts/auto-pr-helper.py

Lines changed: 23 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,190 +1,32 @@
1-
import argparse
2-
import json
3-
import os
4-
import re
5-
import sys
6-
from textwrap import dedent, indent
7-
8-
import pypandoc
9-
10-
scriptpath = "../"
11-
script_dir = os.path.dirname(os.path.abspath(__file__))
12-
parent_dir = os.path.abspath(os.path.join(script_dir, ".."))
13-
sys.path.append(parent_dir)
14-
15-
from generate_guideline_templates import (
16-
guideline_rst_template,
17-
issue_header_map,
18-
)
19-
20-
21-
def md_to_rst(markdown: str) -> str:
22-
return pypandoc.convert_text(
23-
markdown,
24-
'rst',
25-
format='markdown',
26-
extra_args=['--wrap=none']
27-
)
28-
29-
30-
def normalize_list_separation(text: str) -> str:
31-
"""
32-
Ensures every new list block is preceded by a blank line,
33-
required for robust parsing by Pandoc when targeting RST
34-
"""
35-
# Regex to identify any line that starts a Markdown list item (* or -)
36-
_list_item_re = re.compile(r"^[ \t]*[*-][ \t]+")
37-
38-
output_buffer = []
39-
for line in text.splitlines():
40-
is_item = bool(_list_item_re.match(line))
41-
42-
# Get the last line appended to the output buffer
43-
prev = output_buffer[-1] if output_buffer else ""
44-
45-
# Check if a blank line needs to be inserted before list
46-
# (Current is item) AND (Prev is not blank) AND (Prev is not an item)
47-
if is_item and prev.strip() and not _list_item_re.match(prev):
48-
# Insert a blank line to clearly separate the new list block
49-
output_buffer.append("")
50-
51-
output_buffer.append(line)
52-
53-
return "\n".join(output_buffer)
54-
55-
56-
def normalize_md(issue_body: str) -> str:
57-
"""
58-
Fix links and mixed bold/code that confuse Markdown parser
59-
"""
60-
# Fix links with inline-code: [`link`](url) => [link](url)
61-
issue_body = re.sub(
62-
r"\[\s*`([^`]+)`\s*\]\(([^)]+)\)",
63-
r"[\1](\2)",
64-
issue_body
65-
)
66-
67-
# Fix mixed bold/code formatting
68-
# **`code`** => `code`
69-
issue_body = re.sub(
70-
r"\*\*`([^`]+)`\*\*",
71-
r"`\1`",
72-
issue_body
73-
)
74-
75-
# `**code**` => `code`
76-
issue_body = re.sub(
77-
r"`\*\*([^`]+)\*\*`",
78-
r"`\1`",
79-
issue_body
80-
)
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors
814

82-
return issue_body
5+
"""
6+
This script transforms a GitHub issue JSON into RST format for coding guidelines.
837
8+
It reads a GitHub issue's JSON data from standard input, parses its body
9+
(which is expected to follow a specific issue template), and converts it
10+
into a formatted reStructuredText (.rst) guideline.
8411
85-
def extract_form_fields(issue_body: str) -> dict:
86-
"""
87-
This function parses issues json into a dict of important fields
88-
"""
12+
Usage:
13+
cat issue.json | uv run python scripts/auto-pr-helper.py
14+
cat issue.json | uv run python scripts/auto-pr-helper.py --save
8915
90-
fields = dict.fromkeys(issue_header_map.values(), "")
16+
Location: scripts/auto-pr-helper.py (replaces existing file)
17+
"""
9118

92-
lines = issue_body.splitlines()
93-
current_key = None
94-
current_value_lines = []
95-
96-
lines.append("### END") # Sentinel to process last field
97-
98-
# Look for '###' in every line, ### represent a sections/field in a guideline
99-
for line in lines:
100-
header_match = re.match(r"^### (.+)$", line.strip())
101-
if header_match:
102-
# Save previous field value if any
103-
if current_key is not None:
104-
value = "\n".join(current_value_lines).strip()
105-
# `_No response_` represents an empty field
106-
if value == "_No response_":
107-
value = ""
108-
if current_key in fields:
109-
fields[current_key] = value
110-
111-
header = header_match.group(1).strip()
112-
current_key = issue_header_map.get(
113-
header
114-
) # Map to dict key or None if unknown
115-
current_value_lines = []
116-
else:
117-
current_value_lines.append(line)
118-
119-
return fields
120-
121-
122-
def save_guideline_file(content: str, chapter: str):
123-
"""
124-
Appends a guideline to a chapter
125-
"""
126-
filename = f"src/coding-guidelines/{chapter.lower().replace(' ', '-')}.rst"
127-
with open(filename, "a", encoding="utf-8") as f:
128-
f.write(content)
129-
print(f"Saved guideline to {filename}")
130-
131-
132-
def guideline_template(fields: dict) -> str:
133-
"""
134-
This function turns a dictionary that contains the guideline fields
135-
into a proper .rst guideline format
136-
"""
137-
138-
def get(key):
139-
return fields.get(key, "").strip()
140-
141-
def format_code_block(code: str, lang: str = "rust") -> str:
142-
lines = code.strip().splitlines()
143-
if lines and lines[0].strip().startswith("```"):
144-
# Strip the ```rust and ``` lines
145-
lines = lines[1:]
146-
if lines and lines[-1].strip() == "```":
147-
lines = lines[:-1]
148-
149-
# Dedent before adding indentation
150-
dedented_code = dedent("\n".join(lines))
151-
152-
# Add required indentation
153-
indented_code = "\n".join(
154-
f" {line}" for line in dedented_code.splitlines()
155-
)
156-
157-
return f"\n\n{indented_code}\n"
158-
159-
amplification_text = indent(md_to_rst(get("amplification")), " " * 12)
160-
rationale_text = indent(md_to_rst(get("rationale")), " " * 16)
161-
non_compliant_ex_prose_text = indent(
162-
md_to_rst(get("non_compliant_ex_prose")), " " * 16
163-
)
164-
compliant_example_prose_text = indent(
165-
md_to_rst(get("compliant_example_prose")), " " * 16
166-
)
167-
168-
guideline_text = guideline_rst_template(
169-
guideline_title=get("guideline_title"),
170-
category=get("category"),
171-
status=get("status"),
172-
release_begin=get("release_begin"),
173-
release_end=get("release_end"),
174-
fls_id=get("fls_id"),
175-
decidability=get("decidability"),
176-
scope=get("scope"),
177-
tags=get("tags"),
178-
amplification=amplification_text,
179-
rationale=rationale_text,
180-
non_compliant_ex_prose=non_compliant_ex_prose_text,
181-
non_compliant_ex=format_code_block(get("non_compliant_ex")),
182-
compliant_example_prose=compliant_example_prose_text,
183-
compliant_example=format_code_block(get("compliant_example")),
184-
)
185-
186-
return guideline_text
19+
import argparse
20+
import json
21+
import sys
18722

23+
from guideline_utils import (
24+
extract_form_fields,
25+
guideline_template,
26+
normalize_list_separation,
27+
normalize_md,
28+
save_guideline_file,
29+
)
18830

18931
if __name__ == "__main__":
19032
# parse arguments

0 commit comments

Comments
 (0)