Skip to content

Commit cb8cfc0

Browse files
khaliqgantProactive Runtime Bot
andauthored
feat(cloud): pass env vars to scheduled workflows (#935)
Co-authored-by: Proactive Runtime Bot <agent@agent-relay.com>
1 parent 7408bb6 commit cb8cfc0

5 files changed

Lines changed: 69 additions & 1 deletion

File tree

packages/cloud/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export type ScheduleWorkflowOptions = {
183183
cron?: string;
184184
at?: string;
185185
timezone?: string;
186+
envSecrets?: Record<string, string>;
186187
};
187188

188189
export type WorkflowLogsResponse = {

packages/cloud/src/workflows.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,10 @@ describe('workflow schedules', () => {
495495
const result = await scheduleWorkflow(workflowPath, {
496496
cron: '0 * * * *',
497497
name: 'Hourly eval',
498+
envSecrets: {
499+
AI_CLI_UPDATES_DRY_RUN: 'true',
500+
AI_CLI_UPDATES_ONLY: 'codex',
501+
},
498502
});
499503

500504
expect(result.id).toBe('sched-1');
@@ -505,6 +509,10 @@ describe('workflow schedules', () => {
505509
timezone: 'UTC',
506510
workflowRequest: {
507511
fileType: 'yaml',
512+
envSecrets: {
513+
AI_CLI_UPDATES_DRY_RUN: 'true',
514+
AI_CLI_UPDATES_ONLY: 'codex',
515+
},
508516
},
509517
});
510518
expect(

packages/cloud/src/workflows.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,9 @@ export async function scheduleWorkflow(
685685
workflow: input.workflow,
686686
fileType: input.fileType,
687687
...(input.sourceFileType ? { sourceFileType: input.sourceFileType } : {}),
688+
...(options.envSecrets && Object.keys(options.envSecrets).length > 0
689+
? { envSecrets: options.envSecrets }
690+
: {}),
688691
},
689692
};
690693
if (options.description?.trim()) {

src/cli/commands/cloud.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,46 @@ describe('registerCloudCommands', () => {
137137
'0 * * * *',
138138
'--name',
139139
'Hourly eval',
140+
'--env',
141+
'AI_CLI_UPDATES_DRY_RUN=true',
142+
'--env',
143+
'AI_CLI_UPDATES_ONLY=codex',
140144
]);
141145

142146
expect(cloudMocks.scheduleWorkflow).toHaveBeenCalledWith(
143147
'workflow.yaml',
144148
expect.objectContaining({
145149
cron: '0 * * * *',
146150
name: 'Hourly eval',
151+
envSecrets: {
152+
AI_CLI_UPDATES_DRY_RUN: 'true',
153+
AI_CLI_UPDATES_ONLY: 'codex',
154+
},
147155
})
148156
);
149157
expect(deps.log).toHaveBeenCalledWith('Schedule created: sched-1');
150158
});
151159

160+
it('schedule rejects malformed environment assignments', async () => {
161+
const { program } = createHarness();
162+
163+
await expect(
164+
program.parseAsync([
165+
'node',
166+
'agent-relay',
167+
'cloud',
168+
'schedule',
169+
'workflow.yaml',
170+
'--cron',
171+
'0 * * * *',
172+
'--env',
173+
'not-an-assignment',
174+
])
175+
).rejects.toThrow();
176+
177+
expect(cloudMocks.scheduleWorkflow).not.toHaveBeenCalled();
178+
});
179+
152180
it('schedule creates one-time workflow schedules', async () => {
153181
const { program, deps } = createHarness();
154182
cloudMocks.scheduleWorkflow.mockResolvedValueOnce({

src/cli/commands/cloud.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ function parseWorkflowFileType(value: string): WorkflowFileType {
8787
throw new InvalidArgumentError('Expected workflow type to be one of: yaml, ts, py');
8888
}
8989

90+
function parseEnvAssignment(value: string, previous: Record<string, string> = {}): Record<string, string> {
91+
const equalsIndex = value.indexOf('=');
92+
if (equalsIndex <= 0) {
93+
throw new InvalidArgumentError('Expected environment assignment in KEY=VALUE form.');
94+
}
95+
96+
const key = value.slice(0, equalsIndex).trim();
97+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
98+
throw new InvalidArgumentError(`Invalid environment variable name: ${key || '(empty)'}`);
99+
}
100+
101+
return {
102+
...previous,
103+
[key]: value.slice(equalsIndex + 1),
104+
};
105+
}
106+
90107
function isObject(value: unknown): value is Record<string, unknown> {
91108
return value !== null && typeof value === 'object' && !Array.isArray(value);
92109
}
@@ -421,6 +438,12 @@ export function registerCloudCommands(program: Command, overrides: Partial<Cloud
421438
.option('--timezone <timezone>', 'IANA timezone for cron schedules', 'UTC')
422439
.option('--name <name>', 'Schedule name')
423440
.option('--description <description>', 'Schedule description')
441+
.option(
442+
'--env <KEY=VALUE>',
443+
'Environment variable to pass to scheduled runs; repeat for multiple variables',
444+
parseEnvAssignment,
445+
{}
446+
)
424447
.option('--json', 'Print raw JSON response', false)
425448
.action(
426449
async (
@@ -433,14 +456,19 @@ export function registerCloudCommands(program: Command, overrides: Partial<Cloud
433456
timezone?: string;
434457
name?: string;
435458
description?: string;
459+
env?: Record<string, string>;
436460
json?: boolean;
437461
}
438462
) => {
439463
const started = Date.now();
440464
let success = false;
441465
let errorClass: string | undefined;
442466
try {
443-
const result = await scheduleWorkflow(workflow, options);
467+
const { env, ...scheduleOptions } = options;
468+
const result = await scheduleWorkflow(workflow, {
469+
...scheduleOptions,
470+
...(env && Object.keys(env).length > 0 ? { envSecrets: env } : {}),
471+
});
444472
if (options.json) {
445473
deps.log(JSON.stringify(result, null, 2));
446474
} else {

0 commit comments

Comments
 (0)