updated
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# claude-hook
|
||||
|
||||
Listens for Gitea webhooks and runs Claude Code against an issue or pull
|
||||
request whenever the configured owner mentions `@claude` in a comment or
|
||||
issue body. On success it commits any changes Claude made, pushes a branch,
|
||||
opens (or reuses) a pull request, and replies on the issue with a summary.
|
||||
Failures are reported the same way, as a comment, so nothing is silent.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
config.js env parsing + validation, fails fast on startup
|
||||
logger.js structured JSON logging to stdout/stderr
|
||||
exec.js process spawning with timeout + output capture
|
||||
gitea.js Gitea API client (issues, pulls, comments)
|
||||
git.js git clone/checkout/commit/push for one job's working dir
|
||||
claude.js invokes the `claude` CLI non-interactively
|
||||
prompt.js builds the prompt sent to Claude
|
||||
events.js pure webhook-payload -> trigger classification logic
|
||||
jobQueue.js prevents overlapping runs for the same issue/PR
|
||||
job.js orchestrates one end-to-end run
|
||||
server.js HTTP server: signature check, parse, classify, dispatch
|
||||
app.js wires the above together
|
||||
index.js process entrypoint, signal handling
|
||||
test/ node:test unit + integration tests, no external services
|
||||
```
|
||||
|
||||
Every module that touches the outside world (HTTP, git, subprocess) is
|
||||
constructed with its dependencies passed in, so `test/` exercises the real
|
||||
logic against fakes rather than a real Gitea instance or a real `claude`
|
||||
binary.
|
||||
|
||||
## Running the tests
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
No dependencies to install — everything uses Node's built-in `node:test`,
|
||||
`fetch` and `child_process`. Requires Node 20+.
|
||||
|
||||
## Configuration
|
||||
|
||||
All via environment variables, validated on startup (a bad or missing
|
||||
value exits immediately with a clear error rather than failing later):
|
||||
|
||||
| Variable | Required | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `GITEA_URL` | yes | — | e.g. `http://gitea:3000` internally, or `https://git.example.com` |
|
||||
| `GITEA_TOKEN` | yes | — | access token for the bot user, needs write access to target repos |
|
||||
| `CLAUDE_CODE_OAUTH_TOKEN` | yes | — | from `claude setup-token` |
|
||||
| `OWNER_LOGIN` | yes | — | only comments/issues from this Gitea username trigger a run |
|
||||
| `WEBHOOK_SECRET` | no | unset | if set, requires a valid `X-Gitea-Signature` header |
|
||||
| `TRIGGER_PHRASE` | no | `@claude` | |
|
||||
| `CLAUDE_MODEL` | no | `sonnet` | |
|
||||
| `CLAUDE_MAX_TURNS` | no | `25` | |
|
||||
| `JOB_TIMEOUT_MS` | no | `1200000` (20 min) | |
|
||||
| `PORT` | no | `3000` | |
|
||||
| `LOG_LEVEL` | no | `info` | `debug` \| `info` \| `warn` \| `error` |
|
||||
|
||||
See `.env.example`.
|
||||
|
||||
## Deploying (Coolify, Docker Compose build pack)
|
||||
|
||||
1. Push this project to a Gitea repo (e.g. `selimaj-dev/claude-hook`).
|
||||
2. In Coolify: **+ New Resource -> Public Repository**, build pack
|
||||
**Docker Compose**, pointing at that repo.
|
||||
3. Edit `docker-compose.yml`'s `networks.gitea-network.name` to the actual
|
||||
Docker network your Gitea container is on
|
||||
(`docker inspect <gitea-container> --format '{{json .NetworkSettings.Networks}}'`).
|
||||
4. Set the environment variables listed above in Coolify's UI. Leave
|
||||
**Domains** empty — this service has no public route by design.
|
||||
5. Deploy, then confirm from Gitea's container:
|
||||
```
|
||||
docker exec <gitea-container> wget -qO- http://claude-hook:3000/healthz
|
||||
```
|
||||
6. In Gitea: Site Administration -> Integrations -> Webhooks -> **System
|
||||
Webhooks** -> Add, URL `http://claude-hook:3000/`, trigger on **Issue
|
||||
Comment** (and **Issues**, if you want `@claude` in a fresh issue body
|
||||
to work too). If you set `WEBHOOK_SECRET`, put the same value here.
|
||||
7. Add `GITEA__webhook__ALLOWED_HOST_LIST=private` to Gitea's own
|
||||
environment and restart it — Gitea refuses to call private addresses
|
||||
otherwise.
|
||||
|
||||
## Logs
|
||||
|
||||
Every log line is one JSON object on stdout (or stderr for `warn`/`error`),
|
||||
so `docker logs claude-hook` / Coolify's log viewer is directly greppable
|
||||
or pipeable into `jq`. Key events to look for:
|
||||
|
||||
- `webhook received` — Gitea reached the service at all
|
||||
- `ignored` with a `reason` field — why an event didn't trigger a run
|
||||
- `trigger matched` / `job started` / `job finished` — a run went through
|
||||
- `job errored` — something failed; the same message was also posted as a
|
||||
Gitea comment on the issue
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Only `issue_comment` and `issues` events are handled; PR *review* comments
|
||||
(`pull_request_review_comment`) are a separate event type and are not
|
||||
wired up.
|
||||
- Runs execute inside this one long-lived container, not a fresh sandbox
|
||||
per job — don't point `OWNER_LOGIN` checks loosely, and don't run this
|
||||
against repos where you don't trust every collaborator who can comment.
|
||||
- Uses your Claude subscription (via `claude setup-token`), so runs share
|
||||
the same usage limits as interactive Claude Code / claude.ai use.
|
||||
Reference in New Issue
Block a user