feat: release 2.7.0-rc2
This commit is contained in:
+238
@@ -0,0 +1,238 @@
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin, stdout } from "node:process";
|
||||
import { basename } from "node:path";
|
||||
import { defineCommand } from "citty";
|
||||
import { ctx } from "../lib/context";
|
||||
import {
|
||||
addAppToCompose,
|
||||
checkedAppPath,
|
||||
detectAppPaths,
|
||||
type AppOptions,
|
||||
type Source,
|
||||
} from "../lib/scaffold";
|
||||
import {
|
||||
listDockerfileTemplates,
|
||||
templateIds,
|
||||
} from "../lib/scaffold-templates";
|
||||
|
||||
export type ScaffoldPrompt = (
|
||||
question: string,
|
||||
defaultValue?: string,
|
||||
) => Promise<string>;
|
||||
|
||||
export const terminalPrompt: ScaffoldPrompt = async (
|
||||
question,
|
||||
defaultValue,
|
||||
) => {
|
||||
if (!stdin.isTTY)
|
||||
throw new Error(`${question}: supply flags or use an interactive terminal`);
|
||||
const readline = createInterface({ input: stdin, output: stdout });
|
||||
try {
|
||||
const result = (
|
||||
await readline.question(
|
||||
`${question}${defaultValue ? ` [${defaultValue}]` : ""}: `,
|
||||
)
|
||||
).trim();
|
||||
return result || defaultValue || "";
|
||||
} finally {
|
||||
readline.close();
|
||||
}
|
||||
};
|
||||
|
||||
export type AppInput = Partial<Omit<AppOptions, "source">> & {
|
||||
source?: "image" | "auto" | "template";
|
||||
image?: string;
|
||||
template?: string;
|
||||
nonInteractive?: boolean;
|
||||
};
|
||||
|
||||
async function answer(
|
||||
value: string | undefined,
|
||||
question: string,
|
||||
fallback: string | undefined,
|
||||
prompt: ScaffoldPrompt,
|
||||
nonInteractive: boolean,
|
||||
): Promise<string> {
|
||||
if (value !== undefined) return value;
|
||||
if (nonInteractive) {
|
||||
if (fallback !== undefined) return fallback;
|
||||
throw new Error(`${question} is required in non-interactive mode`);
|
||||
}
|
||||
return prompt(question, fallback);
|
||||
}
|
||||
|
||||
export async function resolveAppInput(
|
||||
root: string,
|
||||
input: AppInput,
|
||||
prompt: ScaffoldPrompt = terminalPrompt,
|
||||
): Promise<AppOptions> {
|
||||
const detected = await detectAppPaths(root);
|
||||
let path: string;
|
||||
if (input.path !== undefined) path = input.path;
|
||||
else if (detected.length === 0) path = ".";
|
||||
else if (detected.length === 1) path = detected[0]!;
|
||||
else {
|
||||
if (input.nonInteractive)
|
||||
throw new Error(
|
||||
`Multiple app roots found (${detected.join(", ")}); specify --path`,
|
||||
);
|
||||
path = await prompt(`App path (${detected.join(", ")})`);
|
||||
if (!detected.includes(path))
|
||||
throw new Error(
|
||||
`Select one of the detected app roots: ${detected.join(", ")}`,
|
||||
);
|
||||
}
|
||||
path = await checkedAppPath(root, path);
|
||||
const suggestedName =
|
||||
basename(path === "." ? root : path)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "app";
|
||||
const name = await answer(
|
||||
input.name,
|
||||
"Service name",
|
||||
/^[a-z]/.test(suggestedName) ? suggestedName : `app-${suggestedName}`,
|
||||
prompt,
|
||||
!!input.nonInteractive,
|
||||
);
|
||||
const defaultSource = detected.length === 0 ? "image" : "auto";
|
||||
const selectedSource = await answer(
|
||||
input.source,
|
||||
"Source (image/auto/template)",
|
||||
defaultSource,
|
||||
prompt,
|
||||
!!input.nonInteractive,
|
||||
);
|
||||
let source: Source;
|
||||
if (selectedSource === "image")
|
||||
source = {
|
||||
kind: "image",
|
||||
image: await answer(
|
||||
input.image,
|
||||
"Image",
|
||||
"nginx:stable",
|
||||
prompt,
|
||||
!!input.nonInteractive,
|
||||
),
|
||||
};
|
||||
else if (selectedSource === "auto") source = { kind: "auto" };
|
||||
else if (selectedSource === "template") {
|
||||
const choices = listDockerfileTemplates()
|
||||
.map(({ id, label }) => `${id} (${label})`)
|
||||
.join(", ");
|
||||
const template = await answer(
|
||||
input.template,
|
||||
`Dockerfile template (${choices})`,
|
||||
undefined,
|
||||
prompt,
|
||||
!!input.nonInteractive,
|
||||
);
|
||||
if (!templateIds.includes(template as (typeof templateIds)[number]))
|
||||
throw new Error(`Unknown Dockerfile template: ${template}`);
|
||||
source = {
|
||||
kind: "template",
|
||||
template: template as (typeof templateIds)[number],
|
||||
};
|
||||
} else throw new Error("Source must be image, auto, or template");
|
||||
const postgres = input.nonInteractive
|
||||
? input.postgres
|
||||
: await prompt(
|
||||
"Postgres claim (name or user/database; blank for none)",
|
||||
input.postgres,
|
||||
);
|
||||
const s3 = input.nonInteractive
|
||||
? input.s3
|
||||
: await prompt("S3 claim (name or key/bucket; blank for none)", input.s3);
|
||||
const cpu = input.nonInteractive
|
||||
? input.cpu
|
||||
: await prompt("CPU limit in cores (blank for default)", input.cpu);
|
||||
const memory = input.nonInteractive
|
||||
? input.memory
|
||||
: await prompt("Memory limit (blank for default)", input.memory);
|
||||
const replicasAnswer = input.nonInteractive
|
||||
? input.replicas?.toString()
|
||||
: await prompt("Replicas (blank for default)", input.replicas?.toString());
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
source,
|
||||
...(postgres ? { postgres } : {}),
|
||||
...(s3 ? { s3 } : {}),
|
||||
...(cpu ? { cpu } : {}),
|
||||
...(memory ? { memory } : {}),
|
||||
...(replicasAnswer ? { replicas: Number(replicasAnswer) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const appArgs = {
|
||||
name: { type: "string" as const, description: "Service name" },
|
||||
path: {
|
||||
type: "string" as const,
|
||||
description: "App directory relative to the project",
|
||||
},
|
||||
source: {
|
||||
type: "string" as const,
|
||||
description: "image, auto (Buildpacks), or template",
|
||||
},
|
||||
image: {
|
||||
type: "string" as const,
|
||||
description: "Image reference when source=image",
|
||||
},
|
||||
template: {
|
||||
type: "string" as const,
|
||||
description: "Dockerfile template ID when source=template",
|
||||
},
|
||||
postgres: {
|
||||
type: "string" as const,
|
||||
description: "Postgres claim name or user/database",
|
||||
},
|
||||
s3: { type: "string" as const, description: "S3 claim name or key/bucket" },
|
||||
cpu: { type: "string" as const, description: "CPU limit in cores" },
|
||||
memory: { type: "string" as const, description: "Memory limit, e.g. 512m" },
|
||||
replicas: { type: "string" as const, description: "Replica count" },
|
||||
"non-interactive": {
|
||||
type: "boolean" as const,
|
||||
description: "Use defaults and require explicit choices when ambiguous",
|
||||
},
|
||||
};
|
||||
|
||||
export function appInputFromArgs(args: Record<string, unknown>): AppInput {
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
[
|
||||
"name",
|
||||
"path",
|
||||
"source",
|
||||
"image",
|
||||
"template",
|
||||
"postgres",
|
||||
"s3",
|
||||
"cpu",
|
||||
"memory",
|
||||
]
|
||||
.filter((key) => args[key] !== undefined)
|
||||
.map((key) => [key, String(args[key])]),
|
||||
),
|
||||
...(args.replicas !== undefined ? { replicas: Number(args.replicas) } : {}),
|
||||
nonInteractive: Boolean(args["non-interactive"]),
|
||||
};
|
||||
}
|
||||
|
||||
export const add = defineCommand({
|
||||
meta: { name: "add", description: "Add a resource to this project" },
|
||||
subCommands: {
|
||||
app: defineCommand({
|
||||
meta: {
|
||||
name: "app",
|
||||
description: "Add a service without changing existing services",
|
||||
},
|
||||
args: appArgs,
|
||||
async run({ args }) {
|
||||
const { cwd, path } = ctx();
|
||||
const app = await resolveAppInput(cwd, appInputFromArgs(args));
|
||||
const composePath = await addAppToCompose(cwd, app, await path());
|
||||
console.log(`Added ${app.name} to ${composePath}`);
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -75,6 +75,19 @@ export async function loginUser(
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Share the CLI's credential prompt with onboarding without exposing passwords. */
|
||||
export async function interactiveLogin(): Promise<LoginResponse> {
|
||||
if (!stdin.isTTY)
|
||||
throw new Error(
|
||||
"Login requires an interactive terminal; run kuber login first",
|
||||
);
|
||||
const username = await promptUsername();
|
||||
if (!username) throw new Error("Username is required");
|
||||
const session = await loginUser(username, await promptPassword(), false);
|
||||
console.log(`Logged in as ${session.user.username}`);
|
||||
return session;
|
||||
}
|
||||
|
||||
export const login = defineCommand({
|
||||
meta: {
|
||||
name: "login",
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { apiRequest, KuberApiError, type ApiRequestInit } from "../lib/api";
|
||||
import { readSession, type KuberSession } from "../lib/session";
|
||||
import { resolveTrustIdentity, updateTrust } from "../lib/trust";
|
||||
import type { WorkspaceTrustResponse } from "../shared/api";
|
||||
import { interactiveLogin } from "./auth";
|
||||
import { createCompose, projectName } from "../lib/scaffold";
|
||||
import {
|
||||
appArgs,
|
||||
appInputFromArgs,
|
||||
resolveAppInput,
|
||||
terminalPrompt,
|
||||
type AppInput,
|
||||
type ScaffoldPrompt,
|
||||
} from "./add";
|
||||
import { resolveComposeFile } from "../lib/yaml";
|
||||
import { basename } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { renderDockerfileTemplate } from "../lib/scaffold-templates";
|
||||
import { loadConfig } from "../lib/config";
|
||||
|
||||
type Requester = <T>(path: string, init?: ApiRequestInit) => Promise<T>;
|
||||
export type InitDependencies = {
|
||||
configPath?: string;
|
||||
prompt?: ScaffoldPrompt;
|
||||
session?: () => Promise<KuberSession | undefined>;
|
||||
login?: () => Promise<KuberSession>;
|
||||
request?: Requester;
|
||||
persistTrust?: typeof updateTrust;
|
||||
};
|
||||
|
||||
/** Creates a project and registers trust after a successful authenticated server grant. */
|
||||
export async function initializeProject(
|
||||
root: string,
|
||||
input: AppInput & { project?: string },
|
||||
dependencies: InitDependencies = {},
|
||||
): Promise<string> {
|
||||
if (await resolveComposeFile(root))
|
||||
throw new Error("A Compose file already exists; use kuber add app instead");
|
||||
const config = await loadConfig(root, dependencies.configPath);
|
||||
if (config.composeFile && config.composeFile !== join(root, "compose.yml"))
|
||||
throw new Error(
|
||||
`Existing config expects ${config.composeFile}; kuber init creates compose.yml`,
|
||||
);
|
||||
if (
|
||||
config.projectConfigured &&
|
||||
projectName(config.project) !== config.project
|
||||
)
|
||||
throw new Error(
|
||||
`Configured project ${config.project} must be a lowercase Kubernetes name`,
|
||||
);
|
||||
const prompt = dependencies.prompt ?? terminalPrompt;
|
||||
if (
|
||||
config.projectConfigured &&
|
||||
input.project &&
|
||||
input.project !== config.project
|
||||
)
|
||||
throw new Error(
|
||||
`Existing config selects project ${config.project}; use that project for init`,
|
||||
);
|
||||
const defaultProject = projectName(
|
||||
config.projectConfigured ? config.project : basename(root),
|
||||
);
|
||||
const project = projectName(
|
||||
input.project ??
|
||||
(input.nonInteractive
|
||||
? defaultProject
|
||||
: await prompt("Project name", defaultProject)),
|
||||
);
|
||||
const app = await resolveAppInput(root, input, prompt);
|
||||
const login = dependencies.login ?? interactiveLogin;
|
||||
let session = await (dependencies.session ?? readSession)();
|
||||
if (!session) session = await login();
|
||||
const composePath = await createCompose(root, project, app);
|
||||
const createdCompose = await readFile(composePath, "utf8");
|
||||
const identity = await resolveTrustIdentity(project, root);
|
||||
const request = dependencies.request ?? apiRequest;
|
||||
const trustPath = `/workspaces/${encodeURIComponent(project)}/trust`;
|
||||
let retried = false;
|
||||
let remoteRegistered = false;
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
const remote = await request<WorkspaceTrustResponse>(trustPath);
|
||||
if (!Array.isArray(remote?.fingerprints))
|
||||
throw new Error("Could not verify existing server trust");
|
||||
const alreadyTrusted = remote.fingerprints.includes(
|
||||
identity.fingerprint,
|
||||
);
|
||||
// The server grant is idempotent. A GET cannot establish ownership of it:
|
||||
// another client may grant the same fingerprint before our POST.
|
||||
if (!alreadyTrusted)
|
||||
await request(trustPath, {
|
||||
method: "POST",
|
||||
json: { fingerprint: identity.fingerprint },
|
||||
});
|
||||
remoteRegistered = true;
|
||||
try {
|
||||
await (dependencies.persistTrust ?? updateTrust)((records) => [
|
||||
...records.filter(
|
||||
(record) =>
|
||||
record.project !== identity.project ||
|
||||
record.fingerprint !== identity.fingerprint,
|
||||
),
|
||||
identity,
|
||||
]);
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Server registration exists, but local trust could not be confirmed. Generated project files were kept; run kuber trust to repair local trust.",
|
||||
);
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
if (
|
||||
retried ||
|
||||
!(error instanceof KuberApiError) ||
|
||||
error.status !== 401
|
||||
)
|
||||
throw error;
|
||||
retried = true;
|
||||
session = await login();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Once registered remotely, keep the files needed to repair local trust.
|
||||
// On remote failure, only roll back files still identical to ours.
|
||||
if (
|
||||
!remoteRegistered &&
|
||||
(await readFile(composePath, "utf8").catch(() => undefined)) ===
|
||||
createdCompose
|
||||
) {
|
||||
await rm(composePath, { force: true });
|
||||
if (app.source.kind === "template") {
|
||||
const { dockerfile, dockerignore } = renderDockerfileTemplate(
|
||||
app.source.template,
|
||||
);
|
||||
for (const [name, content] of [
|
||||
["Dockerfile", dockerfile],
|
||||
[".dockerignore", dockerignore],
|
||||
] as const) {
|
||||
const file = join(root, app.path, name);
|
||||
if ((await readFile(file, "utf8").catch(() => undefined)) === content)
|
||||
await rm(file, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
console.log(`Trusted this directory for namespace ${identity.project}`);
|
||||
console.log(`Initialized ${project} in ${composePath}`);
|
||||
return composePath;
|
||||
}
|
||||
|
||||
export const init = defineCommand({
|
||||
meta: {
|
||||
name: "init",
|
||||
description: "Initialize a Compose project and trust this directory",
|
||||
},
|
||||
args: {
|
||||
...appArgs,
|
||||
project: {
|
||||
type: "string",
|
||||
description: "Kubernetes namespace/project name",
|
||||
},
|
||||
"config-path": {
|
||||
type: "string",
|
||||
description: "Project config path (usually passed as global --config)",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
await initializeProject(
|
||||
process.cwd(),
|
||||
{
|
||||
...appInputFromArgs(args),
|
||||
...(args.project !== undefined
|
||||
? { project: String(args.project) }
|
||||
: {}),
|
||||
},
|
||||
{ configPath: args["config-path"] },
|
||||
);
|
||||
},
|
||||
});
|
||||
+4
-2
@@ -37,7 +37,7 @@ function cloneCommand<T extends object>(command: T): T {
|
||||
export const main = defineCommand({
|
||||
meta: {
|
||||
name: "kuber",
|
||||
version: "2.6.2-rc2",
|
||||
version: "2.7.0-rc2",
|
||||
description: "Docker Compose -> K8s translation layer",
|
||||
},
|
||||
args: {
|
||||
@@ -47,12 +47,14 @@ export const main = defineCommand({
|
||||
},
|
||||
},
|
||||
subCommands: {
|
||||
add: lazyCommand(() => import("./add").then((m) => m.add)),
|
||||
audit: lazyCommand(() => import("./audit").then((m) => m.audit)),
|
||||
ci: lazyCommand(() => import("./ci").then((m) => m.ci)),
|
||||
db: lazyCommand(() => import("./db").then((m) => m.db)),
|
||||
down: lazyCommand(() => import("./down").then((m) => m.down)),
|
||||
export: lazyCommand(() => import("./export").then((m) => m.exportCommand)),
|
||||
exec: lazyCommand(() => import("./exec").then((m) => m.exec)),
|
||||
init: lazyCommand(() => import("./init").then((m) => m.init)),
|
||||
logs: lazyCommand(() => import("./logs").then((m) => m.logs)),
|
||||
maintenance: lazyCommand(() =>
|
||||
import("./maintenance").then((m) => m.maintenance),
|
||||
@@ -76,7 +78,7 @@ export const main = defineCommand({
|
||||
});
|
||||
|
||||
type Completion = Awaited<
|
||||
ReturnType<typeof import("@bomb.sh/tab/citty")["default"]>
|
||||
ReturnType<(typeof import("@bomb.sh/tab/citty"))["default"]>
|
||||
>;
|
||||
let completion: Promise<Completion> | undefined;
|
||||
|
||||
|
||||
+71
-14
@@ -625,7 +625,12 @@ export async function reconcileResources(
|
||||
request: ApiRequester = apiRequest,
|
||||
resumeOptions: OperationResumeOptions = {},
|
||||
): Promise<ResourcePlan> {
|
||||
const plan = await planResources(project, resources, request, resumeOptions.signal);
|
||||
const plan = await planResources(
|
||||
project,
|
||||
resources,
|
||||
request,
|
||||
resumeOptions.signal,
|
||||
);
|
||||
await applyResourcePlan(
|
||||
project,
|
||||
plan,
|
||||
@@ -792,6 +797,30 @@ export async function runUp(
|
||||
new Promise((resolve) => pending.set(name, resolve)),
|
||||
]),
|
||||
);
|
||||
let settleUpload!: (error?: unknown) => void;
|
||||
const uploaded = new Promise<unknown>((resolve) => {
|
||||
settleUpload = resolve;
|
||||
});
|
||||
let uploadChild: ListrTaskWrapper<UpContext, any, any> | undefined;
|
||||
let uploadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const unit = bytes < 1024 ** 2 ? "KiB" : "MiB";
|
||||
const divisor = unit === "KiB" ? 1024 : 1024 ** 2;
|
||||
return `${Number((bytes / divisor).toFixed(1))} ${unit}`;
|
||||
};
|
||||
const uploadTitle = (
|
||||
bytes: number,
|
||||
total: number,
|
||||
finished = false,
|
||||
): string =>
|
||||
`Uploading context: ${formatBytes(bytes)}/${formatBytes(total)} (${finished ? 100 : total === 0 ? 0 : Math.min(99, Math.floor((100 * bytes) / total))}%)`;
|
||||
const finishUpload = (error?: unknown) => {
|
||||
if (!error && uploadChild)
|
||||
uploadChild.title = uploadTitle(uploadedBytes, totalBytes, true);
|
||||
settleUpload(error);
|
||||
};
|
||||
let result: Awaited<ReturnType<typeof buildServices>> | undefined;
|
||||
let operationFailure: Error | undefined;
|
||||
let operation: Promise<void> | undefined;
|
||||
@@ -801,19 +830,27 @@ export async function runUp(
|
||||
taskCtx.compose!,
|
||||
cwd,
|
||||
{
|
||||
progress: (message) => {
|
||||
if (
|
||||
!/^Build (?:queued|creating|starting|running|done)\b/.test(message) &&
|
||||
task.output !== message
|
||||
) task.output = message;
|
||||
upload: (bytes, total) => {
|
||||
uploadedBytes = bytes;
|
||||
totalBytes = total;
|
||||
if (uploadChild)
|
||||
uploadChild.title = uploadTitle(bytes, total);
|
||||
},
|
||||
uploadSettled: finishUpload,
|
||||
service: (name) => {
|
||||
let stream: ReturnType<ListrTaskWrapper<UpContext, any, any>["stdout"]> | undefined;
|
||||
let stream:
|
||||
| ReturnType<
|
||||
ListrTaskWrapper<UpContext, any, any>["stdout"]
|
||||
>
|
||||
| undefined;
|
||||
return {
|
||||
progress: (message) => {
|
||||
const child = children.get(name);
|
||||
if (!child) return;
|
||||
const phase = /^Build (queued|creating|starting|running|done)\b/.exec(message)?.[1];
|
||||
const phase =
|
||||
/^Build (queued|creating|starting|running|done)\b/.exec(
|
||||
message,
|
||||
)?.[1];
|
||||
if (phase) child.title = `Build ${name}: ${phase}`;
|
||||
// A failed status includes the full error; the parent bottom bar owns it.
|
||||
},
|
||||
@@ -845,6 +882,7 @@ export async function runUp(
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
operationFailure = failure;
|
||||
task.output = failure.stack ?? failure.message;
|
||||
finishUpload(failure);
|
||||
for (const resolve of pending.values()) resolve(failure);
|
||||
pending.clear();
|
||||
},
|
||||
@@ -853,6 +891,19 @@ export async function runUp(
|
||||
};
|
||||
return task.newListr<UpContext>(
|
||||
[
|
||||
{
|
||||
title: uploadTitle(0, 0),
|
||||
exitOnError: false,
|
||||
task: async (_ctx, child) => {
|
||||
uploadChild = child;
|
||||
if (children.size === services.length) start();
|
||||
const error = await uploaded;
|
||||
if (error) {
|
||||
child.title = "Uploading context: failed";
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
...services.map((name) => ({
|
||||
title: `Build ${name}`,
|
||||
rendererOptions: { outputBar: 10, persistentOutput: true },
|
||||
@@ -863,7 +914,7 @@ export async function runUp(
|
||||
) => {
|
||||
children.set(name, child);
|
||||
child.title = `Build ${name}: queued`;
|
||||
if (children.size === services.length) start();
|
||||
if (children.size === services.length && uploadChild) start();
|
||||
const error = await completed.get(name);
|
||||
if (error) {
|
||||
child.title = `Build ${name}: failed`;
|
||||
@@ -948,7 +999,8 @@ export async function runUp(
|
||||
{ method: "POST", json: { compose: taskCtx.compose } },
|
||||
);
|
||||
} catch (error) {
|
||||
task.output = error instanceof Error ? error.message : String(error);
|
||||
task.output =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw error;
|
||||
}
|
||||
taskCtx.databaseEnv = operationEnvironment(response);
|
||||
@@ -957,8 +1009,7 @@ export async function runUp(
|
||||
},
|
||||
{
|
||||
title: "Reconcile S3 storage",
|
||||
enabled: () =>
|
||||
getComposeS3Claims(taskCtx.compose!).length > 0,
|
||||
enabled: () => getComposeS3Claims(taskCtx.compose!).length > 0,
|
||||
task: async (_ctx, child) => {
|
||||
let response: Record<string, unknown>;
|
||||
try {
|
||||
@@ -969,7 +1020,8 @@ export async function runUp(
|
||||
{ method: "POST", json: { compose: taskCtx.compose } },
|
||||
);
|
||||
} catch (error) {
|
||||
task.output = error instanceof Error ? error.message : String(error);
|
||||
task.output =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw error;
|
||||
}
|
||||
taskCtx.storageEnv = operationEnvironment(response);
|
||||
@@ -1110,7 +1162,12 @@ export async function runUp(
|
||||
renderer: "default",
|
||||
// Only redraw when a task changes; a queued build should not generate
|
||||
// another frame on each spinner tick. Keep the renderer's task tree.
|
||||
rendererOptions: { collapseErrors: false, collapseSubtasks: false, showErrorMessage: false, lazy: true },
|
||||
rendererOptions: {
|
||||
collapseErrors: false,
|
||||
collapseSubtasks: false,
|
||||
showErrorMessage: false,
|
||||
lazy: true,
|
||||
},
|
||||
},
|
||||
).run();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user