Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cloud/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export type ScheduleWorkflowOptions = {
cron?: string;
at?: string;
timezone?: string;
envSecrets?: Record<string, string>;
};

export type WorkflowLogsResponse = {
Expand Down
8 changes: 8 additions & 0 deletions packages/cloud/src/workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions packages/cloud/src/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
28 changes: 28 additions & 0 deletions src/cli/commands/cloud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,46 @@ describe('registerCloudCommands', () => {
'0 * * * *',
'--name',
'Hourly eval',
'--env',
'AI_CLI_UPDATES_DRY_RUN=true',
'--env',
'AI_CLI_UPDATES_ONLY=codex',
]);

expect(cloudMocks.scheduleWorkflow).toHaveBeenCalledWith(
'workflow.yaml',
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({
Expand Down
30 changes: 29 additions & 1 deletion src/cli/commands/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {}): Record<string, string> {
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<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
Expand Down Expand Up @@ -421,6 +438,12 @@ export function registerCloudCommands(program: Command, overrides: Partial<Cloud
.option('--timezone <timezone>', 'IANA timezone for cron schedules', 'UTC')
.option('--name <name>', 'Schedule name')
.option('--description <description>', 'Schedule description')
.option(
'--env <KEY=VALUE>',
'Environment variable to pass to scheduled runs; repeat for multiple variables',
parseEnvAssignment,
{}
)
.option('--json', 'Print raw JSON response', false)
.action(
async (
Expand All @@ -433,14 +456,19 @@ export function registerCloudCommands(program: Command, overrides: Partial<Cloud
timezone?: string;
name?: string;
description?: string;
env?: Record<string, string>;
json?: boolean;
}
) => {
const started = Date.now();
let success = false;
let errorClass: string | undefined;
try {
const result = await scheduleWorkflow(workflow, options);
const { env, ...scheduleOptions } = options;
const result = await scheduleWorkflow(workflow, {
...scheduleOptions,
...(env && Object.keys(env).length > 0 ? { envSecrets: env } : {}),
});
if (options.json) {
deps.log(JSON.stringify(result, null, 2));
} else {
Expand Down
Loading