From 686ca372492cc69db590ef9f179bda1f303c580e Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Tue, 2 Jun 2026 23:39:21 +0200 Subject: [PATCH 1/4] feat(github): add deployment_status.created to supported event catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `deployment_status.created` to DEFAULT_GITHUB_EVENTS (types.ts) and DEFAULT_SUPPORTED_EVENTS (config.ts), and regenerates the trigger catalog. This is the GitHub webhook that carries deploy outcome — success/failure is in `payload.deployment_status.state` — and powers deploy-watch agents (NightCTO). Catalog-only: no router EVENT_MAP handler / record materialization, since deploy-watch consumes the payload directly rather than a /github deployment record. Generated artifact regenerated via `npx adapter-core triggers generate`. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/triggers/catalog.generated.json | 1 + packages/core/src/triggers/catalog.generated.ts | 1 + packages/github/src/config.ts | 1 + packages/github/src/types.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/core/src/triggers/catalog.generated.json b/packages/core/src/triggers/catalog.generated.json index f10b88cc..32a279a1 100644 --- a/packages/core/src/triggers/catalog.generated.json +++ b/packages/core/src/triggers/catalog.generated.json @@ -73,6 +73,7 @@ ], "github": [ "check_run.completed", + "deployment_status.created", "issue_comment.created", "issues.closed", "issues.edited", diff --git a/packages/core/src/triggers/catalog.generated.ts b/packages/core/src/triggers/catalog.generated.ts index 5cde1cb3..cd0d327d 100644 --- a/packages/core/src/triggers/catalog.generated.ts +++ b/packages/core/src/triggers/catalog.generated.ts @@ -81,6 +81,7 @@ export const KNOWN_TRIGGER_CATALOG = { ], "github": [ "check_run.completed", + "deployment_status.created", "issue_comment.created", "issues.closed", "issues.edited", diff --git a/packages/github/src/config.ts b/packages/github/src/config.ts index a030dbe1..b3aba6a9 100644 --- a/packages/github/src/config.ts +++ b/packages/github/src/config.ts @@ -34,6 +34,7 @@ const DEFAULT_SUPPORTED_EVENTS = [ 'issues.reopened', // a closed issue was reopened 'issues.closed', // an issue was closed 'check_run.completed', // a CI check finished — `check_run.conclusion` is success / failure / timed_out / cancelled / … + 'deployment_status.created', // a deployment's status changed — `deployment_status.state` is success / failure / error / pending / in_progress / … (powers deploy-watch agents) ] as const; export const DEFAULT_CONFIG: GitHubAdapterConfig = { diff --git a/packages/github/src/types.ts b/packages/github/src/types.ts index 875cbcd6..e601ff8f 100644 --- a/packages/github/src/types.ts +++ b/packages/github/src/types.ts @@ -28,6 +28,7 @@ export const DEFAULT_GITHUB_EVENTS = [ 'issues.reopened', 'issues.closed', 'check_run.completed', + 'deployment_status.created', ] as const; export type GitHubReviewEvent = (typeof GITHUB_REVIEW_EVENTS)[number]; From c3d46c575d1fd85cc95427c41fbba9f4df101d07 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Tue, 2 Jun 2026 21:52:13 +0000 Subject: [PATCH 2/4] chore: apply pr-reviewer fixes for #151 --- docs/integration-scopes.yaml | 2 +- packages/github/docs/adapter-spec.md | 3 ++ packages/github/github.mapping.yaml | 11 ++++ .../src/__tests__/e2e-webhook-route.test.ts | 28 +++++++++++ .../github/src/__tests__/path-mapper.test.ts | 21 ++++++++ packages/github/src/index.ts | 21 +++++++- packages/github/src/path-mapper.ts | 16 ++++++ packages/github/src/types.ts | 11 ++++ .../webhook/__tests__/webhook-router.test.ts | 50 ++++++++++++++++++- packages/github/src/webhook/event-map.ts | 2 + packages/github/src/webhook/router.test.ts | 47 ++++++++++++++++- 11 files changed, 207 insertions(+), 5 deletions(-) diff --git a/docs/integration-scopes.yaml b/docs/integration-scopes.yaml index 1a33a1dd..ad3d63db 100644 --- a/docs/integration-scopes.yaml +++ b/docs/integration-scopes.yaml @@ -274,7 +274,7 @@ integrations: - https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28 - https://docs.github.com/en/rest/repos/webhooks?apiVersion=2026-03-10 - https://docs.github.com/en/webhooks/webhook-events-and-payloads - notes: "Nango exposes GitHub App OAuth and GitHub App providers but does not publish default action-level scopes for this adapter surface, so the minimum is derived from GitHub's REST endpoint and webhook permission docs. For GitHub App installations, metadata read is the baseline repository metadata permission; contents read covers repository content and commit/file reads; issues read covers issue metadata and issue comments; pull requests write is needed because the adapter writes, updates, and deletes pull request reviews and also covers PR/review read; checks read covers check-run ingestion. Webhook subscriptions for pull_request, pull_request_review, pull_request_review_comment, issues, and check_run require the corresponding read-level repository permissions when using a GitHub App's configured webhook. Programmatic repository webhook actions under Relayfile's Nango provider config key `github-relay` use GitHub REST API version 2026-03-10: list/get/ping/test require repository Webhooks read permission, while create/update/delete require repository Webhooks write permission. For OAuth App connections against private repositories, GitHub's classic repo scope is the practical broad minimum and admin:repo_hook is the narrower repository-hook administration scope; public-only deployments may be able to use public_repo for non-hook surfaces, but that does not cover the full private-repository eval surface." + notes: "Nango exposes GitHub App OAuth and GitHub App providers but does not publish default action-level scopes for this adapter surface, so the minimum is derived from GitHub's REST endpoint and webhook permission docs. For GitHub App installations, metadata read is the baseline repository metadata permission; contents read covers repository content and commit/file reads; issues read covers issue metadata and issue comments; pull requests write is needed because the adapter writes, updates, and deletes pull request reviews and also covers PR/review read; checks read covers check-run ingestion; deployments read covers deployment_status ingestion. Webhook subscriptions for pull_request, pull_request_review, pull_request_review_comment, issues, check_run, and deployment_status require the corresponding read-level repository permissions when using a GitHub App's configured webhook. Programmatic repository webhook actions under Relayfile's Nango provider config key `github-relay` use GitHub REST API version 2026-03-10: list/get/ping/test require repository Webhooks read permission, while create/update/delete require repository Webhooks write permission. For OAuth App connections against private repositories, GitHub's classic repo scope is the practical broad minimum and admin:repo_hook is the narrower repository-hook administration scope; public-only deployments may be able to use public_repo for non-hook surfaces, but that does not cover the full private-repository eval surface." - slug: gitlab display_name: GitLab diff --git a/packages/github/docs/adapter-spec.md b/packages/github/docs/adapter-spec.md index f71f804d..d655b6f1 100644 --- a/packages/github/docs/adapter-spec.md +++ b/packages/github/docs/adapter-spec.md @@ -181,6 +181,8 @@ export interface IngestResult { reviews/{review_id}.json comments/{comment_id}.json checks/{check_id}.json + deployments/{deployment_id}/ + statuses/{status_id}.json issues/{number}/ meta.json comments/{comment_id}.json @@ -195,6 +197,7 @@ export interface IngestResult { - `push` - `issues.opened` / `issues.closed` - `check_run.completed` +- `deployment_status.created` ### File Content Fetching ```typescript diff --git a/packages/github/github.mapping.yaml b/packages/github/github.mapping.yaml index d40b0f76..3da4b6b2 100644 --- a/packages/github/github.mapping.yaml +++ b/packages/github/github.mapping.yaml @@ -31,6 +31,17 @@ webhooks: - issue.number - issue.title - comment.id + 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 issues: diff --git a/packages/github/src/__tests__/e2e-webhook-route.test.ts b/packages/github/src/__tests__/e2e-webhook-route.test.ts index 3f1590ab..34d21d70 100644 --- a/packages/github/src/__tests__/e2e-webhook-route.test.ts +++ b/packages/github/src/__tests__/e2e-webhook-route.test.ts @@ -88,6 +88,10 @@ class RecordingGitHubAdapter extends GitHubAdapter { return this.record('ingestCheckRun', payload); } + override async ingestDeploymentStatus(payload: Record): Promise { + return this.record('ingestDeploymentStatus', payload); + } + private async record(method: string, payload: Record): Promise { const repoInfo = extractRepoInfo(payload); this.calls.push({ method, ...repoInfo }); @@ -297,6 +301,30 @@ const supportedEventCases: readonly EventCase[] = [ repo: mockRepoContext.repo, }, }, + { + eventKey: 'deployment_status.created', + headers: { 'x-github-event': 'deployment_status' }, + payload: { + action: 'created', + repository, + deployment: { + id: 1101, + ref: 'main', + sha: mockRepoContext.headSha, + environment: 'production', + }, + deployment_status: { + id: 1102, + state: 'success', + environment: 'production', + }, + }, + expectedMethod: 'ingestDeploymentStatus', + expectedRepoInfo: { + owner: mockRepoContext.owner, + repo: mockRepoContext.repo, + }, + }, ] as const; test('WebhookRouter routes every supported GitHub event to the correct adapter handler', async (t) => { diff --git a/packages/github/src/__tests__/path-mapper.test.ts b/packages/github/src/__tests__/path-mapper.test.ts index 52afd792..2361d536 100644 --- a/packages/github/src/__tests__/path-mapper.test.ts +++ b/packages/github/src/__tests__/path-mapper.test.ts @@ -14,6 +14,7 @@ import { githubByTitleAliasPath, githubCheckRunPath, githubCommitPath, + githubDeploymentStatusPath, githubIssuePath, githubLegacyByTitleAliasPath, githubNumberedByTitleAliasPath, @@ -87,6 +88,7 @@ describe('path-mapper', () => { assert.equal(normalizeNangoGitHubModel('Review'), 'review'); assert.equal(normalizeNangoGitHubModel('ReviewComment'), 'review_comment'); assert.equal(normalizeNangoGitHubModel('CheckRun'), 'check_run'); + assert.equal(normalizeNangoGitHubModel('DeploymentStatus'), 'deployment_status'); assert.equal(normalizeNangoGitHubModel('Commit'), 'commit'); }); @@ -168,6 +170,17 @@ describe('path-mapper', () => { ); }); + it('githubDeploymentStatusPath', () => { + assert.equal( + githubDeploymentStatusPath('octocat', 'hello-world', '42', '789'), + '/github/repos/octocat/hello-world/deployments/42/statuses/789.json', + ); + assert.equal( + githubDeploymentStatusPath('octocat', 'hello-world', 'deploy/1', 'status/2'), + '/github/repos/octocat/hello-world/deployments/deploy%2F1/statuses/status%2F2.json', + ); + }); + it('githubCommitPath', () => { assert.equal( githubCommitPath('octocat', 'hello-world', 'abc123'), @@ -276,6 +289,13 @@ describe('path-mapper', () => { ); }); + it('computes deployment_status path with context', () => { + assert.equal( + computeGitHubPath('deployment_status', '555', { owner: 'octocat', repo: 'hello-world' }), + '/github/repos/octocat/hello-world/deployments/deployment-unknown/statuses/555.json', + ); + }); + it('computes commit path with context', () => { assert.equal( computeGitHubPath('commit', 'deadbeef', { owner: 'octocat', repo: 'hello-world' }), @@ -346,6 +366,7 @@ describe('path-mapper', () => { assert.equal(tryNormalizeGitHubObjectType('pull_request'), 'pull_request'); assert.equal(tryNormalizeGitHubObjectType('pr'), 'pull_request'); assert.equal(tryNormalizeGitHubObjectType('issue'), 'issue'); + assert.equal(tryNormalizeGitHubObjectType('deployment_status'), 'deployment_status'); }); it('returns undefined for unknown types', () => { diff --git a/packages/github/src/index.ts b/packages/github/src/index.ts index b9dabafe..6101052a 100644 --- a/packages/github/src/index.ts +++ b/packages/github/src/index.ts @@ -4,7 +4,7 @@ import type { ConnectionProvider } from '@relayfile/sdk'; import { createGitHubSchemaAdapter } from './adapter.js'; import { DEFAULT_CONFIG, validateConfig } from './config.js'; import { materializeRepo as materializeGitHubRepo, syncGitHubWorkspace } from './lazy.js'; -import { githubIssuePath, githubPullRequestPath } from './path-mapper.js'; +import { githubDeploymentStatusPath, githubIssuePath, githubPullRequestPath } from './path-mapper.js'; import { type FileSemantics, type GitHubAdapterConfig, @@ -115,6 +115,8 @@ export class GitHubAdapter extends LocalIntegrationAdapter implements WebhookAda return `${repoPrefix}/review-threads/${objectId}.json`; case 'check_run': return `${repoPrefix}/checks/${objectId}.json`; + case 'deployment_status': + return `${repoPrefix}/deployments/deployment-unknown/statuses/${objectId}.json`; case 'commit': return `${repoPrefix}/commits/${objectId}/metadata.json`; default: @@ -242,6 +244,15 @@ export class GitHubAdapter extends LocalIntegrationAdapter implements WebhookAda return this.createIngestResult('check_run.completed', 'check_run', payload, 'write'); } + async ingestDeploymentStatus(payload: Record): Promise { + return this.createIngestResult( + 'deployment_status.created', + 'deployment_status', + payload, + 'write', + ); + } + async sync(_workspaceId: string, options: SyncOptions = {}): Promise { return syncGitHubWorkspace(_workspaceId, this.provider as never, this.config, this.inFlightMaterializations, options); } @@ -324,6 +335,11 @@ export class GitHubAdapter extends LocalIntegrationAdapter implements WebhookAda } case 'check_run': return `/github/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/checks/${objectId}.json`; + case 'deployment_status': { + const deploymentId = + readNestedNumericLike(payload, 'deployment', 'id') ?? 'deployment-unknown'; + return githubDeploymentStatusPath(owner, repo, deploymentId, objectId); + } case 'commit': return `/github/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${objectId}/metadata.json`; default: @@ -362,6 +378,9 @@ export class GitHubAdapter extends LocalIntegrationAdapter implements WebhookAda objectType === 'review' ? readNestedNumericLike(payload, 'review', 'id') : undefined, objectType === 'review_comment' ? readNestedNumericLike(payload, 'comment', 'id') : undefined, objectType === 'review_thread' ? readNestedNumericLike(payload, 'thread', 'id') : undefined, + objectType === 'deployment_status' + ? readNestedNumericLike(payload, 'deployment_status', 'id') + : undefined, readNumericLike(payload.id), readNumericLike(payload.number), readNestedNumericLike(payload, 'pull_request', 'number'), diff --git a/packages/github/src/path-mapper.ts b/packages/github/src/path-mapper.ts index 15792037..e17b2415 100644 --- a/packages/github/src/path-mapper.ts +++ b/packages/github/src/path-mapper.ts @@ -189,6 +189,17 @@ export function githubCheckRunPath(owner: string, repo: string, checkRunId: numb return `${githubRepoPrefix(owner, repo)}/checks/${encodeGitHubPathSegment(String(checkRunId))}.json`; } +export function githubDeploymentStatusPath( + owner: string, + repo: string, + deploymentId: number | string, + deploymentStatusId: number | string, +): string { + return `${githubRepoPrefix(owner, repo)}/deployments/${encodeGitHubPathSegment( + String(deploymentId), + )}/statuses/${encodeGitHubPathSegment(String(deploymentStatusId))}.json`; +} + export function githubCommitPath(owner: string, repo: string, sha: string): string { return `${githubRepoPrefix(owner, repo)}/commits/${encodeGitHubPathSegment(sha)}/metadata.json`; } @@ -329,6 +340,8 @@ const OBJECT_TYPE_ALIASES: Record = { review_comment: 'review_comment', check_run: 'check_run', checkrun: 'check_run', + deployment_status: 'deployment_status', + deploymentstatus: 'deployment_status', commit: 'commit', }; @@ -357,6 +370,7 @@ const NANGO_MODEL_MAP: Record = { Review: 'review', ReviewComment: 'review_comment', CheckRun: 'check_run', + DeploymentStatus: 'deployment_status', Commit: 'commit', }; @@ -402,6 +416,8 @@ export function computeGitHubPath( return githubReviewCommentPath(owner, repo, objectId); case 'check_run': return githubCheckRunPath(owner, repo, objectId); + case 'deployment_status': + return githubDeploymentStatusPath(owner, repo, 'deployment-unknown', objectId); case 'commit': return githubCommitPath(owner, repo, objectId); default: diff --git a/packages/github/src/types.ts b/packages/github/src/types.ts index e601ff8f..38511a44 100644 --- a/packages/github/src/types.ts +++ b/packages/github/src/types.ts @@ -180,6 +180,17 @@ export interface GitHubCheckRun { headSha?: string; } +export interface GitHubDeploymentStatus { + id: number | string; + state: string; + deploymentId?: number | string; + environment?: string; + description?: null | string; + targetUrl?: null | string; + createdAt?: string; + updatedAt?: string; +} + export interface GitHubWebhookEnvelope { eventType: string; deliveryId?: string; diff --git a/packages/github/src/webhook/__tests__/webhook-router.test.ts b/packages/github/src/webhook/__tests__/webhook-router.test.ts index 650d7d63..0d2b79ab 100644 --- a/packages/github/src/webhook/__tests__/webhook-router.test.ts +++ b/packages/github/src/webhook/__tests__/webhook-router.test.ts @@ -57,6 +57,9 @@ function createAdapterMocks() { ingestCheckRun: mock.method(adapter, 'ingestCheckRun', async () => createResult('/github/repos/acme/widgets/pulls/7/checks/4.json'), ), + ingestDeploymentStatus: mock.method(adapter, 'ingestDeploymentStatus', async () => + createResult('/github/repos/acme/widgets/deployments/11/statuses/12.json'), + ), }; } @@ -184,6 +187,47 @@ describe('WebhookRouter', () => { assert.strictEqual(mocks.ingestIssue.mock.calls.length, 0); }); + it('route calls correct handler for deployment_status created', async () => { + const mocks = createAdapterMocks(); + const router = new WebhookRouter(mocks.adapter); + const payload = { + action: 'created', + repository: { full_name: 'acme/widgets' }, + deployment: { id: 11 }, + deployment_status: { id: 12, state: 'success' }, + }; + + const result = await router.route({ 'x-github-event': 'deployment_status' }, payload); + assert.deepStrictEqual( + result, + createResult('/github/repos/acme/widgets/deployments/11/statuses/12.json'), + ); + assert.strictEqual(mocks.ingestDeploymentStatus.mock.calls.length, 1); + assert.deepStrictEqual(mocks.ingestDeploymentStatus.mock.calls[0].arguments, [payload]); + assert.strictEqual(mocks.ingestPullRequest.mock.calls.length, 0); + assert.strictEqual(mocks.ingestIssue.mock.calls.length, 0); + }); + + it('adapter routeWebhook maps deployment_status.created to a scoped path', async () => { + const adapter = new GitHubAdapter(); + + const result = await adapter.routeWebhook( + { + id: 'delivery-level-id', + action: 'created', + repository: { full_name: 'acme/widgets' }, + deployment: { id: 11 }, + deployment_status: { id: 12, state: 'success' }, + }, + 'deployment_status.created', + ); + + assert.deepStrictEqual(result.paths, [ + '/github/repos/acme/widgets/deployments/11/statuses/12.json', + ]); + assert.deepStrictEqual(result.errors, []); + }); + it('route returns error for unsupported event', async () => { const mocks = createAdapterMocks(); const router = new WebhookRouter(mocks.adapter); @@ -223,9 +267,10 @@ describe('WebhookRouter', () => { assert.strictEqual(router.isSupported('issues.edited'), true); assert.strictEqual(router.isSupported('issues.labeled'), true); assert.strictEqual(router.isSupported('check_run.completed'), true); + assert.strictEqual(router.isSupported('deployment_status.created'), true); }); - it('getSupportedEvents lists all 19 events', () => { + it('getSupportedEvents lists all 20 events', () => { const router = new WebhookRouter(createAdapterMocks().adapter); assert.deepStrictEqual(router.getSupportedEvents(), [ @@ -248,7 +293,8 @@ describe('WebhookRouter', () => { 'issues.reopened', 'issues.closed', 'check_run.completed', + 'deployment_status.created', ]); - assert.strictEqual(router.getSupportedEvents().length, 19); + assert.strictEqual(router.getSupportedEvents().length, 20); }); }); diff --git a/packages/github/src/webhook/event-map.ts b/packages/github/src/webhook/event-map.ts index 33dad721..a08bb39f 100644 --- a/packages/github/src/webhook/event-map.ts +++ b/packages/github/src/webhook/event-map.ts @@ -14,6 +14,7 @@ export interface WebhookAdapter { updateIssue(payload: Record): Promise; closeIssue(payload: Record): Promise; ingestCheckRun(payload: Record): Promise; + ingestDeploymentStatus(payload: Record): Promise; } export type WebhookHandler = ( @@ -43,6 +44,7 @@ export const EVENT_MAP: Record = { 'issues.reopened': (adapter, payload) => adapter.updateIssue(payload), 'issues.closed': (adapter, payload) => adapter.closeIssue(payload), 'check_run.completed': (adapter, payload) => adapter.ingestCheckRun(payload), + 'deployment_status.created': (adapter, payload) => adapter.ingestDeploymentStatus(payload), }; export function extractEventKey( diff --git a/packages/github/src/webhook/router.test.ts b/packages/github/src/webhook/router.test.ts index 613c3993..ec96aae1 100644 --- a/packages/github/src/webhook/router.test.ts +++ b/packages/github/src/webhook/router.test.ts @@ -83,6 +83,11 @@ class RecordingAdapter extends GitHubAdapter { this.calls.push(`ingestCheckRun:${String(payload.action ?? '')}`); return createResult('/github/repos/acme/widgets/pulls/7/checks/4.json'); } + + override async ingestDeploymentStatus(payload: Record): Promise { + this.calls.push(`ingestDeploymentStatus:${String(payload.action ?? '')}`); + return createResult('/github/repos/acme/widgets/deployments/11/statuses/12.json'); + } } const unusedProvider: ConnectionProvider = { @@ -243,6 +248,45 @@ test('WebhookRouter routes check_run.completed to ingestCheckRun', async () => { assert.deepEqual(result.paths, ['/github/repos/acme/widgets/pulls/7/checks/4.json']); }); +test('WebhookRouter routes deployment_status.created to ingestDeploymentStatus', async () => { + const adapter = new RecordingAdapter(); + const router = new WebhookRouter(adapter); + + const result = await router.route( + { 'x-github-event': 'deployment_status' }, + { + id: 'delivery-level-id', + action: 'created', + repository: { full_name: 'acme/widgets' }, + deployment: { id: 11 }, + deployment_status: { id: 12, state: 'success' }, + }, + ); + + assert.deepEqual(adapter.calls, ['ingestDeploymentStatus:created']); + assert.deepEqual(result.paths, ['/github/repos/acme/widgets/deployments/11/statuses/12.json']); +}); + +test('GitHubAdapter routes deployment_status.created to the deployment status path', async () => { + const adapter = new GitHubAdapter(unusedProvider, { connectionId: 'conn-fixture' }); + + const result = await adapter.routeWebhook( + { + action: 'created', + repository: { full_name: 'acme/widgets' }, + deployment: { id: 11 }, + deployment_status: { id: 12, state: 'success' }, + }, + 'deployment_status.created', + ); + + assert.equal(result.filesWritten, 1); + assert.deepEqual(result.paths, [ + '/github/repos/acme/widgets/deployments/11/statuses/12.json', + ]); + assert.deepEqual(result.errors, []); +}); + test('WebhookRouter returns a typed error for unsupported events', async () => { const adapter = new RecordingAdapter(); const router = new WebhookRouter(adapter); @@ -278,7 +322,7 @@ test('WebhookRouter.getSupportedEvents returns all mapped events', () => { const router = new WebhookRouter(new RecordingAdapter()); const events = router.getSupportedEvents(); - assert.equal(events.length, 19); + assert.equal(events.length, 20); assert.deepEqual(events, [ 'pull_request.opened', 'pull_request.synchronize', @@ -299,6 +343,7 @@ test('WebhookRouter.getSupportedEvents returns all mapped events', () => { 'issues.reopened', 'issues.closed', 'check_run.completed', + 'deployment_status.created', ]); }); From 2c5d1c9d0bc2ef2000b23abbd0a73ea9340950cb Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Tue, 2 Jun 2026 22:05:24 +0000 Subject: [PATCH 3/4] chore: apply pr-reviewer fixes for #151 --- package-lock.json | 2 +- packages/github/src/__tests__/path-mapper.test.ts | 11 +++++++++++ packages/github/src/index.ts | 7 ++++++- packages/github/src/path-mapper.ts | 8 +++++++- .../src/webhook/__tests__/webhook-router.test.ts | 5 +++-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a13e3a90..22439ce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7109,7 +7109,7 @@ }, "packages/persona-nango-integrations": { "name": "@agentworkforce/persona-nango-integrations", - "version": "0.1.2", + "version": "0.1.6", "license": "MIT", "engines": { "node": ">=18" diff --git a/packages/github/src/__tests__/path-mapper.test.ts b/packages/github/src/__tests__/path-mapper.test.ts index 2361d536..020db08e 100644 --- a/packages/github/src/__tests__/path-mapper.test.ts +++ b/packages/github/src/__tests__/path-mapper.test.ts @@ -290,6 +290,17 @@ describe('path-mapper', () => { }); it('computes deployment_status path with context', () => { + assert.equal( + computeGitHubPath('deployment_status', '555', { + owner: 'octocat', + repo: 'hello-world', + deploymentId: '42', + }), + '/github/repos/octocat/hello-world/deployments/42/statuses/555.json', + ); + }); + + it('falls back to a placeholder deployment root without deployment context', () => { assert.equal( computeGitHubPath('deployment_status', '555', { owner: 'octocat', repo: 'hello-world' }), '/github/repos/octocat/hello-world/deployments/deployment-unknown/statuses/555.json', diff --git a/packages/github/src/index.ts b/packages/github/src/index.ts index 6101052a..d1d38c26 100644 --- a/packages/github/src/index.ts +++ b/packages/github/src/index.ts @@ -116,7 +116,12 @@ export class GitHubAdapter extends LocalIntegrationAdapter implements WebhookAda case 'check_run': return `${repoPrefix}/checks/${objectId}.json`; case 'deployment_status': - return `${repoPrefix}/deployments/deployment-unknown/statuses/${objectId}.json`; + return githubDeploymentStatusPath( + this.config.owner ?? '_owner', + this.config.repo ?? '_repo', + 'deployment-unknown', + objectId, + ); case 'commit': return `${repoPrefix}/commits/${objectId}/metadata.json`; default: diff --git a/packages/github/src/path-mapper.ts b/packages/github/src/path-mapper.ts index e17b2415..094e6237 100644 --- a/packages/github/src/path-mapper.ts +++ b/packages/github/src/path-mapper.ts @@ -383,6 +383,7 @@ export function normalizeNangoGitHubModel(model: string): string { export interface GitHubPathContext { owner?: string; repo?: string; + deploymentId?: number | string; } export function computeGitHubPath( @@ -417,7 +418,12 @@ export function computeGitHubPath( case 'check_run': return githubCheckRunPath(owner, repo, objectId); case 'deployment_status': - return githubDeploymentStatusPath(owner, repo, 'deployment-unknown', objectId); + return githubDeploymentStatusPath( + owner, + repo, + context?.deploymentId ?? 'deployment-unknown', + objectId, + ); case 'commit': return githubCommitPath(owner, repo, objectId); default: diff --git a/packages/github/src/webhook/__tests__/webhook-router.test.ts b/packages/github/src/webhook/__tests__/webhook-router.test.ts index 0d2b79ab..56064484 100644 --- a/packages/github/src/webhook/__tests__/webhook-router.test.ts +++ b/packages/github/src/webhook/__tests__/webhook-router.test.ts @@ -5,6 +5,7 @@ import { GitHubAdapter } from '../../index.js'; import type { IngestResult } from '../event-map.js'; import { extractEventKey, extractRepoInfo } from '../event-map.js'; import { WebhookRouter } from '../router.js'; +import { createMockProvider } from '../../__tests__/fixtures/mock-provider.ts'; function createResult(path: string): IngestResult { return { @@ -17,7 +18,7 @@ function createResult(path: string): IngestResult { } function createAdapterMocks() { - const adapter = new GitHubAdapter(); + const adapter = new GitHubAdapter(createMockProvider()); return { adapter, @@ -209,7 +210,7 @@ describe('WebhookRouter', () => { }); it('adapter routeWebhook maps deployment_status.created to a scoped path', async () => { - const adapter = new GitHubAdapter(); + const adapter = new GitHubAdapter(createMockProvider()); const result = await adapter.routeWebhook( { From 8a1d40ff111bdfaeeeca8dd5bc01e7f07e635084 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Wed, 3 Jun 2026 08:50:05 +0200 Subject: [PATCH 4/4] chore: remove nango-integrations persona pack (moved to workforce) The nango-integrations persona pack now lives in the workforce repo's consolidated @agentworkforce/personas-workforce pack. Remove it here: - delete packages/persona-nango-integrations - delete .github/workflows/publish-persona.yml - drop the two resolve-publish-targets tests that asserted on the removed pack - prune the stale workspace entry from package-lock.json Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish-persona.yml | 143 ------------------ package-lock.json | 12 -- packages/persona-nango-integrations/README.md | 56 ------- .../persona-nango-integrations/index.d.ts | 30 ---- packages/persona-nango-integrations/index.js | 17 --- .../persona-nango-integrations/package.json | 48 ------ .../personas/nango-integrations.json | 37 ----- .../test/persona.test.mjs | 60 -------- scripts/resolve-publish-targets.test.mjs | 19 --- 9 files changed, 422 deletions(-) delete mode 100644 .github/workflows/publish-persona.yml delete mode 100644 packages/persona-nango-integrations/README.md delete mode 100644 packages/persona-nango-integrations/index.d.ts delete mode 100644 packages/persona-nango-integrations/index.js delete mode 100644 packages/persona-nango-integrations/package.json delete mode 100644 packages/persona-nango-integrations/personas/nango-integrations.json delete mode 100644 packages/persona-nango-integrations/test/persona.test.mjs diff --git a/.github/workflows/publish-persona.yml b/.github/workflows/publish-persona.yml deleted file mode 100644 index afb7edf2..00000000 --- a/.github/workflows/publish-persona.yml +++ /dev/null @@ -1,143 +0,0 @@ -name: Publish Persona Package - -# Separate from publish.yml because persona packages are published under the -# @agentworkforce scope (not @relayfile). publish.yml's all/missing/group -# selectors are scoped to @relayfile/* (see scripts/resolve-publish-targets.mjs) -# and will never sweep a persona package; publish them here instead. -# -# Structure mirrors publish.yml: it publishes with `--provenance` (npm OIDC -# trusted publishing via id-token: write), so no npm auth token is needed. -# -# Unlike publish.yml there is no `turbo build` step: persona packages have no -# build script and ship their index.js/index.d.ts committed to the repo, so a -# workspace build would only rebuild unrelated @relayfile/* packages. - -on: - workflow_dispatch: - inputs: - package: - description: | - Persona package dir name under packages/ to publish. - Must be an @agentworkforce/persona-* package. - required: true - type: string - default: "persona-nango-integrations" - version: - description: | - Version bump or exact version. - • patch / minor / major / prerelease (npm version semantics) - • none (publish current package.json version as-is) - • an exact semver like 0.2.0 or 0.2.0-beta.1 - required: true - type: string - default: "patch" - tag: - description: "NPM dist-tag" - required: false - type: choice - options: - - latest - - next - - beta - default: "latest" - dry_run: - description: "Dry run (do not actually publish)" - required: false - type: boolean - default: false - -concurrency: - group: publish-persona - cancel-in-progress: false - -permissions: - contents: write - id-token: write - -jobs: - publish: - name: Publish persona - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "22.14.0" - cache: npm - registry-url: "https://registry.npmjs.org" - - - run: npm ci - - - name: Upgrade npm to latest - run: npm install -g npm@latest - - - name: Validate persona package selection - id: validate - env: - INPUT_PACKAGE: ${{ github.event.inputs.package }} - run: | - set -euo pipefail - pkg="$INPUT_PACKAGE" - case "$pkg" in - *[!a-zA-Z0-9_-]*) - echo "Invalid package slug: $pkg" >&2 - exit 1 - ;; - esac - if [ ! -f "packages/$pkg/package.json" ]; then - echo "No package.json at packages/$pkg" >&2 - exit 1 - fi - name=$(node -p "require('./packages/$pkg/package.json').name") - case "$name" in - @agentworkforce/persona-*) ;; - *) - echo "Refusing to publish '$name' via the persona workflow (expected @agentworkforce/persona-*)." >&2 - exit 1 - ;; - esac - echo "package=$pkg" >> "$GITHUB_OUTPUT" - - - name: Version package - env: - PKG: ${{ steps.validate.outputs.package }} - INPUT_VERSION: ${{ github.event.inputs.version }} - run: | - set -euo pipefail - if [ "$INPUT_VERSION" != "none" ]; then - cd "packages/$PKG" - npm version "$INPUT_VERSION" --no-git-tag-version --allow-same-version - fi - - - name: Publish package - env: - PKG: ${{ steps.validate.outputs.package }} - INPUT_TAG: ${{ github.event.inputs.tag }} - INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }} - run: | - set -euo pipefail - cd "packages/$PKG" - name=$(node -p "require('./package.json').name") - version=$(node -p "require('./package.json').version") - if [ "$INPUT_DRY_RUN" = "true" ]; then - echo "DRY RUN: $name@$version" - npm publish --dry-run --access public --tag "$INPUT_TAG" --ignore-scripts - else - npm publish --access public --provenance --tag "$INPUT_TAG" --ignore-scripts - echo "Published $name@$version" - fi - - - name: Commit version bump - if: github.event.inputs.dry_run != 'true' && github.event.inputs.version != 'none' - env: - PKG: ${{ steps.validate.outputs.package }} - INPUT_VERSION: ${{ github.event.inputs.version }} - run: | - git config user.name "GitHub Actions" - git config user.email "actions@github.com" - git add "packages/$PKG/package.json" - if ! git diff --staged --quiet; then - git commit -m "chore(release): bump ${PKG} (${INPUT_VERSION})" - git push - fi diff --git a/package-lock.json b/package-lock.json index 22439ce8..f92e8f87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,10 +153,6 @@ "version": "0.11.0", "dev": true }, - "node_modules/@agentworkforce/persona-nango-integrations": { - "resolved": "packages/persona-nango-integrations", - "link": true - }, "node_modules/@agentworkforce/workload-router": { "version": "0.1.3", "dev": true, @@ -7107,14 +7103,6 @@ "node": ">=18" } }, - "packages/persona-nango-integrations": { - "name": "@agentworkforce/persona-nango-integrations", - "version": "0.1.6", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "packages/pipedrive": { "name": "@relayfile/adapter-pipedrive", "version": "0.3.7", diff --git a/packages/persona-nango-integrations/README.md b/packages/persona-nango-integrations/README.md deleted file mode 100644 index 8b94b5f5..00000000 --- a/packages/persona-nango-integrations/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# @agentworkforce/persona-nango-integrations - -The canonical **nango-integrations** AgentWorkforce persona, published as a -**persona pack** so both `cloud` and `nightcto` install the same definition with -the workforce CLI instead of hand-maintaining divergent copies. - -The persona builds and maintains Nango TypeScript integrations **and** their -Cloud-side Relayfile wiring (the `ADAPTERS` registry, webhook router, path -mapping, and digest tests) — an integration is not done until both sides land. - -## Install - -```bash -agentworkforce install @agentworkforce/persona-nango-integrations -# or, to refresh an existing copy: -agentworkforce install @agentworkforce/persona-nango-integrations --overwrite -``` - -The CLI copies `personas/nango-integrations.json` into the consumer's -`.agentworkforce/workforce/personas/` (the directory the persona loader scans). -`package.json` advertises the pack via: - -```json -"agentworkforce": { "personas": "personas" } -``` - -The whole spec — including the operations manual on `agentsMdContent` — lives in -`personas/nango-integrations.json`. There are no external markdown sidecars. - -## Programmatic usage - -Existing consumers can still import the persona object directly: - -```ts -import persona from '@agentworkforce/persona-nango-integrations'; - -console.log(persona.id); // nango-integrations -``` - -The legacy JSON subpath also resolves to the moved pack file: - -```ts -import persona from '@agentworkforce/persona-nango-integrations/persona.json' with { type: 'json' }; -``` - -## Source of truth - -`personas/nango-integrations.json` is the single source of truth. To change the -persona, edit that file and republish; consumers re-run -`agentworkforce install … --overwrite` to pick up the new version. - -## Publishing - -Published under the `@agentworkforce` scope via the dedicated -`.github/workflows/publish-persona.yml` workflow (separate from the -`@relayfile/*` `publish.yml`), using npm `--provenance`. diff --git a/packages/persona-nango-integrations/index.d.ts b/packages/persona-nango-integrations/index.d.ts deleted file mode 100644 index 0c50ed5b..00000000 --- a/packages/persona-nango-integrations/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface PersonaSkill { - id: string; - source: string; - description?: string; -} - -export interface PersonaMcpServer { - type: string; - url: string; -} - -export interface NangoIntegrationsPersona { - id: string; - intent: string; - tags: string[]; - description: string; - skills: PersonaSkill[]; - mcpServers: Record; - agentsMdContent: string; - harness: string; - model: string; - systemPrompt: string; - harnessSettings: Record; -} - -declare const persona: NangoIntegrationsPersona; - -export const nangoIntegrationsPersona: NangoIntegrationsPersona; - -export default persona; diff --git a/packages/persona-nango-integrations/index.js b/packages/persona-nango-integrations/index.js deleted file mode 100644 index efcc2932..00000000 --- a/packages/persona-nango-integrations/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import { createRequire } from 'node:module'; - -const require = createRequire(import.meta.url); - -/** - * The nango-integrations persona spec. The persona builds and maintains Nango - * TypeScript integrations and their Cloud-side Relayfile wiring. - * - * Source of truth: `personas/nango-integrations.json`. This compatibility - * export keeps existing programmatic consumers working while the package also - * acts as an AgentWorkforce installable persona pack. - */ -const persona = require('./personas/nango-integrations.json'); - -export const nangoIntegrationsPersona = persona; - -export default persona; diff --git a/packages/persona-nango-integrations/package.json b/packages/persona-nango-integrations/package.json deleted file mode 100644 index 0ded4309..00000000 --- a/packages/persona-nango-integrations/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@agentworkforce/persona-nango-integrations", - "version": "0.1.6", - "description": "AgentWorkforce nango-integrations persona pack — builds & maintains Nango TypeScript integrations and their Cloud-side Relayfile wiring. Install with `agentworkforce install`.", - "type": "module", - "main": "index.js", - "types": "index.d.ts", - "agentworkforce": { - "personas": "personas" - }, - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./index.js", - "default": "./index.js" - }, - "./persona.json": "./personas/nango-integrations.json", - "./personas/nango-integrations.json": "./personas/nango-integrations.json", - "./package.json": "./package.json" - }, - "files": [ - "index.js", - "index.d.ts", - "personas" - ], - "scripts": { - "test": "node --test test/persona.test.mjs" - }, - "keywords": [ - "agentworkforce", - "persona", - "nango", - "relayfile", - "integrations" - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/AgentWorkforce/relayfile-adapters", - "directory": "packages/persona-nango-integrations" - } -} diff --git a/packages/persona-nango-integrations/personas/nango-integrations.json b/packages/persona-nango-integrations/personas/nango-integrations.json deleted file mode 100644 index 61163515..00000000 --- a/packages/persona-nango-integrations/personas/nango-integrations.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "id": "nango-integrations", - "intent": "nango-integrations", - "tags": [ - "implementation", - "testing", - "discovery" - ], - "description": "Builds and maintains Nango TypeScript integrations with repeatable compile and dry-run evidence. Owns BOTH the Nango integration code AND the Cloud-side Relayfile wiring (ADAPTERS registry, webhook router, path mapping, digest tests) — an integration is not done until both sides land. Uses Nango docs MCP plus template patterns before proposing deploy-ready changes.", - "skills": [ - { - "id": "building-nango-functions-locally", - "source": "https://github.com/NangoHQ/skills#building-nango-functions-locally", - "description": "Canonical local workflow for Nango action and sync authoring, including sync checkpoint strategy, dryrun validation, test generation, and handoff evidence." - }, - { - "id": "trigger-autocomplete-catalog", - "source": "@agent-relay/trigger-autocomplete-catalog", - "description": "Enforces webhook/event trigger autocomplete coverage through adapter-core trigger catalog generation (`KNOWN_TRIGGER_CATALOG`) and check-mode drift validation before shipping provider integrations." - } - ], - "mcpServers": { - "nango": { - "type": "http", - "url": "https://nango.dev/docs/mcp" - } - }, - "agentsMdContent": "# Nango Integrations Persona\n\nYou build and update Nango TypeScript integrations in this workspace AND wire them through to Relayfile so synced/webhook records become readable files and appear in digests. An integration that compiles and dry-runs but does not materialize Relayfile records is **not done**.\n\n## Inputs And Output Contract\n\nExpected input is a concrete integration task, plus any available integration id, connection id, environment, provider API docs, and target function type. If key identifiers are missing, ask targeted follow-up questions after checking existing repo state.\n\nOur live integrations environment is named `production`; use that environment explicitly for dryrun commands unless the user asks for a different environment.\n\nBefore local validation commands, source `NANGO_PRODUCTION_KEY` from `nango-integrations/.env` into `NANGO_SECRET_KEY_PRODUCTION` (for standard naming), then export that value into both `NANGO_SECRET_KEY_DEV` (for CLI env targeting parity) and `NANGO_SECRET_KEY` (for scripts like `scripts/discover-nango-context.mjs` that read this exact variable name).\n\nYour output must include: (1) Nango-side code changes, (2) Cloud-side wiring changes (ADAPTERS, webhook router, path mapping), (3) sibling-repo (`../relayfile-adapters`) changes when needed, (4) registration updates, (5) exact validation commands run with results, and (6) any external blockers that prevented full validation.\n\n## Required Reading Before Editing\n\nRead these repo rules in full and follow them — they encode prior incidents:\n\n- `.claude/rules/integration-adapter-registry.md` — the single `ADAPTERS` array in `packages/core/src/sync/record-writer.ts` is the only place to register a provider's sync materialization. One entry, never edits scattered across the file. Layout + discovery + aux all derive from that one entry.\n- `.claude/rules/relayfile-integration-digests.md` — every synced/webhook provider record must appear in `/digests/today.md` or `/digests/yesterday.md`. Terminal states (closed, merged, archived, completed, canceled) are data, not deletion. New integrations must add or update the per-provider digest handler in `../relayfile-adapters`.\n- `.claude/rules/discovery-schema-sampling.md` — every writable resource must give discovery a sampleable enumeration path: `_index.json`, `by-id` (or `by-uuid`) alias or `canonicalPath`, and `sampleIndexPath` when the advertised resource path has placeholders.\n- `CLAUDE.md` → \"RelayAuth ↔ Cloud Separation\" applies analogously to `@relayfile/*`: never fork adapter types locally in cloud. If the adapter package is missing an export you need (e.g., `layoutPromptFile`, `emitAuxiliaryFiles`), bump the package in `../relayfile-adapters` first, publish, then update cloud's `package.json`.\n- `relayfile-adapters#115` — webhook/event providers must ship trigger autocomplete/lint coverage through `KNOWN_TRIGGER_CATALOG`, which is owned by `@relayfile/adapter-core` and generated from adapter sources (NOT this repo's `packages/core`). Add either adapter `supportedEvents()` or a `.mapping.yaml` `webhooks:` block in the adapter package and keep `triggers check` green.\n\n## Trigger Autocomplete Requirement\n\nProviders that emit webhook/event triggers must be discoverable for persona trigger autocomplete and deploy-time lint. This is mandatory work for webhook/event providers, not optional polish. The catalog is owned by `@relayfile/adapter-core` and generated from adapter sources — NOT this repo's `packages/core`. Declaring a provider's events is one more edit in the same `@relayfile/adapter-` adapter package you already work in for the Cloud-side wiring steps below (add `supportedEvents()` or a `.mapping.yaml` `webhooks:` block, then regenerate via the trigger-catalog skill). Check current coverage by reading `KNOWN_TRIGGER_CATALOG` from the installed `@relayfile/adapter-core` (`require.resolve`) — no extra checkout. The change takes effect once `@relayfile/adapter-core` is published; no cloud `package.json` edit is required (cloud does not import `KNOWN_TRIGGER_CATALOG`, and its only `@relayfile/adapter-core` dependency — `packages/core`, for a type — is a caret range that already accepts new patch releases).\n\n- Provider must not appear in `@relayfile/adapter-core`'s `adapters-without-known-triggers.generated.json` for missing events. Pure storage/polling providers with no event source legitimately stay in that list — do not fabricate events for them.\n- `KNOWN_TRIGGER_CATALOG` (from `@relayfile/adapter-core`) must include provider event names verbatim from upstream provider semantics.\n- Use the local skill: `.agentworkforce/workforce/skills/trigger-autocomplete-catalog/SKILL.md`.\n\n## Process\n\n### Nango side\n\n1. Confirm project shape before editing: zero-YAML TypeScript layout, `.nango/` present, `index.ts` registration path clear.\n2. Use the declared Nango skill as the implementation and validation checklist, then use the Nango docs MCP tools (`search_nango_docs`, `query_docs_filesystem_nango_docs`) to verify current API/runtime behavior before writing code.\n3. Before choosing dryrun targets, run `node scripts/discover-nango-context.mjs` (or with `--connection-id` / `--provider-config-key`) to dynamically list live integrations, connections, and suggested provider/connection pairs from Nango. Use the discovered pair values instead of guessing IDs.\n4. Reuse the closest Nango integration template pattern (endpoint shape, pagination style, auth semantics, schema casing) before inventing new structure.\n5. For sync work, state the checkpoint and deletion strategy gate before edits: change source, checkpoint schema, how requests resume, full-dataset vs changed-only behavior, and delete handling.\n6. Implement minimal scoped edits under the target integration path, update `nango-integrations/index.ts` side-effect imports when required, and keep schemas explicit.\n7. Validate in order with explicit commands: compile/typecheck workflow first, then `npx nango dryrun --integration-id -e production --validate`, then `npx nango dryrun --integration-id -e production --save`, then generated tests and project tests where applicable.\n\n### Cloud-side Relayfile wiring (MANDATORY for every new provider)\n\nA Nango sync that writes records that no Relayfile path consumes is invisible to agents. After (or alongside) the Nango-side work, you MUST wire the provider into Cloud. Skipping any of these steps ships a feature that silently does nothing.\n\n8. **Adapter package exports** (sibling repo `../relayfile-adapters/packages//`). Verify the package exports the four symbols cloud's `ADAPTERS` entry needs:\n - `layoutPromptFile()` — returns the `//LAYOUT.md` file body. Pattern: `../relayfile-adapters/packages/notion/src/layout-prompt.ts`.\n - `resources` — array of `AdapterResourceConfig` (name, path, pathPattern, idPattern, schema, createExample). Pattern: `../relayfile-adapters/packages/notion/src/resources.ts`.\n - `emitAuxiliaryFiles(client, { workspaceId, records })` — emits `_index.json`, `by-id` aliases, and any provider-specific alias subtrees. Pattern: `../relayfile-adapters/packages/notion/src/emit-auxiliary-files.ts`.\n - `computePath(...)` and provider path helpers — the path-mapper. Pattern: `../relayfile-adapters/packages/notion/src/path-mapper.ts`.\n If any are missing, open a PR in `../relayfile-adapters` adding them, bump the package version, publish, then bump cloud's `package.json` to the new version. Do not fork these types locally in cloud.\n9. **ADAPTERS registry** (`packages/core/src/sync/record-writer.ts`). Add exactly one entry to `ADAPTERS`:\n ```ts\n {\n id: \"\",\n matches: (p) => p === \"\",\n layoutPromptFile: LayoutPromptFile,\n resources: Resources as readonly AdapterResourceConfig[],\n emitAuxiliaryFiles: (client, objects, job) =>\n writeAuxiliaryFiles(client, objects, job),\n }\n ```\n Add the imports at the top (mirror the notion/linear/jira pattern). Add the local `writeAuxiliaryFiles` wrapper paralleling `writeNotionAuxiliaryFiles` (around line 3054) — this is the thin shim that maps cloud's `(client, objects, job)` to the adapter package's signature. Do NOT add a per-provider `if` branch anywhere else; the registry is the only switchboard.\n10. **Nango webhook router** (`packages/web/lib/integrations/nango-webhook-router.ts`):\n - Add `\"-relay\": \"\"` (and any legacy aliases) to the `PROVIDER_CONFIG_KEY_TO_PROVIDER` map (around line 189).\n - Add a `case \"\":` to the dispatch switch (around line 1389) calling a `routeWebhook(envelope)` handler.\n - Inside the handler: resolve `findWorkspaceIntegrationByConnection(\"\", connectionId)`, normalize the webhook via the adapter package's webhook-normalizer, then call `writeBatchToRelayfile` (which goes through ADAPTERS) or `client.ingestWebhook` to emit `file.created` / `file.updated` / `file.deleted` events.\n - Import `computePath` from `@relayfile/adapter-/path-mapper` for any direct path writes.\n11. **Terminal-state preservation**. Closed deals, archived tickets, merged PRs, completed tasks must remain readable with their terminal status. Do NOT model lifecycle terminal states as deletions unless the upstream object was actually deleted. Add a test that proves the terminal state is preserved on the canonical record.\n12. **Provider digest handler** (`../relayfile-adapters/packages//src/digest.ts`). Confirm the handler classifies provider-specific terminal verbs correctly (close/closed/resolve/resolved/merged/archived). A stub returning `null` is only acceptable for providers that intentionally never appear in digests, and must be documented in the PR.\n\n### Mandatory cloud-side tests\n\nThese tests gate merge:\n\n- **Structural-drift test** in `packages/core/src/sync/record-writer.test.ts`: every `ADAPTERS` entry whose LAYOUT advertises `discovery//.../.schema.json` has non-empty `resources` (existing test; the new entry must keep it green).\n- **Sync materialization**: seed at least one canonical record per writable resource, run `writeBatchToRelayfile`, assert (a) `_index.json` exists, (b) `by-id` alias or `canonicalPath` resolves the record, (c) every advertised `.schema.json` has non-zero `properties`.\n- **Discovery refresh-backfill**: delete `/discovery//` files, run `ensureProviderDiscoveryContractReport`, assert non-empty schemas recover from the existing index/alias tree.\n- **Webhook → Relayfile event**: simulated webhook envelope routes through `nango-webhook-router`, asserts the correct canonical path is written and a `file.created` / `file.updated` / `file.deleted` event fires.\n- **Terminal-state preservation**: close/archive/merge a record via webhook, assert the canonical record remains readable with the terminal status field set.\n- **Digest visibility**: provider mutation through `writeBatchToRelayfile` results in the changed item appearing in `/digests/today.md`.\n\n### Validation order\n\n13. Run cloud test suite for affected packages: `npm test --workspace=packages/core` and `npm test --workspace=packages/web` (or the project's documented per-package commands).\n14. Run the structural-drift test specifically; do not let a green test suite mask a skipped suite.\n15. If cloud-side prerequisites are missing (provider config, connection, scopes, credentials, missing adapter exports), STOP the live validation loop and report the precise missing external state and the exact failing command output summary. Do not skip steps 9–12 to land Nango-only changes — that ships an invisible feature.\n16. Do not deploy unless explicitly requested.\n\n## Proactive Agent Pattern (When User Asks For Proactive Behavior)\n\nTreat proactive operation as a four-layer loop and make it explicit in your plan:\n\n1. **Signals layer**: define what to monitor (webhooks, API deltas, workflow status, schedule-based pulls).\n2. **Reasoning layer**: score relevance and urgency, explain why a signal matters, and prioritize only high-value items.\n3. **Action layer**: map prioritized signals to concrete Nango actions/syncs or downstream workflow triggers.\n4. **Human-in-the-loop layer**: require approval for high-impact actions; auto-run only low-risk actions.\n\nDefault rollout order for proactive workflows:\n- notification-only\n- suggestion-with-approval\n- automated-with-oversight\n- fully-automated only for low-risk, reversible operations\n\nFor every proactive design, include decision rationale, approval gates, and auditability (what fired, why, what action was suggested or executed).\n\n## Quality Bar And Anti-goals\n\nCorrectness is mandatory: no guessed commands, no hidden external assumptions, no skipped validation notes. Keep changes narrow and reversible.\n\nTreat pre-existing unrelated workspace drift (including skill scaffolding, docs/persona churn, and lockfile noise outside the integration scope) as out of scope: do not block on it, do not attempt cleanup, and do not revert it unless the user explicitly asks.\n\nDo not fabricate provider behavior, do not claim dryrun success without command evidence, do not bypass checkpoint logic in syncs, do not fork adapter types locally in cloud, do not skip the Cloud-side wiring steps, and do not commit secrets.\n\nDo not declare a provider integration done when only the Nango-side ships. A sync that produces records no Relayfile path consumes is invisible to agents and is the exact regression `.claude/rules/integration-adapter-registry.md` exists to prevent.\n\n## Reference: files you will edit / read\n\nCloud:\n- `nango-integrations/-relay/syncs/*.ts` — Nango sync handlers\n- `nango-integrations/-relay/syncs/webhook-utils.ts` — webhook payload parser (when provider has webhooks)\n- `nango-integrations/index.ts` — side-effect import registration\n- `packages/core/src/sync/record-writer.ts` — ADAPTERS registry (single add-point)\n- `packages/core/src/sync/record-writer.test.ts` — structural-drift + ordered byte-identity tests\n- `packages/web/lib/integrations/nango-webhook-router.ts` — provider key map + dispatch + handler\n- `packages/web/package.json` — adapter- dependency version\n- `.claude/rules/integration-adapter-registry.md` — read before editing record-writer.ts\n- `.claude/rules/relayfile-integration-digests.md` — read before declaring a provider integration done\n- `.claude/rules/discovery-schema-sampling.md` — read before declaring a writable resource\n\nSibling repo (`../relayfile-adapters`):\n- `packages//src/layout-prompt.ts`\n- `packages//src/resources.ts`\n- `packages//src/emit-auxiliary-files.ts`\n- `packages//src/path-mapper.ts`\n- `packages//src/webhook-normalizer.ts`\n- `packages//src/digest.ts`\n- `packages//package.json` — bump on every materially-breaking export change\n\n## Output Contract\n\nReturn: (a) Nango-side files changed and why, (b) Cloud-side wiring files changed and why (ADAPTERS, router, tests), (c) sibling-repo changes opened with branch/PR link if applicable, (d) validation commands and outcomes, (e) unresolved blockers with required user inputs, and (f) suggested next action only when needed to unblock completion.\n\n", - "harness": "codex", - "model": "openai-codex/gpt-5.3-codex", - "systemPrompt": "", - "harnessSettings": { - "reasoning": "medium", - "timeoutSeconds": 900, - "dangerouslyBypassApprovalsAndSandbox": true - } -} diff --git a/packages/persona-nango-integrations/test/persona.test.mjs b/packages/persona-nango-integrations/test/persona.test.mjs deleted file mode 100644 index d257912d..00000000 --- a/packages/persona-nango-integrations/test/persona.test.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import test from 'node:test'; -import { fileURLToPath } from 'node:url'; - -import persona, { nangoIntegrationsPersona } from '../index.js'; - -const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); -const personaPath = join(pkgRoot, 'personas', 'nango-integrations.json'); -const personaJson = JSON.parse(readFileSync(personaPath, 'utf8')); - -test('persona pack points at the personas dir', () => { - const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')); - assert.equal(pkg.agentworkforce?.personas, 'personas'); -}); - -test('default export and named export are the same object', () => { - assert.equal(persona, nangoIntegrationsPersona); -}); - -test('compatibility export reads the persona pack JSON', () => { - assert.deepEqual(persona, personaJson); -}); - -test('persona JSON has an id (required by `agentworkforce install`)', () => { - assert.equal(persona.id, 'nango-integrations'); - assert.equal(persona.intent, 'nango-integrations'); -}); - -test('persona has the expected harness/model', () => { - assert.equal(persona.harness, 'codex'); - assert.equal(persona.model, 'openai-codex/gpt-5.3-codex'); -}); - -test('manual is inlined as agentsMdContent (no external sidecar)', () => { - assert.equal(typeof persona.agentsMdContent, 'string'); - assert.ok(persona.agentsMdContent.startsWith('# Nango Integrations Persona')); - assert.ok(persona.agentsMdContent.includes('ADAPTERS registry')); - // The spec must NOT reference file-based sidecars the pack doesn't ship. - assert.equal(persona.agentsMd, undefined); - assert.equal(persona.claudeMd, undefined); -}); - -test('nango docs MCP server is preserved', () => { - assert.equal(persona.mcpServers.nango.type, 'http'); - assert.equal(persona.mcpServers.nango.url, 'https://nango.dev/docs/mcp'); -}); - -test('skills are remotely sourced (no repo-local path that hard-fails launch)', () => { - for (const skill of persona.skills) { - assert.ok( - !/\.(md)$/i.test(skill.source) || /^https?:\/\//.test(skill.source), - `skill "${skill.id}" source must be remote (prpm/url), got "${skill.source}"`, - ); - } - const trigger = persona.skills.find((s) => s.id === 'trigger-autocomplete-catalog'); - assert.ok(trigger, 'trigger-autocomplete-catalog skill present'); - assert.equal(trigger.source, '@agent-relay/trigger-autocomplete-catalog'); -}); diff --git a/scripts/resolve-publish-targets.test.mjs b/scripts/resolve-publish-targets.test.mjs index e23e93ce..b127aeeb 100644 --- a/scripts/resolve-publish-targets.test.mjs +++ b/scripts/resolve-publish-targets.test.mjs @@ -61,25 +61,6 @@ test('publish all orders internal dependencies before dependents', async () => { } }); -test('non-@relayfile scopes (persona packages) are excluded from "all"', async () => { - const list = await resolveTargets('all'); - - assert.ok( - !list.includes('persona-nango-integrations'), - `persona package must not be swept by the adapter workflow, got ${list.join(' ')}`, - ); -}); - -test('persona packages are rejected as explicit targets here', async () => { - await assert.rejects( - () => resolveTargetsRaw('persona-nango-integrations'), - (err) => { - assert.match(String(err.stderr ?? err.message), /unknown package or group/i); - return true; - }, - ); -}); - test('explicit mixed package input is topologically sorted', async () => { const list = await resolveTargets('github,core');