Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
46 changes: 46 additions & 0 deletions .github/workflows/rst-preview-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: RST Preview Comment

on:
issues:
types: [opened, edited]

jobs:
post-rst-preview:
# Only run for issues created from the Coding Guideline template
# The template automatically adds the "coding guideline" label
if: contains(github.event.issue.labels.*.name, 'coding guideline')
runs-on: ubuntu-latest
permissions:
issues: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install Pandoc
uses: pandoc/actions/setup@v1

- name: Generate RST preview comment
id: generate-comment
run: |
# Generate the comment content
COMMENT=$(cat <<'EOF' | uv run python scripts/generate-rst-comment.py
${{ toJson(github.event.issue) }}
EOF
)

# Write to file to preserve formatting
echo "$COMMENT" > /tmp/comment-body.md

- name: Post or update comment
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.issue.number }}
body-path: /tmp/comment-body.md
comment-author: 'github-actions[bot]'
# Use a hidden marker to identify and update the same comment
body-includes: '<!-- rst-preview-comment -->'
edit-mode: replace
110 changes: 100 additions & 10 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,115 @@
### `auto-pr-helper.py`
# Scripts

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.
This directory contains utility scripts for managing coding guidelines.

**Location: scripts/README.md (replaces existing file)**

## Scripts Overview

| Script | Purpose |
|--------|---------|
| `auto-pr-helper.py` | Transforms issue JSON to RST format (used by auto-PR workflow) |
| `generate-rst-comment.py` | Generates GitHub comment with RST preview |
| `guideline_utils.py` | Shared utility functions for guideline processing |

---

## `guideline_utils.py`

A shared module containing common functions used by other scripts:

- `md_to_rst()` - Convert Markdown to reStructuredText using Pandoc
- `normalize_md()` - Fix Markdown formatting issues
- `normalize_list_separation()` - Ensure proper list formatting for Pandoc
- `extract_form_fields()` - Parse issue body into field dictionary
- `guideline_template()` - Generate RST from fields dictionary
- `chapter_to_filename()` - Convert chapter name to filename slug
- `save_guideline_file()` - Append guideline to chapter file

---

### How to Use
## `auto-pr-helper.py`

This script transforms a GitHub issue's JSON data into reStructuredText format for coding guidelines.

### Usage

```bash
# From a local JSON file
cat path/to/issue.json | uv run python scripts/auto-pr-helper.py

# From GitHub API directly
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/123 | uv run python scripts/auto-pr-helper.py

# Save the output to the appropriate chapter file
cat path/to/issue.json | uv run python scripts/auto-pr-helper.py --save
```

### Options

- `--save`: Save the generated RST content to the appropriate chapter file in `src/coding-guidelines/`

---

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.
## `generate-rst-comment.py`

#### 1. Using a Local JSON File
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.

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

```bash
cat path/to/your_issue.json | uv run scripts/auto-pr-helper.py
# From a local JSON file
cat path/to/issue.json | uv run python scripts/generate-rst-comment.py

# From GitHub API directly
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/123 | uv run python scripts/generate-rst-comment.py
```

#### 2. Fetching from the GitHub API directly
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.
### Output

The script outputs a Markdown-formatted comment that includes:

1. **Instructions** on how to use the RST content
2. **Target file path** indicating which chapter file to add the guideline to
3. **Collapsible RST content** that can be copied and pasted

### Example Output

```markdown
## 📋 RST Preview for Coding Guideline

This is an automatically generated preview...

### 📁 Target File
Add this guideline to: `src/coding-guidelines/concurrency.rst`

### 📝 How to Use This
1. Fork the repository...
...

<details>
<summary>📄 Click to expand RST content</summary>

\`\`\`rst
.. guideline:: My Guideline Title
:id: gui_ABC123...
\`\`\`

</details>
```

---

## How to Get Issue JSON from GitHub API

To work with these scripts locally, you can fetch issue data from the GitHub API:

```bash
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 | uv run ./scripts/auto-pr-helper.py
curl https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER > issue.json
```

For example:

```bash
curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 > issue.json
```
204 changes: 23 additions & 181 deletions scripts/auto-pr-helper.py
Original file line number Diff line number Diff line change
@@ -1,190 +1,32 @@
import argparse
import json
import os
import re
import sys
from textwrap import dedent, indent

import pypandoc

scriptpath = "../"
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.abspath(os.path.join(script_dir, ".."))
sys.path.append(parent_dir)

from generate_guideline_templates import (
guideline_rst_template,
issue_header_map,
)


def md_to_rst(markdown: str) -> str:
return pypandoc.convert_text(
markdown,
'rst',
format='markdown',
extra_args=['--wrap=none']
)


def normalize_list_separation(text: str) -> str:
"""
Ensures every new list block is preceded by a blank line,
required for robust parsing by Pandoc when targeting RST
"""
# Regex to identify any line that starts a Markdown list item (* or -)
_list_item_re = re.compile(r"^[ \t]*[*-][ \t]+")

output_buffer = []
for line in text.splitlines():
is_item = bool(_list_item_re.match(line))

# Get the last line appended to the output buffer
prev = output_buffer[-1] if output_buffer else ""

# Check if a blank line needs to be inserted before list
# (Current is item) AND (Prev is not blank) AND (Prev is not an item)
if is_item and prev.strip() and not _list_item_re.match(prev):
# Insert a blank line to clearly separate the new list block
output_buffer.append("")

output_buffer.append(line)

return "\n".join(output_buffer)


def normalize_md(issue_body: str) -> str:
"""
Fix links and mixed bold/code that confuse Markdown parser
"""
# Fix links with inline-code: [`link`](url) => [link](url)
issue_body = re.sub(
r"\[\s*`([^`]+)`\s*\]\(([^)]+)\)",
r"[\1](\2)",
issue_body
)

# Fix mixed bold/code formatting
# **`code`** => `code`
issue_body = re.sub(
r"\*\*`([^`]+)`\*\*",
r"`\1`",
issue_body
)

# `**code**` => `code`
issue_body = re.sub(
r"`\*\*([^`]+)\*\*`",
r"`\1`",
issue_body
)
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors

return issue_body
"""
This script transforms a GitHub issue JSON into RST format for coding guidelines.

It reads 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.

def extract_form_fields(issue_body: str) -> dict:
"""
This function parses issues json into a dict of important fields
"""
Usage:
cat issue.json | uv run python scripts/auto-pr-helper.py
cat issue.json | uv run python scripts/auto-pr-helper.py --save

fields = dict.fromkeys(issue_header_map.values(), "")
Location: scripts/auto-pr-helper.py (replaces existing file)
"""

lines = issue_body.splitlines()
current_key = None
current_value_lines = []

lines.append("### END") # Sentinel to process last field

# Look for '###' in every line, ### represent a sections/field in a guideline
for line in lines:
header_match = re.match(r"^### (.+)$", line.strip())
if header_match:
# Save previous field value if any
if current_key is not None:
value = "\n".join(current_value_lines).strip()
# `_No response_` represents an empty field
if value == "_No response_":
value = ""
if current_key in fields:
fields[current_key] = value

header = header_match.group(1).strip()
current_key = issue_header_map.get(
header
) # Map to dict key or None if unknown
current_value_lines = []
else:
current_value_lines.append(line)

return fields


def save_guideline_file(content: str, chapter: str):
"""
Appends a guideline to a chapter
"""
filename = f"src/coding-guidelines/{chapter.lower().replace(' ', '-')}.rst"
with open(filename, "a", encoding="utf-8") as f:
f.write(content)
print(f"Saved guideline to {filename}")


def guideline_template(fields: dict) -> str:
"""
This function turns a dictionary that contains the guideline fields
into a proper .rst guideline format
"""

def get(key):
return fields.get(key, "").strip()

def format_code_block(code: str, lang: str = "rust") -> str:
lines = code.strip().splitlines()
if lines and lines[0].strip().startswith("```"):
# Strip the ```rust and ``` lines
lines = lines[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]

# Dedent before adding indentation
dedented_code = dedent("\n".join(lines))

# Add required indentation
indented_code = "\n".join(
f" {line}" for line in dedented_code.splitlines()
)

return f"\n\n{indented_code}\n"

amplification_text = indent(md_to_rst(get("amplification")), " " * 12)
rationale_text = indent(md_to_rst(get("rationale")), " " * 16)
non_compliant_ex_prose_text = indent(
md_to_rst(get("non_compliant_ex_prose")), " " * 16
)
compliant_example_prose_text = indent(
md_to_rst(get("compliant_example_prose")), " " * 16
)

guideline_text = guideline_rst_template(
guideline_title=get("guideline_title"),
category=get("category"),
status=get("status"),
release_begin=get("release_begin"),
release_end=get("release_end"),
fls_id=get("fls_id"),
decidability=get("decidability"),
scope=get("scope"),
tags=get("tags"),
amplification=amplification_text,
rationale=rationale_text,
non_compliant_ex_prose=non_compliant_ex_prose_text,
non_compliant_ex=format_code_block(get("non_compliant_ex")),
compliant_example_prose=compliant_example_prose_text,
compliant_example=format_code_block(get("compliant_example")),
)

return guideline_text
import argparse
import json
import sys

from guideline_utils import (
extract_form_fields,
guideline_template,
normalize_list_separation,
normalize_md,
save_guideline_file,
)

if __name__ == "__main__":
# parse arguments
Expand Down
Loading