Skip to content

Commit 936aee3

Browse files
committed
Complete deployment API lifecycle support
Change-Id: Id64addfb4d90b3f879d37951c9099b7225644051
1 parent fb9e73b commit 936aee3

18 files changed

Lines changed: 510 additions & 10 deletions

File tree

docs/guides/manage-deployments.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,15 @@ A `file` with a local `source` is uploaded during `apply` (Files API) and the re
5858

5959
```bash
6060
agents deployment list # deployments tracked in state
61+
agents deployment list --remote --provider qoder --all
6162
agents deployment get <name> # status + resolved bindings
63+
agents deployment pause <name> # stop scheduled runs (native providers)
64+
agents deployment unpause <name> # resume scheduled runs (native providers)
6265
agents deployment run <name> # trigger a run
6366
```
6467

68+
Qoder deployments may also declare `environment_variables` as a semicolon- or newline-separated `KEY=VALUE` string. Qoder copies these variables into every Session created by that deployment; Claude does not support this extension.
69+
6570
## Native vs. emulated
6671

6772
| Provider | Deployment tier | What `deployment run` does |

docs/reference/cli.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ Manage scheduled / triggered deployments.
120120
| Subcommand | Description |
121121
|------------|-------------|
122122
| `deployment list` | List deployments tracked in state. |
123+
| `deployment list --remote --provider <provider>` | List deployments from a native provider API; supports status, agent, archive, limit, and pagination filters. |
123124
| `deployment get <name>` | Show a deployment's status and resolved bindings. |
125+
| `deployment pause <name>` | Pause scheduled runs for a native deployment. |
126+
| `deployment unpause <name>` | Resume a paused native deployment. |
124127
| `deployment run <name>` | Trigger a deployment run (native on Qoder/Claude, emulated as a session on Bailian/Volcengine Ark). |
125128

126129
## `agents memory-store`

packages/cli/src/commands/deployment.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,66 @@ import {
22
getDeploymentDetailsForContext,
33
getDeploymentRuntimeProviderForContext,
44
listDeploymentsForContext,
5+
listRemoteDeploymentsForContext,
6+
pauseDeploymentForContext,
57
runDeploymentForContext,
68
UserError,
79
} from "@openagentpack/sdk";
810
import chalk from "chalk";
911
import { buildCliRuntime } from "../config-loader.ts";
1012
import { log } from "../logger.ts";
1113
import { columnWidth, printTableFooter, printTableHeader, printTableRow, printTableTitle } from "../render-table.ts";
14+
import { fetchAllPages } from "../utils/pagination.ts";
1215

1316
interface DeploymentListOpts {
1417
file: string;
1518
provider?: string;
19+
remote?: boolean;
20+
status?: "active" | "paused";
21+
includeArchived?: boolean;
22+
agentId?: string;
23+
limit?: number;
24+
all?: boolean;
1625
}
1726

1827
export async function deploymentListCommand(options: DeploymentListOpts) {
1928
const ctx = await buildCliRuntime(options.file);
29+
if (options.remote) {
30+
if (!options.provider) throw new UserError("Remote deployment listing requires --provider.");
31+
if (options.provider === "claude" && options.status && options.includeArchived) {
32+
throw new UserError("Claude remote deployment listing cannot combine --status with --include-archived.");
33+
}
34+
const { items, hasMore } = await fetchAllPages(async (page) => {
35+
const result = await listRemoteDeploymentsForContext(ctx, options.provider!, {
36+
status: options.status,
37+
include_archived: options.includeArchived,
38+
agent_id: options.agentId,
39+
limit: options.limit,
40+
page,
41+
});
42+
return { items: result.deployments, hasMore: result.has_more, nextPage: result.next_page };
43+
}, options.all);
44+
if (items.length === 0) {
45+
log.info("No remote deployments found.");
46+
return;
47+
}
48+
printTableTitle("Remote Deployments", items.length);
49+
printTableHeader(["Name".padEnd(24), "ID".padEnd(28), "Status".padEnd(10), "Schedule"], 82);
50+
for (const item of items) {
51+
const raw = item.attributes ?? {};
52+
const name = String(raw.name ?? "")
53+
.slice(0, 22)
54+
.padEnd(24);
55+
const id = String(item.id ?? "")
56+
.slice(0, 26)
57+
.padEnd(28);
58+
const schedule = item.schedule?.expression ?? "manual";
59+
printTableRow([chalk.bold(name), id, item.status.padEnd(10), schedule]);
60+
}
61+
printTableFooter();
62+
if (hasMore) log.info("More deployments available. Use --all to fetch all.");
63+
return;
64+
}
2065
const rows = listDeploymentsForContext(ctx, options.provider);
2166

2267
if (rows.length === 0) {
@@ -42,6 +87,18 @@ export async function deploymentListCommand(options: DeploymentListOpts) {
4287
printTableFooter();
4388
}
4489

90+
interface DeploymentPauseOpts {
91+
file: string;
92+
provider?: string;
93+
}
94+
95+
export async function deploymentPauseCommand(name: string, options: DeploymentPauseOpts, paused = true) {
96+
const ctx = await buildCliRuntime(options.file);
97+
const info = await pauseDeploymentForContext(ctx, name, paused, options.provider);
98+
log.success(`Deployment '${name}' ${paused ? "paused" : "unpaused"}.`);
99+
console.log(` Status: ${info.status}`);
100+
}
101+
45102
interface DeploymentGetOpts {
46103
file: string;
47104
provider?: string;

packages/cli/src/program.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { dirname, resolve } from "node:path";
33
import { fileURLToPath } from "node:url";
44
import { Command, Option } from "commander";
55
import { applyCommand } from "./commands/apply.ts";
6-
import { deploymentGetCommand, deploymentListCommand, deploymentRunCommand } from "./commands/deployment.ts";
6+
import {
7+
deploymentGetCommand,
8+
deploymentListCommand,
9+
deploymentPauseCommand,
10+
deploymentRunCommand,
11+
} from "./commands/deployment.ts";
712
import { destroyCommand } from "./commands/destroy.ts";
813
import { initCommand } from "./commands/init.ts";
914
import {
@@ -292,6 +297,12 @@ deploymentCmd
292297
.description("List deployments tracked in state")
293298
.addOption(configFileOption())
294299
.addOption(providerOption("Filter by provider"))
300+
.option("--remote", "List deployments from the provider API")
301+
.addOption(new Option("--status <status>", "Filter remote deployments by status").choices(["active", "paused"]))
302+
.option("--include-archived", "Include archived remote deployments")
303+
.option("--agent-id <id>", "Filter remote deployments by agent ID")
304+
.option("--limit <count>", "Maximum remote deployments per page", parsePositiveInteger)
305+
.option("--all", "Fetch all remote pages")
295306
.action(withResolvedConfigFile(deploymentListCommand));
296307

297308
deploymentCmd
@@ -301,9 +312,23 @@ deploymentCmd
301312
.addOption(providerOption("Target provider"))
302313
.action(withResolvedConfigFile(deploymentGetCommand));
303314

315+
deploymentCmd
316+
.command("pause <name>")
317+
.description("Pause a native deployment's scheduled runs")
318+
.addOption(configFileOption())
319+
.addOption(providerOption("Target provider"))
320+
.action(withResolvedConfigFile((name, options) => deploymentPauseCommand(name, options, true)));
321+
322+
deploymentCmd
323+
.command("unpause <name>")
324+
.description("Resume a paused native deployment")
325+
.addOption(configFileOption())
326+
.addOption(providerOption("Target provider"))
327+
.action(withResolvedConfigFile((name, options) => deploymentPauseCommand(name, options, false)));
328+
304329
deploymentCmd
305330
.command("run <name>")
306-
.description("Trigger a deployment run (native on Claude, emulated as a session on Qoder)")
331+
.description("Trigger a deployment run (native on Qoder/Claude, emulated on Bailian/Ark)")
307332
.addOption(configFileOption())
308333
.addOption(providerOption("Target provider"))
309334
.action(withResolvedConfigFile(deploymentRunCommand));

packages/cli/tests/unit/cli-contracts.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,49 @@ test("deployment list renders deployment rows through the core runtime", async (
422422
expect(result.stdout).not.toMatch(/[\u4e00-\u9fff]/);
423423
});
424424

425+
test("deployment help exposes remote list and lifecycle commands", async () => {
426+
const help = await runAgents(["deployment", "--help"]);
427+
expect(help.exitCode).toBe(0);
428+
expect(help.stdout).toContain("pause");
429+
expect(help.stdout).toContain("unpause");
430+
431+
const listHelp = await runAgents(["deployment", "list", "--help"]);
432+
expect(listHelp.exitCode).toBe(0);
433+
expect(listHelp.stdout).toContain("--remote");
434+
expect(listHelp.stdout).toContain("--include-archived");
435+
});
436+
437+
test("deployment remote list validates provider filter combinations locally", async () => {
438+
const invalidStatus = await runAgents([
439+
"deployment",
440+
"list",
441+
"--remote",
442+
"--provider",
443+
"claude",
444+
"--status",
445+
"running",
446+
]);
447+
expect(invalidStatus.exitCode).not.toBe(0);
448+
expect(invalidStatus.stderr).toContain("Allowed choices are active, paused");
449+
450+
const dir = await makeTempDir();
451+
const configPath = await writeDeploymentConfig(dir);
452+
const incompatible = await runAgents([
453+
"deployment",
454+
"list",
455+
"--file",
456+
configPath,
457+
"--remote",
458+
"--provider",
459+
"claude",
460+
"--status",
461+
"active",
462+
"--include-archived",
463+
]);
464+
expect(incompatible.exitCode).not.toBe(0);
465+
expect(incompatible.stderr).toContain("cannot combine --status with --include-archived");
466+
});
467+
425468
test("destroy with empty state is handled through the core runtime", async () => {
426469
const dir = await makeTempDir();
427470
const configPath = await writeConfig(dir);

packages/sdk/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ export {
6565
getDeploymentDetailsForContext,
6666
getDeploymentRuntimeProviderForContext,
6767
listDeploymentsForContext,
68+
listRemoteDeploymentsForContext,
69+
pauseDeploymentForContext,
6870
runDeploymentForContext,
6971
} from "./internal/core/deployment-runtime.ts";
7072

73+
export type { DeploymentListFilter, DeploymentListResult } from "./internal/providers/interface.ts";
74+
7175
export type { DestroyResourceResult } from "./internal/core/destroy-runtime.ts";
7276
export {
7377
destroyPlannedProjectResources,

packages/sdk/src/internal/core/deployment-runtime.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { UserError } from "../errors.ts";
22
import { resolveDeploymentRefs } from "../executor/resolver.ts";
3-
import type { DeploymentContext } from "../providers/interface.ts";
3+
import type {
4+
DeploymentContext,
5+
DeploymentListFilter,
6+
DeploymentListResult,
7+
DeploymentInfo as ProviderDeploymentInfo,
8+
} from "../providers/interface.ts";
49
import type { DeploymentRunAdapter } from "../providers/resource-workflow.ts";
510
import type { ProjectConfig } from "../types/config.ts";
611
import type { ResourceState } from "../types/state.ts";
@@ -51,6 +56,17 @@ export interface DeploymentRun {
5156
result: DeploymentRunResult;
5257
}
5358

59+
export async function listRemoteDeploymentsForContext(
60+
ctx: ProjectRuntimeContext,
61+
provider: string,
62+
filter?: DeploymentListFilter,
63+
): Promise<DeploymentListResult> {
64+
const adapter = getRuntimeProvider(ctx, provider);
65+
if (!adapter.listDeployments)
66+
throw new UserError(`Provider '${provider}' does not support remote deployment listing.`);
67+
return adapter.listDeployments(filter);
68+
}
69+
5470
export function listDeploymentsForContext(ctx: ProjectRuntimeContext, providerFilter?: string): DeploymentSummary[] {
5571
let rows = ctx.state.listResources().filter((resource) => resource.address.type === "deployment");
5672
if (providerFilter) {
@@ -109,6 +125,20 @@ export async function runDeploymentForContext(
109125
};
110126
}
111127

128+
export async function pauseDeploymentForContext(
129+
ctx: ProjectRuntimeContext,
130+
name: string,
131+
paused: boolean,
132+
resolvedProvider?: string,
133+
): Promise<ProviderDeploymentInfo> {
134+
const provider = resolvedProvider ?? resolveDeploymentProvider(name, ctx.config);
135+
const adapter = getRuntimeProvider(ctx, provider);
136+
const operation = paused ? adapter.pauseDeployment : adapter.unpauseDeployment;
137+
if (!operation)
138+
throw new UserError(`Provider '${provider}' does not support ${paused ? "pausing" : "unpausing"} deployments.`);
139+
return operation.call(adapter, buildDeploymentContext(ctx, name, provider));
140+
}
141+
112142
export function getDeploymentRuntimeProviderForContext(
113143
ctx: ProjectRuntimeContext,
114144
name: string,

packages/sdk/src/internal/core/validate-config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,15 @@ export function collectProviderCapabilities(
308308
}
309309
}
310310
for (const [name, deployment] of Object.entries(config.deployments ?? {})) {
311+
if (deployment.provider && deployment.provider !== providerName) continue;
312+
if (deployment.environment_variables !== undefined) {
313+
diagnostics.error(
314+
`${providerName}.deployment.environment_variables.unsupported`,
315+
`deployment.${name}: environment_variables is supported only by Qoder deployments; ` +
316+
`remove it or pin this deployment to the qoder provider.`,
317+
{ type: "deployment", name, provider: providerName },
318+
);
319+
}
311320
if (deployment.tunnel && (!deployment.provider || deployment.provider === providerName)) {
312321
diagnostics.error(
313322
`${providerName}.deployment.tunnel.unsupported`,

packages/sdk/src/internal/parser/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ const deploymentSchema = z.object({
288288
description: z.string().optional(),
289289
provider: z.string().optional(),
290290
metadata: z.record(z.string(), z.string()).optional(),
291+
environment_variables: z.string().optional(),
291292
});
292293

293294
export const projectConfigSchema = z.object({

0 commit comments

Comments
 (0)