Claude: address #2

This commit is contained in:
2026-09-28 15:09:16 +00:00
parent 1380eee989
commit 963667a2eb
3 changed files with 137 additions and 9 deletions
+5 -4
View File
@@ -78,7 +78,11 @@ See `.env.example`.
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.
to work too). To also support pull requests, enable **Pull Request**
(for `@claude` in a fresh PR description), **Pull Request Comment**
(general PR comments — routed the same as issue comments), **Pull
Request Review**, and **Pull Request Review Comment** (inline diff
comments). 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.
@@ -97,9 +101,6 @@ or pipeable into `jq`. Key events to look for:
## 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.
+30 -5
View File
@@ -25,13 +25,38 @@ export function classifyEvent(eventName, payload, { ownerLogin, triggerPhrase })
let author;
let text;
let entity; // the issue or pull_request object carrying number/title/body
let isPullRequest;
if (eventName === 'issue_comment' && payload.action === 'created') {
// Gitea (like GitHub) routes comments on both issues and pull requests
// through this single event; payload.issue.pull_request distinguishes them.
author = payload.comment?.user?.login;
text = payload.comment?.body;
entity = payload.issue;
isPullRequest = Boolean(payload.issue?.pull_request);
} else if (eventName === 'issues' && ['opened', 'edited'].includes(payload.action)) {
author = payload.issue?.user?.login;
text = payload.issue?.body;
entity = payload.issue;
isPullRequest = false;
} else if (eventName === 'pull_request' && ['opened', 'edited'].includes(payload.action)) {
author = payload.pull_request?.user?.login;
text = payload.pull_request?.body;
entity = payload.pull_request;
isPullRequest = true;
} else if (eventName === 'pull_request_review_comment' && payload.action === 'created') {
// Inline review comments on a PR's diff; a separate event type from issue_comment.
author = payload.comment?.user?.login;
text = payload.comment?.body;
entity = payload.pull_request;
isPullRequest = true;
} else if (eventName === 'pull_request_review' && payload.action === 'submitted') {
// A review's overall summary comment, left when submitting an approval/rejection.
author = payload.review?.user?.login;
text = payload.review?.body ?? payload.review?.content;
entity = payload.pull_request;
isPullRequest = true;
} else {
return { trigger: null, reason: `event "${eventName}" action "${payload.action}" is not handled` };
}
@@ -42,17 +67,17 @@ export function classifyEvent(eventName, payload, { ownerLogin, triggerPhrase })
if (!text || !text.includes(triggerPhrase)) {
return { trigger: null, reason: `text does not contain trigger phrase "${triggerPhrase}"` };
}
if (!payload.repository?.full_name || !payload.issue?.number) {
if (!payload.repository?.full_name || !entity?.number) {
return { trigger: null, reason: 'payload is missing repository or issue information' };
}
return {
trigger: {
repo: payload.repository.full_name,
issueNumber: payload.issue.number,
issueTitle: payload.issue.title ?? '',
issueBody: payload.issue.body ?? '',
isPullRequest: Boolean(payload.issue.pull_request),
issueNumber: entity.number,
issueTitle: entity.title ?? '',
issueBody: entity.body ?? '',
isPullRequest,
defaultBranch: payload.repository.default_branch || 'main',
requestText: text,
author,
+102
View File
@@ -90,3 +90,105 @@ test('defaults default_branch to main when absent', () => {
const { trigger } = classifyEvent('issue_comment', payload, opts);
assert.equal(trigger.defaultBranch, 'main');
});
function pullRequestPayload(overrides = {}) {
return {
action: 'opened',
pull_request: { number: 5, title: 'Add feature', body: 'please @claude review', user: { login: 'selimaj-dev' } },
repository: { full_name: 'selimaj-dev/repo', default_branch: 'main' },
...overrides,
};
}
test('triggers on a new pull request whose body mentions the phrase', () => {
const { trigger, reason } = classifyEvent('pull_request', pullRequestPayload(), opts);
assert.equal(reason, null);
assert.deepEqual(trigger, {
repo: 'selimaj-dev/repo',
issueNumber: 5,
issueTitle: 'Add feature',
issueBody: 'please @claude review',
isPullRequest: true,
defaultBranch: 'main',
requestText: 'please @claude review',
author: 'selimaj-dev',
});
});
test('triggers on an edited pull request body', () => {
const { trigger } = classifyEvent('pull_request', pullRequestPayload({ action: 'edited' }), opts);
assert.ok(trigger);
});
test('ignores pull_request actions other than opened/edited', () => {
const { trigger, reason } = classifyEvent('pull_request', pullRequestPayload({ action: 'closed' }), opts);
assert.equal(trigger, null);
assert.match(reason, /not handled/);
});
test('ignores pull requests not from the configured owner', () => {
const payload = pullRequestPayload({ pull_request: { number: 5, title: 'x', body: '@claude help', user: { login: 'someone-else' } } });
const { trigger, reason } = classifyEvent('pull_request', payload, opts);
assert.equal(trigger, null);
assert.match(reason, /is not the configured owner/);
});
function pullRequestReviewCommentPayload(overrides = {}) {
return {
action: 'created',
comment: { user: { login: 'selimaj-dev' }, body: '@claude fix this line' },
pull_request: { number: 9, title: 'PR title', body: 'pr description' },
repository: { full_name: 'selimaj-dev/repo', default_branch: 'main' },
...overrides,
};
}
test('triggers on a pull request review (inline) comment mentioning the phrase', () => {
const { trigger, reason } = classifyEvent('pull_request_review_comment', pullRequestReviewCommentPayload(), opts);
assert.equal(reason, null);
assert.deepEqual(trigger, {
repo: 'selimaj-dev/repo',
issueNumber: 9,
issueTitle: 'PR title',
issueBody: 'pr description',
isPullRequest: true,
defaultBranch: 'main',
requestText: '@claude fix this line',
author: 'selimaj-dev',
});
});
test('ignores non-created pull_request_review_comment actions', () => {
const { trigger, reason } = classifyEvent(
'pull_request_review_comment',
pullRequestReviewCommentPayload({ action: 'deleted' }),
opts,
);
assert.equal(trigger, null);
assert.match(reason, /not handled/);
});
function pullRequestReviewPayload(overrides = {}) {
return {
action: 'submitted',
review: { user: { login: 'selimaj-dev' }, body: '@claude address these comments' },
pull_request: { number: 11, title: 'PR title', body: 'pr description' },
repository: { full_name: 'selimaj-dev/repo', default_branch: 'main' },
...overrides,
};
}
test('triggers on a submitted pull request review mentioning the phrase', () => {
const { trigger, reason } = classifyEvent('pull_request_review', pullRequestReviewPayload(), opts);
assert.equal(reason, null);
assert.equal(trigger.requestText, '@claude address these comments');
assert.equal(trigger.isPullRequest, true);
assert.equal(trigger.issueNumber, 11);
});
test('ignores pull_request_review without the trigger phrase', () => {
const payload = pullRequestReviewPayload({ review: { user: { login: 'selimaj-dev' }, body: 'looks good' } });
const { trigger, reason } = classifyEvent('pull_request_review', payload, opts);
assert.equal(trigger, null);
assert.match(reason, /trigger phrase/);
});