diff --git a/.github/workflows/auto-pr-on-issue.yml b/.github/workflows/auto-pr-on-issue.yml new file mode 100644 index 0000000..7d4859b --- /dev/null +++ b/.github/workflows/auto-pr-on-issue.yml @@ -0,0 +1,47 @@ +name: Auto Guideline PR + +on: + issues: + types: + - labeled + +jobs: + auto-pr: + if: contains(github.event.label.name, 'accepted') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Git + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + + # - name: Save issue JSON payload to file + # run: echo '${{ toJson(github.event.issue) }}' > issue.json + # pass it directly -- mayne fallback to this if an issue happened due to + # pipe-ing or encoding + + - name: Run Python script to generate guideline file + run: | + echo '${{ toJson(github.event.issue) }}' | python3 scripts/auto-pr-helper.py + + - name: Commit generated guideline files + run: | + git add src/coding-guidelines/ + git commit -m "Add guideline for issue #${{ github.event.issue.number }}" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + commit-message: "Add guideline for issue #${{ github.event.issue.number }}" + branch: guideline-${{ github.event.issue.number }} + title: "[auto-pr] #${{ github.event.issue.number }}: ${{ github.event.issue.title }}" + body: | + This PR was automatically generated from issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }}. diff --git a/scripts/auto-pr-helper.py b/scripts/auto-pr-helper.py new file mode 100644 index 0000000..8372f05 --- /dev/null +++ b/scripts/auto-pr-helper.py @@ -0,0 +1,158 @@ +import json +import re +import random +import string + +CHARS = string.ascii_letters + string.digits +ID_LENGTH = 12 + +def generate_id(prefix): + """Generate a random ID with the given prefix.""" + random_part = "".join(random.choice(CHARS) for _ in range(ID_LENGTH)) + return f"{prefix}_{random_part}" + +def extract_form_fields(issue_body: str) -> dict: + # Mapping from issue body headers to dict keys + header_map = { + "Chapter": "Chapter", + "Guideline Title": "Guideline Title", + "Category": "Category", + "Status": "Status", + "Release Begin": "Release Begin", + "Release End": "Release End", + "FLS Paragraph ID": "Fls Paragraph Id", + "Decidability": "Decidability", + "Scope": "Scope", + "Tags": "Tags", + "Amplification": "Amplification", + "Exception(s)": "Exception(S)", + "Rationale": "Rationale", + "Non-Compliant Example - Prose": "Non Compliant Example Prose", + "Non-Compliant Example - Code": "Non Compliant Example Code", + "Compliant Example - Prose": "Compliant Example Prose", + "Compliant Example - Code": "Compliant Example Code", + } + + fields = {v: "" for v in header_map.values()} + + lines = issue_body.splitlines() + current_key = None + current_value_lines = [] + + lines.append("### END") # Sentinel to process last field + + 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() + if value == "_No response_": + value = "" + if current_key in fields: + fields[current_key] = value + + header = header_match.group(1).strip() + current_key = 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): + content = "\n" + content + "\n" + # os.makedirs(f"src/coding-guidelines/{chapter}", exist_ok=True) + filename = f"src/coding-guidelines/{chapter.lower()}.rst" + + # for testing in the GA summary + print("=====CONTENT=====") + print(content) + print("=====CONTENT=END=====") + + with open(filename, 'a', encoding='utf-8') as f: + f.write(content) + print(f"Saved guideline to {filename}") + +def guideline_template(fields: dict) -> str: + + # taken from generate-guideline-templates.py + guideline_id = generate_id("gui") + rationale_id = generate_id("rat") + non_compliant_example_id = generate_id("non_compl_ex") + compliant_example_id = generate_id("compl_ex") + + def get(key): + return fields.get(key, "").strip() + + + guideline_text = f""".. guideline:: {get('Guideline Title')} + :id: {guideline_id} + :category: {get('Category').lower()} + :status: {get('Status').lower()} + :release: {get('Release Begin').lower()} + :fls: {get('Fls Paragraph Id').lower()} + :decidability: {get('Decidability').lower()} + :scope: {get('Scope').lower()} + :tags: {",".join(get('Tags').split(" "))} + + {get('Amplification')} + + .. rationale:: + :id: {rationale_id} + :status: {get('Status').lower()} + + {get('Rationale')} + + .. non_compliant_example:: + :id: {non_compliant_example_id} + :status: {get('Status').lower()} + + {get('Non Compliant Example Prose')} + + .. code-block:: rust + + {get('Non Compliant Example Code')} + + .. compliant_example:: + :id: {compliant_example_id} + :status: {get('Status').lower()} + + {get('Compliant Example Prose')} + + .. code-block:: rust + + {get('Compliant Example Code')} +""" + + return guideline_text + +import sys +if __name__ == "__main__": + # for testing purposes pull an issue in json format + # eg https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4 + # this json is how the issue is visible to the GA bot + # + # with open('scripts/issue_sample.json', 'r', encoding='utf-8') as f: + # issue = json.load(f) + # + + ## locally test with `cat scripts/test_issue_sample.json | python3 scripts/auto-pr-helper.py` + + issue_json = sys.stdin.read() + issue = json.loads(issue_json) + + issue_number = issue['number'] + issue_title = issue['title'] + issue_body = issue['body'] + + fields = extract_form_fields(issue_body) + chapter = fields["Chapter"] + + + content = guideline_template(fields) + + + save_guideline_file(content, chapter) + + diff --git a/scripts/test_issue_sample.json b/scripts/test_issue_sample.json new file mode 100644 index 0000000..d981e78 --- /dev/null +++ b/scripts/test_issue_sample.json @@ -0,0 +1,124 @@ +{ + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4", + "repository_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines", + "labels_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/labels{/name}", + "comments_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/comments", + "events_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/events", + "html_url": "https://github.com/x0rw/safety-critical-rust-coding-guidelines/issues/4", + "id": 3104390263, + "node_id": "I_kwDOOMMjbs65CTx3", + "number": 4, + "title": "[Coding Guideline]: testtt", + "user": { + "login": "x0rw", + "id": 14003018, + "node_id": "MDQ6VXNlcjE0MDAzMDE4", + "avatar_url": "https://avatars.githubusercontent.com/u/14003018?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/x0rw", + "html_url": "https://github.com/x0rw", + "followers_url": "https://api.github.com/users/x0rw/followers", + "following_url": "https://api.github.com/users/x0rw/following{/other_user}", + "gists_url": "https://api.github.com/users/x0rw/gists{/gist_id}", + "starred_url": "https://api.github.com/users/x0rw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/x0rw/subscriptions", + "organizations_url": "https://api.github.com/users/x0rw/orgs", + "repos_url": "https://api.github.com/users/x0rw/repos", + "events_url": "https://api.github.com/users/x0rw/events{/privacy}", + "received_events_url": "https://api.github.com/users/x0rw/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 8703664686, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiLg", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/category:%20advisory", + "name": "category: advisory", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703664688, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiMA", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/status:%20draft", + "name": "status: draft", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703664689, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiMQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/decidability:%20decidable", + "name": "decidability: decidable", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703686409, + "node_id": "LA_kwDOOMMjbs8AAAACBse3CQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/chapter:%20concurrency", + "name": "chapter: concurrency", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703686412, + "node_id": "LA_kwDOOMMjbs8AAAACBse3DA", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/scope:%20crate", + "name": "scope: crate", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703732885, + "node_id": "LA_kwDOOMMjbs8AAAACBshslQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/accepted", + "name": "accepted", + "color": "6AABE8", + "default": false, + "description": "" + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2025-05-30T22:24:07Z", + "updated_at": "2025-05-30T22:48:17Z", + "closed_at": null, + "author_association": "OWNER", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": "### Chapter\n\nConcurrency\n\n### Guideline Title\n\ntest ga\n\n### Category\n\nAdvisory\n\n### Status\n\nDraft\n\n### Release Begin\n\n1.1.1\n\n### Release End\n\n1.1.1\n\n### FLS Paragraph ID\n\nfls_fsdjkfslkdfj\n\n### Decidability\n\nDecidable\n\n### Scope\n\nCrate\n\n### Tags\n\ntest gatest ga\n\n### Amplification\n\nhehehehe\n\n### Exception(s)\n\n_No response_\n\n### Rationale\n\ntest ga\n\n### Non-Compliant Example - Prose\n\ntest ga\n\n### Non-Compliant Example - Code\n\ntest ga\n\n### Compliant Example - Prose\n\ntest ga\n\n### Compliant Example - Code\n\ntest ga", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/timeline", + "performed_via_github_app": null, + "state_reason": null +} diff --git a/src/coding-guidelines/concurrency.rst b/src/coding-guidelines/concurrency.rst index 701b2b7..6a07c7e 100644 --- a/src/coding-guidelines/concurrency.rst +++ b/src/coding-guidelines/concurrency.rst @@ -5,3 +5,42 @@ Concurrency =========== + +.. guideline:: test ga + :id: gui_ofjFRRp0goGU + :category: advisory + :status: draft + :release: 1.1.1 + :fls: fls_fsdjkfslkdfj + :decidability: decidable + :scope: crate + :tags: test,gatest,ga + + + + .. rationale:: + :id: rat_4ZG4G7FBTRpw + :status: draft + + test ga + + .. non_compliant_example:: + :id: non_compl_ex_rFC9SU9BbwXd + :status: draft + + test ga + + .. code-block:: rust + + test ga + + .. compliant_example:: + :id: compl_ex_Z6gknEHscAkG + :status: draft + + test ga + + .. code-block:: rust + + test ga + diff --git a/test/test.test b/test/test.test new file mode 100644 index 0000000..e69de29