diff --git a/README.md b/README.md index ab42058..49867f4 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,132 @@ go build -o oz-agent-worker ./oz-agent-worker --api-key "wk-abc123" --worker-id "my-worker" ``` +## GitHub PR Integration + +Self-hosted Kubernetes workers can be triggered from GitHub PR comments using an `@oz-agent` mention pattern, equivalent to the `respond-to-comment.yml` workflow in [`oz-agent-action`](https://github.com/warpdotdev/oz-agent-action) but routing work to your own k8s cluster. + +### How it works + +The trigger workflow runs on standard GitHub-hosted runners. It does not install the `oz` CLI; instead it calls the Warp REST API directly with `config.worker_host` set to your worker ID. Warp routes the task to whichever connected worker matches that ID — your k8s deployment. The Oz agent then runs inside your cluster, checks out the PR branch, makes the requested changes, and replies to the comment. + +``` +Developer comments "@oz-agent fix the null check" + └─► GitHub Actions workflow fires (GitHub-hosted runner) + └─► POST https://app.warp.dev/api/v1/agent/runs + { config: { worker_host: "your-k8s-worker-id" } } + └─► Task routed to your oz-agent-worker in k8s + └─► Oz checks out PR branch, implements fix, + commits, pushes, replies to comment +``` + +### Setup + +**1. Prepare your k8s worker for GitHub access** + +The Oz agent running in your cluster needs credentials to clone your repository and push commits. Store them as a Kubernetes Secret and inject them via `pod_template`: + +```yaml +# Create a secret with your GitHub credentials +kubectl create secret generic oz-github-creds \ + --from-literal=GITHUB_TOKEN="ghp_..." \ + --namespace agents +``` + +In your Helm values, inject the secret into task containers: + +```yaml +kubernetesBackend: + podTemplate: + containers: + - name: task + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: oz-github-creds + key: GITHUB_TOKEN +``` + +For SSH-based cloning, mount your deploy key instead: + +```yaml +kubernetesBackend: + podTemplate: + volumes: + - name: ssh-key + secret: + secretName: oz-github-deploy-key + defaultMode: 0400 + containers: + - name: task + volumeMounts: + - name: ssh-key + mountPath: /root/.ssh + readOnly: true +``` + +**2. Add the GitHub Actions workflow to your repository** + +Copy `consumer-workflows/gh-pr-comment.yml` from this repository into `.github/workflows/` in the repository where you want the `@oz-agent` trigger. + +**3. Configure secrets and variables** + +In your GitHub repository settings, add: + +| Type | Name | Value | +|------|------|-------| +| Secret | `WARP_API_KEY` | Your Warp service account API key (team-scoped) | +| Variable | `OZ_WORKER_ID` | The `worker_id` from your `oz-agent-worker` config (e.g. `my-k8s-worker`) | + +**4. Use it** + +Comment on any PR or inline review comment: + +``` +@oz-agent refactor the error handling in auth.go to use the new ErrorResponse type +``` + +Oz will acknowledge with 👀, run in your k8s cluster, push any commits, and reply with a summary. + +### Comparison: self-hosted vs. cloud runner approach + +| | `gh-pr-comment.yml` (this repo) | `oz-agent-action` `respond-to-comment.yml` | +|---|---|---| +| Oz runs on | Your k8s cluster | Warp-hosted cloud agents | +| GitHub runner | GitHub-hosted (ubuntu-latest) | GitHub-hosted (ubuntu-latest) | +| oz CLI needed on runner | No (curl only) | Yes (installed by the action) | +| Output capture | Async — Oz posts its own comment | Synchronous — workflow posts reply | +| Trigger | `@oz-agent` comment | `@oz-agent` comment | +| Self-hosted runner support | Yes (change `runs-on`) | Requires Linux (apt-installable) | + +### Running on self-hosted GitHub Actions runners + +If you also run self-hosted GitHub Actions runners (e.g., in the same k8s cluster), change `runs-on` in the workflow to your runner label: + +```yaml +jobs: + trigger: + runs-on: [self-hosted, linux, my-org-runners] +``` + +The workflow only needs `curl`, `jq`, and the `gh` CLI available on the runner. No `oz` CLI is needed. + +### Customizing the prompt + +The consumer workflow passes a structured prompt to Oz. Modify the `PROMPT` variable in the workflow to add team-specific instructions, coding standards, or technology-specific context: + +```yaml +PROMPT="... + +## Team conventions +- All Go errors must be wrapped with fmt.Errorf and include context +- Run 'go test ./...' before committing + +${PROMPT}" +``` + +Alternatively, set a `skill` in the API payload to provide reusable base instructions that can be maintained separately from the workflow file. + ## Environment Variables for Task Containers Use `-e` / `--env` to pass environment variables into task containers: diff --git a/consumer-workflows/gh-pr-comment.yml b/consumer-workflows/gh-pr-comment.yml new file mode 100644 index 0000000..80f7733 --- /dev/null +++ b/consumer-workflows/gh-pr-comment.yml @@ -0,0 +1,276 @@ +# ====================================================================================== +# Workflow: Trigger Oz Agent on PR Comment (Self-Hosted Kubernetes Workers) +# ====================================================================================== +# Usage: +# - Comment "@oz-agent " on any PR or inline review comment. +# - Oz will implement the requested changes on the PR branch and reply with a summary. +# +# Setup: +# 1. Copy this file into .github/workflows/ in your repository. +# 2. Set the following secrets/variables in your repository or organization: +# - WARP_API_KEY (secret): A Warp service account API key scoped to your team. +# - OZ_WORKER_ID (variable): The worker_id value of your self-hosted Oz worker +# (the value you passed as --worker-id or worker_id in your worker config). +# +# How it works: +# This workflow runs on GitHub-hosted runners. When triggered, it calls the Warp +# REST API directly to create an Oz run targeted at your self-hosted k8s worker. +# The Oz agent runs inside your k8s cluster, checks out the PR branch, implements +# the requested changes, commits them, and replies to the original comment. +# +# No oz CLI installation is required on the GitHub runner. Only curl is needed. +# +# Prerequisites on the Oz worker side: +# - The oz-agent-worker must be running in your k8s cluster with the Helm chart +# or equivalent deployment, configured with the kubernetes backend. +# - The worker must have access to your GitHub repository (e.g., a mounted SSH key +# or a GitHub App credential in a Kubernetes Secret injected via pod_template). +# - The GITHUB_TOKEN variable below must be passed to task containers so the Oz +# agent can push commits and post GitHub comments. Inject it as a Kubernetes +# Secret and reference it in your Helm values under kubernetesBackend.podTemplate. +# +# Expected output: +# - Oz checks out the PR branch, implements the requested change, commits it, and +# pushes it back to the PR branch. +# - Oz posts a GitHub comment on the PR with a summary of what it did and a link +# to the Warp run. +# +# Customization: +# - Adjust the trigger phrase by changing "@oz-agent" in the `if:` condition. +# - Pass additional context in the prompt (e.g., coding standards, team conventions). +# - Set a `skill` in the API payload to provide reusable base instructions. +# - Set `model_id` in the API config block to override the default LLM. +# +# When to use vs. oz-agent-action respond-to-comment.yml: +# - Use THIS workflow when your Oz workloads must run on your self-hosted k8s workers. +# - Use oz-agent-action respond-to-comment.yml when you are happy with Warp-hosted +# (cloud) agent execution and want synchronous output captured in the workflow. +# ====================================================================================== + +name: Oz Agent PR Comment (Self-Hosted) + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + trigger: + runs-on: ubuntu-latest + # Only fire when the comment contains the trigger phrase, is on a PR, and was + # not posted by the Actions bot itself (to prevent loops). + if: | + contains(github.event.comment.body, '@oz-agent') && + ( + github.event_name == 'pull_request_review_comment' || + (github.event_name == 'issue_comment' && github.event.issue.pull_request) + ) && + github.actor != 'github-actions[bot]' + permissions: + contents: read + pull-requests: write + issues: write + steps: + # ── Step 1: Acknowledge the comment ──────────────────────────────────────── + # React with 👀 so the commenter knows the request was received. + - name: Acknowledge comment + env: + GH_TOKEN: ${{ github.token }} + run: | + COMMENT_ID="${{ github.event.comment.id }}" + + if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/reactions" \ + -f content="eyes" || true + else + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" \ + -f content="eyes" || true + fi + + # ── Step 2: Resolve PR number and branch ──────────────────────────────────── + - name: Resolve PR context + id: pr + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER="${{ github.event.issue.number }}" + fi + + PR_JSON=$(gh api "/repos/${{ github.repository }}/pulls/$PR_NUMBER") + PR_TITLE=$(echo "$PR_JSON" | jq -r '.title') + PR_BODY=$(echo "$PR_JSON" | jq -r '.body // ""') + PR_BRANCH=$(echo "$PR_JSON" | jq -r '.head.ref') + PR_REPO=$(echo "$PR_JSON" | jq -r '.head.repo.clone_url') + CHANGED_FILES=$(gh api "/repos/${{ github.repository }}/pulls/$PR_NUMBER/files" \ + | jq -r '[.[].filename] | join(", ")') + + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "title=$PR_TITLE" >> "$GITHUB_OUTPUT" + echo "branch=$PR_BRANCH" >> "$GITHUB_OUTPUT" + echo "repo=$PR_REPO" >> "$GITHUB_OUTPUT" + echo "files=$CHANGED_FILES" >> "$GITHUB_OUTPUT" + # Store multi-line body in env file to avoid quoting issues + echo "PR_BODY<> "$GITHUB_ENV" + echo "$PR_BODY" >> "$GITHUB_ENV" + echo "EOF" >> "$GITHUB_ENV" + + # ── Step 3: Submit the Oz run to your self-hosted k8s worker ──────────────── + # Calls the Warp REST API with config.worker_host set to your worker ID. + # The Oz agent will run inside your k8s cluster and push changes back to GitHub. + - name: Trigger Oz agent on self-hosted worker + id: oz_run + env: + WARP_API_KEY: ${{ secrets.WARP_API_KEY }} + GH_TOKEN: ${{ github.token }} + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + COMMENT_AUTHOR="${{ github.event.comment.user.login }}" + COMMENT_URL="${{ github.event.comment.html_url }}" + PR_NUMBER="${{ steps.pr.outputs.number }}" + PR_TITLE="${{ steps.pr.outputs.title }}" + PR_BRANCH="${{ steps.pr.outputs.branch }}" + PR_REPO="${{ steps.pr.outputs.repo }}" + CHANGED_FILES="${{ steps.pr.outputs.files }}" + REPO="${{ github.repository }}" + + # Build diff context for inline review comments + DIFF_CONTEXT="" + if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then + FILE_PATH="${{ github.event.comment.path }}" + LINE_NUMBER="${{ github.event.comment.line }}" + DIFF_HUNK=$(echo '${{ toJSON(github.event.comment.diff_hunk) }}' | jq -r '.') + DIFF_CONTEXT=" + The comment is on file '${FILE_PATH}' at line ${LINE_NUMBER}. + Diff context around the comment: + \`\`\`diff + ${DIFF_HUNK} + \`\`\`" + fi + + # Construct the prompt for the Oz agent. + # The agent will run in your k8s environment with access to your repo. + PROMPT="You are an expert software engineer responding to a GitHub PR comment. + + ## Your task + A developer has tagged you in a PR comment with a request. Implement it. + + ## PR context + - Repository: ${REPO} + - PR: #${PR_NUMBER} — ${PR_TITLE} + - Branch: ${PR_BRANCH} + - Changed files: ${CHANGED_FILES} + + ## The request + @${COMMENT_AUTHOR} wrote: \"${COMMENT_BODY}\" + ${DIFF_CONTEXT} + + ## Instructions + 1. Clone or check out the branch '${PR_BRANCH}' from '${PR_REPO}' if it is not already available. + 2. Read the relevant files to understand the full context. + 3. Implement the requested change, following existing code style and conventions. + 4. If the request is a question rather than a change, answer it in your reply comment. + 5. Commit any changes with a clear commit message and push to branch '${PR_BRANCH}'. + 6. Post a GitHub comment on PR #${PR_NUMBER} in the repository '${REPO}' summarizing what you did (or your answer). Tag @${COMMENT_AUTHOR} in the reply. You can use the \`gh\` CLI for this: + gh pr comment ${PR_NUMBER} --repo ${REPO} --body \"@${COMMENT_AUTHOR}: \" + 7. Do not output code diffs in your final comment — only summarize the change or answer the question. + + Original comment URL for reference: ${COMMENT_URL}" + + # Call the Warp REST API to create a run on the self-hosted k8s worker. + # OZ_WORKER_ID must match the worker_id configured in your oz-agent-worker deployment. + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST "https://app.warp.dev/api/v1/agent/runs" \ + -H "Authorization: Bearer $WARP_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg prompt "$PROMPT" \ + --arg worker_id "${{ vars.OZ_WORKER_ID }}" \ + --arg run_name "PR #${PR_NUMBER} — oz-agent comment" \ + '{ + prompt: $prompt, + config: { + worker_host: $worker_id, + name: $run_name + } + }' + )") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | head -n -1) + + if [ "$HTTP_CODE" != "200" ]; then + echo "::error::Failed to create Oz run (HTTP $HTTP_CODE): $BODY" + exit 1 + fi + + RUN_ID=$(echo "$BODY" | jq -r '.run_id') + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" + echo "Oz run created: $RUN_ID" + + # ── Step 4: Post a link to the run ───────────────────────────────────────── + # Lets the commenter track the run while Oz works. + - name: Post run link + if: success() && steps.oz_run.outputs.run_id != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + RUN_ID="${{ steps.oz_run.outputs.run_id }}" + COMMENT_AUTHOR="${{ github.event.comment.user.login }}" + PR_NUMBER="${{ steps.pr.outputs.number }}" + RUN_URL="https://app.warp.dev/run/${RUN_ID}" + + REPLY="@${COMMENT_AUTHOR}: I've started working on your request on your self-hosted Oz worker. You can follow the run here: ${RUN_URL} + + I'll push any changes directly to this PR branch and leave a summary comment when done." + + if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then + COMMENT_ID="${{ github.event.comment.id }}" + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/replies" \ + -f body="$REPLY" || true + else + gh pr comment "$PR_NUMBER" \ + --repo "${{ github.repository }}" \ + --body "$REPLY" || true + fi + + # ── Step 5: Report failure ────────────────────────────────────────────────── + - name: Report failure + if: failure() + env: + GH_TOKEN: ${{ github.token }} + run: | + COMMENT_AUTHOR="${{ github.event.comment.user.login }}" + PR_NUMBER="${{ steps.pr.outputs.number }}" + WORKFLOW_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + REPLY="@${COMMENT_AUTHOR}: ⚠️ I encountered an error while trying to start your request. Please check the [workflow logs]($WORKFLOW_URL) for details." + + if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then + COMMENT_ID="${{ github.event.comment.id }}" + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/replies" \ + -f body="$REPLY" || true + else + gh pr comment "$PR_NUMBER" \ + --repo "${{ github.repository }}" \ + --body "$REPLY" || true + fi