From 239d5da3ec39adf4652df77d003db1fd010625ca Mon Sep 17 00:00:00 2001 From: Oz Sayag Date: Sun, 18 Oct 2026 14:54:48 +0300 Subject: [PATCH] ci(platform): sync the webhook event types from production every hour A sibling of sync-asyncapi.yml. Each hour it adds the event types the production Outbound Webhooks spec publishes to webhook-event-types.json, regenerates every type from the published schemas, and keeps one rolling PR open when that changes anything, so new events and changed schemas both reach the SDK. Two changes let that PR pass without hand edits: the server entrypoint re-exports every generated data type instead of naming each one, and the type test no longer pins the full list of event types. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/sync-webhook-events.yml | 102 ++++++++++++++++++ packages/platform/package.json | 3 +- .../scripts/sync-webhook-event-types.mjs | 22 ++++ packages/platform/src/server/index.ts | 7 +- .../platform/tests/types/webhooks.types.ts | 9 +- 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/sync-webhook-events.yml create mode 100644 packages/platform/scripts/sync-webhook-event-types.mjs diff --git a/.github/workflows/sync-webhook-events.yml b/.github/workflows/sync-webhook-events.yml new file mode 100644 index 00000000..97cca5f6 --- /dev/null +++ b/.github/workflows/sync-webhook-events.yml @@ -0,0 +1,102 @@ +name: Sync Platform SDK webhook events + +# Adds the webhook event types production publishes to packages/platform/scripts/webhook-event-types.json +# and regenerates every type from the published schemas, so it picks up a new event and a changed +# schema alike. Changes go to one rolling PR; closing it +# without merging only skips that run, since the next one proposes the same changes again. +# The sibling of sync-asyncapi.yml, which does the same for the builder socket events. + +on: + schedule: + - cron: "45 * * * *" # Every hour, apart from sync-asyncapi's :15 + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: sync-webhook-events + cancel-in-progress: false + +env: + BRANCH: bot/platform-webhook-events-sync + WEBHOOKS_OPENAPI_URL: https://app.base44.com/api/v1/outbound-webhooks/openapi.json + EVENT_LIST: packages/platform/scripts/webhook-event-types.json + GENERATED: packages/platform/src/server/modules/webhooks.events.generated.ts + +jobs: + sync: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: main + + - name: Wix gateway proxy (mandatory) + uses: ./.github/actions/wix-gateway-proxy + + - name: Use Node.js 20.x + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "20.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Sync the event list and regenerate the types + run: | + npm run sync:webhooks -w @base44/platform -- "$WEBHOOKS_OPENAPI_URL" + npm run generate:webhooks -w @base44/platform + + - name: Check for changes + id: changes + run: | + if git diff --quiet -- "$EVENT_LIST" "$GENERATED"; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "✅ The webhook event types are up to date" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Close a stale sync PR + if: steps.changes.outputs.changed == 'false' + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') + if [ -n "$pr" ]; then gh pr close "$pr" --delete-branch --comment "main already has every published event type."; fi + + - name: Build the docs and test the Platform SDK + if: steps.changes.outputs.changed == 'true' + run: | + npm run create-docs -w @base44/platform + npm test -w @base44/platform + + - name: Update the sync PR + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + git add "$EVENT_LIST" "$GENERATED" + git commit -m "chore(platform): sync the webhook event types from production" + + pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') + # An open sync PR whose branch already has this exact tree needs no new commit or force-push. + if [ -n "$pr" ] && git fetch --depth=1 origin "$BRANCH" && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then + echo "✅ #$pr already has these changes" + exit 0 + fi + # Rebuilt from main every run, so the branch never drifts or conflicts. + git push --force origin "$BRANCH" + + body="Adds any new webhook event types published at $WEBHOOKS_OPENAPI_URL to \`webhook-event-types.json\` and regenerates \`webhooks.events.generated.ts\` from https://base44.com/webhooks/schemas, which also picks up changed schemas. The docs build and the Platform SDK tests passed on this change. Checked against production every hour." + if [ -z "$pr" ]; then + gh pr create --head "$BRANCH" --base main --title "Sync Platform SDK webhook events from production" --body "$body" + fi diff --git a/packages/platform/package.json b/packages/platform/package.json index 40c4003d..82c7e047 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -45,7 +45,8 @@ "copy-docs-local": "node ../sdk/scripts/mintlify-post-processing/copy-to-local-docs.js --package-dir .", "create-docs:generate": "node -e \"fs.rmSync('docs',{recursive:true,force:true})\" && typedoc --options typedoc.mintlify.json", "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir .", - "generate:webhooks": "node scripts/generate-webhook-types.mjs" + "generate:webhooks": "node scripts/generate-webhook-types.mjs", + "sync:webhooks": "node scripts/sync-webhook-event-types.mjs" }, "dependencies": { "socket.io-client": "^4.8.3" diff --git a/packages/platform/scripts/sync-webhook-event-types.mjs b/packages/platform/scripts/sync-webhook-event-types.mjs new file mode 100644 index 00000000..3ba010f7 --- /dev/null +++ b/packages/platform/scripts/sync-webhook-event-types.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node +// Adds the webhook event types production publishes to scripts/webhook-event-types.json: +// npm run sync:webhooks [-- ] +// Then run `npm run generate:webhooks`. The hourly sync-webhook-events workflow does both. +// +// Only adds. A retired type leaves the published spec, but endpoints that selected it keep +// receiving it, so the SDK keeps its type. +import { readFile, writeFile } from "node:fs/promises"; + +const url = + process.argv[2] ?? process.env.WEBHOOKS_OPENAPI_URL ?? "https://app.base44.com/api/v1/outbound-webhooks/openapi.json"; +const target = new URL("./webhook-event-types.json", import.meta.url); + +const response = await fetch(url); +if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`); +const published = Object.keys((await response.json()).webhooks ?? {}); +if (!published.length) throw new Error(`${url} has no webhooks section. Kept the committed list.`); + +const known = JSON.parse(await readFile(target, "utf8")); +const added = published.filter(type => !known.includes(type)); +await writeFile(target, JSON.stringify([...known, ...added], null, 2) + "\n"); +console.log(added.length ? `Added ${added.join(", ")} from ${url}.` : `No new event types at ${url}.`); diff --git a/packages/platform/src/server/index.ts b/packages/platform/src/server/index.ts index 47d1a82d..63eaf098 100644 --- a/packages/platform/src/server/index.ts +++ b/packages/platform/src/server/index.ts @@ -4,7 +4,8 @@ export { WebhookConfigurationError, WebhookVerificationError } from "./errors.js export type { WebhookConfigurationErrorCode, WebhookVerificationErrorCode } from "./errors.types.js"; export type { PlatformServer } from "./server.types.js"; export type { - AppCreatedV1Data, AppDeletedV1Data, AppPublishedV1Data, AppRestoredV1Data, AppUnpublishedV1Data, CloudEvent, CreateHandlerOptions, - DeliveredWebhookEvent, UnknownWebhookEvent, VerifyEventOptions, WebhookEventHandlers, WebhookEvent, WebhookEventMap, WebhookEventType, WebhookHeaders, - WebhookPublicKeys, WebhooksModule, WebhookTestV1Data, + CloudEvent, CreateHandlerOptions, DeliveredWebhookEvent, UnknownWebhookEvent, VerifyEventOptions, WebhookEventHandlers, WebhookEvent, + WebhookEventType, WebhookHeaders, WebhookPublicKeys, WebhooksModule, } from "./modules/webhooks.types.js"; +// Every event's data type, so a type the sync adds is exported and documented without an edit here. +export type * from "./modules/webhooks.events.generated.js"; diff --git a/packages/platform/tests/types/webhooks.types.ts b/packages/platform/tests/types/webhooks.types.ts index 98ce34fe..4dea7967 100644 --- a/packages/platform/tests/types/webhooks.types.ts +++ b/packages/platform/tests/types/webhooks.types.ts @@ -1,5 +1,6 @@ import { - createPlatformServer, type AppCreatedV1Data, type WebhookEvent, type WebhookEventMap, type WebhookTestV1Data, + createPlatformServer, type AppCreatedV1Data, type WebhookEvent, type WebhookEventMap, type WebhookEventType, + type WebhookTestV1Data, } from "@base44/platform/server"; const platform = createPlatformServer(); @@ -27,8 +28,8 @@ async function handle(body: string, headers: Headers) { break; } default: { - // A default branch still sees the event (the remaining known types), so it can be logged. - const type: "app.unpublished.v1" | "app.restored.v1" = event.type; + // A default branch still sees the event (the known types not handled above), so it can be logged. + const type: Exclude = event.type; void type; } } @@ -45,7 +46,7 @@ platform.webhooks.createHandler({ // Examples published with the schemas must stay assignable to the generated types. const created: AppCreatedV1Data = { app_id: "68b0c1d2e3f4a5b6c7d8e9f0", workspace_id: "68a1b2c3d4e5f6a7b8c9d0e1", owner_id: "6899aabbccddeeff00112233", - owner_service_external_id: "sunny-9f2c4a", app_type: "user_app", created_at: "2026-09-01T10:15:30.123Z", + app_type: "user_app", created_at: "2026-09-01T10:15:30.123Z", }; const probe: WebhookTestV1Data = { endpoint_id: "e", workspace_id: "w", sent_at: "2026-09-01T10:15:30.123Z", challenge: "8f14e45f" }; const map: Pick = { "app.created.v1": created, "webhook.test.v1": probe };