Skip to content

feat(cloud): add Trigger.dev to polylane cloud connect - #101

Merged
justinhelmer merged 1 commit into
mainfrom
feat/triggerdev-cloud-connect
Sep 17, 2026
Merged

justinhelmer merged 1 commit into
mainfrom
feat/triggerdev-cloud-connect

Conversation

@justinhelmer

Copy link
Copy Markdown
Contributor

Adds Trigger.dev to polylane cloud connect, so a Trigger.dev environment API key can be connected from the terminal the same way the other providers are. The API side lands in coreplanelabs/nominal from the Trigger.dev cloud provider design record; this PR speaks the request contract that record fixes.

What & why

Trigger.dev becomes a Polylane cloud provider (nominal #3242 added the enum, the skeleton PR adds the connect route). The CLI wizard and its headless flags need to know the provider so onboarding can connect it without the console. One Trigger.dev key is one environment; the wizard tells the user to mint a production key with the No restrictions access preset (the only kind on Free and Hobby plans) and explains that re-running the command with another key adds it to the same account. The connect body is { workspaceId, provider: "triggerdev", apiKey, projectRef? }; the project ref is only asked for when the API says it is required (a 400 whose message names the project ref), once, then the call is retried.

The code was written by the Switchboard coding run in this thread and delivered as a patch because the run's credential is scoped to nominal; the patch was applied unchanged with git am.

Tour

1. The provider joins the union and the picker

Trigger.dev is a selectable provider with the hint the wizard shows beside it.

| 'triggerdev'

{ value: 'triggerdev', label: 'Trigger.dev', hint: 'environment API key' },

2. The request contract and the project-ref retry

The generated client trails the deployed API spec, so the body type is declared here from the design record's contract and cast at the call site (drop the cast once codegen picks up the route). connectTriggerdev sends the body once; only a 400 that names the project ref, on a body that did not carry one, prompts for it (or, headless, fails with the --project-ref hint) and retries once. Every other error, including the "key cannot read runs" 400 that already carries the API's guidance, is rethrown untouched.

Look for: the guard order in the catch: non-API error, non-400, ref already sent, message without "project ref" all rethrow.

const TRIGGERDEV_PROJECT_REF_HINT =
'The project ref is the `project` line in trigger.config.ts and starts with proj_.';
const TRIGGERDEV_HEADLESS_HINT =
'Create an environment API key in your Trigger.dev project (production environment > API Keys, No restrictions access preset), then re-run:\n' +
'polylane cloud connect --provider triggerdev --api-key <key>\n' +
`Add --project-ref <proj_...> when the API asks for it. ${TRIGGERDEV_PROJECT_REF_HINT}`;
// The generated client trails the deployed API spec; the triggerdev body
// shape is the contract from the API-side design record.
export type TriggerdevConnectBody = {
workspaceId: string;
provider: 'triggerdev';
apiKey: string;
projectRef?: string;
};
// The API resolves the project from the key alone when it can; it answers 400
// when it needs the project ref to disambiguate. Prompt for the ref only then,

export async function connectTriggerdev(
config: Config,
api: PolylaneAPI,
body: TriggerdevConnectBody
): Promise<typeof BACK | ConnectResult> {
const send = (b: TriggerdevConnectBody): Promise<ConnectResult> =>
api.cloudAccountsConnect(b as unknown as ConnectBody);
try {
return await send(body);
} catch (err) {
if (
!isApiError(err) ||
err.status !== 400 ||
body.projectRef !== undefined ||
!/project ref/i.test(err.message)
) {
throw err;
}
if (!isInteractive(config.nonInteractive)) {
throw new CLIError(
err.message,
ExitCode.USAGE,
`Pass --project-ref <proj_...>.\n${TRIGGERDEV_PROJECT_REF_HINT}`
);
}
note(`${err.message}\n${TRIGGERDEV_PROJECT_REF_HINT}`, 'Trigger.dev project ref');
const picked = await promptTextOrBack(
{ nonInteractive: config.nonInteractive },
'Trigger.dev project ref (proj_...)',
{ validate: (v: string) => (v.trim() ? undefined : 'Required') }
);
if (picked === BACK) return BACK;
return send({ ...body, projectRef: picked.trim() });
}
}

3. The wizard branch

Same shape as the sibling secret steps: headless requires --api-key, the interactive step explains where the key comes from and offers to open cloud.trigger.dev, then the connect call above runs with the optional --project-ref.

if (provider === 'triggerdev') {
if (!isInteractive(config.nonInteractive) && getArgString(args, 'apiKey') === undefined) {
throw new CLIError('Missing required flag: --api-key', ExitCode.USAGE, TRIGGERDEV_HEADLESS_HINT);
}
let apiKey = '';
const ok = await runSteps([
secretStep(
config,
args,
'apiKey',
'--api-key',
{
message: 'Trigger.dev environment API key',
instructions:
'In your Trigger.dev project, open the production environment, then API Keys, and create a key with the No restrictions access preset. That preset is the only kind on the Free and Hobby plans; on Pro you may instead use restricted keys such as Observer plus Deploy only, adding them one at a time. Re-running this command with another key adds it to the same account.',
link: 'https://cloud.trigger.dev',
linkLabel: 'Open Trigger.dev',
},
(v) => {
apiKey = v;
}
),
]);
if (!ok) return BACK;
const projectRef = getArgString(args, 'projectRef');
const result = await connectTriggerdev(config, api, {
workspaceId,
provider: 'triggerdev',
apiKey,
...(projectRef !== undefined ? { projectRef } : {}),
});
if (result === BACK) return BACK;
printConnectSuccess(config, result);
return 'connected';
}

4. Flags, description and examples

--api-key is shared with Render; --project-ref is new; the command description and examples list the provider.

// Render
{ flag: '--api-key <key>', description: 'Render API key, or Trigger.dev environment API key', type: 'string' },
{ flag: '--project-ref <ref>', description: 'Trigger.dev: project ref (proj_...), only needed when the API asks for it', type: 'string' },
// ClickHouse

'polylane cloud connect --provider triggerdev --api-key <key>',
'polylane cloud connect --provider triggerdev --api-key <key> --project-ref proj_abc123',

5. Tests

Six cases mirroring the Turso suite: plain send, projectRef passthrough, the headless project-ref 400 becoming a usage error with the flag hint, the same 400 rethrown when a ref was already sent, the cannot-read-runs 400 rethrown untouched, and a non-400 rethrown untouched.

describe('connectTriggerdev', () => {
it('sends the body without projectRef and returns the result', async () => {
const seen: unknown[] = [];
const result = { provider: 'triggerdev', accounts: [], failures: [] };
const api = mockApi(async (b) => {
seen.push(b);
return result;
});
assert.equal(await connectTriggerdev(config, api, body), result);
assert.deepEqual(seen, [body]);
});
it('sends projectRef through when given', async () => {
const seen: unknown[] = [];
const withRef = { ...body, projectRef: 'proj_abc123' };
const api = mockApi(async (b) => {
seen.push(b);
return { provider: 'triggerdev', accounts: [], failures: [] };
});
await connectTriggerdev(config, api, withRef);
assert.deepEqual(seen, [withRef]);
});
it('turns the project-ref-required 400 into a usage error with a --project-ref hint when not interactive', async () => {
const api = mockApi(async () => {
throw new ApiError(400, REF_REQUIRED, ExitCode.USAGE);
});
await assert.rejects(
() => connectTriggerdev(config, api, body),
(err: unknown) =>
err instanceof CLIError &&
err.exitCode === ExitCode.USAGE &&

6. Remaining changes

  • None: the two files above are the whole diff. No provider docs table exists in this repo to update.

Decisions

  • Body typed locally, cast at the call site rather than waiting for codegen: the API route is landing in parallel and the CLI would otherwise block on it. The cast is the one thing to remove after the next codegen run.
  • Ask for the project ref only on demand. A No restrictions key identifies its project through whoami, so most users never see the prompt; restricted keys cannot call whoami, and the API answers with the message the CLI matches.
  • Second key = same command again. No separate "add key" subcommand; the API adds a key to an existing account for the same environment.

Validation

  • npm run typecheck: clean at e92109b (codegen ran first, no generated drift).
  • npm run lint: clean.
  • npm test: 524 tests, 524 pass, 0 fail (includes the six new cases in test/cloud-connect-triggerdev.test.ts).
  • Human-gated: interactive wizard run against UAT once the nominal connect route is deployed there (polylane cloud connect --provider triggerdev), receipt to be posted on this PR.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@coreplane-switchboard coreplane-switchboard Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM: Clean, well-tested addition mirroring the existing Turso provider pattern; no issues found.

Verdict: approve — no findings. PR #101 adds Trigger.dev to polylane cloud connect as a faithful mirror of the Turso provider flow (union + picker entry, on-demand project-ref retry with correct guard ordering, headless usage errors, flags/examples/description updated), with six focused tests covering the happy path and every rethrow guard. The one temporary wart — the as unknown as ConnectBody cast pending codegen — is documented in-code and in the PR body. Ready to merge (pending the human-gated UAT run the PR body promises).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: coreplane-switchboard[bot] reviewed this PR and posted an LGTM verdict (see its review). A repo admin enabled this via the auto-approve workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant