diff --git a/README.md b/README.md index 9899653..1a0bc81 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ reproduced, and changed through PRs instead of server-only edits. - `src/server.ts` - Node HTTP entrypoint for `/`, `/healthz`, and `/webhook`. - `tests/webhook-adapter.test.ts` - parity coverage for signature handling, request body edge cases, and task routing guards. +- `config/example-policy.json` - example installation/repository policy that + connects a GitHub App install to a familiar route. +- `docs/coven-github-connection.md` - operator guide for connecting this + TypeScript deployment bundle to the canonical `coven-github` app manifest. - `scripts/smoke-webhook.sh` - local HMAC signature smoke test for a running webhook endpoint. @@ -87,6 +91,10 @@ Deployments should provide `coven-github-policy.json` through `COVEN_GITHUB_POLICY_PATH`. That file is intentionally ignored because it is environment-specific. +Start from [`config/example-policy.json`](config/example-policy.json) and the +connection guide in +[`docs/coven-github-connection.md`](docs/coven-github-connection.md). + ## Current behavior - Emits headless contract v2 session briefs. diff --git a/config/example-policy.json b/config/example-policy.json new file mode 100644 index 0000000..f5c5e3e --- /dev/null +++ b/config/example-policy.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "installations": { + "123456": { + "repositories": { + "987654321": { + "enabled_triggers": [ + "issues.labeled", + "issue_comment.created", + "pull_request_review_comment.created" + ], + "trigger_labels": [ + "coven:fix", + "coven:review" + ], + "bot_usernames": [ + "coven-cody[bot]" + ], + "default_branch": "main", + "familiar": { + "id": "cody", + "display_name": "Cody", + "model": "openai/gpt-5.5", + "skills": [ + "systematic-debugging", + "test-driven-development" + ] + }, + "publication": { + "mode": "record_only" + } + } + } + } + } +} diff --git a/docs/coven-github-connection.md b/docs/coven-github-connection.md new file mode 100644 index 0000000..6eb9e9c --- /dev/null +++ b/docs/coven-github-connection.md @@ -0,0 +1,106 @@ +# Connecting to coven-github + +`coven-github-webhook` is the TypeScript deployment bundle for the +[`OpenCoven/coven-github`](https://github.com/OpenCoven/coven-github) product +surface. It receives GitHub App webhooks, verifies signatures, applies +installation/repository routing policy, and starts `coven-code` headless runs. + +Use the canonical `coven-github` repo for the product spec, GitHub App +permissions, event list, headless contract, and hosted/self-hosted operating +model. Use this repo when you want the lightweight Node deployment entrypoint. + +## App Registration + +Register the GitHub App with the manifest from `coven-github`: + +- `OpenCoven/coven-github/docs/app-manifest.json` + +Set the manifest webhook URL to this service: + +```text +https://your-host/webhook +``` + +The manifest subscribes to the events this adapter can route: + +- `issues` +- `issue_comment` +- `pull_request_review` +- `pull_request_review_comment` +- `check_suite` +- `check_run` + +After creating the App, keep these values outside git: + +- App ID -> `GITHUB_APP_ID` +- Webhook secret -> `GITHUB_WEBHOOK_SECRET` +- Private key PEM path -> `GITHUB_APP_PRIVATE_KEY_PATH` + +## Policy + +This adapter uses a local JSON policy file to map a GitHub installation and +repository to a familiar route. Start with: + +```bash +cp config/example-policy.json coven-github-policy.json +``` + +Then replace: + +- `123456` with the GitHub App installation ID. +- `987654321` with the repository ID. +- `bot_usernames` with the GitHub App bot login, for example + `coven-cody[bot]`. +- `trigger_labels` with labels that should start tasks. +- `familiar` with the familiar id, display name, model, and skills to pass to + `coven-code`. + +Point the service at the file: + +```bash +export COVEN_GITHUB_POLICY_PATH="$PWD/coven-github-policy.json" +``` + +Keep `publication.mode` as `record_only` for first smoke runs. Switch it to +`comment` only after you have verified the App installation, `coven-code` +runtime, Codex token, and workspace permissions. + +## Runtime Checklist + +```bash +npm ci +npm run build + +export GITHUB_APP_ID="123456" +export GITHUB_WEBHOOK_SECRET="replace-with-github-secret" +export GITHUB_APP_PRIVATE_KEY_PATH="$PWD/keys/coven-github.private-key.pem" +export COVEN_GITHUB_POLICY_PATH="$PWD/coven-github-policy.json" +export COVEN_GITHUB_STATE_DIR="$PWD/coven-github-state" +export COVEN_CODE_BIN="$(command -v coven-code)" + +npm start +``` + +In another shell: + +```bash +WEBHOOK_SECRET="$GITHUB_WEBHOOK_SECRET" \ + scripts/smoke-webhook.sh http://localhost:3000/webhook +``` + +That proves the HTTP endpoint and HMAC signature path before any GitHub token or +runtime work is attempted. + +## Functional App Smoke + +On a repository where the App is installed: + +1. Copy the installation ID and repository ID into `coven-github-policy.json`. +2. Add one of the configured labels, such as `coven:fix`, to an issue. +3. Confirm a delivery record appears under `COVEN_GITHUB_STATE_DIR/deliveries`. +4. Confirm a task record appears under `COVEN_GITHUB_STATE_DIR/tasks`. +5. Inspect the attempt directory if the runtime fails before enabling comment + publication. + +This keeps the first app connection auditable: GitHub delivery, policy route, +task creation, runtime attempt, and publication are separate files. diff --git a/tests/webhook-adapter.test.ts b/tests/webhook-adapter.test.ts index b1b396f..5960df9 100644 --- a/tests/webhook-adapter.test.ts +++ b/tests/webhook-adapter.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { createHmac } from "node:crypto"; -import { mkdtempSync } from "node:fs"; +import { mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; @@ -293,3 +293,47 @@ test("missing familiar policy does not fall back to hardcoded installation", () assert.equal(task.state, "ignored"); assert.equal(task.ignored_reason, "missing_familiar_policy"); }); + +test("example policy routes a labeled issue to the configured familiar", () => { + const policyFile = new URL("../config/example-policy.json", import.meta.url); + const policyRoot = JSON.parse(readFileSync(policyFile, "utf8")) as JsonObject; + const installation = (policyRoot.installations as JsonObject)["123456"] as JsonObject; + const policy = ((installation.repositories as JsonObject)["987654321"]) as JsonObject; + + const task = buildTaskFromEvent( + "issues", + "delivery-example-policy", + { + action: "labeled", + installation: {id: 123456}, + repository: { + id: 987654321, + full_name: "OpenCoven/example", + clone_url: "https://github.com/OpenCoven/example.git", + default_branch: "main", + }, + issue: { + number: 42, + title: "Wire the app", + body: "Make the first app route functional.", + labels: [{name: "coven:fix"}], + }, + } as JsonObject, + policy, + ); + + assert.equal(task.state, "queued"); + assert.equal(task.trigger, "issue_mention"); + assert.deepEqual(task.task, { + kind: "fix_issue", + issue_number: 42, + issue_title: "Wire the app", + issue_body: "Make the first app route functional.", + }); + assert.deepEqual(task.familiar, { + id: "cody", + display_name: "Cody", + model: "openai/gpt-5.5", + skills: ["systematic-debugging", "test-driven-development"], + }); +});