Skip to content
Closed
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
52 changes: 39 additions & 13 deletions .github/workflows/suggest_docs.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
name: Suggest Documentation Update
name: Suggest Documentation Update (Enhanced)

on:
pull_request:
types: [opened, synchronize]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
force_run:
description: 'Force run even if filters would normally skip'
required: false
type: boolean
default: false

permissions:
contents: read
issues: read
pull-requests: read

jobs:
suggest-docs:
runs-on: ubuntu-latest
if: |
github.event.inputs.force_run == 'true' ||
(github.event.issue.pull_request && contains(github.event.comment.body, '[update-docs]'))

steps:
- name: Get PR information
id: pr_info
if: github.event.issue.pull_request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.issue.number }}
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
echo "head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_OUTPUT
echo "head_repo=$(echo "$PR_DATA" | jq -r '.head.repo.full_name')" >> $GITHUB_OUTPUT

- name: Checkout PR Code
uses: actions/checkout@v3
with:
repository: ${{ steps.pr_info.outputs.head_repo || github.repository }}
ref: ${{ steps.pr_info.outputs.head_ref || github.ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.10
python-version: '3.10'

- name: Install Python Dependencies
run: |
pip install google-generativeai
pip install -U google-genai

- name: Configure Git
run: |
Expand All @@ -28,18 +59,13 @@ jobs:

- name: Set up GitHub CLI
run: |
sudo apt-get install gh

- name: Authenticate GitHub CLI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${GH_TOKEN}" | gh auth login --with-token
sudo apt-get update
sudo apt-get install -y gh

- name: Run Documentation Suggester
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
DOCS_REPO_URL: ${{ secrets.DOCS_REPO_URL }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
python scripts/suggest_docs.py
163 changes: 135 additions & 28 deletions scripts/suggest_docs.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,68 @@
import os
import subprocess
import argparse
from pathlib import Path
from google import genai
from google.genai import types

# === CONFIG ===
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
client = genai.Client()

client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
DOCS_REPO_URL = os.environ["DOCS_REPO_URL"]
BRANCH_NAME = "doc-update-from-pr"


def get_diff():
result = subprocess.run(["git", "diff", "origin/main...HEAD"], capture_output=True, text=True)
return result.stdout.strip()

def get_commit_info():
"""Get commit information for the documentation PR reference"""
try:
# Get the HEAD commit - this is what GitHub Actions checked out for the PR
current_commit_result = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True)
if current_commit_result.returncode != 0:
return None
commit_hash = current_commit_result.stdout.strip()

# Get remote origin URL to construct proper commit links
remote_url = subprocess.run(["git", "config", "--get", "remote.origin.url"], capture_output=True, text=True)
if remote_url.returncode != 0:
return None

# Convert SSH URL to HTTPS if needed
repo_url = remote_url.stdout.strip()
if repo_url.startswith("git@github.com:"):
repo_url = repo_url.replace("git@github.com:", "https://github.com/").replace(".git", "")
elif repo_url.endswith(".git"):
repo_url = repo_url.replace(".git", "")

# Get commit details
short_hash = commit_hash[:7]

# Just get the current commit that triggered the pipeline
return {
'repo_url': repo_url,
'current_commit': commit_hash,
'short_hash': short_hash
}

except Exception as e:
print(f"Warning: Could not get commit info: {e}")
return None

def clone_docs_repo():
subprocess.run(["git", "clone", DOCS_REPO_URL, "docs_repo"])
os.chdir("docs_repo")
subprocess.run(["git", "checkout", "-b", BRANCH_NAME])

# Try to check out the branch if it already exists
result = subprocess.run(["git", "ls-remote", "--heads", "origin", BRANCH_NAME], capture_output=True, text=True)
if result.stdout.strip():
print(f"Reusing existing branch: {BRANCH_NAME}")
subprocess.run(["git", "fetch", "origin", BRANCH_NAME])
subprocess.run(["git", "checkout", BRANCH_NAME])
subprocess.run(["git", "pull", "origin", BRANCH_NAME])
else:
print(f"Creating new branch: {BRANCH_NAME}")
subprocess.run(["git", "checkout", "-b", BRANCH_NAME])


def get_file_previews():
Expand All @@ -29,13 +71,13 @@ def get_file_previews():
for path in adoc_files:
try:
with open(path, encoding="utf-8") as f:
first_lines = "".join([next(f) for _ in range(10)])
lines = f.readlines()[:10] # Get first 10 lines (or fewer if file is short)
first_lines = "".join(lines)
previews.append((str(path), first_lines.strip()))
except Exception as e:
print(f"Skipping file {path}: {e}")
return previews


def ask_gemini_for_relevant_files(diff, file_previews):
context = "\n\n".join(
[f"File: {fname}\nPreview:\n{preview}" for fname, preview in file_previews]
Expand Down Expand Up @@ -63,19 +105,26 @@ def ask_gemini_for_relevant_files(diff, file_previews):
)
return [line.strip() for line in response.text.strip().splitlines() if line.strip()]


def load_full_content(file_path):
try:
return Path(file_path).read_text(encoding="utf-8")
except Exception as e:
print(f"Failed to read {file_path}: {e}")
return ""


def ask_gemini_for_updated_content(diff, file_path, current_content):
prompt = f"""
You are a documentation assistant.

CRITICAL FORMATTING REQUIREMENTS FOR ASCIIDOC FILES:
- NEVER use markdown code fences like ```adoc or ``` anywhere in the file
- AsciiDoc files start directly with content (comments, headers, or text)
- Use ONLY AsciiDoc syntax: ==== for headers, |=== for tables, ---- for code blocks
- Do NOT mix markdown and AsciiDoc syntax
- Maintain proper table structures with matching |=== opening and closing
- Keep all cross-references (xref) intact and properly formatted
- Ensure consistent indentation and spacing

A developer made the following code changes:
{diff}

Expand All @@ -87,13 +136,21 @@ def ask_gemini_for_updated_content(diff, file_path, current_content):
Analyze the diff and check whether **new, important information** is introduced that is not already covered in this file.

- If the file already includes everything important, return exactly: `NO_UPDATE_NEEDED`
- If the file is missing key information, return the **full updated file content** in valid .adoc format, with your additions inserted in the correct section.
- If the file is missing key information, return the **full updated file content**, modifying only what is necessary. in valid AsciiDoc format

VALIDATION CHECKLIST - Before responding, verify:
1. No markdown code fences (```) anywhere in the content
2. All tables have matching |=== opening and closing
3. All section headers use correct ==== syntax
4. All cross-references are properly formatted
5. No broken formatting or incomplete structures

Do not explain or summarize — only return either:
- `NO_UPDATE_NEEDED` (if nothing is missing), or
- The full updated .adoc file content
- The full updated AsciiDoc file content with perfect syntax (NO markdown!)
"""


response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
Expand All @@ -103,8 +160,6 @@ def ask_gemini_for_updated_content(diff, file_path, current_content):
)
return response.text.strip()



def overwrite_file(file_path, new_content):
try:
Path(file_path).write_text(new_content, encoding="utf-8")
Expand All @@ -113,29 +168,60 @@ def overwrite_file(file_path, new_content):
print(f"Failed to write {file_path}: {e}")
return False


def push_and_open_pr(modified_files):
def push_and_open_pr(modified_files, commit_info=None):
subprocess.run(["git", "add"] + modified_files)

# Build commit message with source commit references
commit_msg = "Auto-generated doc updates from code PR"

if commit_info:
repo_name = commit_info['repo_url'].split('/')[-1]
commit_url = f"{commit_info['repo_url']}/commit/{commit_info['current_commit']}"
commit_msg += f"\n\nSource commit: {commit_info['short_hash']} from {repo_name}"
commit_msg += f"\nLink: {commit_url}"

commit_msg += "\n\nAssisted-by: Gemini"

subprocess.run([
"git", "commit",
"-m", "Auto-generated doc updates from code PR\n\nAssisted-by: Gemini"
]) subprocess.run(["git", "push", "--set-upstream", "origin", BRANCH_NAME])
"git", "commit",
"-m", commit_msg
])
# Add remote with token auth
gh_token = os.environ["GH_TOKEN"]
docs_repo_url = DOCS_REPO_URL.replace("https://", f"https://{gh_token}@")

subprocess.run(["git", "remote", "set-url", "origin", docs_repo_url])
subprocess.run(["git", "push", "--set-upstream", "origin", BRANCH_NAME, "--force"])

# Build PR body (simple, without commit references)
pr_body = "This PR updates the following documentation files based on code changes:\n\n"
pr_body += "\n".join([f"- `{f}`" for f in modified_files])
pr_body += "\n\n*Note: Each commit in this PR contains references to the specific source code commits that triggered the documentation updates.*"

subprocess.run([
"gh", "pr", "create",
"--title", "Auto-Generated Doc Updates from Code PR",
"--body", f"This PR updates the following documentation files based on the code changes:\n\n" +
"\n".join([f"- `{f}`" for f in modified_files]),
"--body", pr_body,
"--base", "main",
"--head", BRANCH_NAME
])


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", help="Simulate changes without writing files or pushing PR")
args = parser.parse_args()

diff = get_diff()
if not diff:
print("No changes detected.")
return

# Get commit info before switching to docs repo
commit_info = get_commit_info()
if commit_info:
print(f"Source repository: {commit_info['repo_url']}")
print(f"Latest commit: {commit_info['short_hash']}")

clone_docs_repo()
previews = get_file_previews()

Expand All @@ -160,17 +246,38 @@ def main():
print(f"No update needed for {file_path}")
continue

print(f"Updating {file_path}...")
if overwrite_file(file_path, updated):
modified_files.append(file_path)
if args.dry_run:
print(f"[Dry Run] Would update {file_path} with:\n{updated}\n")
else:
print(f"Updating {file_path}...")
if overwrite_file(file_path, updated):
modified_files.append(file_path)

if modified_files:
push_and_open_pr(modified_files)
if args.dry_run:
print("[Dry Run] Would push and open PR for the following files:")
for f in modified_files:
print(f"- {f}")

if commit_info:
# Show what the commit message would look like
repo_name = commit_info['repo_url'].split('/')[-1]
commit_url = f"{commit_info['repo_url']}/commit/{commit_info['current_commit']}"
commit_msg = "Auto-generated doc updates from code PR"
commit_msg += f"\n\nSource commit: {commit_info['short_hash']} from {repo_name}"
commit_msg += f"\nLink: {commit_url}"
commit_msg += "\n\nAssisted-by: Gemini"

print(f"\n[Dry Run] Commit message would be:")
print("=" * 50)
print(commit_msg)
print("=" * 50)

print(f"\n[Dry Run] PR body would be simple (commit reference is in commit message only)")
else:
push_and_open_pr(modified_files, commit_info)
else:
print("All documentation is already up to date — no PR created.")



if __name__ == "__main__":
main()