Files
paseo/packages/cli/tests/37-hub-deploy.test.ts
T
Mohamed BoudraandGitHub 05a0cabc07 Deploy multi-file Hub workflows (#3128)
* feat(hub): deploy canonical workflow bundles

Keep CLI deployment aligned with Hub's authored bundle boundary so validation and activation receive the same deterministic source files. Public documentation now teaches that boundary as the only configuration format.

* test(hub): assert validation diagnostics directly

* refactor(hub): deepen HTTP client module

* feat(hub): validate named agent configurations

* docs(hub): align Claude mode example
2026-08-10 12:32:59 +02:00

118 lines
3.9 KiB
TypeScript

#!/usr/bin/env npx tsx
import assert from "node:assert/strict";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { runLocalPaseo } from "./helpers/local-cli.js";
const cwd = await mkdtemp(path.join(tmpdir(), "paseo-hub-installed-"));
const requests: Array<{ url: string | undefined; body: unknown }> = [];
const server = createServer((request, response) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk: string) => {
body += chunk;
});
request.on("end", () => {
requests.push({ url: request.url, body: JSON.parse(body) });
const validation = request.url?.endsWith("/validate") === true;
response.writeHead(validation ? 200 : 201, { "content-type": "application/json" });
response.end(
JSON.stringify(
validation
? { projectSlug: "studio", valid: true }
: {
projectSlug: "studio",
version: 1,
versionId: "2de5b143-1c88-42db-8a8d-71ca2af97830",
active: true,
},
),
);
});
});
try {
const workflows = path.join(cwd, ".paseo", "workflows");
await mkdir(path.join(workflows, "partials"), { recursive: true });
const files = [
{
path: ".paseo/hub.yml",
content:
"environments:\n studio:\n kind: daemon\n daemon: local\n cwd: /workspace\nagents:\n codex-safe:\n provider: codex\n options:\n sandbox_workspace_write:\n writable_roots: [/var/cache/npm]\n network_access: false\n",
},
{
path: ".paseo/workflows/run.yml",
content:
"name: run\non: manual.run\nmax_runtime: 1h\ninputs:\n repo:\n type: string\n choices: [studio]\n agent:\n type: string\n choices: [codex-safe]\nsteps:\n - id: work\n environment: ${{ paseo.inputs.repo }}\n max_runtime: 30m\n idle_timeout: 5m\n agent: ${{ paseo.inputs.agent }}\n prompt:\n - include: partials/instructions.md\n - text: ${{ paseo.prompt }}\n",
},
{
path: ".paseo/workflows/partials/instructions.md",
content: "Keep structured provider options unchanged.\n",
},
];
for (const file of files) await writeFile(path.join(cwd, file.path), file.content);
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address() as AddressInfo;
const origin = `http://127.0.0.1:${address.port}`;
const validate = await runLocalPaseo(
[
"hub",
"deploy",
"-p",
"studio",
"--dry-run",
"--hub",
origin,
"--api-key",
"test-secret",
"--json",
],
{},
cwd,
);
assert.equal(validate.exitCode, 0, validate.stderr);
assert.deepEqual(JSON.parse(validate.stdout), {
projectSlug: "studio",
valid: true,
workflows: 1,
origin,
});
const install = await runLocalPaseo(
["hub", "deploy", "-p", "studio", "--hub", origin, "--api-key", "test-secret", "--json"],
{},
cwd,
);
assert.equal(install.exitCode, 0, install.stderr);
assert.deepEqual(
requests.map(({ url, body }) => ({ url, body })),
[
{
url: "/api/v1/configurations/validate",
body: { projectSlug: "studio", files: files.toSorted(compareFiles) },
},
{
url: "/api/v1/configurations/install",
body: { projectSlug: "studio", files: files.toSorted(compareFiles) },
},
],
);
console.log("✅ installed Hub CLI deploys and validates the canonical bundle");
} finally {
server.closeAllConnections();
await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(cwd, { recursive: true });
}
function compareFiles(left: { path: string }, right: { path: string }): number {
if (left.path < right.path) return -1;
if (left.path > right.path) return 1;
return 0;
}