From 2ade78a3dd6850374a06a32e22fbee56646638f3 Mon Sep 17 00:00:00 2001 From: Benjamin Kapner Date: Mon, 17 Aug 2026 08:44:56 +0300 Subject: [PATCH 1/2] feat(action): add detect-only mode The detection half of the tool is far more reliable and cheaper than generation, but currently they are bundled. Add a detect-only mode that identifies docs affected by a diff, compares against docs actually changed in the PR, and reports which files may need updates. Generates nothing, opens no PR. Runnable on pull_request events, not just issue_comment. --- action.yml | 10 +++++++++ src/detect.py | 51 +++++++++++++++++++++++++++++++++++++++++++ src/suggest_docs.py | 25 +++++++++++++++++++++ tests/test_detect.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 src/detect.py create mode 100644 tests/test_detect.py diff --git a/action.yml b/action.yml index cf6382d..3a77743 100644 --- a/action.yml +++ b/action.yml @@ -73,6 +73,14 @@ inputs: description: 'Path to a Markdown style configuration file (.md) containing documentation style guidelines. If not set, auto-detects .code-to-docs/style.md in the repository root.' required: false default: '' + mode: + description: 'Execution mode: "comment" (default, triggered by PR comments) or "detect-only" (identify affected docs without generating, for use as a status check on pull_request events)' + required: false + default: 'comment' + docs-drift-severity: + description: 'For detect-only mode: "warn" (always exit 0, default) or "error" (exit non-zero when affected docs are untouched)' + required: false + default: 'warn' outputs: status: @@ -104,3 +112,5 @@ runs: GOOGLE_SA_KEY: ${{ inputs.google-sa-key }} MAX_CONTEXT_CHARS: ${{ inputs.max-context-chars }} STYLE_CONFIG_PATH: ${{ inputs.style-config-path }} + MODE: ${{ inputs.mode }} + DOCS_DRIFT_SEVERITY: ${{ inputs.docs-drift-severity }} diff --git a/src/detect.py b/src/detect.py new file mode 100644 index 0000000..45f38c3 --- /dev/null +++ b/src/detect.py @@ -0,0 +1,51 @@ +"""Detect-only mode: identify docs affected by a diff without generating anything.""" + +import re +import sys +from pathlib import Path + + +def extract_changed_doc_paths(diff_text): + """Extract documentation file paths that were modified in the diff.""" + doc_extensions = {".md", ".rst", ".adoc"} + paths = set() + for match in re.finditer(r"^diff --git a/(.+?) b/", diff_text, re.MULTILINE): + path = match.group(1) + if Path(path).suffix in doc_extensions: + paths.add(path) + return paths + + +def run_detect_only(diff, relevant_files, changed_docs): + """Compare affected docs against docs actually changed in the PR. + + Returns (affected_but_untouched, summary_lines). + """ + affected_set = set(relevant_files) if relevant_files else set() + untouched = affected_set - changed_docs + + lines = [] + if untouched: + lines.append(f"Found {len(untouched)} doc file(s) that may need updates:") + for f in sorted(untouched): + lines.append(f" - {f}") + lines.append("") + lines.append( + "Comment [review-docs] on the PR to review suggested changes, " + "or [update-docs] to generate updates directly." + ) + else: + lines.append("All affected documentation files are already updated in this PR.") + + return untouched, lines + + +def exit_with_severity(untouched, severity): + """Exit with the appropriate code based on severity setting.""" + if not untouched: + return + + severity = (severity or "warn").lower() + if severity == "error": + print(f"Error: {len(untouched)} doc file(s) may need updates but were not changed.") + sys.exit(1) diff --git a/src/suggest_docs.py b/src/suggest_docs.py index 805f09d..83d9c62 100644 --- a/src/suggest_docs.py +++ b/src/suggest_docs.py @@ -258,6 +258,31 @@ def main(): print(f"Index build complete: {result['status']}") return + # Handle detect-only mode (runs on pull_request events, not comments) + mode = os.environ.get("MODE", "comment") + if mode == "detect-only": + from detect import exit_with_severity, extract_changed_doc_paths, run_detect_only + + print("Mode: detect-only") + if not setup_docs_environment(): + print("Failed to set up docs environment") + return + diff = get_diff() + if not diff: + print("No diff found.") + return + changed_docs = extract_changed_doc_paths(diff) + relevant_files = find_relevant_files_optimized(diff) + if relevant_files is None: + file_previews = get_file_content_or_summaries() + relevant_files = ask_ai_for_relevant_files(diff, file_previews) if file_previews else [] + untouched, summary = run_detect_only(diff, relevant_files, changed_docs) + for line in summary: + print(line) + severity = os.environ.get("DOCS_DRIFT_SEVERITY", "warn") + exit_with_severity(untouched, severity) + return + # Detect which command was used comment_body = os.environ.get("COMMENT_BODY", "") diff --git a/tests/test_detect.py b/tests/test_detect.py new file mode 100644 index 0000000..6f2485b --- /dev/null +++ b/tests/test_detect.py @@ -0,0 +1,52 @@ +"""Tests for detect-only mode.""" + +from detect import extract_changed_doc_paths, run_detect_only + + +class TestExtractChangedDocPaths: + def test_finds_doc_files(self): + diff = ( + "diff --git a/src/main.py b/src/main.py\n" + "+added\n" + "diff --git a/docs/guide.md b/docs/guide.md\n" + "+updated\n" + "diff --git a/docs/api.rst b/docs/api.rst\n" + "+updated\n" + ) + paths = extract_changed_doc_paths(diff) + assert paths == {"docs/guide.md", "docs/api.rst"} + + def test_ignores_non_doc_files(self): + diff = "diff --git a/src/main.py b/src/main.py\n+added\n" + assert extract_changed_doc_paths(diff) == set() + + def test_empty_diff(self): + assert extract_changed_doc_paths("") == set() + + +class TestRunDetectOnly: + def test_reports_untouched_files(self): + untouched, lines = run_detect_only( + diff="", + relevant_files=["docs/guide.md", "docs/api.md"], + changed_docs={"docs/guide.md"}, + ) + assert untouched == {"docs/api.md"} + assert any("docs/api.md" in line for line in lines) + + def test_all_updated(self): + untouched, lines = run_detect_only( + diff="", + relevant_files=["docs/guide.md"], + changed_docs={"docs/guide.md"}, + ) + assert untouched == set() + assert any("already updated" in line for line in lines) + + def test_no_relevant_files(self): + untouched, lines = run_detect_only( + diff="", + relevant_files=[], + changed_docs=set(), + ) + assert untouched == set() From 777de710fff4c041b01efca0420aca0b3da31347 Mon Sep 17 00:00:00 2001 From: Benjamin Kapner Date: Mon, 17 Aug 2026 08:45:16 +0300 Subject: [PATCH 2/2] docs(readme): position detect-only as the low-cost entry point Add a README section with an example workflow for detect-only mode on pull_request events. Frame it as the first thing a cautious team should adopt before using the full generation pipeline. --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 60a7e46..48c5bbe 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,36 @@ uv run pre-commit install CI enforces lint, format, and a 60% test coverage threshold on every PR. +## Detect-Only Mode (Docs Drift Check) + +For teams that want to catch documentation drift without generating updates, use `mode: detect-only` on `pull_request` events: + +```yaml +name: Docs Drift Check + +on: + pull_request: + branches: [main] + +jobs: + check-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: redhat-community-ai-tools/code-to-docs@main + with: + model-api-base: ${{ secrets.MODEL_API_BASE }} + model-api-key: ${{ secrets.MODEL_API_KEY }} + model-name: ${{ secrets.MODEL_NAME }} + docs-repo-url: ${{ secrets.DOCS_REPO_URL }} + mode: detect-only + docs-drift-severity: warn # or "error" to fail the check +``` + +This identifies which doc files are affected by the PR's code changes and reports any that were not updated. It generates nothing and opens no PR. Set `docs-drift-severity: error` to use it as a required status check. + ## Performance Optimization The action builds semantic indexes stored in `.doc-index/`: