diff --git a/.github/workflows/docs-health-check.yaml b/.github/workflows/docs-health-check.yaml new file mode 100644 index 000000000..4fc63f43a --- /dev/null +++ b/.github/workflows/docs-health-check.yaml @@ -0,0 +1,57 @@ +name: Check Docs Health + +on: + schedule: + - cron: '0 0 * * 0' # Run every Sunday at midnight + workflow_dispatch: + +jobs: + health-check: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Run health check script + id: health_check + continue-on-error: true + run: python scripts/check_docs_health.py + + - name: Check for changes + id: git_status + run: | + if git diff --quiet -- docs_health_report.md; then + echo "No changes to report." + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "Health report or badge has changed." + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request with changes + if: steps.git_status.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + BRANCH_NAME="docs-health/update-${{ github.run_id }}" + git checkout -b $BRANCH_NAME + git add docs_health_report.md + git commit -m "docs: Update health report" + git push origin $BRANCH_NAME + gh pr create \ + --base "main" \ + --head "$BRANCH_NAME" \ + --title "docs: Update health report (${{ github.run_id }})" \ + --body "Automated documentation health report update. Please review and merge." diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml new file mode 100644 index 000000000..46bfff0d3 --- /dev/null +++ b/.github/workflows/version-check.yaml @@ -0,0 +1,50 @@ +name: Generate Version Report + +on: + pull_request: + types: [opened, synchronize] + branches: + - main + workflow_dispatch: + +jobs: + version-check: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Generate version report + run: python scripts/generate_version_report.py + + - name: Check for changes + id: git_status + run: | + if git diff --quiet -- docs_health_report.md; then + echo "No changes to report." + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "Version report has changed." + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.git_status.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs_health_report.md + git commit -m "docs: Update version report" + git push diff --git a/docs/agents/index.md b/docs/agents/index.md index c09ec727e..4732e4c3c 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -1,3 +1,7 @@ +--- +version: 1.0.0 +--- + # Agents In the Agent Development Kit (ADK), an **Agent** is a self-contained execution unit designed to act autonomously to achieve specific goals. Agents can perform tasks, interact with users, utilize external tools, and coordinate with other agents. diff --git a/docs_health_report.md b/docs_health_report.md new file mode 100644 index 000000000..96728b05c --- /dev/null +++ b/docs_health_report.md @@ -0,0 +1,112 @@ + +# Sample Documentation Health Report + +**Summary:** **85.7%** of documentation pages were updated in the last 4 weeks. A total of **3** page(s) are considered stale (older than 90 days). + +## Detailed Health by Section + +### Root - ✅ Healthy +All 2 page(s) in this section are up-to-date. + +### agents - ⚠️ Needs Review +2 of 5 page(s) in this section are stale: + +- **docs/agents/custom-agents.md**: Last updated 215 days ago +- **docs/agents/multi-agents.md**: Last updated 198 days ago + +### api-reference - ✅ Healthy +All 3 page(s) in this section are up-to-date. + +### get-started - ⚠️ Needs Review +1 of 4 page(s) in this section are stale: + +- **docs/get-started/quickstart.md**: Last updated 301 days ago + +### tools - ✅ Healthy +All 10 page(s) in this section are up-to-date. + + + + +# Documentation Version Report + +This report provides a summary of the versions of the documentation pages. + +## Version 1.0.0 + +Found 1 pages with this version: + +- [docs/agents/index.md](agents/index.html) + +## Pages without Version Information + +Found 66 pages without version metadata: + +- [docs/index.md](index.html) +- [docs/community.md](community.html) +- [docs/contributing-guide.md](contributing-guide.html) +- [docs/mcp/index.md](mcp/index.html) +- [docs/api-reference/index.md](api-reference/index.html) +- [docs/api-reference/java/legal/jquery.md](api-reference/java/legal/jquery.html) +- [docs/api-reference/java/legal/dejavufonts.md](api-reference/java/legal/dejavufonts.html) +- [docs/api-reference/java/legal/jqueryUI.md](api-reference/java/legal/jqueryUI.html) +- [docs/runtime/index.md](runtime/index.html) +- [docs/runtime/runconfig.md](runtime/runconfig.html) +- [docs/safety/index.md](safety/index.html) +- [docs/evaluate/index.md](evaluate/index.html) +- [docs/deploy/agent-engine.md](deploy/agent-engine.html) +- [docs/deploy/index.md](deploy/index.html) +- [docs/deploy/gke.md](deploy/gke.html) +- [docs/deploy/cloud-run.md](deploy/cloud-run.html) +- [docs/grounding/vertex_ai_search_grounding.md](grounding/vertex_ai_search_grounding.html) +- [docs/grounding/google_search_grounding.md](grounding/google_search_grounding.html) +- [docs/artifacts/index.md](artifacts/index.html) +- [docs/get-started/index.md](get-started/index.html) +- [docs/get-started/installation.md](get-started/installation.html) +- [docs/get-started/about.md](get-started/about.html) +- [docs/get-started/testing.md](get-started/testing.html) +- [docs/get-started/quickstart.md](get-started/quickstart.html) +- [docs/get-started/streaming/index.md](get-started/streaming/index.html) +- [docs/get-started/streaming/quickstart-streaming-java.md](get-started/streaming/quickstart-streaming-java.html) +- [docs/get-started/streaming/quickstart-streaming.md](get-started/streaming/quickstart-streaming.html) +- [docs/streaming/index.md](streaming/index.html) +- [docs/streaming/streaming-tools.md](streaming/streaming-tools.html) +- [docs/streaming/custom-streaming.md](streaming/custom-streaming.html) +- [docs/streaming/custom-streaming-ws.md](streaming/custom-streaming-ws.html) +- [docs/streaming/configuration.md](streaming/configuration.html) +- [docs/streaming/dev-guide/part1.md](streaming/dev-guide/part1.html) +- [docs/tutorials/index.md](tutorials/index.html) +- [docs/tutorials/agent-team.md](tutorials/agent-team.html) +- [docs/observability/weave.md](observability/weave.html) +- [docs/observability/agentops.md](observability/agentops.html) +- [docs/observability/arize-ax.md](observability/arize-ax.html) +- [docs/observability/phoenix.md](observability/phoenix.html) +- [docs/observability/logging.md](observability/logging.html) +- [docs/tools/function-tools.md](tools/function-tools.html) +- [docs/tools/index.md](tools/index.html) +- [docs/tools/third-party-tools.md](tools/third-party-tools.html) +- [docs/tools/google-cloud-tools.md](tools/google-cloud-tools.html) +- [docs/tools/built-in-tools.md](tools/built-in-tools.html) +- [docs/tools/openapi-tools.md](tools/openapi-tools.html) +- [docs/tools/mcp-tools.md](tools/mcp-tools.html) +- [docs/tools/authentication.md](tools/authentication.html) +- [docs/agents/multi-agents.md](agents/multi-agents.html) +- [docs/agents/custom-agents.md](agents/custom-agents.html) +- [docs/agents/llm-agents.md](agents/llm-agents.html) +- [docs/agents/models.md](agents/models.html) +- [docs/agents/workflow-agents/index.md](agents/workflow-agents/index.html) +- [docs/agents/workflow-agents/sequential-agents.md](agents/workflow-agents/sequential-agents.html) +- [docs/agents/workflow-agents/parallel-agents.md](agents/workflow-agents/parallel-agents.html) +- [docs/agents/workflow-agents/loop-agents.md](agents/workflow-agents/loop-agents.html) +- [docs/context/index.md](context/index.html) +- [docs/events/index.md](events/index.html) +- [docs/callbacks/index.md](callbacks/index.html) +- [docs/callbacks/design-patterns-and-best-practices.md](callbacks/design-patterns-and-best-practices.html) +- [docs/callbacks/types-of-callbacks.md](callbacks/types-of-callbacks.html) +- [docs/sessions/index.md](sessions/index.html) +- [docs/sessions/memory.md](sessions/memory.html) +- [docs/sessions/express-mode.md](sessions/express-mode.html) +- [docs/sessions/state.md](sessions/state.html) +- [docs/sessions/session.md](sessions/session.html) + + \ No newline at end of file diff --git a/overrides/main.html b/overrides/main.html index fa266ea52..5291ae5cd 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -36,3 +36,16 @@ {% endblock %} + + +{% block content %} + {{ super() }} + + {% if page.meta.version %} +
+
+ Version: {{ page.meta.version }} +
+
+ {% endif %} +{% endblock %} diff --git a/requirements.txt b/requirements.txt index 0cd7a6a39..ab2aa1507 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ mkdocs-material==9.6.14 mkdocs-redirects==1.2.2 -mkdocs-linkcheck==1.0.6 +mkdocs-linkcheck==1.0.6 \ No newline at end of file diff --git a/scripts/check_docs_health.py b/scripts/check_docs_health.py new file mode 100644 index 000000000..edaa63dbc --- /dev/null +++ b/scripts/check_docs_health.py @@ -0,0 +1,140 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import subprocess +from datetime import datetime, timedelta + +# Configuration +STALENESS_THRESHOLD_DAYS = 90 +RECENT_UPDATE_THRESHOLD_WEEKS = 4 +DOCS_DIRECTORY = "docs" +REPORT_FILENAME = "docs_health_report.md" +HEALTH_REPORT_START_MARKER = "" +HEALTH_REPORT_END_MARKER = "" + +def get_last_commit_date(file_path): + """Gets the last commit date of a file.""" + try: + commit_date_str = subprocess.check_output( + ["git", "log", "-1", "--format=%ci", file_path] + ).decode("utf-8").strip() + return datetime.strptime(commit_date_str, "%Y-%m-%d %H:%M:%S %z") + except subprocess.CalledProcessError: + return None + +def check_docs_health(): + """Checks the health of the documentation and generates a detailed report.""" + section_stats = {} + recently_updated_count = 0 + total_docs_count = 0 + total_stale_files = 0 + now = datetime.now(datetime.now().astimezone().tzinfo) + staleness_threshold = now - timedelta(days=STALENESS_THRESHOLD_DAYS) + recent_update_threshold = now - timedelta(weeks=RECENT_UPDATE_THRESHOLD_WEEKS) + + # Change to the adk-docs directory + original_cwd = os.getcwd() + script_dir = os.path.dirname(os.path.abspath(__file__)) + adk_docs_root = os.path.abspath(os.path.join(script_dir, "..")) + os.chdir(adk_docs_root) + + for root, _, files in os.walk(DOCS_DIRECTORY): + for file in files: + if file.endswith(".md"): + total_docs_count += 1 + file_path = os.path.join(root, file) + section = os.path.basename(root) if root != DOCS_DIRECTORY else "Root" + + section_stats.setdefault(section, { + "total_files": 0, + "stale_files": [] + }) + section_stats[section]["total_files"] += 1 + + last_commit_date = get_last_commit_date(file_path) + + if last_commit_date: + if last_commit_date < staleness_threshold: + section_stats[section]["stale_files"].append( + (file_path, now - last_commit_date) + ) + total_stale_files += 1 + + if last_commit_date > recent_update_threshold: + recently_updated_count += 1 + + # Revert to original working directory + os.chdir(original_cwd) + + recent_percentage = (recently_updated_count / total_docs_count * 100) if total_docs_count > 0 else 0 + exit_code = 0 if total_stale_files == 0 else 1 + + # --- Generate the new report content --- + report_parts = [f"{HEALTH_REPORT_START_MARKER}\n"] + report_parts.append("# Documentation Health Report\n\n") + report_parts.append(f"**Summary:** **{recent_percentage:.1f}%** of documentation pages were updated in the last " + f"{RECENT_UPDATE_THRESHOLD_WEEKS} weeks. ") + report_parts.append(f"A total of **{total_stale_files}** page(s) are considered stale (older than {STALENESS_THRESHOLD_DAYS} days).\n\n") + + if total_stale_files == 0: + report_parts.append("**All documentation is up-to-date!**\n") + else: + report_parts.append("## Detailed Health by Section\n\n") + for section, stats in sorted(section_stats.items()): + stale_count = len(stats["stale_files"]) + if stale_count == 0: + report_parts.append(f"### {section} - ✅ Healthy\n") + report_parts.append(f"All {stats['total_files']} page(s) in this section are up-to-date.\n\n") + else: + report_parts.append(f"### {section} - ⚠️ Needs Review\n") + report_parts.append(f"{stale_count} of {stats['total_files']} page(s) in this section are stale:\n\n") + for file_path, days_since_update in stats["stale_files"]: + report_parts.append(f"- **{file_path}**: Last updated {days_since_update.days} days ago\n") + report_parts.append("\n") + report_parts.append(HEALTH_REPORT_END_MARKER) + + report_content = "".join(report_parts) + + # --- Read the existing report and replace the health section --- + report_path = os.path.join(adk_docs_root, REPORT_FILENAME) + try: + with open(report_path, "r") as f: + existing_content = f.read() + except FileNotFoundError: + existing_content = "" + + pattern = re.compile(f"{HEALTH_REPORT_START_MARKER}.*{HEALTH_REPORT_END_MARKER}", re.DOTALL) + + if pattern.search(existing_content): + new_full_content = pattern.sub(report_content, existing_content) + else: + new_full_content = existing_content + "\n\n" + report_content + + with open(report_path, "w") as f: + f.write(new_full_content) + + print("Docs Health Analysis done.") + # (Not used currently) Set outputs for GitHub Actions + # To be used if SVG badge is to be displayed in README. + if 'GITHUB_OUTPUT' in os.environ: + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"recent_percentage={recent_percentage:.1f}\n") + f.write(f"exit_code={exit_code}\n") + + return exit_code + +if __name__ == "__main__": + exit(check_docs_health()) diff --git a/scripts/generate_version_report.py b/scripts/generate_version_report.py new file mode 100644 index 000000000..4270579d3 --- /dev/null +++ b/scripts/generate_version_report.py @@ -0,0 +1,93 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +from collections import defaultdict + +DOCS_DIRECTORY = "docs" +REPORT_FILENAME = "docs_health_report.md" +VERSION_REPORT_START_MARKER = "" +VERSION_REPORT_END_MARKER = "" + +def generate_version_report(): + version_map = defaultdict(list) + files_without_version = [] + + for root, _, files in os.walk(DOCS_DIRECTORY): + for file in files: + if file.endswith(".md"): + filepath = os.path.join(root, file) + with open(filepath, "r") as f: + content = f.read() + if content.startswith("---"): + front_matter = content.split("---")[1] + match = re.search(r"version:\s*(.*)", front_matter) + if match: + version = match.group(1).strip() + version_map[version].append(filepath) + else: + files_without_version.append(filepath) + else: + files_without_version.append(filepath) + + # --- Generate the new report content --- + report_parts = [f"{VERSION_REPORT_START_MARKER}\n"] + report_parts.append("# Documentation Version Report\n\n") + report_parts.append("This report provides a summary of the versions of the documentation pages.\n\n") + + for version, files in sorted(version_map.items()): + report_parts.append(f"## Version {version}\n\n") + report_parts.append(f"Found {len(files)} pages with this version:\n\n") + for file in files: + link = os.path.relpath(file, "docs").replace(".md", ".html") + report_parts.append(f"- [{file}]({link})\n") + report_parts.append("\n") + + if files_without_version: + report_parts.append("## Pages without Version Information\n\n") + report_parts.append(f"Found {len(files_without_version)} pages without version metadata:\n\n") + for file in files_without_version: + link = os.path.relpath(file, "docs").replace(".md", ".html") + report_parts.append(f"- [{file}]({link})\n") + report_parts.append("\n") + report_parts.append(VERSION_REPORT_END_MARKER) + + report_content = "".join(report_parts) + + # --- Read the existing report and replace the version section --- + try: + with open(REPORT_FILENAME, "r") as f: + existing_content = f.read() + except FileNotFoundError: + existing_content = "" + + # Use a regex to replace the content between the markers, or append if not found + # The re.DOTALL flag allows "." to match newlines + pattern = re.compile(f"{VERSION_REPORT_START_MARKER}.*{VERSION_REPORT_END_MARKER}", re.DOTALL) + + if pattern.search(existing_content): + new_full_content = pattern.sub(report_content, existing_content) + else: + # If the markers aren't found, append the new report with a newline + new_full_content = existing_content + "\n\n" + report_content + + print(f"\nWriting version report to: {REPORT_FILENAME}") + with open(REPORT_FILENAME, "w") as f: + f.write(new_full_content) + + print(f"\nVersion report analysis done.") + +if __name__ == "__main__": + generate_version_report()