From c222290c29d86d2d36652dc544e40577f91aa5ed Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Wed, 6 May 2026 15:05:05 +0200 Subject: [PATCH] fix(router): forward project credentials to sentry-bound workers (#1259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `extractProjectIdFromJob` had no `sentry` branch, so sentry jobs hit the `return null` fall-through and `buildWorkerEnvWithProjectId` skipped credential loading entirely (the `if (projectId)` gate). Worker spawned without `CASCADE_CREDENTIAL_KEYS`, the in-worker resolver auto-selector fell back to `DbCredentialResolver`, hit an encrypted row, and crashed with "CREDENTIAL_MASTER_KEY is not set" — workers intentionally don't have the master key. This was the first sentry-bound agent run in prod (cascade project, 2026-05-06 12:48 UTC). The router pipeline succeeded end-to-end; only the worker boot failed. Add the `sentry` branch (sentry jobs carry `projectId` directly per `SentryJob.projectId`) and pin the regression in the worker-env unit test. Co-authored-by: Claude Opus 4.7 (1M context) --- src/router/worker-env.ts | 3 +++ tests/unit/router/worker-env.test.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/router/worker-env.ts b/src/router/worker-env.ts index 134699d0..f611583e 100644 --- a/src/router/worker-env.ts +++ b/src/router/worker-env.ts @@ -42,6 +42,9 @@ export async function extractProjectIdFromJob(data: CascadeJob): Promise { expect(await extractProjectIdFromJob(job)).toBe('proj-jira'); }); + it('returns projectId for sentry jobs', async () => { + // Regression: prior to this branch, sentry jobs hit the `return null` + // fall-through, so the worker-env builder skipped credential loading + // entirely. The first real sentry-bound agent run in prod (cascade + // project, 2026-05-06) crashed on boot with "CREDENTIAL_MASTER_KEY is + // not set" because no CASCADE_CREDENTIAL_KEYS reached the worker. + const job = { + type: 'sentry', + source: 'sentry', + projectId: 'proj-sentry', + eventType: 'event_alert', + payload: {}, + receivedAt: '2026-05-06T12:48:09Z', + } as unknown as CascadeJob; + expect(await extractProjectIdFromJob(job)).toBe('proj-sentry'); + }); + it('returns projectId resolved from repo for github jobs', async () => { const job = { type: 'github', repoFullName: 'owner/repo' } as CascadeJob; mockFindProjectByRepo.mockResolvedValue({ id: 'proj-gh' } as never);