Claude: address #2
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user