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 };