From 4528e1f16e62b019f15eae2ce5bd0301f5d2996c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Feb 2026 19:08:18 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20Add=20Jules=20maintenance=20work?= =?UTF-8?q?flows=20for=20dead=20code=20cleanup=20and=20doc=20drift=20detec?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two automated Jules workflows (Wednesdays 2pm/4pm UTC) with policy guideline files and an onboarding step to enable/disable them. Co-Authored-By: Claude Opus 4.6 --- .../workflows/jules-find-outdated-docs.yml | 228 ++++++++++++++++++ .../jules-prune-unnecessary-code.yml | 222 +++++++++++++++++ OUTDATED_DOCUMENTATION_GUIDELINE.md | 87 +++++++ UNNECESSARY_CODE_GUIDELINE.md | 77 ++++++ onboard.py | 105 +++++++- 5 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/jules-find-outdated-docs.yml create mode 100644 .github/workflows/jules-prune-unnecessary-code.yml create mode 100644 OUTDATED_DOCUMENTATION_GUIDELINE.md create mode 100644 UNNECESSARY_CODE_GUIDELINE.md diff --git a/.github/workflows/jules-find-outdated-docs.yml b/.github/workflows/jules-find-outdated-docs.yml new file mode 100644 index 0000000..af9cd40 --- /dev/null +++ b/.github/workflows/jules-find-outdated-docs.yml @@ -0,0 +1,228 @@ +# ───────────────────────────────────────────────────────────────────── +# Jules Outdated Documentation Drift Detection +# ───────────────────────────────────────────────────────────────────── +# Automated weekly documentation drift check via Jules API. Runs every +# Wednesday at 4pm UTC and on manual dispatch. Opens a PR with +# any corrections. +# +# Jules reads OUTDATED_DOCUMENTATION_GUIDELINE.md before making changes. +# +# Required secrets: JULES_API_KEY +# ───────────────────────────────────────────────────────────────────── +name: Jules Find Outdated Docs + +on: + schedule: + - cron: "0 16 * * 3" # Wednesday 4pm UTC + workflow_dispatch: + +permissions: + contents: read + actions: read + +concurrency: + group: jules-find-outdated-docs + cancel-in-progress: false + +env: + JULES_API_BASE: "https://jules.googleapis.com/v1alpha" + MAX_POLL_ATTEMPTS: 60 + POLL_INTERVAL_SECONDS: 30 + +jobs: + find-outdated-docs: + name: Detect and fix outdated documentation via Jules + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + # ── 1. Checkout ─────────────────────────────────────────────── + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # ── 2. Create Jules documentation drift session ─────────────── + - name: Create Jules documentation drift session + id: jules-create + env: + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + run: | + set -euo pipefail + + if [[ -z "${JULES_API_KEY:-}" ]]; then + echo "::error::JULES_API_KEY is not configured. Add it in repository Actions secrets." + exit 1 + fi + + REPO="${{ github.repository }}" + OWNER="${REPO%%/*}" + REPO_NAME="${REPO##*/}" + SOURCE="sources/github/${OWNER}/${REPO_NAME}" + + PROMPT="IMPORTANT: Before you begin, read the file OUTDATED_DOCUMENTATION_GUIDELINE.md in the root of this repository. + It contains the rules and validation requirements you must follow strictly when detecting + and fixing outdated documentation. + + Also read docs/translation-guide.md if it exists, to understand the translation policy. + Any documentation changes you make must respect the translation workflow - only edit English + source files, never edit translation files directly. + + You are a documentation maintenance assistant for this project. + + Scan the repository for documentation drift including: + - README.md sections that no longer match actual project behavior + - CLAUDE.md instructions that reference moved, renamed, or deleted files + - Docstrings that describe outdated function signatures or behavior + - Code examples in docs that use deprecated APIs or patterns + - Configuration references that point to renamed or removed settings + - Broken internal cross-references between documentation files + - Changelog entries that reference incorrect versions or dates + - Install/setup instructions that skip required steps or list wrong commands + + Strict rules - you MUST follow every one: + 1. Follow ALL rules in OUTDATED_DOCUMENTATION_GUIDELINE.md without exception. + 2. Only fix documentation that is verifiably incorrect by cross-referencing the actual code. + 3. Do NOT rewrite documentation style, tone, or structure - only fix factual inaccuracies. + 4. Do NOT add new documentation sections - only update existing content. + 5. Do NOT modify translation files (*.lang.mdx, meta.lang.json) - only English sources. + 6. For each fix, include the evidence (file path and line) showing why the doc is wrong. + 7. Preserve all existing formatting, heading levels, and link structures. + 8. If a referenced file was moved, update the path. If deleted, note the removal clearly. + 9. Group related fixes into logical commits with clear messages. + 10. If no outdated documentation is found, do not create a PR." + + TITLE="docs: fix outdated documentation (automated weekly drift check)" + + BODY=$(jq -n \ + --arg prompt "$PROMPT" \ + --arg title "$TITLE" \ + --arg source "$SOURCE" \ + '{ + prompt: $prompt, + title: $title, + automationMode: "AUTO_CREATE_PR", + sourceContext: { + source: $source, + githubRepoContext: { + startingBranch: "main" + } + } + }') + + MAX_RETRIES=3 + RETRY_DELAY=5 + + for attempt in $(seq 1 "$MAX_RETRIES"); do + HTTP_CODE=$(curl -s -o /tmp/jules_response.json -w "%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -H "X-Goog-Api-Key: ${JULES_API_KEY}" \ + -d "$BODY" \ + "${JULES_API_BASE}/sessions") + + if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then + break + elif [[ "$HTTP_CODE" -ge 500 && $attempt -lt $MAX_RETRIES ]]; then + echo "::warning::Jules API returned ${HTTP_CODE} - retrying in ${RETRY_DELAY}s (attempt ${attempt}/${MAX_RETRIES})" + sleep "$RETRY_DELAY" + RETRY_DELAY=$((RETRY_DELAY * 2)) + else + echo "::error::Jules session creation failed with HTTP ${HTTP_CODE}" + cat /tmp/jules_response.json + exit 1 + fi + done + + SESSION_NAME=$(jq -r '.name' /tmp/jules_response.json) + SESSION_URL=$(jq -r '.url // empty' /tmp/jules_response.json) + + if [[ -z "$SESSION_NAME" || "$SESSION_NAME" == "null" ]]; then + echo "::error::No session name in Jules response" + cat /tmp/jules_response.json + exit 1 + fi + + { + echo "session_name<> "$GITHUB_OUTPUT" + + echo "Jules session created: ${SESSION_NAME}" + echo "Jules UI: ${SESSION_URL}" + + # ── 3. Poll until Jules finishes ────────────────────────────── + - name: Poll Jules session until completion + id: jules-poll + env: + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + JULES_SESSION_NAME: ${{ steps.jules-create.outputs.session_name }} + run: | + set -euo pipefail + + SESSION_NAME="$JULES_SESSION_NAME" + MAX_ATTEMPTS="$MAX_POLL_ATTEMPTS" + INTERVAL="$POLL_INTERVAL_SECONDS" + + echo "Polling ${SESSION_NAME} (max ${MAX_ATTEMPTS} x ${INTERVAL}s)" + + for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + RETRY_DELAY=5 + for retry in 1 2 3; do + HTTP_CODE=$(curl -s -o /tmp/jules_session.json -w "%{http_code}" \ + -H "X-Goog-Api-Key: ${JULES_API_KEY}" \ + "${JULES_API_BASE}/${SESSION_NAME}") + + if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then + break + elif [[ $retry -lt 3 ]]; then + echo "::warning::Poll returned ${HTTP_CODE} - retry ${retry}/3 in ${RETRY_DELAY}s" + sleep "$RETRY_DELAY" + RETRY_DELAY=$((RETRY_DELAY * 2)) + else + echo "::error::Failed to poll session after 3 retries (HTTP ${HTTP_CODE})" + exit 1 + fi + done + + STATE=$(jq -r '.state // "UNKNOWN"' /tmp/jules_session.json) + echo "[${attempt}/${MAX_ATTEMPTS}] state=${STATE}" + + case "$STATE" in + COMPLETED) + PR_URL=$(jq -r '.outputs[0].pullRequest.url // empty' /tmp/jules_session.json) + PR_TITLE=$(jq -r '.outputs[0].pullRequest.title // empty' /tmp/jules_session.json) + + { + echo "jules_state<> "$GITHUB_OUTPUT" + + echo "Session completed. PR: ${PR_URL}" + exit 0 + ;; + FAILED) + echo "jules_state=FAILED" >> "$GITHUB_OUTPUT" + echo "::error::Jules session ended in FAILED state" + jq '.' /tmp/jules_session.json + exit 1 + ;; + esac + + sleep "$INTERVAL" + done + + echo "jules_state=TIMEOUT" >> "$GITHUB_OUTPUT" + echo "::error::Timed out after ${MAX_ATTEMPTS} poll attempts" + exit 1 diff --git a/.github/workflows/jules-prune-unnecessary-code.yml b/.github/workflows/jules-prune-unnecessary-code.yml new file mode 100644 index 0000000..7a45dad --- /dev/null +++ b/.github/workflows/jules-prune-unnecessary-code.yml @@ -0,0 +1,222 @@ +# ───────────────────────────────────────────────────────────────────── +# Jules Unnecessary Code Cleanup +# ───────────────────────────────────────────────────────────────────── +# Automated weekly dead code cleanup via Jules API. Runs every +# Wednesday at 2pm UTC and on manual dispatch. Opens a PR with +# any removals. +# +# Jules reads UNNECESSARY_CODE_GUIDELINE.md before making changes. +# +# Required secrets: JULES_API_KEY +# ───────────────────────────────────────────────────────────────────── +name: Jules Prune Unnecessary Code + +on: + schedule: + - cron: "0 14 * * 3" # Wednesday 2pm UTC + workflow_dispatch: + +permissions: + contents: read + actions: read + +concurrency: + group: jules-prune-unnecessary-code + cancel-in-progress: false + +env: + JULES_API_BASE: "https://jules.googleapis.com/v1alpha" + MAX_POLL_ATTEMPTS: 60 + POLL_INTERVAL_SECONDS: 30 + +jobs: + prune-unnecessary-code: + name: Find and remove unnecessary code via Jules + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + # ── 1. Checkout ─────────────────────────────────────────────── + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # ── 2. Create Jules cleanup session ─────────────────────────── + - name: Create Jules cleanup session + id: jules-create + env: + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + run: | + set -euo pipefail + + if [[ -z "${JULES_API_KEY:-}" ]]; then + echo "::error::JULES_API_KEY is not configured. Add it in repository Actions secrets." + exit 1 + fi + + REPO="${{ github.repository }}" + OWNER="${REPO%%/*}" + REPO_NAME="${REPO##*/}" + SOURCE="sources/github/${OWNER}/${REPO_NAME}" + + PROMPT="IMPORTANT: Before you begin, read the file UNNECESSARY_CODE_GUIDELINE.md in the root of this repository. + It contains the conservative rules and protected patterns you must follow strictly when identifying + and removing unnecessary code. + + You are a careful code maintenance assistant for this Python project. + + Scan the codebase for unnecessary code including: + - Dead functions, methods, or classes that are never called or referenced + - Unused imports + - Commented-out code blocks (not explanatory comments) + - Unreachable code paths + - Unused variables and constants + - Deprecated compatibility shims that are no longer needed + + Strict rules - you MUST follow every one: + 1. Follow ALL rules in UNNECESSARY_CODE_GUIDELINE.md without exception. + 2. Only remove code you are confident is unused. If in doubt, leave it. + 3. Do NOT remove any test files, test functions, fixtures, or conftest code. + 4. Do NOT remove any code referenced in __init__.py exports or __all__ lists. + 5. Do NOT remove any code decorated with @app, @router, or framework decorators. + 6. Do NOT remove any code that might be used via dynamic import or reflection. + 7. Preserve all public API surfaces - only remove clearly internal dead code. + 8. Each removal must be explained in the PR description with rationale. + 9. Group related removals into logical commits with clear messages. + 10. If no unnecessary code is found, do not create a PR." + + TITLE="chore: prune unnecessary code (automated weekly cleanup)" + + BODY=$(jq -n \ + --arg prompt "$PROMPT" \ + --arg title "$TITLE" \ + --arg source "$SOURCE" \ + '{ + prompt: $prompt, + title: $title, + automationMode: "AUTO_CREATE_PR", + sourceContext: { + source: $source, + githubRepoContext: { + startingBranch: "main" + } + } + }') + + MAX_RETRIES=3 + RETRY_DELAY=5 + + for attempt in $(seq 1 "$MAX_RETRIES"); do + HTTP_CODE=$(curl -s -o /tmp/jules_response.json -w "%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -H "X-Goog-Api-Key: ${JULES_API_KEY}" \ + -d "$BODY" \ + "${JULES_API_BASE}/sessions") + + if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then + break + elif [[ "$HTTP_CODE" -ge 500 && $attempt -lt $MAX_RETRIES ]]; then + echo "::warning::Jules API returned ${HTTP_CODE} - retrying in ${RETRY_DELAY}s (attempt ${attempt}/${MAX_RETRIES})" + sleep "$RETRY_DELAY" + RETRY_DELAY=$((RETRY_DELAY * 2)) + else + echo "::error::Jules session creation failed with HTTP ${HTTP_CODE}" + cat /tmp/jules_response.json + exit 1 + fi + done + + SESSION_NAME=$(jq -r '.name' /tmp/jules_response.json) + SESSION_URL=$(jq -r '.url // empty' /tmp/jules_response.json) + + if [[ -z "$SESSION_NAME" || "$SESSION_NAME" == "null" ]]; then + echo "::error::No session name in Jules response" + cat /tmp/jules_response.json + exit 1 + fi + + { + echo "session_name<> "$GITHUB_OUTPUT" + + echo "Jules session created: ${SESSION_NAME}" + echo "Jules UI: ${SESSION_URL}" + + # ── 3. Poll until Jules finishes ────────────────────────────── + - name: Poll Jules session until completion + id: jules-poll + env: + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + JULES_SESSION_NAME: ${{ steps.jules-create.outputs.session_name }} + run: | + set -euo pipefail + + SESSION_NAME="$JULES_SESSION_NAME" + MAX_ATTEMPTS="$MAX_POLL_ATTEMPTS" + INTERVAL="$POLL_INTERVAL_SECONDS" + + echo "Polling ${SESSION_NAME} (max ${MAX_ATTEMPTS} x ${INTERVAL}s)" + + for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + RETRY_DELAY=5 + for retry in 1 2 3; do + HTTP_CODE=$(curl -s -o /tmp/jules_session.json -w "%{http_code}" \ + -H "X-Goog-Api-Key: ${JULES_API_KEY}" \ + "${JULES_API_BASE}/${SESSION_NAME}") + + if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then + break + elif [[ $retry -lt 3 ]]; then + echo "::warning::Poll returned ${HTTP_CODE} - retry ${retry}/3 in ${RETRY_DELAY}s" + sleep "$RETRY_DELAY" + RETRY_DELAY=$((RETRY_DELAY * 2)) + else + echo "::error::Failed to poll session after 3 retries (HTTP ${HTTP_CODE})" + exit 1 + fi + done + + STATE=$(jq -r '.state // "UNKNOWN"' /tmp/jules_session.json) + echo "[${attempt}/${MAX_ATTEMPTS}] state=${STATE}" + + case "$STATE" in + COMPLETED) + PR_URL=$(jq -r '.outputs[0].pullRequest.url // empty' /tmp/jules_session.json) + PR_TITLE=$(jq -r '.outputs[0].pullRequest.title // empty' /tmp/jules_session.json) + + { + echo "jules_state<> "$GITHUB_OUTPUT" + + echo "Session completed. PR: ${PR_URL}" + exit 0 + ;; + FAILED) + echo "jules_state=FAILED" >> "$GITHUB_OUTPUT" + echo "::error::Jules session ended in FAILED state" + jq '.' /tmp/jules_session.json + exit 1 + ;; + esac + + sleep "$INTERVAL" + done + + echo "jules_state=TIMEOUT" >> "$GITHUB_OUTPUT" + echo "::error::Timed out after ${MAX_ATTEMPTS} poll attempts" + exit 1 diff --git a/OUTDATED_DOCUMENTATION_GUIDELINE.md b/OUTDATED_DOCUMENTATION_GUIDELINE.md new file mode 100644 index 0000000..d728c4f --- /dev/null +++ b/OUTDATED_DOCUMENTATION_GUIDELINE.md @@ -0,0 +1,87 @@ +# Outdated Documentation Guideline + +This document defines the rules Jules must follow when detecting and fixing +documentation drift in this repository. + +## Philosophy + +Documentation must accurately reflect the current state of the code. We fix +**factual inaccuracies only** - never rewrite style, tone, or structure. Every +fix must be backed by concrete evidence from the codebase. + +## What Counts as Outdated Documentation + +- **Wrong file paths**: References to files or directories that have been moved, + renamed, or deleted. +- **Incorrect commands**: CLI commands, Make targets, or setup instructions that + no longer work as described. +- **Stale API references**: Docstrings or docs describing function signatures, + parameters, or return types that no longer match the actual code. +- **Deprecated patterns**: Code examples showing usage patterns that have been + replaced (e.g., `datetime.utcnow()` instead of `datetime.now(timezone.utc)`). +- **Wrong configuration keys**: References to config keys that have been renamed + or removed from `global_config.yaml` or `pyproject.toml`. +- **Broken cross-references**: Internal links between docs that point to + non-existent anchors or files. +- **Incorrect version requirements**: Python version, dependency versions, or + tool versions that don't match `pyproject.toml`. + +## What Is NOT Outdated Documentation + +Do **not** flag or modify: + +- **Style preferences**: Wording choices, tone, or paragraph structure. +- **Aspirational content**: Roadmap items, planned features, or TODOs. +- **External links**: Links to third-party sites (these are checked by + `make lint_links` separately). +- **Translation files**: Never edit `*.lang.mdx` or `meta.lang.json` files + directly. The Jules Translation Sync workflow handles translations + automatically when English sources change. +- **Generated content**: Auto-generated API docs, badges, or CI status lines. + +## Translation Policy Alignment + +This repository uses an automated Jules Translation Sync workflow. When you +fix English documentation: + +1. **Only edit English source files** (e.g., `file.mdx`, not `file.ja.mdx`). +2. **Never create or modify translation files** - the translation workflow will + pick up your English changes and translate them automatically. +3. **Preserve frontmatter and MDX structure** exactly as-is so translations + remain aligned. +4. If you find a translation file that is outdated relative to its English + source, note it in the PR description but do **not** fix it directly. + +## Files to Scan + +Priority order for drift detection: + +1. `README.md` - Primary entry point, most visible. +2. `CLAUDE.md` - Agent instructions, must match actual project structure. +3. `docs/content/**/*.mdx` - English documentation pages only. +4. `AGENTS.md` - Agent configuration, if present. +5. Docstrings in `src/`, `common/`, `utils/` - Inline documentation. +6. `pyproject.toml` description and metadata fields. + +## Validation Before Fixing + +For each documentation fix, verify: + +1. **Cross-reference the code**: Confirm the actual current behavior by reading + the relevant source file. Never guess. +2. **Check git history**: If a file was recently moved, use the new path. If + deleted, note the removal. +3. **Test commands**: For CLI/Make instructions, verify the command exists in the + Makefile or CLI entry point. +4. **Config key existence**: For config references, verify the key exists in the + current `global_config.yaml` or relevant config file. + +## PR Requirements + +- Title format: `docs: fix outdated documentation (automated weekly drift check)` +- Each fix must be listed in the PR description with: + - File path and line range + - What was wrong (old content) + - What was fixed (new content) + - Evidence file path showing the correct information +- If no outdated documentation is found, do **not** create a PR. diff --git a/UNNECESSARY_CODE_GUIDELINE.md b/UNNECESSARY_CODE_GUIDELINE.md new file mode 100644 index 0000000..c880e8d --- /dev/null +++ b/UNNECESSARY_CODE_GUIDELINE.md @@ -0,0 +1,77 @@ +# Unnecessary Code Cleanup Guideline + +This document defines the conservative rules Jules must follow when identifying +and removing unnecessary (dead) code from this repository. + +## Philosophy + +We prefer **false negatives over false positives**. It is better to leave +questionable code in place than to remove something that turns out to be needed. +Every removal must be justified with concrete evidence that the code is unused. + +## What Counts as Unnecessary Code + +- **Dead functions/methods/classes**: Defined but never called, imported, or + referenced anywhere in the codebase (including tests). +- **Unused imports**: Imports that are not referenced in the file. +- **Commented-out code**: Blocks of commented-out Python/JS code (not + explanatory comments or TODOs). +- **Unreachable code**: Code after unconditional `return`, `raise`, `break`, or + `continue` statements. +- **Unused variables/constants**: Assigned but never read. Excludes `_` + convention variables. +- **Stale compatibility shims**: Version checks or feature flags for versions + no longer supported per `pyproject.toml` `requires-python`. + +## Protected Patterns - NEVER Remove + +The following patterns must **never** be removed, even if they appear unused by +static analysis: + +1. **FastAPI/Starlette routes**: Any function decorated with `@app`, `@router`, + or framework routing decorators. +2. **SQLAlchemy/Pydantic models**: Model classes, even if not directly imported + elsewhere (they may be used via ORM relationships or migrations). +3. **Celery/RQ tasks**: Functions decorated with `@task`, `@shared_task`, or + registered as background workers. +4. **Signal handlers and hooks**: Functions connected to framework signals, + event hooks, or lifecycle callbacks. +5. **`__init__.py` exports**: Anything listed in `__all__` or imported in + `__init__.py` for re-export. +6. **CLI entry points**: Functions referenced in `pyproject.toml` + `[project.scripts]` or Typer/Click commands. +7. **Test fixtures**: pytest fixtures, conftest definitions, and test helper + functions in `tests/`. +8. **Configuration classes**: Pydantic `Settings` or `BaseModel` subclasses in + `common/`. +9. **Dunder methods**: `__str__`, `__repr__`, `__hash__`, `__eq__`, etc. +10. **Security-related code**: Authentication, authorization, rate limiting, + input validation, and CSRF protection code. +11. **Migration files**: Database migration scripts. +12. **Protocol/ABC implementations**: Methods implementing an abstract base + class or Protocol interface. +13. **Vulture whitelist entries**: Anything referenced in + `pyproject.toml [tool.vulture]` exclusions. + +## Validation Before Removal + +For each piece of code you plan to remove, verify: + +1. **No dynamic references**: Search for string-based lookups like `getattr()`, + `importlib.import_module()`, `globals()`, or `locals()` that might reference it. +2. **No config references**: Check YAML, JSON, TOML, and `.env` files for + references to the symbol name. +3. **No test references**: Search `tests/` directory for any usage. +4. **No documentation references**: Check that docs don't describe the code as + part of the public API. +5. **No cross-repo usage**: If this is a library/package, the code may be used + by consumers. + +## PR Requirements + +- Title format: `chore: prune unnecessary code (automated weekly cleanup)` +- Each removal must be listed in the PR description with: + - File path and symbol name + - Evidence it is unused (e.g., "zero references found in codebase") +- Group removals by file, not by type. +- If no unnecessary code is found, do **not** create a PR. diff --git a/onboard.py b/onboard.py index e314096..994372a 100644 --- a/onboard.py +++ b/onboard.py @@ -48,6 +48,7 @@ def _validate_kebab_case(value: str) -> bool | str: ("Environment Variables", "env"), ("Pre-commit Hooks", "hooks"), ("Media Generation", "media"), + ("Jules Workflows", "jules"), ] STEP_FUNCTIONS: dict[str, object] = {} @@ -64,7 +65,8 @@ def _run_orchestrator() -> None: " 2. Dependencies - Install project dependencies\n" " 3. Environment - Configure API keys and secrets\n" " 4. Hooks - Activate pre-commit hooks\n" - " 5. Media - Generate banner and logo assets", + " 5. Media - Generate banner and logo assets\n" + " 6. Jules - Enable/disable automated maintenance workflows", title="Welcome to Project Onboarding", border_style="blue", ) @@ -536,6 +538,106 @@ def media() -> None: rprint(f" {f}") +_JULES_WORKFLOWS: list[tuple[str, str]] = [ + ( + "jules-prune-unnecessary-code.yml", + "Dead code cleanup (Wednesdays 2pm UTC)", + ), + ( + "jules-find-outdated-docs.yml", + "Documentation drift check (Wednesdays 4pm UTC)", + ), +] + +_WORKFLOWS_DIR = PROJECT_ROOT / ".github" / "workflows" + + +def _workflow_enabled(filename: str) -> bool: + """Check if a Jules workflow file is enabled (not disabled).""" + return (_WORKFLOWS_DIR / filename).exists() and not ( + _WORKFLOWS_DIR / f"{filename}.disabled" + ).exists() + + +def _enable_workflow(filename: str) -> None: + """Enable a workflow by renaming .disabled back to .yml.""" + disabled = _WORKFLOWS_DIR / f"{filename}.disabled" + enabled = _WORKFLOWS_DIR / filename + if disabled.exists() and not enabled.exists(): + disabled.rename(enabled) + + +def _disable_workflow(filename: str) -> None: + """Disable a workflow by renaming .yml to .yml.disabled.""" + enabled = _WORKFLOWS_DIR / filename + if enabled.exists(): + enabled.rename(_WORKFLOWS_DIR / f"{filename}.disabled") + + +@app.command() +def jules() -> None: + """Step 6: Enable or disable automated Jules maintenance workflows.""" + if not _WORKFLOWS_DIR.is_dir(): + rprint("[red]✗ .github/workflows/ directory not found.[/red]") + raise typer.Exit(code=1) + + table = Table(title="Jules Maintenance Workflows") + table.add_column("Workflow", style="cyan") + table.add_column("Schedule", style="white") + table.add_column("Status", style="white") + + for filename, description in _JULES_WORKFLOWS: + enabled = _workflow_enabled(filename) + status = "[green]enabled[/green]" if enabled else "[yellow]disabled[/yellow]" + table.add_row(filename, description, status) + + console.print(table) + rprint("") + + choices = [] + for filename, description in _JULES_WORKFLOWS: + enabled = _workflow_enabled(filename) + label = f"{description}" + if enabled: + label += " (enabled)" + choices.append(questionary.Choice(title=label, value=filename, checked=enabled)) + + selected = questionary.checkbox( + "Select which Jules workflows to enable:", + choices=choices, + ).ask() + if selected is None: + raise typer.Abort() + + selected_set = set(selected) + changes: list[str] = [] + + for filename, description in _JULES_WORKFLOWS: + was_enabled = _workflow_enabled(filename) + should_enable = filename in selected_set + + if should_enable and not was_enabled: + _enable_workflow(filename) + changes.append(f"[green]✓[/green] Enabled {description}") + elif not should_enable and was_enabled: + _disable_workflow(filename) + changes.append(f"[yellow]-[/yellow] Disabled {description}") + elif should_enable: + changes.append(f"[blue]·[/blue] {description} (already enabled)") + else: + changes.append(f"[blue]·[/blue] {description} (already disabled)") + + rprint( + Panel( + "\n".join(changes) + + "\n\n[dim]Note: JULES_API_KEY secret must be configured in " + "repository Actions settings.[/dim]", + title="Jules Workflows", + border_style="green", + ) + ) + + # Register step functions for the orchestrator STEP_FUNCTIONS.update( { @@ -544,6 +646,7 @@ def media() -> None: "env": env, "hooks": hooks, "media": media, + "jules": jules, } )