Secrets migrate: ensure unique backup ids per write

This commit is contained in:
joshavant
2026-02-22 14:44:24 -08:00
committed by Peter Steinberger
parent a74067d00b
commit 8e439e2d81
2 changed files with 28 additions and 1 deletions

View File

@@ -201,4 +201,16 @@ describe("secrets migrate", () => {
const rolledBackEnv = await fs.readFile(envPath, "utf8");
expect(rolledBackEnv).toContain("OPENAI_API_KEY=sk-openai-plaintext");
});
it("uses a unique backup id when multiple writes happen in the same second", async () => {
const now = new Date("2026-02-22T00:00:00.000Z");
const first = await runSecretsMigration({ env, write: true, now });
await rollbackSecretsMigration({ env, backupId: first.backupId! });
const second = await runSecretsMigration({ env, write: true, now });
expect(first.backupId).toBeTruthy();
expect(second.backupId).toBeTruthy();
expect(second.backupId).not.toBe(first.backupId);
});
});

View File

@@ -129,6 +129,21 @@ function formatBackupId(now: Date): string {
return `${year}${month}${day}T${hour}${minute}${second}Z`;
}
function resolveUniqueBackupId(stateDir: string, now: Date): string {
const backupRoot = resolveBackupRoot(stateDir);
const base = formatBackupId(now);
let candidate = base;
let attempt = 0;
while (fs.existsSync(path.join(backupRoot, candidate))) {
attempt += 1;
const suffix = `${String(attempt).padStart(2, "0")}-${crypto.randomBytes(2).toString("hex")}`;
candidate = `${base}-${suffix}`;
}
return candidate;
}
function parseEnvValue(raw: string): string {
const trimmed = raw.trim();
if (
@@ -778,7 +793,7 @@ export async function runSecretsMigration(
}
const now = options.now ?? new Date();
const backupId = formatBackupId(now);
const backupId = resolveUniqueBackupId(plan.stateDir, now);
const backup = createBackupManifest({
stateDir: plan.stateDir,
targets: plan.backupTargets,