From 6f002de05330c25136dca39aa600165fe6341b57 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:49:44 +0300 Subject: [PATCH] fix(cli): snapshot --publish skips when nothing changed since the live snapshot Re-running a setup script made a new snapshot every time, padding the history. With --publish and no pending changes against the live snapshot (the store's pendingChanges, which ignores the site node's publish pointer), the command now returns the live snapshot and prints "nothing changed since (live); no new snapshot". Closes #51 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/snapshot-unchanged.md | 5 +++++ packages/cli/src/cli/commands-local.ts | 4 ++++ packages/cli/src/commands/snapshot.ts | 29 +++++++++++++++++++++++--- packages/cli/test/e2e.test.ts | 3 +++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .changeset/snapshot-unchanged.md 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));