From 9d6c2819d13557b9e7828d35cb40284e30ffe903 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Thu, 17 Sep 2026 16:05:00 -0700 Subject: [PATCH] feat(core): ship every adapter's webhook mapping in @relayfile/adapter-core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumers that depend only on @relayfile/adapter-core saw two mapping files: the github and slack fallbacks under mappings/. AgentWorkforce/flows generates its typed trigger namespaces from those `webhooks:` blocks, so 23 adapters that declare webhooks (notion, gitlab, cloudflare, ramp, neon, shortcut, gmail, …) could never trigger a flow from the published package. The package now also ships mappings/adapters/.mapping.yaml, a verbatim copy of each packages//.mapping.yaml, generated by scripts/bundle-adapter-mappings.mjs and committed like the other catalogs. `catalog:check` (turbo test) and prepublishOnly run its --check, which fails on a missing, stale or orphaned copy, so a tarball cannot carry a stale bundle. The fallbacks are untouched; consumers apply adapter-local over fallback, the precedence flows' generator already uses against a repo checkout. Additive: a patch release suffices. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 7 +- packages/core/README.md | 18 +++ .../mappings/adapters/azure-blob.mapping.yaml | 7 + .../core/mappings/adapters/box.mapping.yaml | 7 + .../mappings/adapters/cloudflare.mapping.yaml | 51 +++++++ .../mappings/adapters/daytona.mapping.yaml | 12 ++ .../mappings/adapters/dropbox.mapping.yaml | 3 + .../mappings/adapters/fathom.mapping.yaml | 17 +++ .../core/mappings/adapters/gcp.mapping.yaml | 56 +++++++ .../core/mappings/adapters/gcs.mapping.yaml | 7 + .../mappings/adapters/github.mapping.yaml | 143 ++++++++++++++++++ .../mappings/adapters/gitlab.mapping.yaml | 72 +++++++++ .../core/mappings/adapters/gmail.mapping.yaml | 29 ++++ .../adapters/google-drive.mapping.yaml | 7 + .../mappings/adapters/granola.mapping.yaml | 17 +++ .../core/mappings/adapters/neon.mapping.yaml | 38 +++++ .../mappings/adapters/notion.mapping.yaml | 105 +++++++++++++ .../mappings/adapters/onedrive.mapping.yaml | 7 + .../mappings/adapters/postgres.mapping.yaml | 7 + .../mappings/adapters/posthog.mapping.yaml | 50 ++++++ .../core/mappings/adapters/ramp.mapping.yaml | 61 ++++++++ .../mappings/adapters/recall.mapping.yaml | 26 ++++ .../core/mappings/adapters/redis.mapping.yaml | 7 + .../core/mappings/adapters/s3.mapping.yaml | 7 + .../mappings/adapters/sharepoint.mapping.yaml | 7 + .../mappings/adapters/shortcut.mapping.yaml | 7 + .../core/mappings/adapters/teams.mapping.yaml | 95 ++++++++++++ packages/core/package.json | 6 +- scripts/bundle-adapter-mappings.mjs | 66 ++++++++ scripts/bundle-adapter-mappings.test.mjs | 31 ++++ 30 files changed, 968 insertions(+), 5 deletions(-) create mode 100644 packages/core/mappings/adapters/azure-blob.mapping.yaml create mode 100644 packages/core/mappings/adapters/box.mapping.yaml create mode 100644 packages/core/mappings/adapters/cloudflare.mapping.yaml create mode 100644 packages/core/mappings/adapters/daytona.mapping.yaml create mode 100644 packages/core/mappings/adapters/dropbox.mapping.yaml create mode 100644 packages/core/mappings/adapters/fathom.mapping.yaml create mode 100644 packages/core/mappings/adapters/gcp.mapping.yaml create mode 100644 packages/core/mappings/adapters/gcs.mapping.yaml create mode 100644 packages/core/mappings/adapters/github.mapping.yaml create mode 100644 packages/core/mappings/adapters/gitlab.mapping.yaml create mode 100644 packages/core/mappings/adapters/gmail.mapping.yaml create mode 100644 packages/core/mappings/adapters/google-drive.mapping.yaml create mode 100644 packages/core/mappings/adapters/granola.mapping.yaml create mode 100644 packages/core/mappings/adapters/neon.mapping.yaml create mode 100644 packages/core/mappings/adapters/notion.mapping.yaml create mode 100644 packages/core/mappings/adapters/onedrive.mapping.yaml create mode 100644 packages/core/mappings/adapters/postgres.mapping.yaml create mode 100644 packages/core/mappings/adapters/posthog.mapping.yaml create mode 100644 packages/core/mappings/adapters/ramp.mapping.yaml create mode 100644 packages/core/mappings/adapters/recall.mapping.yaml create mode 100644 packages/core/mappings/adapters/redis.mapping.yaml create mode 100644 packages/core/mappings/adapters/s3.mapping.yaml create mode 100644 packages/core/mappings/adapters/sharepoint.mapping.yaml create mode 100644 packages/core/mappings/adapters/shortcut.mapping.yaml create mode 100644 packages/core/mappings/adapters/teams.mapping.yaml create mode 100644 scripts/bundle-adapter-mappings.mjs create mode 100644 scripts/bundle-adapter-mappings.test.mjs diff --git a/package.json b/package.json index 2517cf41..0863a087 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,21 @@ "version": "0.1.0", "private": true, "packageManager": "npm@10.8.2", - "description": "Relayfile adapters — map external services to the relayfile VFS", + "description": "Relayfile adapters \u2014 map external services to the relayfile VFS", "workspaces": [ "packages/*" ], "scripts": { "build": "npx turbo build", - "test": "npm run test:publish-targets && npm run test:internal-dependencies && npm run test:writeback-discovery && npm run test:digest-contracts && npm run test:catalog-task-graph && npx turbo test catalog:check", + "test": "npm run test:publish-targets && npm run test:internal-dependencies && npm run test:writeback-discovery && npm run test:digest-contracts && npm run test:catalog-task-graph && npm run test:adapter-mapping-bundle && npx turbo test catalog:check", "test:catalog-task-graph": "node scripts/verify-catalog-check-task.mjs", "test:publish-targets": "node --test scripts/resolve-publish-targets.test.mjs", "test:internal-dependencies": "node scripts/sync-internal-dependencies.mjs --check", "test:writeback-discovery": "node --test scripts/writeback-discovery-normalizer.test.mjs && node scripts/verify-writeback-discovery.mjs", "test:digest-contracts": "node --test scripts/digest-layout-contracts.test.mjs && node scripts/digest-layout-contracts.mjs", "test:writeback-e2e": "node scripts/verify-file-native-writeback-e2e.mjs", - "typecheck": "npx turbo typecheck" + "typecheck": "npx turbo typecheck", + "test:adapter-mapping-bundle": "node --test scripts/bundle-adapter-mappings.test.mjs" }, "devDependencies": { "@agentworkforce/workload-router": "^0.1.3", diff --git a/packages/core/README.md b/packages/core/README.md index 9c6b1093..724ce708 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -28,6 +28,24 @@ writebacks: See [../../docs/MAPPING_YAML_SPEC.md](../../docs/MAPPING_YAML_SPEC.md) for the full mapping YAML specification. +## Bundled adapter mappings + +The published package ships every adapter's mapping YAML, not only the core +fallbacks: + +- `mappings/.mapping.yaml` — the core fallbacks (`github`, `slack`), + unchanged. +- `mappings/adapters/.mapping.yaml` — a verbatim copy of each + `packages//.mapping.yaml`, so a consumer that depends only + on `@relayfile/adapter-core` (for example `AgentWorkforce/flows`, which + generates its trigger namespaces from the `webhooks:` blocks) sees the + adapter's own events. Apply adapter-local over fallback. + +The copies are generated and committed: `npm run mappings:bundle -w +@relayfile/adapter-core` refreshes them and `catalog:check` (run by +`turbo test`) fails when they drift, so a tarball can never carry a stale +bundle. + ## CLI ```bash diff --git a/packages/core/mappings/adapters/azure-blob.mapping.yaml b/packages/core/mappings/adapters/azure-blob.mapping.yaml new file mode 100644 index 00000000..e0ff5af1 --- /dev/null +++ b/packages/core/mappings/adapters/azure-blob.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new blob was created in the Azure Blob Storage container. + file.updated: + description: An existing blob was updated in the Azure Blob Storage container. + file.deleted: + description: A blob was deleted from the Azure Blob Storage container. diff --git a/packages/core/mappings/adapters/box.mapping.yaml b/packages/core/mappings/adapters/box.mapping.yaml new file mode 100644 index 00000000..73a0d2ed --- /dev/null +++ b/packages/core/mappings/adapters/box.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new file or folder was created in Box. + file.updated: + description: An existing file or folder was updated in Box. + file.deleted: + description: A file or folder was deleted from Box. diff --git a/packages/core/mappings/adapters/cloudflare.mapping.yaml b/packages/core/mappings/adapters/cloudflare.mapping.yaml new file mode 100644 index 00000000..9f52bead --- /dev/null +++ b/packages/core/mappings/adapters/cloudflare.mapping.yaml @@ -0,0 +1,51 @@ +adapter: + name: cloudflare + version: "1.0.0" + baseUrl: https://api.cloudflare.com/client/v4 +scopeKeys: + - accountId + - zoneId +resources: + workers-scripts: + path: /cloudflare/workers/scripts + worker-usage: + path: /cloudflare/analytics/workers/scripts + pages-projects: + path: /cloudflare/pages/projects + d1-databases: + path: /cloudflare/d1/databases + kv-namespaces: + path: /cloudflare/kv/namespaces + r2-buckets: + path: /cloudflare/r2/buckets + queues: + path: /cloudflare/queues + tunnels: + path: /cloudflare/tunnels + zones: + path: /cloudflare/zones + dns-records: + path: /cloudflare/zones/{zoneId}/dns-records + notification-webhooks: + path: /cloudflare/notifications/webhooks + notification-policies: + path: /cloudflare/notifications/policies + notification-events: + path: /cloudflare/notifications/events +webhooks: + workers_alert: + path: /cloudflare/notifications/events + workers_observability_alert: + path: /cloudflare/notifications/events + advanced_http_alert_error: + path: /cloudflare/notifications/events + http_alert_origin_error: + path: /cloudflare/notifications/events + billing_usage_alert: + path: /cloudflare/notifications/events + health_check_status_notification: + path: /cloudflare/notifications/events + dedicated_ssl_certificate_event_type: + path: /cloudflare/notifications/events + access_custom_certificate_expiration_type: + path: /cloudflare/notifications/events diff --git a/packages/core/mappings/adapters/daytona.mapping.yaml b/packages/core/mappings/adapters/daytona.mapping.yaml new file mode 100644 index 00000000..c5a71fa2 --- /dev/null +++ b/packages/core/mappings/adapters/daytona.mapping.yaml @@ -0,0 +1,12 @@ +adapter: + name: daytona + version: "1.0.0" + baseUrl: https://api.daytona.io +webhooks: + sandbox.created: {} + sandbox.state.updated: {} + snapshot.created: {} + snapshot.state.updated: {} + snapshot.removed: {} + volume.created: {} + volume.state.updated: {} diff --git a/packages/core/mappings/adapters/dropbox.mapping.yaml b/packages/core/mappings/adapters/dropbox.mapping.yaml new file mode 100644 index 00000000..1baebe1b --- /dev/null +++ b/packages/core/mappings/adapters/dropbox.mapping.yaml @@ -0,0 +1,3 @@ +webhooks: + file.changed: + description: Dropbox account-level webhook notification that one or more files/folders changed. diff --git a/packages/core/mappings/adapters/fathom.mapping.yaml b/packages/core/mappings/adapters/fathom.mapping.yaml new file mode 100644 index 00000000..a8141e34 --- /dev/null +++ b/packages/core/mappings/adapters/fathom.mapping.yaml @@ -0,0 +1,17 @@ +adapter: + name: fathom + version: "1.0.0" + baseUrl: https://api.fathom.ai +# Fathom delivers a single webhook event when new meeting content (summary, +# transcript, action items) becomes available. The adapter webhook-normalizer +# canonicalizes the upstream `new_meeting_content_ready` payload to the runtime +# event type `new-meeting-content-ready`, which is the name persona triggers and +# deploy-time lint match against. +# +# No destination-path template is declared: fathom uses a hand-written +# normalizer + path-mapper rather than the schema-driven runtime, and its +# normalizer resolves the record id from `recording_id ?? id ?? recordingId` +# (a fallback a single `{{...}}` template can't express). The trigger-catalog +# generator consumes only the event-name key below. +webhooks: + new-meeting-content-ready: {} diff --git a/packages/core/mappings/adapters/gcp.mapping.yaml b/packages/core/mappings/adapters/gcp.mapping.yaml new file mode 100644 index 00000000..fb222343 --- /dev/null +++ b/packages/core/mappings/adapters/gcp.mapping.yaml @@ -0,0 +1,56 @@ +adapter: + name: gcp + version: "1.0.0" + # GCP exposes data over REST across multiple API hosts, so there is no single + # baseUrl. The provider/proxy (e.g. @relayfile/provider-nango via a Nango GCP + # connection) overrides baseUrl per call. Per-resource API hosts: + baseUrls: + cloud-run-services: https://run.googleapis.com + monitoring-alerts: https://monitoring.googleapis.com + billing: https://cloudbilling.googleapis.com + error-reporting-groups: https://clouderrorreporting.googleapis.com +# This adapter is auth-agnostic and declarative: it contains no HTTP client and +# no credentials. Auth is supplied at runtime by a provider via a Nango GCP +# connection. +scopeKeys: + - project + - location + - billingAccountId +resources: + cloud-run-services: + # Cloud Run Admin API + # GET https://run.googleapis.com/v2/projects/{project}/locations/{location}/services + # (and .../services/{service}/revisions). One file per service. + path: /gcp/run/services + monitoring-alerts: + # Cloud Monitoring API alert policies + # GET https://monitoring.googleapis.com/v3/projects/{project}/alertPolicies + # plus firing incidents. One file per alert policy. + path: /gcp/monitoring/alerts + billing: + # Cloud Billing API + # GET https://cloudbilling.googleapis.com/v1/billingAccounts/{id} + # and project billing info (the FinOps mount). A single current-state file. + path: /gcp/billing/current + error-reporting-groups: + # Error Reporting API + # GET https://clouderrorreporting.googleapis.com/v1beta1/projects/{project}/groupStats + # One file per error group. + path: /gcp/error-reporting/groups +webhooks: + # GCP push signals arrive as Cloud Monitoring alert notifications delivered via + # Pub/Sub -> an HTTP push subscription. The incident.state open/closed drives + # alert create/update. + monitoring.incident.open: {} + monitoring.incident.closed: {} + # Cloud Run admin changes are routed from Cloud Audit Logs -> Pub/Sub -> HTTP push. + cloud-run.service.created: {} + cloud-run.service.updated: {} + cloud-run.service.deleted: {} + # Cloud Billing budgets publish current budget status to Pub/Sub multiple times per day. + billing.budget.alert: {} + # Error signals can arrive either as native Error Reporting webhooks or as + # Cloud Run error-log push signals that refresh Error Reporting groups. + error-reporting.event.logged: {} + error-reporting.group.opened: {} + error-reporting.group.reopened: {} diff --git a/packages/core/mappings/adapters/gcs.mapping.yaml b/packages/core/mappings/adapters/gcs.mapping.yaml new file mode 100644 index 00000000..1cc4b855 --- /dev/null +++ b/packages/core/mappings/adapters/gcs.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new object was created in the Google Cloud Storage bucket. + file.updated: + description: An existing object was updated in the Google Cloud Storage bucket. + file.deleted: + description: An object was deleted from the Google Cloud Storage bucket. diff --git a/packages/core/mappings/adapters/github.mapping.yaml b/packages/core/mappings/adapters/github.mapping.yaml new file mode 100644 index 00000000..f2b51aed --- /dev/null +++ b/packages/core/mappings/adapters/github.mapping.yaml @@ -0,0 +1,143 @@ +adapter: + name: github + version: "1.0.0" + baseUrl: https://api.github.com + source: + openapi: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml +webhooks: + pull_request: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{number}}/meta.json + extract: + - action + - number + - pull_request.title + - pull_request.state + pull_request_review: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/reviews/{{review.id}}.json + pull_request_review.submitted: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/reviews/{{review.id}}.json + pull_request_review.edited: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/reviews/{{review.id}}.json + pull_request_review.dismissed: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/reviews/{{review.id}}.json + pull_request_review_comment.created: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/comments/{{comment.id}}.json + pull_request_review_thread.resolved: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/review-threads/{{thread.id}}.json + issue_comment: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/comments/{{comment.id}}/meta.json + extract: + - action + - issue.number + - issue.title + - comment.id + issue_comment.created: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/comments/{{comment.id}}/meta.json + extract: + - action + - issue.number + - issue.title + - comment.id + check_run: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/checks/{{check_run.id}}.json + extract: + - action + - check_run.id + - check_run.name + - check_run.status + - check_run.conclusion + - check_run.head_sha + check_run.completed: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/checks/{{check_run.id}}.json + extract: + - action + - check_run.id + - check_run.name + - check_run.status + - check_run.conclusion + - check_run.head_sha + deployment_status.created: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/deployments/{{deployment.id}}/statuses/{{deployment_status.id}}.json + extract: + - action + - deployment.id + - deployment.ref + - deployment.sha + - deployment.environment + - deployment_status.id + - deployment_status.state + - deployment_status.environment + push: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/commits/{{head_commit.id}}/metadata.json + status: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/gate-status/{{sha}}.json + issues: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/meta.json + pull_request.edited: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/meta.json + extract: + - action + - pull_request.number + - pull_request.title + pull_request.reopened: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/pulls/{{pull_request.number}}/meta.json + extract: + - action + - pull_request.number + - pull_request.title + issues.edited: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/meta.json + extract: + - action + - issue.number + - issue.title + issues.labeled: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/meta.json + extract: + - action + - issue.number + - issue.title + - label.name + issues.unlabeled: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/meta.json + extract: + - action + - issue.number + - issue.title + - label.name + issues.reopened: + path: /github/repos/{{repository.owner.login}}/{{repository.name}}/issues/{{issue.number}}/meta.json + extract: + - action + - issue.number + - issue.title +resources: + pull_request: + endpoint: GET /repos/{owner}/{repo}/pulls/{pull_number} + path: /github/repos/{{owner}}/{{repo}}/pulls/{{number}}/meta.json + pull_request_files: + endpoint: GET /repos/{owner}/{repo}/pulls/{pull_number}/files + path: /github/repos/{{owner}}/{{repo}}/pulls/{{pull_number}}/files/{{filename}} + iterate: true +writebacks: + create_pull_request: + match: /github/repos/*/*/pull-requests/*.json + endpoint: POST /repos/{owner}/{repo}/pulls + update_ref: + match: /github/repos/*/*/refs/refs%2F*%2F*.json + endpoint: PATCH /repos/{owner}/{repo}/git/refs/{namespace}/{ref} + create_ref: + match: /github/repos/*/*/refs/*.json + endpoint: POST /repos/{owner}/{repo}/git/refs + close_pull_request: + match: /github/repos/*/*/pulls/*/close.json + endpoint: PATCH /repos/{owner}/{repo}/pulls/{pull_number} + review: + match: /github/repos/*/*/pulls/*/reviews/*.json + endpoint: POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews + merge: + match: /github/repos/*/*/pulls/*/merge.json + endpoint: PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge + comment: + match: /github/repos/*/*/pulls/*/comments/*.json + endpoint: POST /repos/{owner}/{repo}/pulls/{pull_number}/comments diff --git a/packages/core/mappings/adapters/gitlab.mapping.yaml b/packages/core/mappings/adapters/gitlab.mapping.yaml new file mode 100644 index 00000000..9d420bd5 --- /dev/null +++ b/packages/core/mappings/adapters/gitlab.mapping.yaml @@ -0,0 +1,72 @@ +adapter: + name: gitlab + version: "1.0.0" + baseUrl: https://gitlab.com/api/v4 +webhooks: + merge_request: + path: /gitlab/projects/{{project.path_with_namespace}}/merge_requests/{{object_attributes.iid}}__{{object_attributes.title}}/meta.json + note: + merge_request: + match: object_attributes.noteable_type == "MergeRequest" + path: /gitlab/projects/{{project.path_with_namespace}}/merge_requests/{{merge_request.iid}}__{{merge_request.title}}/discussions/{{object_attributes.discussion_id}}.json + issue: + match: object_attributes.noteable_type == "Issue" + path: /gitlab/projects/{{project.path_with_namespace}}/issues/{{issue.iid}}__{{issue.title}}/comments/{{object_attributes.id}}.json + commit: + match: object_attributes.noteable_type == "Commit" + path: /gitlab/projects/{{project.path_with_namespace}}/commits/{{commit.id}}__{{commit.title}}/comments/{{object_attributes.id}}.json + snippet: + match: object_attributes.noteable_type == "Snippet" + path: /gitlab/projects/{{project.path_with_namespace}}/snippets/{{object_attributes.noteable_id}}/comments/{{object_attributes.id}}.json + push: + path: /gitlab/projects/{{project.path_with_namespace}}/commits/{{after}}/meta.json + pipeline: + path: /gitlab/projects/{{project.path_with_namespace}}/pipelines/{{object_attributes.id}}__{{object_attributes.ref}}/meta.json + build: + path: /gitlab/projects/{{project.path_with_namespace}}/pipelines/{{pipeline_id}}__{{ref}}/jobs/{{build_id}}.json + issue: + path: /gitlab/projects/{{project.path_with_namespace}}/issues/{{object_attributes.iid}}__{{object_attributes.title}}/meta.json + deployment: + path: /gitlab/projects/{{project.path_with_namespace}}/deployments/{{id}}.json + tag_push: + path: /gitlab/projects/{{project.path_with_namespace}}/tags/{{ref_slug}}__{{encoded_ref}}.json +resources: + merge_request: + endpoint: GET /projects/{project_id}/merge_requests/{iid} + path: /gitlab/projects/{namespace}/{project}/merge_requests/{iid}__{slug}/meta.json + merge_request_diffs: + endpoint: GET /projects/{project_id}/merge_requests/{iid}/diffs + path: /gitlab/projects/{namespace}/{project}/merge_requests/{iid}__{slug}/diff.patch + merge_request_discussions: + endpoint: GET /projects/{project_id}/merge_requests/{iid}/discussions + path: /gitlab/projects/{namespace}/{project}/merge_requests/{iid}__{slug}/discussions/{discussion_id}.json + iterate: true + merge_request_approvals: + endpoint: GET /projects/{project_id}/merge_requests/{iid}/approvals + path: /gitlab/projects/{namespace}/{project}/merge_requests/{iid}__{slug}/approvals.json + issue: + endpoint: GET /projects/{project_id}/issues/{iid} + path: /gitlab/projects/{namespace}/{project}/issues/{iid}__{slug}/meta.json + commit: + endpoint: GET /projects/{project_id}/repository/commits/{sha} + path: /gitlab/projects/{namespace}/{project}/commits/{sha}__{slug}/meta.json + pipeline: + endpoint: GET /projects/{project_id}/pipelines/{pipeline_id} + path: /gitlab/projects/{namespace}/{project}/pipelines/{id}__{ref}/meta.json + pipeline_jobs: + endpoint: GET /projects/{project_id}/pipelines/{pipeline_id}/jobs + path: /gitlab/projects/{namespace}/{project}/pipelines/{id}__{ref}/jobs/{job_id}.json + iterate: true +writebacks: + merge_request_update: + match: /gitlab/projects/*/merge_requests/*/meta.json + endpoint: PUT /projects/{project_id}/merge_requests/{iid} + merge_request_discussion: + match: /gitlab/projects/*/merge_requests/*/discussions/*.json + endpoint: POST /projects/{project_id}/merge_requests/{iid}/discussions + issue_update: + match: /gitlab/projects/*/issues/*/meta.json + endpoint: PUT /projects/{project_id}/issues/{iid} + issue_note: + match: /gitlab/projects/*/issues/*/comments/*.json + endpoint: POST /projects/{project_id}/issues/{iid}/notes diff --git a/packages/core/mappings/adapters/gmail.mapping.yaml b/packages/core/mappings/adapters/gmail.mapping.yaml new file mode 100644 index 00000000..b1eb7d68 --- /dev/null +++ b/packages/core/mappings/adapters/gmail.mapping.yaml @@ -0,0 +1,29 @@ +# Gmail trigger-event surface for `adapter-core triggers generate` (issue #115). +# +# Gmail is a *storage-bridge* adapter: messages/threads are materialized as +# files in the Relayfile VFS (`/gmail/{account}/threads/...`), and Gmail API +# push (users.watch → Pub/Sub) is normalized into storage change events. It is +# NOT a schema-driven webhook adapter, so this file deliberately contains no +# `adapter:`/`resources:`/`writebacks:` blocks — only the `webhooks:` keys the +# catalog generator reads (`Object.keys(parsed.webhooks)`). +# +# The event names below are emitted VERBATIM by the runtime as `event.type` +# (see src/metadata.ts `toIngestEnvelope`, which maps the storage-bridge +# changeType to these `file.*` names). They must match exactly so a persona's +# `integrations.gmail.triggers[].on` actually fires at runtime — the catalog +# never normalizes across providers. +provider: gmail +webhooks: + # A Gmail message/thread arrived in (or moved into) the watched mailbox — + # i.e. new mail. This is the event most digest/triage personas subscribe to. + file.created: + description: A Gmail message/thread was added to the watched mailbox (new mail). + # A watched Gmail message/thread changed — e.g. labels added/removed, read + # state, or a thread gained a reply (surfaced via the Nango scheduled-sync + # fallback, which emits an `updated` change). + file.updated: + description: A watched Gmail message/thread changed (labels, read state, new reply). + # A Gmail message/thread was removed from the watched mailbox (deleted, or + # the history record reported messagesDeleted). + file.deleted: + description: A Gmail message/thread was removed from the watched mailbox. diff --git a/packages/core/mappings/adapters/google-drive.mapping.yaml b/packages/core/mappings/adapters/google-drive.mapping.yaml new file mode 100644 index 00000000..76ff65c3 --- /dev/null +++ b/packages/core/mappings/adapters/google-drive.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new file or folder was created in Google Drive. + file.updated: + description: An existing file or folder was updated in Google Drive. + file.deleted: + description: A file or folder was deleted from Google Drive. diff --git a/packages/core/mappings/adapters/granola.mapping.yaml b/packages/core/mappings/adapters/granola.mapping.yaml new file mode 100644 index 00000000..b4516fbd --- /dev/null +++ b/packages/core/mappings/adapters/granola.mapping.yaml @@ -0,0 +1,17 @@ +adapter: + name: granola + version: "1.0.0" + baseUrl: https://api.granola.ai +# Granola webhook delivery is normalized by a hand-written webhook-normalizer +# plus path-mapper, not by schema-driven mapping interpolation. The trigger +# catalog consumes only the webhook keys below. +webhooks: + note.generated: {} + note.edited: {} + note.access_granted: {} + # Storage-bridge event: emitted when any pipeline (e.g. NB-Whisper) writes + # a note directly to the granola VFS mount (/granola/notes/.json). + # This is NOT a Granola API webhook — it fires via the storage-change + # dispatcher the same way gmail/google-drive file.created events do. + file.created: + description: A note was written to the granola VFS mount (storage-bridge path, e.g. from the NB-Whisper transcript pipeline). diff --git a/packages/core/mappings/adapters/neon.mapping.yaml b/packages/core/mappings/adapters/neon.mapping.yaml new file mode 100644 index 00000000..c605b17b --- /dev/null +++ b/packages/core/mappings/adapters/neon.mapping.yaml @@ -0,0 +1,38 @@ +adapter: + name: neon + version: "1.0.0" + baseUrls: + api: https://console.neon.tech/api/v2 +scopeKeys: + - organizationId + - projectId +webhooks: + operation.failed: + path: /neon/operations + operation.cancelled: + path: /neon/operations + operation.succeeded: + path: /neon/operations + endpoint.state_changed: + path: /neon/endpoints + advisor.issue_raised: + path: /neon/advisors +resources: + organizations: + path: /neon/organizations + projects: + path: /neon/projects + branches: + path: /neon/branches + endpoints: + path: /neon/endpoints + operations: + path: /neon/operations + project-consumption: + path: /neon/consumption/projects + branch-consumption: + path: /neon/consumption/branches + spending-limits: + path: /neon/spending-limits + advisor-issues: + path: /neon/advisors diff --git a/packages/core/mappings/adapters/notion.mapping.yaml b/packages/core/mappings/adapters/notion.mapping.yaml new file mode 100644 index 00000000..75686785 --- /dev/null +++ b/packages/core/mappings/adapters/notion.mapping.yaml @@ -0,0 +1,105 @@ +provider: notion +root: /notion +paths: + - pattern: /notion/databases/{database_id}/metadata.json + object_type: database + source: + method: GET + endpoint: /v1/databases/{database_id} + - pattern: /notion/databases/{database_id}/pages/{page_id}.json + object_type: page + source: + method: GET + endpoint: /v1/pages/{page_id} + writeback: + method: PATCH + endpoint: /v1/pages/{page_id} + - pattern: /notion/databases/{database_id}/pages/{page_id}/content.md + object_type: page_content + source: + method: GET + endpoint: /v1/pages/{page_id}/markdown + notion_version: 2026-03-11 + writeback: + method: PATCH + endpoint: /v1/pages/{page_id}/markdown + notion_version: 2026-03-11 + - pattern: /notion/databases/{database_id}/pages/{page_id}/blocks/{block_id}.json + object_type: block + source: + method: GET + endpoint: /v1/blocks/{block_id} + - pattern: /notion/databases/{database_id}/pages/{page_id}/comments.json + object_type: comment + source: + method: GET + endpoint: /v1/comments?block_id={page_id} + writeback: + method: POST + endpoint: /v1/comments + - pattern: /notion/pages/{page_id}.json + object_type: page + source: + method: GET + endpoint: /v1/pages/{page_id} + writeback: + method: PATCH + endpoint: /v1/pages/{page_id} + - pattern: /notion/pages/{page_id}/content.md + object_type: page_content + source: + method: GET + endpoint: /v1/pages/{page_id}/markdown + notion_version: 2026-03-11 + writeback: + method: PATCH + endpoint: /v1/pages/{page_id}/markdown + notion_version: 2026-03-11 + - pattern: /notion/pages/{page_id}/comments.json + object_type: comment + source: + method: GET + endpoint: /v1/comments?block_id={page_id} + writeback: + method: POST + endpoint: /v1/comments +collections: + - pattern: /notion/databases/{database_id}/pages + writeback: + method: POST + endpoint: /v1/pages + body: + parent: + database_id: '{database_id}' +# Notion webhook event types this provider emits at runtime. These are the +# verbatim event names the notion-relay syncs subscribe to (see cloud +# nango-integrations/notion-relay/syncs/fetch-pages.ts PAGE_WEBHOOK_EVENTS and +# fetch-databases.ts DATABASE_WEBHOOK_EVENTS) and are what persona trigger +# autocomplete and deploy-time lint match against via KNOWN_TRIGGER_CATALOG. +webhooks: + page.created: + path: /notion/pages/{page_id}.json + page.content_updated: + path: /notion/pages/{page_id}.json + page.properties_updated: + path: /notion/pages/{page_id}.json + page.moved: + path: /notion/pages/{page_id}.json + page.deleted: + path: /notion/pages/{page_id}.json + page.undeleted: + path: /notion/pages/{page_id}.json + page.locked: + path: /notion/pages/{page_id}.json + page.unlocked: + path: /notion/pages/{page_id}.json + database.created: + path: /notion/databases/{database_id}/metadata.json + database.schema_updated: + path: /notion/databases/{database_id}/metadata.json + database.moved: + path: /notion/databases/{database_id}/metadata.json + database.deleted: + path: /notion/databases/{database_id}/metadata.json + database.undeleted: + path: /notion/databases/{database_id}/metadata.json diff --git a/packages/core/mappings/adapters/onedrive.mapping.yaml b/packages/core/mappings/adapters/onedrive.mapping.yaml new file mode 100644 index 00000000..cdf57df2 --- /dev/null +++ b/packages/core/mappings/adapters/onedrive.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new file or folder was created in OneDrive. + file.updated: + description: An existing file or folder was updated in OneDrive. + file.deleted: + description: A file or folder was deleted from OneDrive. diff --git a/packages/core/mappings/adapters/postgres.mapping.yaml b/packages/core/mappings/adapters/postgres.mapping.yaml new file mode 100644 index 00000000..7c040e2e --- /dev/null +++ b/packages/core/mappings/adapters/postgres.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new record or schema object was synced from Postgres. + file.updated: + description: An existing record or schema object was updated in Postgres. + file.deleted: + description: A record or schema object was deleted from Postgres. diff --git a/packages/core/mappings/adapters/posthog.mapping.yaml b/packages/core/mappings/adapters/posthog.mapping.yaml new file mode 100644 index 00000000..9a7f3839 --- /dev/null +++ b/packages/core/mappings/adapters/posthog.mapping.yaml @@ -0,0 +1,50 @@ +adapter: + name: posthog + version: "1.0.0" + baseUrl: https://us.posthog.com +scopeKeys: + - project_id +resources: + projects: + path: /posthog/projects/{projectId}.json + insights: + path: /posthog/projects/{projectId}/insights/{slug}__{insightId}.json + dashboards: + path: /posthog/projects/{projectId}/dashboards/{slug}__{dashboardId}.json + feature-flags: + path: /posthog/projects/{projectId}/feature-flags/{slug}__{featureFlagId}.json + annotations: + path: /posthog/projects/{projectId}/annotations/{slug}__{annotationId}.json + experiments: + path: /posthog/projects/{projectId}/experiments/{slug}__{experimentId}.json + surveys: + path: /posthog/projects/{projectId}/surveys/{slug}__{surveyId}.json + alert-events: + path: /posthog/projects/{projectId}/alert-events/{slug}__{eventId}.json +webhooks: + # Normalized forward-webhook events currently supported by the cloud ingress. + # These are alert lifecycle payloads materialized under /posthog/projects//alert-events/. + posthog.alert.triggered: + path: /posthog/projects/{{project_id}}/alert-events/{{id}}.json + extract: + - id + - project_id + - title + - severity + - state + - event_type + - occurred_at + - timestamp + - url + posthog.alert.resolved: + path: /posthog/projects/{{project_id}}/alert-events/{{id}}.json + extract: + - id + - project_id + - title + - severity + - state + - event_type + - occurred_at + - timestamp + - url diff --git a/packages/core/mappings/adapters/ramp.mapping.yaml b/packages/core/mappings/adapters/ramp.mapping.yaml new file mode 100644 index 00000000..f661e2bd --- /dev/null +++ b/packages/core/mappings/adapters/ramp.mapping.yaml @@ -0,0 +1,61 @@ +adapter: + name: ramp + version: "1.0.0" + baseUrls: + production: https://api.ramp.com + sandbox: https://demo-api.ramp.com +scopeKeys: + - entityId + - vendorId + - userId + - departmentId + - locationId + - since +webhooks: + bills.approved: {} + bills.archived: {} + bills.created: {} + bills.paid: {} + bills.ready_to_sync: {} + bills.rejected: {} + bills.updated: {} + item_receipts.created: {} + payments.updated: {} + purchase_orders.archived: {} + purchase_orders.created: {} + purchase_orders.updated: {} + reimbursements.batch_payment_reimbursed: {} + reimbursements.ready_for_review: {} + reimbursements.ready_to_sync: {} + reimbursements.sync_requested: {} + transactions.all_requirements_met_and_approved_changed: {} + transactions.authorized: {} + transactions.body_coding_updated: {} + transactions.cleared: {} + transactions.declined: {} + transactions.ready_for_review: {} + transactions.ready_to_sync: {} + transactions.receipt_added: {} + transactions.sync_requested: {} + transactions.synced: {} + vendor_agreements.archived: {} + vendor_agreements.created: {} + vendor_agreements.deleted: {} + vendor_agreements.document_added: {} + vendor_agreements.renewal_milestone: {} + vendor_agreements.updated: {} + vendors.activated: {} + vendors.approved: {} + vendors.updated: {} + entities.created: {} + users.invite_accepted: {} + unified_requests.created: {} + unified_requests.external_approval_request: {} + unified_requests.external_approval_request_reset: {} + unified_requests.modified: {} + unified_requests.node_advanced: {} + unified_requests.override_approved: {} + unified_requests.updated: {} + spend_requests.comment_created: {} + spend_requests.created: {} + applications.status_updated: {} diff --git a/packages/core/mappings/adapters/recall.mapping.yaml b/packages/core/mappings/adapters/recall.mapping.yaml new file mode 100644 index 00000000..0816c4f2 --- /dev/null +++ b/packages/core/mappings/adapters/recall.mapping.yaml @@ -0,0 +1,26 @@ +adapter: + name: recall + version: "1.0.0" + baseUrl: https://us-west-2.recall.ai +webhooks: + recording.created: + path: /recall/recordings/{{id}}.json + extract: + - id + - title + - created_at + - updated_at + - status + - transcript_text + transcript.done: + path: /recall/recordings/{{recording_id}}.json + extract: + - recording_id + - transcript_text + - transcript + # Storage-bridge event: emitted when any pipeline (e.g. NB-Whisper) writes + # a recording directly to the recall VFS mount (/recall/recordings/.json). + # This is NOT a Recall API webhook — it fires via the storage-change + # dispatcher the same way gmail/google-drive file.created events do. + file.created: + description: A recording was written to the recall VFS mount (storage-bridge path, e.g. from the NB-Whisper transcript pipeline). diff --git a/packages/core/mappings/adapters/redis.mapping.yaml b/packages/core/mappings/adapters/redis.mapping.yaml new file mode 100644 index 00000000..e0ba6c01 --- /dev/null +++ b/packages/core/mappings/adapters/redis.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new key was created in the Redis instance. + file.updated: + description: An existing key was updated in the Redis instance. + file.deleted: + description: A key was deleted from the Redis instance. diff --git a/packages/core/mappings/adapters/s3.mapping.yaml b/packages/core/mappings/adapters/s3.mapping.yaml new file mode 100644 index 00000000..67fc026e --- /dev/null +++ b/packages/core/mappings/adapters/s3.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new object was created in the Amazon S3 bucket. + file.updated: + description: An existing object was updated in the Amazon S3 bucket. + file.deleted: + description: An object was deleted from the Amazon S3 bucket. diff --git a/packages/core/mappings/adapters/sharepoint.mapping.yaml b/packages/core/mappings/adapters/sharepoint.mapping.yaml new file mode 100644 index 00000000..66759693 --- /dev/null +++ b/packages/core/mappings/adapters/sharepoint.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + file.created: + description: A new file or list item was created in SharePoint. + file.updated: + description: An existing file or list item was updated in SharePoint. + file.deleted: + description: A file or list item was deleted from SharePoint. diff --git a/packages/core/mappings/adapters/shortcut.mapping.yaml b/packages/core/mappings/adapters/shortcut.mapping.yaml new file mode 100644 index 00000000..67fc08fa --- /dev/null +++ b/packages/core/mappings/adapters/shortcut.mapping.yaml @@ -0,0 +1,7 @@ +webhooks: + story.create: {} + story.update: {} + story.delete: {} + epic.create: {} + epic.update: {} + epic.delete: {} diff --git a/packages/core/mappings/adapters/teams.mapping.yaml b/packages/core/mappings/adapters/teams.mapping.yaml new file mode 100644 index 00000000..826b027e --- /dev/null +++ b/packages/core/mappings/adapters/teams.mapping.yaml @@ -0,0 +1,95 @@ +# Declarative mapping for @relayfile/adapter-teams + +provider: teams +version: "1.0" +apiBaseUrl: "https://graph.microsoft.com/v1.0" + +paths: + team: + pattern: "/teams/{teamId}/metadata.json" + source: "GET /teams/{teamId}" + + channel: + pattern: "/teams/{teamId}/channels/{channelId}/metadata.json" + source: "GET /teams/{teamId}/channels/{channelId}" + + message: + pattern: "/teams/{teamId}/channels/{channelId}/messages/{messageId}.json" + source: "GET /teams/{teamId}/channels/{channelId}/messages/{messageId}" + + reply: + pattern: "/teams/{teamId}/channels/{channelId}/messages/{messageId}/replies/{replyId}.json" + source: "GET /teams/{teamId}/channels/{channelId}/messages/{messageId}/replies/{replyId}" + + tab: + pattern: "/teams/{teamId}/channels/{channelId}/tabs/{tabId}.json" + source: "GET /teams/{teamId}/channels/{channelId}/tabs/{tabId}" + + member: + pattern: "/teams/{teamId}/members/{userId}.json" + source: "GET /teams/{teamId}/members/{memberId}" + note: "The adapter rewrites member IDs to user IDs when payload data is available." + + chat: + pattern: "/teams/chats/{chatId}/metadata.json" + source: "GET /chats/{chatId}" + + chat_message: + pattern: "/teams/chats/{chatId}/messages/{messageId}.json" + source: "GET /chats/{chatId}/messages/{messageId}" + + reaction: + pattern: "/teams/{teamId}/channels/{channelId}/messages/{messageId}/reactions/{reactionType}--{userId}.json" + source: "derived from chatMessage.reactions[]" + +subscriptions: + tenant_messages: + resource: "/teams/getAllMessages" + changeTypes: [created, updated, deleted] + richNotifications: true + + tenant_chats: + resource: "/chats/getAllMessages" + changeTypes: [created, updated, deleted] + richNotifications: true + + tenant_channels: + resource: "/teams/getAllChannels" + changeTypes: [created, updated, deleted] + richNotifications: false + + team_updates: + resource: "/teams" + changeTypes: [updated, deleted] + richNotifications: false + + team_members: + resource: "/teams/{teamId}/members" + changeTypes: [created, updated, deleted] + richNotifications: false + + channel_messages: + resource: "/teams/{teamId}/channels/{channelId}/messages" + changeTypes: [created, updated, deleted] + richNotifications: true + + chat_messages: + resource: "/chats/{chatId}/messages" + changeTypes: [created, updated, deleted] + richNotifications: true + +writeback: + channel_message: + match: "/teams/{teamId}/channels/{channelId}/messages/{messageId}.json" + method: POST + endpoint: "/teams/{teamId}/channels/{channelId}/messages" + + thread_reply: + match: "/teams/{teamId}/channels/{channelId}/messages/{messageId}/replies/{replyId}.json" + method: POST + endpoint: "/teams/{teamId}/channels/{channelId}/messages/{messageId}/replies" + + chat_message: + match: "/teams/chats/{chatId}/messages/{messageId}.json" + method: POST + endpoint: "/chats/{chatId}/messages" diff --git a/packages/core/package.json b/packages/core/package.json index 40dd480c..4050505d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,11 +56,13 @@ ], "scripts": { "build": "rm -rf dist/tests/round-trip && tsc -p tsconfig.json", + "catalog:check": "node dist/src/cli.js writeback-paths check && node dist/src/cli.js inbound check && npm run mappings:check", "clean": "rm -rf dist", + "mappings:bundle": "node ../../scripts/bundle-adapter-mappings.mjs", + "mappings:check": "node ../../scripts/bundle-adapter-mappings.mjs --check", + "prepublishOnly": "npm run mappings:check && npm run build", "test": "node --import tsx --test dist/tests/**/*.test.js dist/src/**/*.test.js", "test:round-trip": "vitest run tests/round-trip", - "catalog:check": "node dist/src/cli.js writeback-paths check && node dist/src/cli.js inbound check", - "prepublishOnly": "npm run build", "typecheck": "tsc --noEmit" }, "peerDependencies": { diff --git a/scripts/bundle-adapter-mappings.mjs b/scripts/bundle-adapter-mappings.mjs new file mode 100644 index 00000000..f4be3bd8 --- /dev/null +++ b/scripts/bundle-adapter-mappings.mjs @@ -0,0 +1,66 @@ +#!/usr/bin/env node +// Bundle every adapter's mapping YAML into @relayfile/adapter-core's shipped +// `mappings/adapters/` directory, so a consumer that only depends on the core +// package (AgentWorkforce/flows generates its trigger namespaces this way) +// sees each adapter's own `webhooks:` block, not just the two core fallbacks. +// +// Layout of the published package: +// mappings/.mapping.yaml core fallbacks (github, slack) — unchanged +// mappings/adapters/.mapping.yaml verbatim copy of packages//.mapping.yaml +// +// Consumers apply adapter-local over fallback, matching the precedence +// flows' generator already uses against a repo checkout. The copies are +// committed (like every other generated catalog here) and `--check` fails the +// build when they drift, so the tarball can never ship a stale bundle. +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +const packages = join(root, 'packages'); +const bundleDir = join(packages, 'core', 'mappings', 'adapters'); +const check = process.argv.includes('--check'); + +const expected = new Map(); +for (const entry of (await readdir(packages, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) { + if (!entry.isDirectory() || entry.name === 'core') continue; + const dir = join(packages, entry.name); + for (const name of (await readdir(dir)).sort()) { + if (!name.endsWith('.mapping.yaml')) continue; + if (expected.has(name)) { + throw new Error(`Two adapters ship ${name}: ${expected.get(name).source} and packages/${entry.name}`); + } + expected.set(name, { source: `packages/${entry.name}`, content: await readFile(join(dir, name), 'utf8') }); + } +} +if (expected.size === 0) throw new Error('No adapter mapping YAML found under packages/*/'); + +const actual = new Map(); +if (existsSync(bundleDir)) { + for (const name of (await readdir(bundleDir)).sort()) { + if (name.endsWith('.mapping.yaml')) actual.set(name, await readFile(join(bundleDir, name), 'utf8')); + } +} + +const missing = [...expected.keys()].filter((name) => !actual.has(name)); +const stale = [...expected].filter(([name, { content }]) => actual.has(name) && actual.get(name) !== content).map(([name]) => name); +const orphaned = [...actual.keys()].filter((name) => !expected.has(name)); + +if (check) { + const problems = [ + ...missing.map((name) => `missing from bundle: ${name}`), + ...stale.map((name) => `stale in bundle: ${name}`), + ...orphaned.map((name) => `no adapter owns bundled ${name}`), + ]; + if (problems.length) { + console.error(`packages/core/mappings/adapters is out of date; run \`npm run mappings:bundle -w @relayfile/adapter-core\`:\n ${problems.join('\n ')}`); + process.exit(1); + } + console.log(`Bundled adapter mappings verified: ${expected.size} providers`); +} else { + await rm(bundleDir, { recursive: true, force: true }); + await mkdir(bundleDir, { recursive: true }); + for (const [name, { content }] of expected) await writeFile(join(bundleDir, name), content); + console.log(`Bundled ${expected.size} adapter mappings into packages/core/mappings/adapters`); +} diff --git a/scripts/bundle-adapter-mappings.test.mjs b/scripts/bundle-adapter-mappings.test.mjs new file mode 100644 index 00000000..e74e2f0f --- /dev/null +++ b/scripts/bundle-adapter-mappings.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { readdirSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); + +test('the committed adapter mapping bundle matches every adapter', () => { + const result = spawnSync(process.execPath, [join(root, 'scripts/bundle-adapter-mappings.mjs'), '--check'], { encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); +}); + +test('the bundle resolves from the package the way a consumer reads it', () => { + // AgentWorkforce/flows: join(dirname(require.resolve('@relayfile/adapter-core/package.json')), 'mappings'). + const require = createRequire(join(root, 'packages/github/package.json')); + const mappings = join(dirname(require.resolve('@relayfile/adapter-core/package.json')), 'mappings'); + const fallbacks = readdirSync(mappings).filter((name) => name.endsWith('.mapping.yaml')); + const bundled = readdirSync(join(mappings, 'adapters')).filter((name) => name.endsWith('.mapping.yaml')); + assert.deepEqual(fallbacks.sort(), ['github.mapping.yaml', 'slack.mapping.yaml']); + assert.ok(bundled.length >= 25, `expected the adapter bundle, found ${bundled.length}`); + for (const name of bundled) { + const provider = name.replace('.mapping.yaml', ''); + assert.equal(readFileSync(join(mappings, 'adapters', name), 'utf8'), + readFileSync(join(root, 'packages', provider, name), 'utf8'), `${name} differs from its adapter`); + } + const pkg = JSON.parse(readFileSync(join(root, 'packages/core/package.json'), 'utf8')); + assert.ok(pkg.files.includes('mappings'), 'mappings/ must be in files so the bundle is published'); +});