diff --git a/.github/workflows/sdlc-orchestrator.yml b/.github/workflows/sdlc-orchestrator.yml new file mode 100644 index 0000000..643f576 --- /dev/null +++ b/.github/workflows/sdlc-orchestrator.yml @@ -0,0 +1,75 @@ +name: Tutti SDLC Orchestrator + +on: + workflow_dispatch: + inputs: + mode: + description: "Workflow mode to execute" + required: true + default: "smoke" + type: choice + options: + - smoke + - auto + issue_label: + description: "GitHub issue label to select (for automation scripts)" + required: false + default: "agent-ops" + +jobs: + orchestrate: + runs-on: ubuntu-latest + concurrency: + group: sdlc-orchestrator-${{ github.repository }} + cancel-in-progress: false + permissions: + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Create automation config + run: | + cp docs/examples/tutti-codex-sdlc.toml tutti.toml + + - name: Verify gh auth + env: + GH_TOKEN: ${{ github.token }} + run: | + gh auth status + + - name: Preflight tests + run: cargo test -q + + - name: Run smoke workflow + if: ${{ inputs.mode == 'smoke' }} + env: + GH_TOKEN: ${{ github.token }} + ISSUE_LABEL: ${{ inputs.issue_label }} + run: | + cargo run --quiet -- run sdlc-smoke --strict + + - name: Verify codex CLI is available + if: ${{ inputs.mode == 'auto' }} + run: | + command -v codex >/dev/null || { echo "codex CLI not found on runner"; exit 1; } + + - name: Run full auto workflow + if: ${{ inputs.mode == 'auto' }} + env: + GH_TOKEN: ${{ github.token }} + ISSUE_LABEL: ${{ inputs.issue_label }} + run: | + cargo run --quiet -- run sdlc-auto --strict diff --git a/docs/AGENT_OPS_ROADMAP.md b/docs/AGENT_OPS_ROADMAP.md new file mode 100644 index 0000000..ab2ec38 --- /dev/null +++ b/docs/AGENT_OPS_ROADMAP.md @@ -0,0 +1,99 @@ +# Tutti Agent Operations Roadmap (Execution Plan) + +## Goal +Position Tutti as the agent operations layer (not just a coding assistant) by closing reliability, governance, observability, and adoption gaps. + +## Priorities + +### P0 — Foundation (must ship first) +1. **Identity + Auth + RBAC** + - User/org/workspace identities + - API keys and role-based permissions + - Audit trail for sensitive actions + +2. **Production Control Plane Hardening** + - Stable API contracts + - Durable event stream semantics + - Idempotency for mutating endpoints + - Queryable run/event history + +3. **Reliability Primitives** + - Retry policies + backoff + - Checkpoint/resume guarantees + - Dead-letter handling for failed actions + - Circuit breakers / provider failover + +4. **Approval and Guardrail Workflows** + - Human approval gates for risky actions + - Policy-as-code enforcement + - Exception handling with traceability + +5. **Operator Observability** + - Per-run timeline and failure stages + - Root cause attribution (routing/tool/model/policy) + - SLO dashboards and alerting hooks + +### P1 — Scale and Governance +6. **Cost Governance** + - Workspace/agent budgets + - Forecasting and anomaly detection + - Hard-stop + escalation behavior + +7. **Deployment and Operations Story** + - Managed and self-hosted deployment paths + - Secrets/backup/migration tooling + - Upgrade safety and rollback docs + +8. **Ecosystem + Templates** + - Connector starter set (CRM, ticketing, docs, messaging) + - Template arrangements/workflows by use case + - Under 30 min time-to-first-value path + +9. **Trust and Compliance Pack** + - Security posture docs + - Data retention/export controls + - Auditability and incident response basics + +10. **Positioning + Onboarding Narrative** + - Replace generic “agent framework” messaging + - “Run reliable agent operations in one day” onboarding flow + - Product-led proof of value in first session + +## Telemetry Plan (parallel to all milestones) + +### North Star +- **Activation rate:** installs that reach first successful workflow run within 24h + +### Event instrumentation +- Install: `tutti_install_started`, `tutti_install_completed` +- Activation: `first_workspace_created`, `first_up_success`, `first_send`, `first_workflow_run` +- Habit: DAW (daily active workspaces), returning workspace cohorts (D1/D7/D30), runs per workspace +- Outcome: run success/failure, failure type (`routing/tool/model/policy`), time-to-success +- Commercial: conversion funnel install → first up → first successful run → D7 return + +**PII boundaries (mandatory):** +- Allowed fields: timestamps, boolean flags, counters, duration buckets, workflow/run status, failure category. +- Identifier handling: workspace/user identifiers must be one-way pseudonymized (salted hash). +- Explicitly excluded: workspace names, user emails, full prompts/messages, raw payload bodies. +- Retention: raw event payloads 30 days; aggregated cohort and DAW metrics 12 months. +- Enforcement: apply and verify via the "Redaction/privacy defaults" deliverable before dashboard rollout. + +### Telemetry deliverables +- Event schema + versioning +- Redaction/privacy defaults +- Dashboard for activation, reliability, retention +- Alerting on activation regression and failure spikes + +## Suggested sequencing (8-week sketch) +- **Weeks 1-2:** P0.1 + P0.2 + telemetry schema foundation +- **Weeks 3-4:** P0.3 + P0.4 + failure attribution dashboards +- **Weeks 5-6:** P0.5 + P1.6 (observability + cost governance) +- **Weeks 7-8:** P1.7 + P1.8 + onboarding narrative improvements +- Ongoing: P1.9 + P1.10 hardening and GTM tuning + +## Success criteria +- Activation: >= 60% of installs reach first successful workflow run within 24h +- Reliability: run failure rate < 5% and MTTR < 2 hours +- Operator trust: approval/audit/cost controls are production-usable and exercised in at least one documented runbook +- Adoption: >= 3 production-ready templates with documented use cases +- Retention: D7 returning-workspace cohort >= 40% diff --git a/docs/CODEX_SDLC_ORCHESTRATION.md b/docs/CODEX_SDLC_ORCHESTRATION.md new file mode 100644 index 0000000..684f12a --- /dev/null +++ b/docs/CODEX_SDLC_ORCHESTRATION.md @@ -0,0 +1,75 @@ +# Codex SDLC Orchestration (Tutti-for-Tutti) + +This framework automates the SDLC loop for Tutti using Codex agents: + +1. Select issue from GitHub +2. Create issue branch +3. Implement with specialized agents (implementation, testing, docs/release) +4. Validate locally +5. Open PR +6. Wait for CodeRabbit review +7. Apply review fixes +8. Re-validate and update PR + +## Specialized agent topology (recommended) + +Use 6 focused agents: +- `planner` (Claude) — issue decomposition, risk/test/release planning +- `conductor` (Codex) — orchestration/handoffs only +- `implementer` (Codex) — code changes in `src/**` +- `tester` (Codex) — tests and validation ownership +- `docs-release` (Codex) — docs/changelog/version responsibilities +- `reviewer` (Codex) — strict release-readiness review + +This keeps each agent independent and accountable to one concern while preserving Codex-heavy execution. + +## Prerequisites + +- `gh` authenticated with repo access +- `codex` CLI authenticated and available in PATH +- `git`, `python3` available +- Repo has labels for issue intake (default `agent-ops`) + +## Test run first (required) + +Before unattended automation, run a smoke workflow that: + +- selects issue (`select_issue.sh`) +- creates branch (`create_issue_branch.sh`) +- runs validation (`cargo test --quiet`) +- does **not** open PR or push + +Only after successful smoke, run full cycle. + +## Example workflow file + +Use `docs/examples/tutti-codex-sdlc.toml` as a starting point. + +## Core scripts + +- `scripts/automation/select_issue.sh` +- `scripts/automation/create_issue_branch.sh` +- `scripts/automation/wait_coderabbit.sh` +- `scripts/automation/collect_coderabbit_feedback.sh` + +## Operational notes + +- Keep branch naming deterministic: `auto/issue--` +- Always include issue reference in commit and PR body +- Enforce docs/version updates in implementation prompt +- Require test pass before PR open and before merge/land +- If CodeRabbit fails, gather feedback and route to Codex fix step + +## Suggested runbook + +1. `tt run sdlc-smoke --strict` +2. Inspect logs/output artifacts under `.tutti/state/auto/` +3. `tt run sdlc-auto --strict` +4. Monitor with `tt watch` / `tt logs` +5. Land only after checks + review pass + +## Safety + +- Start with `fail_mode = "closed"` on command steps +- Keep PR creation and land as explicit steps (no hidden auto-merge) +- Add approval gate before land for now diff --git a/docs/examples/tutti-codex-sdlc.toml b/docs/examples/tutti-codex-sdlc.toml new file mode 100644 index 0000000..08e33d5 --- /dev/null +++ b/docs/examples/tutti-codex-sdlc.toml @@ -0,0 +1,218 @@ +[workspace] +name = "tutti" +description = "Codex-driven SDLC automation with Claude planning" + +[defaults] +runtime = "codex" +worktree = true + +# Specialized agents: each owns one SDLC concern. + +[[agent]] +name = "planner" +runtime = "claude-code" +worktree = true +persistent = true +prompt = "You are the planning specialist. Translate the issue into an implementation plan, risks, test strategy, and release notes checklist. Do not implement code." + +[[agent]] +name = "conductor" +runtime = "codex" +worktree = true +persistent = true +prompt = "You orchestrate workflow state and handoffs only. You do not implement features directly." + +[[agent]] +name = "implementer" +runtime = "codex" +worktree = true +persistent = true +scope = "src/**" +prompt = "Implement feature/code changes for the selected issue. Keep changes minimal and aligned to issue scope." + +[[agent]] +name = "tester" +runtime = "codex" +worktree = true +persistent = true +scope = "tests/**" +prompt = "Own test strategy and test updates. Ensure failing tests are fixed and coverage reflects behavior changes." + +[[agent]] +name = "docs-release" +runtime = "codex" +worktree = true +persistent = true +scope = "**" +prompt = "Own docs, changelog, and version bump policy. Update README/docs/CHANGELOG and Cargo.toml when required." + +[[agent]] +name = "reviewer" +runtime = "codex" +worktree = true +persistent = false +prompt = "Perform strict review and release-readiness validation. Block if risk is unresolved." + +[[workflow]] +name = "sdlc-smoke" +description = "Preflight test run before unattended automation" + +[[workflow.step]] +id = "select_issue" +type = "command" +run = "scripts/automation/select_issue.sh .tutti/state/auto/selected_issue.json \"${ISSUE_LABEL:-agent-ops}\"" +fail_mode = "closed" + +[[workflow.step]] +id = "create_branch" +type = "command" +agent = "implementer" +run = "scripts/automation/create_issue_branch.sh .tutti/state/auto/selected_issue.json .tutti/state/auto/branch.json" +fail_mode = "closed" + +[[workflow.step]] +id = "validate_smoke" +type = "command" +run = "cargo test --quiet" +fail_mode = "closed" + +[[workflow]] +name = "sdlc-auto" +description = "Full issue -> plan -> branch -> code -> test -> docs/version -> PR -> CodeRabbit -> fix loop" + +# 1) Ensure specialized agents are running +[[workflow.step]] +id = "ensure_planner" +type = "ensure_running" +agent = "planner" + +[[workflow.step]] +id = "ensure_conductor" +type = "ensure_running" +agent = "conductor" + +[[workflow.step]] +id = "ensure_implementer" +type = "ensure_running" +agent = "implementer" + +[[workflow.step]] +id = "ensure_tester" +type = "ensure_running" +agent = "tester" + +[[workflow.step]] +id = "ensure_docs_release" +type = "ensure_running" +agent = "docs-release" + +# 2) Intake and branch setup +[[workflow.step]] +id = "select_issue" +type = "command" +run = "scripts/automation/select_issue.sh .tutti/state/auto/selected_issue.json \"${ISSUE_LABEL:-agent-ops}\"" +fail_mode = "closed" + +[[workflow.step]] +id = "create_branch" +type = "command" +agent = "implementer" +run = "scripts/automation/create_issue_branch.sh .tutti/state/auto/selected_issue.json .tutti/state/auto/branch.json" +fail_mode = "closed" + +# 3) Planning first (Claude) +[[workflow.step]] +id = "plan_issue" +type = "prompt" +agent = "planner" +inject_files = [".tutti/state/auto/selected_issue.json"] +wait_for_idle = true +wait_timeout_secs = 1200 +text = "Create a concrete execution plan for the selected issue. Include: scope, constraints, implementation tasks, tests to add/update, docs/changelog/version actions, and potential risks. Summarize key steps in your final response for downstream agents." + +# 4) Implementation specialization (Codex) +[[workflow.step]] +id = "implement_code" +type = "prompt" +agent = "implementer" +inject_files = [".tutti/state/auto/selected_issue.json", ".tutti/state/auto/branch.json"] +wait_for_idle = true +wait_timeout_secs = 3600 +text = "Implement the selected issue in src/ using the provided plan. Keep scope tight. Commit code changes when done and ensure branch is pushed." + +[[workflow.step]] +id = "test_update_and_validate" +type = "prompt" +agent = "tester" +inject_files = [".tutti/state/auto/selected_issue.json", ".tutti/state/auto/branch.json"] +wait_for_idle = true +wait_timeout_secs = 2400 +text = "Update/add tests for the implemented behavior and run test suite. Fix test failures. Commit test changes and push." + +[[workflow.step]] +id = "docs_and_version" +type = "prompt" +agent = "docs-release" +inject_files = [".tutti/state/auto/selected_issue.json", ".tutti/state/auto/branch.json"] +wait_for_idle = true +wait_timeout_secs = 1800 +text = "Update docs and changelog. If behavior changed, increment version in Cargo.toml and note release impact. Commit docs/release changes and push." + +# 5) Hard validation gate +[[workflow.step]] +id = "validate" +type = "command" +run = "cargo test --quiet" +fail_mode = "closed" + +# 6) Ensure branch has commits before PR +[[workflow.step]] +id = "verify_branch_has_changes" +type = "command" +run = "python3 scripts/automation/verify_branch_has_changes.py .tutti/state/auto/branch.json" +fail_mode = "closed" + +# 7) Open PR +[[workflow.step]] +id = "open_pr" +type = "command" +run = "python3 scripts/automation/open_or_create_pr.py .tutti/state/auto/selected_issue.json .tutti/state/auto/branch.json main" +fail_mode = "closed" + +# 8) Review loop +[[workflow.step]] +id = "wait_coderabbit" +type = "command" +run = "python3 scripts/automation/wait_and_collect_coderabbit.py .tutti/state/auto/branch.json 45 .tutti/state/auto/coderabbit_status.json .tutti/state/auto/coderabbit-feedback.md" +fail_mode = "open" + +[[workflow.step]] +id = "apply_coderabbit_feedback" +type = "prompt" +agent = "implementer" +inject_files = [".tutti/state/auto/coderabbit-feedback.md", ".tutti/state/auto/branch.json"] +wait_for_idle = true +wait_timeout_secs = 2400 +text = "Apply CodeRabbit feedback if actionable, rerun tests, and push changes. If no actionable items, state that explicitly." + +[[workflow.step]] +id = "revalidate" +type = "command" +run = "cargo test --quiet" +fail_mode = "closed" + +# 9) Final review + summary +[[workflow.step]] +id = "final_review" +type = "review" +agent = "implementer" +reviewer = "reviewer" + +[[workflow.step]] +id = "ready_summary" +type = "prompt" +agent = "conductor" +inject_files = [".tutti/state/auto/selected_issue.json", ".tutti/state/auto/branch.json", ".tutti/state/auto/coderabbit-feedback.md"] +wait_for_idle = true +wait_timeout_secs = 900 +text = "Summarize final readiness, residual risks, and merge recommendation." diff --git a/scripts/automation/collect_coderabbit_feedback.sh b/scripts/automation/collect_coderabbit_feedback.sh new file mode 100755 index 0000000..919520d --- /dev/null +++ b/scripts/automation/collect_coderabbit_feedback.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Collect CodeRabbit review/comments into markdown for follow-up prompt. +# Usage: collect_coderabbit_feedback.sh [output_md] + +PR_NUMBER="${1:?PR number required}" +OUT_FILE="${2:-.tutti/state/auto/coderabbit-feedback.md}" +REPO="${GITHUB_REPOSITORY:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" + +mkdir -p "$(dirname "$OUT_FILE")" + +DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json comments,reviews,url,title) +INLINE_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments") + +python3 - <<'PY' "$OUT_FILE" "$DATA" "$INLINE_COMMENTS" +import json,sys,re +out=obj=None +out=sys.argv[1] +obj=json.loads(sys.argv[2]) +inline_comments=json.loads(sys.argv[3]) + +def is_coderabbit(author): + if not author: + return False + login=(author.get("login") or author.get("name") or "").lower() + return "coderabbit" in login or "code-rabbit" in login or "code_rabbit" in login + +lines=[] +lines.append(f"# CodeRabbit feedback for PR #{obj.get('url','').split('/')[-1]}") +lines.append("") +lines.append(f"Title: {obj.get('title','')}") +lines.append(f"URL: {obj.get('url','')}") +lines.append("") + +found=False +for review in obj.get("reviews", []): + if is_coderabbit(review.get("author")): + found=True + body=(review.get("body") or "").strip() + if body: + lines.append("## Review") + lines.append(body) + lines.append("") + +for c in obj.get("comments", []): + if is_coderabbit(c.get("author")): + found=True + body=(c.get("body") or "").strip() + if body: + lines.append("## Comment") + lines.append(body) + lines.append("") + +for c in inline_comments: + # REST PR review comments use `user` instead of `author` + if is_coderabbit(c.get("user")): + found=True + body=(c.get("body") or "").strip() + path=(c.get("path") or "").strip() + line_num=c.get("line") or c.get("original_line") + if body: + where = f" ({path}:{line_num})" if path and line_num else (f" ({path})" if path else "") + lines.append(f"## Inline comment{where}") + lines.append(body) + lines.append("") + +if not found: + lines.append("No CodeRabbit comments/reviews found.") + +with open(out,'w',encoding='utf-8') as f: + f.write("\n".join(lines).strip()+"\n") +print(out) +PY diff --git a/scripts/automation/create_issue_branch.sh b/scripts/automation/create_issue_branch.sh new file mode 100755 index 0000000..2cd013f --- /dev/null +++ b/scripts/automation/create_issue_branch.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Create a feature branch from selected issue JSON. +# Usage: create_issue_branch.sh [selected_issue_json] [output_json] + +ISSUE_JSON="${1:-.tutti/state/auto/selected_issue.json}" +OUT_FILE="${2:-.tutti/state/auto/branch.json}" +BASE_BRANCH="${BASE_BRANCH:-main}" + +mkdir -p "$(dirname "$OUT_FILE")" + +ISSUE_NUM=$(python3 - <<'PY' "$ISSUE_JSON" +import json,sys +with open(sys.argv[1], 'r', encoding='utf-8') as f: + d=json.load(f) +print(d["issue_number"]) +PY +) + +STAMP=$(date +%Y%m%d%H%M%S) +BRANCH="auto/issue-${ISSUE_NUM}-${STAMP}" + +git fetch origin "$BASE_BRANCH" +git checkout -B "$BRANCH" "origin/$BASE_BRANCH" + +python3 - <<'PY' "$OUT_FILE" "$BRANCH" "$ISSUE_NUM" +import json,sys +out, branch, issue = sys.argv[1], sys.argv[2], int(sys.argv[3]) +with open(out, 'w', encoding='utf-8') as f: + json.dump({"branch": branch, "issue_number": issue}, f, indent=2) +print(out) +PY diff --git a/scripts/automation/open_or_create_pr.py b/scripts/automation/open_or_create_pr.py new file mode 100755 index 0000000..890726e --- /dev/null +++ b/scripts/automation/open_or_create_pr.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +from pathlib import Path + +selected_issue_file = Path(sys.argv[1] if len(sys.argv) > 1 else ".tutti/state/auto/selected_issue.json") +branch_file = Path(sys.argv[2] if len(sys.argv) > 2 else ".tutti/state/auto/branch.json") +base = sys.argv[3] if len(sys.argv) > 3 else "main" + +with selected_issue_file.open("r", encoding="utf-8") as f: + issue = json.load(f) +with branch_file.open("r", encoding="utf-8") as f: + branch = json.load(f)["branch"] + +issue_number = issue["issue_number"] +title = f"[auto] #{issue_number} {issue['title']}" +body = ( + f"Automated SDLC cycle for #{issue_number}.\n\n" + "- planner: completed\n" + "- implementation: completed\n" + "- tests: updated\n" + "- docs/changelog: updated\n" + "- version: bumped if required" +) + +existing = subprocess.check_output( + ["gh", "pr", "list", "--state", "open", "--head", branch, "--json", "number"], + text=True, +) +prs = json.loads(existing) +if prs: + print(f"PR already exists: #{prs[0]['number']}") +else: + subprocess.check_call([ + "gh", + "pr", + "create", + "--title", + title, + "--body", + body, + "--head", + branch, + "--base", + base, + ]) diff --git a/scripts/automation/select_issue.sh b/scripts/automation/select_issue.sh new file mode 100755 index 0000000..0d766ff --- /dev/null +++ b/scripts/automation/select_issue.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Select next GitHub issue for automated SDLC cycle. +# Usage: select_issue.sh [output_json] [label] + +OUT_FILE="${1:-.tutti/state/auto/selected_issue.json}" +LABEL="${2:-agent-ops}" +REPO="${GITHUB_REPOSITORY:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" + +mkdir -p "$(dirname "$OUT_FILE")" + +JSON=$(gh issue list --repo "$REPO" --state open --label "$LABEL" --limit 100 \ + --json number,title,url,labels,author,createdAt) + +ISSUE_NUM=$(python3 - "$OUT_FILE" "$LABEL" "$JSON" <<'PY' +import json,sys +out, label, raw = sys.argv[1], sys.argv[2], sys.argv[3] +items = json.loads(raw or "[]") +items = [ + i for i in items + if "automation-claimed" not in {l.get("name") for l in i.get("labels", [])} +] +if not items: + raise SystemExit(f"No unclaimed open issues found for label '{label}'") +items.sort(key=lambda i: i.get("createdAt") or "") +issue = items[0] +payload = { + "issue_number": issue["number"], + "title": issue["title"], + "url": issue["url"], + "labels": [l.get("name") for l in issue.get("labels", [])], + "author": issue.get("author", {}).get("login"), + "created_at": issue.get("createdAt"), +} +with open(f"{out}.tmp", "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) +print(issue["number"]) +PY +) + +gh issue edit "$ISSUE_NUM" --repo "$REPO" --add-label "automation-claimed" >/dev/null +mv "${OUT_FILE}.tmp" "$OUT_FILE" + +echo "$OUT_FILE" diff --git a/scripts/automation/verify_branch_has_changes.py b/scripts/automation/verify_branch_has_changes.py new file mode 100755 index 0000000..a027426 --- /dev/null +++ b/scripts/automation/verify_branch_has_changes.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +from pathlib import Path + +branch_file = Path(sys.argv[1] if len(sys.argv) > 1 else ".tutti/state/auto/branch.json") + +with branch_file.open("r", encoding="utf-8") as f: + branch = json.load(f)["branch"] + +subprocess.check_call(["git", "fetch", "origin", "main"]) +log = subprocess.check_output(["git", "log", "--oneline", f"origin/main..{branch}"], text=True).strip() +if not log: + print("No commits found on automation branch vs origin/main") + sys.exit(1) + +print(log.splitlines()[0]) diff --git a/scripts/automation/wait_and_collect_coderabbit.py b/scripts/automation/wait_and_collect_coderabbit.py new file mode 100755 index 0000000..b43f36e --- /dev/null +++ b/scripts/automation/wait_and_collect_coderabbit.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +from pathlib import Path + +branch_file = Path(sys.argv[1] if len(sys.argv) > 1 else ".tutti/state/auto/branch.json") +wait_mins = sys.argv[2] if len(sys.argv) > 2 else "45" +status_file = sys.argv[3] if len(sys.argv) > 3 else ".tutti/state/auto/coderabbit_status.json" +feedback_file = sys.argv[4] if len(sys.argv) > 4 else ".tutti/state/auto/coderabbit-feedback.md" + +with branch_file.open("r", encoding="utf-8") as f: + branch = json.load(f)["branch"] + +out = subprocess.check_output( + ["gh", "pr", "list", "--state", "open", "--head", branch, "--json", "number"], + text=True, +) +prs = json.loads(out) +if not prs: + print(f"No open PR found for branch: {branch}", file=sys.stderr) + sys.exit(1) +pr = prs[0]["number"] + +wait_result = subprocess.run( + ["scripts/automation/wait_coderabbit.sh", str(pr), str(wait_mins), status_file], + check=False, +) +try: + subprocess.check_call([ + "scripts/automation/collect_coderabbit_feedback.sh", + str(pr), + feedback_file, + ]) +except subprocess.CalledProcessError as e: + print(f"Warning: failed to collect CodeRabbit feedback: {e}", file=sys.stderr) +except Exception as e: + print(f"Warning: unexpected feedback collection error: {e}", file=sys.stderr) + +sys.exit(wait_result.returncode) diff --git a/scripts/automation/wait_coderabbit.sh b/scripts/automation/wait_coderabbit.sh new file mode 100755 index 0000000..a52d159 --- /dev/null +++ b/scripts/automation/wait_coderabbit.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wait for CodeRabbit check on a PR. +# Usage: wait_coderabbit.sh [timeout_minutes] [output_json] + +PR_NUMBER="${1:?PR number required}" +TIMEOUT_MIN="${2:-45}" +OUT_FILE="${3:-.tutti/state/auto/coderabbit_status.json}" +REPO="${GITHUB_REPOSITORY:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" + +mkdir -p "$(dirname "$OUT_FILE")" + +DEADLINE=$(( $(date +%s) + TIMEOUT_MIN*60 )) +FOUND=0 + +while true; do + NOW=$(date +%s) + if (( NOW > DEADLINE )); then + python3 - <<'PY' "$OUT_FILE" "$PR_NUMBER" +import json,sys +out,pr = sys.argv[1], int(sys.argv[2]) +with open(out,'w',encoding='utf-8') as f: + json.dump({"pr_number":pr,"status":"timeout","found":False},f,indent=2) +print(out) +PY + exit 1 + fi + + DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json statusCheckRollup) + RESULT=$(python3 - <<'PY' "$DATA" +import json,sys +obj=json.loads(sys.argv[1]) +checks=obj.get("statusCheckRollup") or [] +cr=[] +for c in checks: + name=(c.get("name") or "").lower() + if "coderabbit" in name or "code rabbit" in name: + cr.append(c) +if not cr: + print("NONE") + raise SystemExit +# if multiple, require all completed and explicitly successful +states=[(c.get("status") or "", c.get("conclusion") or "") for c in cr] +if any(s != "COMPLETED" for s,_ in states): + print("PENDING") +else: + allowed=("SUCCESS",) + bad=[x for x in states if x[1] not in allowed] + print("FAIL" if bad else "PASS") +PY +) + + case "$RESULT" in + NONE) + sleep 30 + ;; + PENDING) + FOUND=1 + sleep 30 + ;; + PASS) + python3 - <<'PY' "$OUT_FILE" "$PR_NUMBER" "$FOUND" +import json,sys +out,pr,found = sys.argv[1], int(sys.argv[2]), bool(int(sys.argv[3])) +with open(out,'w',encoding='utf-8') as f: + json.dump({"pr_number":pr,"status":"pass","found":found},f,indent=2) +print(out) +PY + exit 0 + ;; + FAIL) + python3 - <<'PY' "$OUT_FILE" "$PR_NUMBER" +import json,sys +out,pr = sys.argv[1], int(sys.argv[2]) +with open(out,'w',encoding='utf-8') as f: + json.dump({"pr_number":pr,"status":"fail","found":True},f,indent=2) +print(out) +PY + exit 2 + ;; + esac +done