diff --git a/cli/conf.ts b/cli/conf.ts index b2b7b64f54..14de9bcdda 100644 --- a/cli/conf.ts +++ b/cli/conf.ts @@ -4,6 +4,7 @@ export interface SyncOptions { stateful?: boolean; raw?: boolean; yes?: boolean; + dryRun?: boolean; skipPull?: boolean; failConflicts?: boolean; plainSecrets?: boolean; diff --git a/cli/instance.ts b/cli/instance.ts index 5a68f29c30..e3abdb4eaa 100644 --- a/cli/instance.ts +++ b/cli/instance.ts @@ -36,7 +36,6 @@ import { type SimplifiedSettings, } from "./settings.ts"; import { deepEqual } from "./utils.ts"; -import { GlobalOptions } from "./types.ts"; import { getActiveWorkspace } from "./workspace.ts"; export interface Instance { @@ -182,6 +181,7 @@ export type InstanceSyncOptions = { baseUrl?: string; token?: string; folderPerInstance?: boolean; + dryRun?: boolean; yes?: boolean; prefix?: string; prefixSettings?: boolean; @@ -290,6 +290,10 @@ async function instancePull(opts: InstanceSyncOptions) { if (totalChanges > 0) { let confirm = true; + if (opts.dryRun) { + log.info(colors.gray(`Dry run complete.`)); + return; + } if (opts.yes !== true) { confirm = await Confirm.prompt({ message: `Do you want to pull these ${totalChanges} instance-level changes?`, @@ -368,6 +372,7 @@ async function instancePull(opts: InstanceSyncOptions) { includeUsers: true, includeKey: true, yes: opts.yes, + dryRun: opts.dryRun, }); } @@ -535,6 +540,7 @@ async function instancePush(opts: InstanceSyncOptions) { includeUsers: true, includeKey: true, yes: opts.yes, + dryRun: opts.dryRun, }); } @@ -711,6 +717,7 @@ const command = new Command() "Pull instance settings, users, configs, instance groups and overwrite local" ) .option("--yes", "Pull without needing confirmation") + .option("--dry-run", "Perform a dry run without making changes") .option("--skip-users", "Skip pulling users") .option("--skip-settings", "Skip pulling settings") .option("--skip-configs", "Skip pulling configs (worker groups and SMTP)") @@ -735,6 +742,7 @@ const command = new Command() "Push instance settings, users, configs, group and overwrite remote" ) .option("--yes", "Push without needing confirmation") + .option("--dry-run", "Perform a dry run without making changes") .option("--skip-users", "Skip pushing users") .option("--skip-settings", "Skip pushing settings") .option("--skip-configs", "Skip pushing configs (worker groups and SMTP)") diff --git a/cli/script.ts b/cli/script.ts index 467047bdd6..6a7a574826 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -119,7 +119,7 @@ export async function findResourceFile(path: string) { if (validCandidates.length > 1) { throw new Error( "Found two resource files for the same resource" + - validCandidates.join(", ") + validCandidates.join(", ") ); } if (validCandidates.length < 1) { @@ -249,20 +249,20 @@ export async function handleFile( let typed = opts?.skipScriptsMetadata ? undefined : ( - await parseMetadataFile( - remotePath, - opts - ? { - ...opts, - path, - workspaceRemote: workspace, - schemaOnly: codebase ? true : undefined, - } - : undefined, - globalDeps, - codebases - ) - )?.payload; + await parseMetadataFile( + remotePath, + opts + ? { + ...opts, + path, + workspaceRemote: workspace, + schemaOnly: codebase ? true : undefined, + } + : undefined, + globalDeps, + codebases + ) + )?.payload; const workspaceId = workspace.workspaceId; @@ -347,19 +347,19 @@ export async function handleFile( deepEqual(typed.schema, remote.schema) && typed.tag == remote.tag && (typed.ws_error_handler_muted ?? false) == - remote.ws_error_handler_muted && + remote.ws_error_handler_muted && typed.dedicated_worker == remote.dedicated_worker && typed.cache_ttl == remote.cache_ttl && typed.concurrency_time_window_s == - remote.concurrency_time_window_s && + remote.concurrency_time_window_s && typed.concurrent_limit == remote.concurrent_limit && Boolean(typed.restart_unless_cancelled) == - Boolean(remote.restart_unless_cancelled) && + Boolean(remote.restart_unless_cancelled) && Boolean(typed.visible_to_runner_only) == - Boolean(remote.visible_to_runner_only) && + Boolean(remote.visible_to_runner_only) && Boolean(typed.no_main_func) == Boolean(remote.no_main_func) && Boolean(typed.has_preprocessor) == - Boolean(remote.has_preprocessor) && + Boolean(remote.has_preprocessor) && typed.priority == Boolean(remote.priority) && typed.timeout == remote.timeout && //@ts-ignore @@ -450,7 +450,8 @@ async function createScript( }); } catch (e: any) { throw Error( - `Script creation for ${body.path} with parent ${body.parent_hash + `Script creation for ${body.path} with parent ${ + body.parent_hash } was not successful: ${e.body ?? e.message} ` ); } @@ -476,7 +477,8 @@ async function createScript( }); if (req.status != 201) { throw Error( - `Script snapshot creation was not successful: ${req.status} - ${req.statusText + `Script snapshot creation was not successful: ${req.status} - ${ + req.statusText } - ${await req.text()} ` ); } @@ -488,8 +490,8 @@ export async function findContentFile(filePath: string) { const candidates = filePath.endsWith("script.json") ? exts.map((x) => filePath.replace(".script.json", x)) : filePath.endsWith("script.lock") - ? exts.map((x) => filePath.replace(".script.lock", x)) - : exts.map((x) => filePath.replace(".script.yaml", x)); + ? exts.map((x) => filePath.replace(".script.lock", x)) + : exts.map((x) => filePath.replace(".script.yaml", x)); const validCandidates = ( await Promise.all( @@ -508,7 +510,7 @@ export async function findContentFile(filePath: string) { if (validCandidates.length > 1) { throw new Error( "No content path given and more than one candidate found: " + - validCandidates.join(", ") + validCandidates.join(", ") ); } if (validCandidates.length < 1) { @@ -964,6 +966,10 @@ async function generateMetadata( } } if (hasAny) { + if (opts.dryRun) { + log.info(colors.gray(`Dry run complete.`)); + return; + } if ( !opts.yes && !(await Confirm.prompt({ @@ -1027,6 +1033,7 @@ const command = new Command() ) .arguments("[script:file]") .option("--yes", "Skip confirmation prompt") + .option("--dry-run", "Perform a dry run without making changes") .option("--lock-only", "re-generate only the lock") .option("--schema-only", "re-generate only script schema") .option( diff --git a/cli/sync.ts b/cli/sync.ts index 1f2d71a3fa..eb94ce5eb7 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -117,7 +117,7 @@ async function addCodebaseDigestIfRelevant( const parsed: any = yamlParseContent(path, content); if (parsed && typeof parsed == "object") { if (ignoreCodebaseChanges) { - parsed["codebase"] = undefined + parsed["codebase"] = undefined; } else { parsed["codebase"] = await c.getDigest(); } @@ -367,12 +367,12 @@ function ZipFSElement( ) ? "flow" : p.endsWith("app.json") - ? "app" - : p.endsWith("script.json") - ? "script" - : p.endsWith("resource.json") - ? "resource" - : "other"; + ? "app" + : p.endsWith("script.json") + ? "script" + : p.endsWith("resource.json") + ? "resource" + : "other"; const isJson = p.endsWith(".json"); @@ -402,7 +402,7 @@ function ZipFSElement( yield { isDirectory: false, path: path.join(finalPath, s.path), - async *getChildren() { }, + async *getChildren() {}, // deno-lint-ignore require-await async getContentText() { return s.content; @@ -413,7 +413,7 @@ function ZipFSElement( yield { isDirectory: false, path: path.join(finalPath, "flow.yaml"), - async *getChildren() { }, + async *getChildren() {}, // deno-lint-ignore require-await async getContentText() { return yamlStringify(flow, yamlOptions); @@ -429,7 +429,7 @@ function ZipFSElement( yield { isDirectory: false, path: path.join(finalPath, s.path), - async *getChildren() { }, + async *getChildren() {}, // deno-lint-ignore require-await async getContentText() { return s.content; @@ -440,7 +440,7 @@ function ZipFSElement( yield { isDirectory: false, path: path.join(finalPath, "app.yaml"), - async *getChildren() { }, + async *getChildren() {}, // deno-lint-ignore require-await async getContentText() { return yamlStringify(app, yamlOptions); @@ -508,7 +508,7 @@ function ZipFSElement( r.push({ isDirectory: false, path: removeSuffix(finalPath, ".json") + ".lock", - async *getChildren() { }, + async *getChildren() {}, // deno-lint-ignore require-await async getContentText() { return lock; @@ -531,7 +531,7 @@ function ZipFSElement( removeSuffix(finalPath, ".resource.json") + ".resource.file." + formatExtension, - async *getChildren() { }, + async *getChildren() {}, // deno-lint-ignore require-await async getContentText() { return fileContent; @@ -592,19 +592,19 @@ export async function* readDirRecursiveWithIgnore( // getContentBytes(): Promise; getContentText(): Promise; }[] = [ - { - path: root.path, - ignored: ignore(root.path, root.isDirectory), - isDirectory: root.isDirectory, - c: root.getChildren, - // getContentBytes(): Promise { - // throw undefined; - // }, - getContentText(): Promise { - throw undefined; - }, + { + path: root.path, + ignored: ignore(root.path, root.isDirectory), + isDirectory: root.isDirectory, + c: root.getChildren, + // getContentBytes(): Promise { + // throw undefined; + // }, + getContentText(): Promise { + throw undefined; }, - ]; + }, + ]; while (stack.length > 0) { const e = stack.pop()!; @@ -667,7 +667,8 @@ export async function elementsToMap( if (!skips.includeSettings && path === "settings" + ext) continue; if (!skips.includeKey && path === "encryption_key") continue; if (skips.skipResources && path.endsWith(".resource" + ext)) continue; - if (skips.skipResourceTypes && path.endsWith(".resource-type" + ext)) continue; + if (skips.skipResourceTypes && path.endsWith(".resource-type" + ext)) + continue; if (skips.skipVariables && path.endsWith(".variable" + ext)) continue; @@ -745,9 +746,9 @@ async function compareDynFSElement( ): Promise { const [m1, m2] = els2 ? await Promise.all([ - elementsToMap(els1, ignore, json, skips), - elementsToMap(els2, ignore, json, skips), - ]) + elementsToMap(els1, ignore, json, skips), + elementsToMap(els2, ignore, json, skips), + ]) : [await elementsToMap(els1, ignore, json, skips), {}]; const changes: Change[] = []; @@ -808,7 +809,6 @@ async function compareDynFSElement( continue; } if (!ignoreCodebaseChanges) { - if (before.codebase != undefined) { delete before.codebase; m2[k] = yamlStringify(before, yamlOptions); @@ -835,7 +835,6 @@ async function compareDynFSElement( } } - const remoteCodebase: Record = {}; for (const [k] of Object.entries(m2)) { if (m1[k] === undefined) { @@ -864,7 +863,7 @@ async function compareDynFSElement( continue; } const c = findCodebase(tsFile, codebases); - if (await c?.getDigest() != v) { + if ((await c?.getDigest()) != v) { changes.push({ name: "edited", path: tsFile, @@ -1126,7 +1125,7 @@ export async function pull(opts: GlobalOptions & SyncOptions) { opts.includeGroups, opts.includeSettings, opts.includeKey, - opts.defaultTs, + opts.defaultTs ))!, !opts.json, opts.defaultTs ?? "bun", @@ -1152,6 +1151,10 @@ export async function pull(opts: GlobalOptions & SyncOptions) { ); if (changes.length > 0) { prettyChanges(changes); + if (opts.dryRun) { + log.info(colors.gray(`Dry run complete.`)); + return; + } if ( !opts.yes && !(await Confirm.prompt({ @@ -1317,8 +1320,8 @@ function prettyChanges(changes: Change[]) { log.info( colors.yellow( `~ ${getTypeStrFromPath(change.path)} ` + - change.path + - (change.codebase ? ` (codebase changed)` : "") + change.path + + (change.codebase ? ` (codebase changed)` : "") ) ); if (change.before != change.after) { @@ -1401,7 +1404,7 @@ export async function push(opts: GlobalOptions & SyncOptions) { opts.includeGroups, opts.includeSettings, opts.includeKey, - opts.defaultTs, + opts.defaultTs ))!, !opts.json, opts.defaultTs ?? "bun", @@ -1409,7 +1412,7 @@ export async function push(opts: GlobalOptions & SyncOptions) { false ); - const local = await FSFSElement(path.join(Deno.cwd(), "",), codebases, false); + const local = await FSFSElement(path.join(Deno.cwd(), ""), codebases, false); const changes = await compareDynFSElement( local, remote, @@ -1423,7 +1426,6 @@ export async function push(opts: GlobalOptions & SyncOptions) { const globalDeps = await findGlobalDeps(); - const tracker: ChangeTracker = await buildTracker(changes); const staleScripts: string[] = []; @@ -1489,6 +1491,10 @@ export async function push(opts: GlobalOptions & SyncOptions) { if (changes.length > 0) { prettyChanges(changes); + if (opts.dryRun) { + log.info(colors.gray(`Dry run complete.`)); + return; + } if ( !opts.yes && !(await Confirm.prompt({ @@ -1527,7 +1533,8 @@ export async function push(opts: GlobalOptions & SyncOptions) { } const groupedChangesArray = Array.from(groupedChanges.entries()); log.info( - `found changes for ${groupedChangesArray.length + `found changes for ${ + groupedChangesArray.length } items with a total of ${groupedChangesArray.reduce( (acc, [_, changes]) => acc + changes.length, 0 @@ -1840,7 +1847,8 @@ export async function push(opts: GlobalOptions & SyncOptions) { } log.info( colors.bold.green.underline( - `\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId + `\nDone! All ${changes.length} changes pushed to the remote workspace ${ + workspace.workspaceId } named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)` ) ); @@ -1858,6 +1866,10 @@ const command = new Command() .command("pull") .description("Pull any remote changes and apply them locally.") .option("--yes", "Pull without needing confirmation") + .option( + "--dry-run", + "Show changes that would be pulled without actually pushing" + ) .option("--plain-secrets", "Pull secrets as plain text") .option("--json", "Use JSON instead of YAML") .option("--skip-variables", "Skip syncing variables (including secrets)") @@ -1888,6 +1900,10 @@ const command = new Command() .command("push") .description("Push any local changes and apply them remotely.") .option("--yes", "Push without needing confirmation") + .option( + "--dry-run", + "Show changes that would be pushed without actually pushing" + ) .option("--plain-secrets", "Push secrets as plain text") .option("--json", "Use JSON instead of YAML") .option("--skip-variables", "Skip syncing variables (including secrets)")