diff --git a/.changeset/snapshot-unchanged.md b/.changeset/snapshot-unchanged.md new file mode 100644 index 00000000..e1b213c6 --- /dev/null +++ b/.changeset/snapshot-unchanged.md @@ -0,0 +1,5 @@ +--- +"@cloudhal/cli": patch +--- + +`cloudhal snapshot --publish` makes no new snapshot when nothing changed since the live one; it says "nothing changed since " instead (#51). diff --git a/packages/cli/src/cli/commands-local.ts b/packages/cli/src/cli/commands-local.ts index 4d85d427..14a4ee59 100644 --- a/packages/cli/src/cli/commands-local.ts +++ b/packages/cli/src/cli/commands-local.ts @@ -45,6 +45,10 @@ const snapshotCommand: CliCommand = async (_rest, values, { projectDir }) => { ...(values.label ? { label: values.label } : {}), ...(values.publish ? { publish: true } : {}), }); + if (snap.unchanged) { + console.log(`nothing changed since ${snap.id} (live); no new snapshot`); + return 0; + } console.log( `${snap.id} nodes=${snap.nodes.length} +${snap.summary.added} ~${snap.summary.changed} -${snap.summary.removed}${values.publish ? " (published)" : ""}`, ); diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index 4a3ec245..366c58e5 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -1,6 +1,6 @@ import path from "node:path"; import type { Snapshot } from "@cloudhal/core-contracts"; -import { openLocalStore } from "@cloudhal/core-ts/local"; +import { type LocalStore, openLocalStore } from "@cloudhal/core-ts/local"; import { readProjectConfig } from "../config.ts"; export interface SnapshotOptions { @@ -9,11 +9,22 @@ export interface SnapshotOptions { readonly publish?: boolean; } -/** Creates a snapshot of the live site (and optionally publishes it). */ -export async function snapshot(options: SnapshotOptions): Promise { +/** A snapshot, or (`unchanged`) the live one when publishing would change nothing. */ +export type SnapshotResult = Snapshot & { readonly unchanged?: true }; + +/** + * Creates a snapshot of the live site (and optionally publishes it). Publishing a site whose + * graph matches its live snapshot makes no new one (#51): re-running a setup script does not pad + * the history with empty snapshots. + */ +export async function snapshot(options: SnapshotOptions): Promise { const cfg = await readProjectConfig(options.projectDir); const local = await openLocalStore(path.join(options.projectDir, cfg.dataDir)); try { + if (options.publish) { + const live = await unchangedLive(local.store, cfg.siteId); + if (live) return { ...live, unchanged: true }; + } const snap = await local.store.createSnapshot(cfg.siteId, { actor: { kind: "human", id: "user_localcli0001", name: "cloudhal CLI" }, reason: options.publish ? "publish" : "manual", @@ -30,6 +41,18 @@ export async function snapshot(options: SnapshotOptions): Promise { } } +/** The live snapshot when the site has one and nothing is pending against it. */ +async function unchangedLive( + store: LocalStore["store"], + siteId: string, +): Promise { + const site = await store.getSite(siteId); + if (!site?.publishedSnapshotId) return undefined; + const d = await store.pendingChanges(siteId); + if (d.added.length || d.removed.length || d.changed.length) return undefined; + return (await store.getSnapshot(site.publishedSnapshotId)) ?? undefined; +} + /** The snapshot `cloudhal build` renders: the published one, else the latest, else a fresh one. */ export async function resolveBuildSnapshot(projectDir: string): Promise { const cfg = await readProjectConfig(projectDir); diff --git a/packages/cli/test/e2e.test.ts b/packages/cli/test/e2e.test.ts index 7a9e9de2..73e145b0 100644 --- a/packages/cli/test/e2e.test.ts +++ b/packages/cli/test/e2e.test.ts @@ -218,6 +218,9 @@ describe("cloudhal init → dev → snapshot → build", () => { } const snap = await snapshot({ projectDir, label: "v1", publish: true }); expect(snap.summary.added).toBeGreaterThan(0); + // #51: publishing again with nothing changed makes no new snapshot + const again = await snapshot({ projectDir, publish: true }); + expect(again).toMatchObject({ id: snap.id, unchanged: true }); const cfg = await readProjectConfig(projectDir); const local = await openLocalStore(path.join(projectDir, cfg.dataDir));