From faa19c4fcb9cf01062ea8532bad0bb4b15cb2030 Mon Sep 17 00:00:00 2001 From: RelayFile Adapters Bot Date: Sun, 20 Sep 2026 15:26:57 -0700 Subject: [PATCH] feat(github): route babysitter pull request events Session-Id: 2fc28d5e-2a7c-4168-9cdf-9460746c0212 --- CHANGELOG.md | 1 + .../core/src/triggers/catalog.generated.json | 3 +++ .../core/src/triggers/catalog.generated.ts | 3 +++ packages/github/src/config.ts | 3 +++ packages/github/src/scaffold.test.ts | 3 +++ packages/github/src/types.ts | 3 +++ .../webhook/__tests__/webhook-router.test.ts | 24 +++++++++++++++++-- packages/github/src/webhook/event-map.ts | 3 +++ packages/github/src/webhook/router.test.ts | 5 +++- 9 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cee70997..14fc3e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ published version with a date and open a fresh empty `[Unreleased]` above it. ### Added - `@relayfile/adapter-gitlab` now creates issues, branches, and merge requests from file-native drafts; accepts or closes/reopens merge requests through canonical sidecars; and publishes schemas, examples, catalog paths, and `LAYOUT.md` guidance for every supported GitLab writeback route. +- `@relayfile/adapter-github` now declares and routes pull-request `ready_for_review`, `labeled`, and `unlabeled` webhooks so review flows can wake on draft and policy-label transitions. - `@relayfile/adapter-github` and the core GitHub mapping now declare `check_run` and `issue_comment` webhook keys, so consumers that read the mapping's `webhooks:` block can subscribe to CI check completions and issue/PR conversation comments. - `@relayfile/adapter-github` now exports a cursor-resumable pull-index convergence primitive that backfills `headRef` with one GitHub list request per invocation and no per-record metadata, file, or diff fetches. - `@relayfile/adapter-linear` now materializes `/linear/issues/by-project//.json` aliases so project-scoped consumers can avoid mounting the full issue tree. The path mapper and generated `LAYOUT.md` contract expose the new lookup; existing mounts must resync to materialize and discover it. diff --git a/packages/core/src/triggers/catalog.generated.json b/packages/core/src/triggers/catalog.generated.json index e86ce49d..34d0941c 100644 --- a/packages/core/src/triggers/catalog.generated.json +++ b/packages/core/src/triggers/catalog.generated.json @@ -131,9 +131,12 @@ "issues.unlabeled", "pull_request.closed", "pull_request.edited", + "pull_request.labeled", "pull_request.opened", + "pull_request.ready_for_review", "pull_request.reopened", "pull_request.synchronize", + "pull_request.unlabeled", "pull_request_review.dismissed", "pull_request_review.edited", "pull_request_review.submitted", diff --git a/packages/core/src/triggers/catalog.generated.ts b/packages/core/src/triggers/catalog.generated.ts index 9c824ef2..1cc8b2ba 100644 --- a/packages/core/src/triggers/catalog.generated.ts +++ b/packages/core/src/triggers/catalog.generated.ts @@ -139,9 +139,12 @@ export const KNOWN_TRIGGER_CATALOG = { "issues.unlabeled", "pull_request.closed", "pull_request.edited", + "pull_request.labeled", "pull_request.opened", + "pull_request.ready_for_review", "pull_request.reopened", "pull_request.synchronize", + "pull_request.unlabeled", "pull_request_review.dismissed", "pull_request_review.edited", "pull_request_review.submitted", diff --git a/packages/github/src/config.ts b/packages/github/src/config.ts index a50cff0e..9f702807 100644 --- a/packages/github/src/config.ts +++ b/packages/github/src/config.ts @@ -23,6 +23,9 @@ const DEFAULT_SUPPORTED_EVENTS = [ 'pull_request.synchronize', // new commits were pushed to an open PR (re-review / re-run CI) 'pull_request.edited', // PR title/body/base metadata changed 'pull_request.reopened', // a closed PR was reopened + 'pull_request.ready_for_review', // a draft PR became ready for review + 'pull_request.labeled', // a label was added to an open PR + 'pull_request.unlabeled', // a label was removed from an open PR 'pull_request.closed', // a PR was closed (check `merged` in the payload to tell merge from abandon) 'pull_request_review.submitted', // someone (human or bot) submitted a review — has `review.state` (approved / changes_requested / commented) 'pull_request_review.edited', // review body or metadata changed diff --git a/packages/github/src/scaffold.test.ts b/packages/github/src/scaffold.test.ts index cd35a138..ce39b407 100644 --- a/packages/github/src/scaffold.test.ts +++ b/packages/github/src/scaffold.test.ts @@ -40,6 +40,9 @@ describe('scaffold', () => { assert.strictEqual(adapter.name, 'github'); assert.strictEqual(adapter.version, '0.1.0'); assert.ok(adapter.supportedEvents().includes('check_run.completed')); + assert.ok(adapter.supportedEvents().includes('pull_request.ready_for_review')); + assert.ok(adapter.supportedEvents().includes('pull_request.labeled')); + assert.ok(adapter.supportedEvents().includes('pull_request.unlabeled')); const result = await adapter.ingestPullRequest({ number: 42, diff --git a/packages/github/src/types.ts b/packages/github/src/types.ts index 66391a04..a15f83b2 100644 --- a/packages/github/src/types.ts +++ b/packages/github/src/types.ts @@ -20,6 +20,9 @@ export const DEFAULT_GITHUB_EVENTS = [ 'pull_request.synchronize', 'pull_request.edited', 'pull_request.reopened', + 'pull_request.ready_for_review', + 'pull_request.labeled', + 'pull_request.unlabeled', 'pull_request.closed', 'pull_request_review.submitted', 'pull_request_review.edited', diff --git a/packages/github/src/webhook/__tests__/webhook-router.test.ts b/packages/github/src/webhook/__tests__/webhook-router.test.ts index f223a874..7559dbd3 100644 --- a/packages/github/src/webhook/__tests__/webhook-router.test.ts +++ b/packages/github/src/webhook/__tests__/webhook-router.test.ts @@ -138,6 +138,23 @@ describe('WebhookRouter', () => { assert.strictEqual(mocks.ingestCheckRun.mock.calls.length, 0); }); + it('routes PR review-readiness and label changes through pull request updates', async () => { + for (const action of ['ready_for_review', 'labeled', 'unlabeled']) { + const mocks = createAdapterMocks(); + const router = new WebhookRouter(mocks.adapter); + const payload = { + action, + repository: { full_name: 'acme/widgets' }, + pull_request: { number: 7 }, + }; + + const result = await router.route({ 'x-github-event': 'pull_request' }, payload); + assert.deepStrictEqual(result, createResult('/github/repos/acme/widgets/pulls/7/diff.patch')); + assert.strictEqual(mocks.updatePullRequest.mock.calls.length, 1); + assert.deepStrictEqual(mocks.updatePullRequest.mock.calls[0].arguments, [payload]); + } + }); + it('route calls correct handler for issue opened', async () => { const mocks = createAdapterMocks(); const router = new WebhookRouter(mocks.adapter); @@ -274,7 +291,7 @@ describe('WebhookRouter', () => { assert.strictEqual(router.isSupported('deployment_status.created'), true); }); - it('getSupportedEvents lists all 21 events', () => { + it('getSupportedEvents lists all 24 events', () => { const router = new WebhookRouter(createAdapterMocks().adapter); assert.deepStrictEqual(router.getSupportedEvents(), [ @@ -282,6 +299,9 @@ describe('WebhookRouter', () => { 'pull_request.synchronize', 'pull_request.edited', 'pull_request.reopened', + 'pull_request.ready_for_review', + 'pull_request.labeled', + 'pull_request.unlabeled', 'pull_request.closed', 'pull_request_review.submitted', 'pull_request_review.edited', @@ -300,6 +320,6 @@ describe('WebhookRouter', () => { 'status', 'deployment_status.created', ]); - assert.strictEqual(router.getSupportedEvents().length, 21); + assert.strictEqual(router.getSupportedEvents().length, 24); }); }); diff --git a/packages/github/src/webhook/event-map.ts b/packages/github/src/webhook/event-map.ts index 6e398f63..268d681a 100644 --- a/packages/github/src/webhook/event-map.ts +++ b/packages/github/src/webhook/event-map.ts @@ -28,6 +28,9 @@ export const EVENT_MAP: Record = { 'pull_request.synchronize': (adapter, payload) => adapter.updatePullRequest(payload), 'pull_request.edited': (adapter, payload) => adapter.updatePullRequest(payload), 'pull_request.reopened': (adapter, payload) => adapter.updatePullRequest(payload), + 'pull_request.ready_for_review': (adapter, payload) => adapter.updatePullRequest(payload), + 'pull_request.labeled': (adapter, payload) => adapter.updatePullRequest(payload), + 'pull_request.unlabeled': (adapter, payload) => adapter.updatePullRequest(payload), 'pull_request.closed': (adapter, payload) => adapter.closePullRequest(payload), 'pull_request_review.submitted': (adapter, payload) => adapter.ingestReview(payload), 'pull_request_review.edited': (adapter, payload) => adapter.ingestReview(payload), diff --git a/packages/github/src/webhook/router.test.ts b/packages/github/src/webhook/router.test.ts index 0fdc6e83..9c74076a 100644 --- a/packages/github/src/webhook/router.test.ts +++ b/packages/github/src/webhook/router.test.ts @@ -327,12 +327,15 @@ test('WebhookRouter.getSupportedEvents returns all mapped events', () => { const router = new WebhookRouter(new RecordingAdapter()); const events = router.getSupportedEvents(); - assert.equal(events.length, 21); + assert.equal(events.length, 24); assert.deepEqual(events, [ 'pull_request.opened', 'pull_request.synchronize', 'pull_request.edited', 'pull_request.reopened', + 'pull_request.ready_for_review', + 'pull_request.labeled', + 'pull_request.unlabeled', 'pull_request.closed', 'pull_request_review.submitted', 'pull_request_review.edited',