diff --git a/packages/cloud/src/types.ts b/packages/cloud/src/types.ts index 04f0fbcfe..b64c3e99c 100644 --- a/packages/cloud/src/types.ts +++ b/packages/cloud/src/types.ts @@ -183,6 +183,7 @@ export type ScheduleWorkflowOptions = { cron?: string; at?: string; timezone?: string; + envSecrets?: Record; }; export type WorkflowLogsResponse = { diff --git a/packages/cloud/src/workflows.test.ts b/packages/cloud/src/workflows.test.ts index 370f40435..215545457 100644 --- a/packages/cloud/src/workflows.test.ts +++ b/packages/cloud/src/workflows.test.ts @@ -495,6 +495,10 @@ describe('workflow schedules', () => { const result = await scheduleWorkflow(workflowPath, { cron: '0 * * * *', name: 'Hourly eval', + envSecrets: { + AI_CLI_UPDATES_DRY_RUN: 'true', + AI_CLI_UPDATES_ONLY: 'codex', + }, }); expect(result.id).toBe('sched-1'); @@ -505,6 +509,10 @@ describe('workflow schedules', () => { timezone: 'UTC', workflowRequest: { fileType: 'yaml', + envSecrets: { + AI_CLI_UPDATES_DRY_RUN: 'true', + AI_CLI_UPDATES_ONLY: 'codex', + }, }, }); expect( diff --git a/packages/cloud/src/workflows.ts b/packages/cloud/src/workflows.ts index f079615d1..c924f0aa6 100644 --- a/packages/cloud/src/workflows.ts +++ b/packages/cloud/src/workflows.ts @@ -685,6 +685,9 @@ export async function scheduleWorkflow( workflow: input.workflow, fileType: input.fileType, ...(input.sourceFileType ? { sourceFileType: input.sourceFileType } : {}), + ...(options.envSecrets && Object.keys(options.envSecrets).length > 0 + ? { envSecrets: options.envSecrets } + : {}), }, }; if (options.description?.trim()) { diff --git a/src/cli/commands/cloud.test.ts b/src/cli/commands/cloud.test.ts index 9df584b5f..d67883fe0 100644 --- a/src/cli/commands/cloud.test.ts +++ b/src/cli/commands/cloud.test.ts @@ -137,6 +137,10 @@ describe('registerCloudCommands', () => { '0 * * * *', '--name', 'Hourly eval', + '--env', + 'AI_CLI_UPDATES_DRY_RUN=true', + '--env', + 'AI_CLI_UPDATES_ONLY=codex', ]); expect(cloudMocks.scheduleWorkflow).toHaveBeenCalledWith( @@ -144,11 +148,35 @@ describe('registerCloudCommands', () => { expect.objectContaining({ cron: '0 * * * *', name: 'Hourly eval', + envSecrets: { + AI_CLI_UPDATES_DRY_RUN: 'true', + AI_CLI_UPDATES_ONLY: 'codex', + }, }) ); expect(deps.log).toHaveBeenCalledWith('Schedule created: sched-1'); }); + it('schedule rejects malformed environment assignments', async () => { + const { program } = createHarness(); + + await expect( + program.parseAsync([ + 'node', + 'agent-relay', + 'cloud', + 'schedule', + 'workflow.yaml', + '--cron', + '0 * * * *', + '--env', + 'not-an-assignment', + ]) + ).rejects.toThrow(); + + expect(cloudMocks.scheduleWorkflow).not.toHaveBeenCalled(); + }); + it('schedule creates one-time workflow schedules', async () => { const { program, deps } = createHarness(); cloudMocks.scheduleWorkflow.mockResolvedValueOnce({ diff --git a/src/cli/commands/cloud.ts b/src/cli/commands/cloud.ts index 87482cc8c..27e7c7533 100644 --- a/src/cli/commands/cloud.ts +++ b/src/cli/commands/cloud.ts @@ -87,6 +87,23 @@ function parseWorkflowFileType(value: string): WorkflowFileType { throw new InvalidArgumentError('Expected workflow type to be one of: yaml, ts, py'); } +function parseEnvAssignment(value: string, previous: Record = {}): Record { + const equalsIndex = value.indexOf('='); + if (equalsIndex <= 0) { + throw new InvalidArgumentError('Expected environment assignment in KEY=VALUE form.'); + } + + const key = value.slice(0, equalsIndex).trim(); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { + throw new InvalidArgumentError(`Invalid environment variable name: ${key || '(empty)'}`); + } + + return { + ...previous, + [key]: value.slice(equalsIndex + 1), + }; +} + function isObject(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value); } @@ -421,6 +438,12 @@ export function registerCloudCommands(program: Command, overrides: Partial', 'IANA timezone for cron schedules', 'UTC') .option('--name ', 'Schedule name') .option('--description ', 'Schedule description') + .option( + '--env ', + 'Environment variable to pass to scheduled runs; repeat for multiple variables', + parseEnvAssignment, + {} + ) .option('--json', 'Print raw JSON response', false) .action( async ( @@ -433,6 +456,7 @@ export function registerCloudCommands(program: Command, overrides: Partial; json?: boolean; } ) => { @@ -440,7 +464,11 @@ export function registerCloudCommands(program: Command, overrides: Partial 0 ? { envSecrets: env } : {}), + }); if (options.json) { deps.log(JSON.stringify(result, null, 2)); } else {