From 46a137316f4b6b2e87cabc4b46da36a70d849c1a Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Wed, 2 Sep 2026 15:52:44 +0200 Subject: [PATCH] faster incremental backups when multiple stacks share one repository --- src/lib/server/backups/backup-script.ts | 7 ++++++- tests/backups/backup-script.test.ts | 12 +++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/lib/server/backups/backup-script.ts b/src/lib/server/backups/backup-script.ts index e587be3..ff05061 100644 --- a/src/lib/server/backups/backup-script.ts +++ b/src/lib/server/backups/backup-script.ts @@ -114,7 +114,12 @@ export function buildBackupArgs(input: { swapArtifacts: readonly string[]; }): string[] { const paths = input.hasVolumes ? ['/volumes/', '/metadata/'] : ['/metadata/']; - const args = ['backup', '--json', '--retry-lock', '5m', '--host', input.host, ...paths]; + // Every stack backs up from the SAME host + paths (/volumes/, /metadata/), so restic's + // default parent selection (host,paths) picks the newest snapshot of ANY stack in the + // repo as the parent - making a stack look entirely new whenever another stack was + // backed up in between, forcing a full re-hash of its source (#1494). Group by the + // dockhand tags too so a stack's parent is its OWN previous snapshot. + const args = ['backup', '--json', '--retry-lock', '5m', '--group-by', 'host,paths,tags', '--host', input.host, ...paths]; for (const t of input.tags) args.push('--tag', t); for (const a of input.swapArtifacts) args.push('--exclude', a); for (const p of input.excludePatterns ?? []) if (p.trim()) args.push('--exclude', p.trim()); diff --git a/tests/backups/backup-script.test.ts b/tests/backups/backup-script.test.ts index 4a81dd8..d6d9122 100644 --- a/tests/backups/backup-script.test.ts +++ b/tests/backups/backup-script.test.ts @@ -12,10 +12,20 @@ describe('buildBackupArgs', () => { it('builds the core backup argv over /volumes and /metadata', () => { const a = buildBackupArgs(base); - expect(a.slice(0, 6)).toEqual(['backup', '--json', '--retry-lock', '5m', '--host', 'dockhand.local']); + expect(a.slice(0, 4)).toEqual(['backup', '--json', '--retry-lock', '5m']); + const hostIdx = a.indexOf('--host'); + expect(a[hostIdx + 1]).toBe('dockhand.local'); expect(a).toContain('/volumes/'); expect(a).toContain('/metadata/'); }); + it('groups parent selection by the dockhand tags so a stack parents its own snapshot (#1494)', () => { + // Without this, every stack shares host+paths (/volumes/, /metadata/), so restic picks + // the newest snapshot of ANY stack as the parent and re-hashes the whole source. + const a = buildBackupArgs(base); + const i = a.indexOf('--group-by'); + expect(i).toBeGreaterThanOrEqual(0); + expect(a[i + 1]).toBe('host,paths,tags'); + }); it('backs up ONLY /metadata when the target has no volumes (config-only snapshot)', () => { const a = buildBackupArgs({ ...base, hasVolumes: false }); expect(a).toContain('/metadata/');