Skip to content

Commit 2dc16bf

Browse files
etserendclaude
andcommitted
fix(ci): address reviewer bugs in publish-docs and regenerate-api-client (HYBIM-725)
- Remove pull_request trigger (was testing-only; pipeline now validated end-to-end) - Fix MDX acorn parse errors in create_docs.py: convert RST :: code blocks to fenced blocks, escape curly braces in all description/returns/raises fields, and pass through already-fenced example blocks without double-wrapping - Drop SSH signing from regenerate-api-client; use PAT_AO_DOC_AUTOMATION for push + PR creation (same pattern as publish-docs). SPLUNK_AO_API_URL is the only remaining unprovisioned secret needed to make the workflow operational. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent dd011f1 commit 2dc16bf

4 files changed

Lines changed: 102 additions & 86 deletions

File tree

.github/workflows/publish-docs.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ name: Publish Docs
33

44
on:
55
workflow_dispatch:
6-
pull_request:
7-
types: [opened, synchronize]
86
workflow_run:
97
workflows: ["Package Release"]
108
types:
119
- completed
1210

1311
jobs:
1412
publish-docs:
13+
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
1514
runs-on: ubuntu-latest
1615

1716
steps:
Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# Regenerates the OpenAPI Python client from the Splunk AO backend spec.
22
# TODO(HYBIM-725): Set the following secrets in repo Settings → Secrets before this workflow
33
# is operational:
4-
# - SPLUNK_AO_AUTOMATION_SSH_PRIVATE_KEY: SSH key for GPG-signed commits
5-
# - PAT_AO_DOC_AUTOMATION: GitHub token to open PRs
6-
# TODO(HYBIM-725): Replace the HOST_URL below with the Splunk AO backend endpoint.
4+
# - SPLUNK_AO_API_URL: Splunk AO backend endpoint for fetching the OpenAPI spec
5+
# PAT_AO_DOC_AUTOMATION is already provisioned and used for push + PR creation.
76
name: Update API Client
87

98
on:
@@ -16,8 +15,7 @@ jobs:
1615
runs-on: ubuntu-latest
1716

1817
permissions:
19-
contents: "read"
20-
id-token: "write"
18+
contents: "write"
2119

2220
env:
2321
CI_ENV: dev
@@ -44,40 +42,29 @@ jobs:
4442
pip install poetry
4543
poetry install --all-extras --with dev
4644
47-
- name: Create SSH key
48-
run: |
49-
install -m 600 -D /dev/null ~/.ssh/id_rsa
50-
echo "${{ secrets.SPLUNK_AO_AUTOMATION_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
51-
52-
- name: Set up ssh-agent
53-
uses: webfactory/ssh-agent@v0.9.0
54-
with:
55-
ssh-private-key: ${{ secrets.SPLUNK_AO_AUTOMATION_SSH_PRIVATE_KEY }}
56-
5745
- name: Get current date
5846
id: date
59-
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
47+
run: echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
6048

6149
- name: Fetch API types and regenerate
50+
env:
51+
PAT: ${{ secrets.PAT_AO_DOC_AUTOMATION }}
52+
BRANCH_NAME: chore/splunk-ao-automation/update-api-client-${{ steps.date.outputs.date }}
6253
run: |
63-
git config --global gpg.format ssh
64-
git config --global user.signingKey ~/.ssh/id_rsa
65-
git config --global commit.gpgsign true
6654
# TODO(HYBIM-725): replace with Splunk bot email and name
6755
git config --global user.email "splunk-ao-automation@splunk.com"
6856
git config --global user.name "splunk-ao-automation"
57+
git remote set-url origin https://x-access-token:${PAT}@github.com/splunk/splunk-ao-python.git
6958
git checkout -b $BRANCH_NAME
7059
# TODO(HYBIM-725): replace with the Splunk AO backend URL
7160
poetry run ./scripts/import-openapi-yaml.sh ${{ secrets.SPLUNK_AO_API_URL }}
7261
poetry run ./scripts/auto-generate-api-client.sh
7362
git add .
7463
git commit -m "Update API Client"
75-
git push --force origin $BRANCH_NAME --no-verify
76-
env:
77-
BRANCH_NAME: chore/splunk-ao-automation/update-api-client-${{ steps.date.outputs.date }}
64+
git push --force origin $BRANCH_NAME
7865
7966
- name: Create pull request
80-
run: gh pr create -B main -H $BRANCH_NAME --label 'dependencies' --title 'Update API Client' --body 'Fix any breaking changes if this pull request fails to deploy'
8167
env:
8268
BRANCH_NAME: chore/splunk-ao-automation/update-api-client-${{ steps.date.outputs.date }}
83-
GITHUB_TOKEN: ${{ secrets.PAT_AO_DOC_AUTOMATION }}
69+
GH_TOKEN: ${{ secrets.PAT_AO_DOC_AUTOMATION }}
70+
run: gh pr create -B main -H $BRANCH_NAME --label 'dependencies' --title 'Update API Client' --body 'Fix any breaking changes if this pull request fails to deploy'

scripts/auto-generate-api-client.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ echo "$HOME_DIR"
1818
cd "$HOME_DIR"
1919

2020
# Backup the current output path
21-
mv "$OUTPUT_PATH" "$OUTPUT_PATH"_backup
21+
mv "$OUTPUT_PATH" "$OUTPUT_PATH"_backup
2222

2323
# Run the OpenAPI Python client generator
2424
poetry run openapi-python-client generate --meta none --path "$OPENAPI_SPEC_PATH" --output-path "$OUTPUT_PATH" --custom-template-path ../codegen_templates --config "$CONFIG_PATH" --overwrite

scripts/create_docs.py

Lines changed: 89 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,59 @@ def build_docs(files_docs: list[FileDoc], source_root: str, output_root: str) ->
501501
print(f"Documentation generation complete. Output written to {out_root}")
502502

503503

504+
def _escape_curly_braces(text: str) -> str:
505+
"""Escape { and } outside fenced and inline code blocks so MDX doesn't treat them as JSX."""
506+
triple_segments = re.split(r"(```.*?```)", text, flags=re.DOTALL)
507+
for i, triple_seg in enumerate(triple_segments):
508+
if triple_seg.startswith("```"):
509+
continue
510+
single_segments = re.split(r"(`[^`]*`)", triple_seg)
511+
for j, single_seg in enumerate(single_segments):
512+
if not single_seg.startswith("`"):
513+
single_seg = single_seg.replace("{", r"\{").replace("}", r"\}")
514+
single_segments[j] = single_seg
515+
triple_segments[i] = "".join(single_segments)
516+
return "".join(triple_segments)
517+
518+
519+
def _convert_rst_code_blocks(text: str) -> str:
520+
"""Convert RST :: indented code blocks to fenced ```python blocks."""
521+
lines = text.split("\n")
522+
out: list[str] = []
523+
i = 0
524+
while i < len(lines):
525+
line = lines[i]
526+
if line.rstrip().endswith("::"):
527+
# Emit the line with :: stripped (or removed if standalone)
528+
stripped = line.rstrip()[:-2].rstrip()
529+
out.append(stripped)
530+
i += 1
531+
# Collect the indented block that follows
532+
block: list[str] = []
533+
while i < len(lines) and (lines[i].strip() == "" or lines[i].startswith(" ")):
534+
block.append(lines[i])
535+
i += 1
536+
# Strip trailing blank lines from block
537+
while block and block[-1].strip() == "":
538+
block.pop()
539+
if block:
540+
out.append("```python")
541+
for bl in block:
542+
out.append(bl[4:] if bl.startswith(" ") else bl)
543+
out.append("```")
544+
else:
545+
out.append(line)
546+
i += 1
547+
return "\n".join(out)
548+
549+
550+
def _sanitize_description(text: str) -> str:
551+
"""Convert RST code blocks and escape MDX-unsafe curly braces in description text."""
552+
text = _convert_rst_code_blocks(text)
553+
text = _escape_curly_braces(text)
554+
return text
555+
556+
504557
def write_function(parts: list[str], fn: Any, heading_level: int = 3) -> None:
505558
"""
506559
Append formatted documentation for a function to the provided parts list.
@@ -537,34 +590,15 @@ def write_function(parts: list[str], fn: Any, heading_level: int = 3) -> None:
537590
fdoc = fn.doc
538591
if isinstance(fdoc, dict):
539592
if fdoc.get("description"):
540-
parts.append(f"{fdoc.get('description')}\n")
593+
parts.append(f"{_sanitize_description(fdoc.get('description'))}\n")
541594
if fdoc.get("params"):
542595
parts.append("**Arguments**\n")
543596
for p in fdoc.get("params", []):
544597
name = p.get("name") or "<arg>"
545598
t = p.get("type")
546599
desc = p.get("description") or ""
547600
desc = desc.replace("\n", "\n ")
548-
549-
# Escape curly braces outside code blocks (triple or single backticks)
550-
def escape_curly_braces(text: str) -> str:
551-
# Split by triple backtick code blocks first
552-
triple_segments = re.split(r"(```.*?```)", text, flags=re.DOTALL)
553-
for i, triple_seg in enumerate(triple_segments):
554-
if triple_seg.startswith("```"):
555-
# Inside triple backtick code block, leave as is
556-
continue
557-
# Now split by single backtick code blocks
558-
single_segments = re.split(r"(`[^`]*`)", triple_seg)
559-
for j, single_seg in enumerate(single_segments):
560-
if not single_seg.startswith("`"):
561-
single_seg = single_seg.replace("{", r"\{").replace("}", r"\}")
562-
single_segments[j] = single_seg
563-
triple_segments[i] = "".join(single_segments)
564-
return "".join(triple_segments)
565-
566-
desc = escape_curly_braces(desc)
567-
601+
desc = _escape_curly_braces(desc)
568602
if t:
569603
parts.append(f"- `{name}` (`{t}`): {desc}")
570604
else:
@@ -574,37 +608,36 @@ def escape_curly_braces(text: str) -> str:
574608
parts.append("**Raises**\n")
575609
for r in fdoc.get("raises", []):
576610
typ = r.get("type") or "Exception"
577-
desc = r.get("description") or ""
611+
desc = _escape_curly_braces(r.get("description") or "")
578612
parts.append(f"- `{typ}`: {desc}")
579613
parts.append("")
580614
if fdoc.get("returns"):
581615
parts.append("**Returns**\n")
582616
ret = fdoc.get("returns")
583617
if ret:
584618
rtyp = ret.get("type") or ""
585-
rdesc = ret.get("description") or ""
619+
rdesc = _escape_curly_braces(ret.get("description") or "")
586620
if rtyp:
587621
parts.append(f"- `{rtyp}`: {rdesc}\n")
588622
else:
589623
parts.append(f"- {rdesc}\n")
590624
if fdoc.get("examples"):
591625
parts.append("**Examples**\n")
592-
is_in_code_block = False
593626
for ex in fdoc.get("examples", []):
594627
code = ex.get("code")
595-
if code:
596-
if not is_in_code_block and code.startswith(">>>"):
597-
is_in_code_block = True
598-
parts.append("```python")
599-
if is_in_code_block and not code.startswith(">>>") and not code.startswith("..."):
600-
is_in_code_block = False
601-
parts.append("```\n")
602-
if not code.startswith(">>>") and not code.startswith("..."):
603-
parts.append(code)
604-
else:
605-
parts.append(code[4:])
606-
if is_in_code_block:
607-
parts.append("```\n")
628+
if not code:
629+
continue
630+
if "```" in code:
631+
# Already contains fenced blocks — output as-is (may include prose + code)
632+
parts.append(code)
633+
elif code.startswith(">>>") or code.startswith("..."):
634+
parts.append("```python")
635+
parts.append(code[4:])
636+
parts.append("```")
637+
else:
638+
parts.append("```python")
639+
parts.append(code)
640+
parts.append("```")
608641
parts.append("")
609642
if fdoc.get("notes"):
610643
parts.append("**Notes**\n")
@@ -651,44 +684,43 @@ def write_class(parts: list[str], cls: Any) -> None:
651684
cdoc = cls.doc
652685
if isinstance(cdoc, dict):
653686
if cdoc.get("description"):
654-
parts.append(f"{cdoc.get('description')}\n")
687+
parts.append(f"{_sanitize_description(cdoc.get('description'))}\n")
655688
if cdoc.get("params"):
656689
parts.append("**Arguments**\n")
657690
for p in cdoc.get("params", []):
658691
name = p.get("name") or "<arg>"
659692
t = p.get("type")
660693
desc = p.get("description") or ""
661694
desc = desc.replace("\n", "\n ")
695+
desc = _escape_curly_braces(desc)
662696
if t:
663697
parts.append(f"- `{name}` (`{t}`): {desc}\n")
664698
else:
665699
parts.append(f"- `{name}`: {desc}\n")
666700

667701
if cdoc.get("examples"):
668702
parts.append("**Examples**\n")
669-
is_in_code_block = False
670703
for ex in cdoc.get("examples", []):
671704
code = ex.get("code")
672-
if code:
673-
if not is_in_code_block and code.startswith(">>>"):
674-
is_in_code_block = True
675-
parts.append("```python")
676-
if is_in_code_block and not code.startswith(">>>") and not code.startswith("..."):
677-
is_in_code_block = False
678-
parts.append("```\n")
679-
if not code.startswith(">>>") and not code.startswith("..."):
680-
parts.append(code)
681-
else:
682-
parts.append(code[4:])
683-
if is_in_code_block:
684-
parts.append("```\n")
705+
if not code:
706+
continue
707+
if "```" in code:
708+
parts.append(code)
709+
elif code.startswith(">>>") or code.startswith("..."):
710+
parts.append("```python")
711+
parts.append(code[4:])
712+
parts.append("```")
713+
else:
714+
parts.append("```python")
715+
parts.append(code)
716+
parts.append("```")
685717
parts.append("")
686718
if cdoc.get("returns"):
687719
parts.append("**Returns**\n")
688720
ret = cdoc.get("returns")
689721
if ret:
690722
rtyp = ret.get("type") or ""
691-
rdesc = ret.get("description") or ""
723+
rdesc = _escape_curly_braces(ret.get("description") or "")
692724
if rtyp:
693725
parts.append(f"- `{rtyp}`: {rdesc}\n")
694726
else:
@@ -736,17 +768,15 @@ def write_module(fd: FileDoc, parts: list[str]) -> None:
736768
md = fd.module.doc
737769
if isinstance(md, dict):
738770
if md.get("description"):
739-
parts.append(f"{md.get('description')}\n")
771+
parts.append(f"{_sanitize_description(md.get('description'))}\n")
740772
if md.get("params"):
741773
parts.append("**Arguments**\n")
742774
for p in md.get("params", []):
743775
name = p.get("name") or "<arg>"
744776
t = p.get("type")
745777
desc = p.get("description") or ""
746-
747-
# If description is multi-line, indent subsequent lines for better Markdown rendering
748778
desc = desc.replace("\n", "\n ")
749-
779+
desc = _escape_curly_braces(desc)
750780
if t:
751781
parts.append(f"- `{name}` (`{t}`): {desc}\n")
752782
else:
@@ -755,14 +785,14 @@ def write_module(fd: FileDoc, parts: list[str]) -> None:
755785
parts.append("**Raises**\n")
756786
for r in md.get("raises", []):
757787
typ = r.get("type") or "Exception"
758-
desc = r.get("description") or ""
788+
desc = _escape_curly_braces(r.get("description") or "")
759789
parts.append(f"- `{typ}`: {desc}\n")
760790
if md.get("returns"):
761791
parts.append("**Returns**\n")
762792
ret = md.get("returns")
763793
if ret:
764794
rtyp = ret.get("type") or ""
765-
rdesc = ret.get("description") or ""
795+
rdesc = _escape_curly_braces(ret.get("description") or "")
766796
if rtyp:
767797
parts.append(f"- `{rtyp}`: {rdesc}\n")
768798
else:

0 commit comments

Comments
 (0)