From 7bf5570a402fcebe2d5c8416089c24085752305a Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Fri, 18 Sep 2026 19:57:31 -0700 Subject: [PATCH 1/2] fix(shortcut): keep supported actions when a webhook payload mixes in unsupported ones Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + packages/shortcut/package.json | 2 +- packages/shortcut/src/shortcut.test.ts | 30 ++++++++++++++++- packages/shortcut/src/webhook-normalizer.ts | 36 ++++++++++++++++++--- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daeda2c1..db4e3dc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ published version with a date and open a fresh empty `[Unreleased]` above it. ### Fixed +- `@relayfile/adapter-shortcut` now keeps supported story and epic actions from bundled webhook deliveries that also contain unsupported Shortcut entities, and exposes skipped event types for delivery diagnostics. - `@relayfile/adapter-github` pull-index convergence now fails closed when a page cannot be persisted and reports fetched versus persisted row counts separately, preventing exhausted CAS writes from appearing complete. - `@relayfile/adapter-linear` issue creates now accept synced `team.id` and label ids directly, resolve `team.key`/`team.name` plus label names through mounted team/label indexes, and keep explicit `teamId`/`labelIds` authoritative; adapter-core now enforces the schema's at-least-one team reference. - `@relayfile/adapter-github` now backfills label names into legacy issue `_index.json` rows from materialized issue artifacts, keeping label-filtered consumers on the index-only path. diff --git a/packages/shortcut/package.json b/packages/shortcut/package.json index 33ed3308..ccb93600 100644 --- a/packages/shortcut/package.json +++ b/packages/shortcut/package.json @@ -1,6 +1,6 @@ { "name": "@relayfile/adapter-shortcut", - "version": "0.1.2", + "version": "0.1.3", "description": "Shortcut adapter package for Relayfile", "type": "module", "main": "dist/index.js", diff --git a/packages/shortcut/src/shortcut.test.ts b/packages/shortcut/src/shortcut.test.ts index b64d7000..680a8644 100644 --- a/packages/shortcut/src/shortcut.test.ts +++ b/packages/shortcut/src/shortcut.test.ts @@ -52,9 +52,37 @@ describe("Shortcut adapter", () => { ]); }); + it("keeps a supported story create when a bundled action is unsupported", () => { + const normalized = normalizeShortcutWebhook({ + actions: [ + { id: 35, entity_type: "story", action: "create" }, + { id: 9, entity_type: "label", action: "create" }, + ], + }); + + assert.deepEqual(normalized.actions.map((action) => action.eventType), ["story.create"]); + assert.deepEqual(normalized.skippedEventTypes, ["label.create"]); + }); + + it("keeps a supported story update after unsupported bundled actions", () => { + const normalized = normalizeShortcutWebhook({ + actions: [ + { id: 8, entity_type: "pull-request", action: "update" }, + { id: 7, entity_type: "branch", action: "push" }, + { id: 35, entity_type: "story", action: "update" }, + ], + }); + + assert.deepEqual(normalized.actions.map((action) => action.eventType), ["story.update"]); + assert.deepEqual(normalized.skippedEventTypes, ["pull-request.update", "branch.push"]); + }); + it("rejects malformed and unsupported webhook actions consistently", () => { assert.throws(() => normalizeShortcutWebhook({ action: { id: 1, entity_type: "story", action: "archive" } })); - assert.throws(() => normalizeShortcutWebhook({ action: { id: 1, entity_type: "unknown", action: "update" } })); + assert.throws( + () => normalizeShortcutWebhook({ action: { id: 1, entity_type: "unknown", action: "update" } }), + /Unsupported Shortcut webhook event: unknown\.update; no supported actions; skipped event types: unknown\.update/, + ); assert.throws(() => normalizeShortcutWebhook({ actions: [{ id: 1, entity_type: "story" }] })); }); diff --git a/packages/shortcut/src/webhook-normalizer.ts b/packages/shortcut/src/webhook-normalizer.ts index 0a23588a..74d0acad 100644 --- a/packages/shortcut/src/webhook-normalizer.ts +++ b/packages/shortcut/src/webhook-normalizer.ts @@ -38,11 +38,27 @@ export interface ShortcutNormalizedWebhook { eventId?: string; changedAt?: string; actions: ShortcutNormalizedWebhookAction[]; + /** + * Valid Shortcut actions that do not produce a Relayfile trigger. These are + * retained so callers can log an otherwise partial webhook delivery. + */ + skippedEventTypes: string[]; headers: Record; connectionId?: string; deliveryId?: string; } +/** + * Creates the error used when a Shortcut webhook contains actions but none + * match the events this adapter supports. + */ +export function unsupportedShortcutWebhookEventError(unsupportedEventTypes: readonly string[]): Error { + const [firstUnsupported] = unsupportedEventTypes; + return new Error( + `Unsupported Shortcut webhook event: ${firstUnsupported}; no supported actions; skipped event types: ${unsupportedEventTypes.join(", ")}`, + ); +} + export function normalizeShortcutWebhook( rawPayload: unknown, headers: ShortcutWebhookHeaders = {}, @@ -52,7 +68,8 @@ export function normalizeShortcutWebhook( if (!payload) throw new Error("Shortcut webhook payload must be a JSON object"); const normalizedHeaders = normalizeHeaders(headers); - const actions = readActions(payload).map((action) => { + const skippedEventTypes: string[] = []; + const actions = readActions(payload).flatMap((action) => { const objectType = String(action.entity_type ?? "").trim().toLowerCase(); const verb = String(action.action ?? "").trim().toLowerCase(); const objectId = String(action.id ?? "").trim(); @@ -60,7 +77,11 @@ export function normalizeShortcutWebhook( throw new Error("Shortcut webhook action must include entity_type, action, and id"); } const eventType = normalizeEventType(objectType, verb); - return { + if (!eventType) { + skippedEventTypes.push(`${objectType}.${verb}`); + return []; + } + return [{ provider: SHORTCUT_PROVIDER as typeof SHORTCUT_PROVIDER, eventType, action: verb, @@ -69,14 +90,19 @@ export function normalizeShortcutWebhook( payload: { ...payload, action: { ...action } }, ...(options.connectionId ? { connectionId: options.connectionId } : {}), ...(options.deliveryId ? { deliveryId: options.deliveryId } : {}), - }; + }]; }); + if (actions.length === 0 && skippedEventTypes.length > 0) { + throw unsupportedShortcutWebhookEventError(skippedEventTypes); + } + return { provider: SHORTCUT_PROVIDER, eventId: readString(payload, "id"), changedAt: readString(payload, "changed_at"), actions, + skippedEventTypes, headers: normalizedHeaders, ...(options.connectionId ? { connectionId: options.connectionId } : {}), ...(options.deliveryId ? { deliveryId: options.deliveryId } : {}), @@ -98,14 +124,14 @@ function readActions(payload: Record): ShortcutWebhookAction[] }); } -function normalizeEventType(objectType: string, verb: string): string { +function normalizeEventType(objectType: string, verb: string): string | undefined { const direct = `${objectType}.${verb}`; if ((SHORTCUT_SUPPORTED_EVENTS as readonly string[]).includes(direct)) return direct; const parentType = NESTED_PARENT_TYPES[objectType]; if (parentType && ["create", "update", "delete"].includes(verb)) { return `${parentType}.update`; } - throw new Error(`Unsupported Shortcut webhook event: ${direct}`); + return undefined; } function isWebhookAction(value: Record): boolean { From 90a2791a89f6038d595e49123a42a0c618f81577 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 18 Sep 2026 20:17:41 -0700 Subject: [PATCH 2/2] chore(shortcut): release the required skippedEventTypes field as 0.2.0 `ShortcutNormalizedWebhook` is publicly exported; adding a required field is a breaking change for anyone constructing it by hand, so bump minor under 0.x rather than weakening the field to optional. `normalizeShortcutWebhook` always populates it. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- packages/shortcut/package.json | 2 +- packages/shortcut/src/webhook-normalizer.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db4e3dc4..f13117a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ published version with a date and open a fresh empty `[Unreleased]` above it. ### Fixed -- `@relayfile/adapter-shortcut` now keeps supported story and epic actions from bundled webhook deliveries that also contain unsupported Shortcut entities, and exposes skipped event types for delivery diagnostics. +- `@relayfile/adapter-shortcut` now keeps supported story and epic actions from bundled webhook deliveries that also contain unsupported Shortcut entities, and exposes skipped event types for delivery diagnostics. **Breaking (0.2.0):** `ShortcutNormalizedWebhook` gains a required `skippedEventTypes: string[]`; consumers that construct the type by hand must supply it (`normalizeShortcutWebhook` always does). - `@relayfile/adapter-github` pull-index convergence now fails closed when a page cannot be persisted and reports fetched versus persisted row counts separately, preventing exhausted CAS writes from appearing complete. - `@relayfile/adapter-linear` issue creates now accept synced `team.id` and label ids directly, resolve `team.key`/`team.name` plus label names through mounted team/label indexes, and keep explicit `teamId`/`labelIds` authoritative; adapter-core now enforces the schema's at-least-one team reference. - `@relayfile/adapter-github` now backfills label names into legacy issue `_index.json` rows from materialized issue artifacts, keeping label-filtered consumers on the index-only path. diff --git a/packages/shortcut/package.json b/packages/shortcut/package.json index ccb93600..97b82006 100644 --- a/packages/shortcut/package.json +++ b/packages/shortcut/package.json @@ -1,6 +1,6 @@ { "name": "@relayfile/adapter-shortcut", - "version": "0.1.3", + "version": "0.2.0", "description": "Shortcut adapter package for Relayfile", "type": "module", "main": "dist/index.js", diff --git a/packages/shortcut/src/webhook-normalizer.ts b/packages/shortcut/src/webhook-normalizer.ts index 74d0acad..f5319bef 100644 --- a/packages/shortcut/src/webhook-normalizer.ts +++ b/packages/shortcut/src/webhook-normalizer.ts @@ -41,6 +41,11 @@ export interface ShortcutNormalizedWebhook { /** * Valid Shortcut actions that do not produce a Relayfile trigger. These are * retained so callers can log an otherwise partial webhook delivery. + * + * Required, not optional: `normalizeShortcutWebhook` always populates it, and + * a consumer reading the result must be able to rely on that. Adding it was + * released as a breaking change (0.2.0) for anyone constructing this type + * by hand. */ skippedEventTypes: string[]; headers: Record;