Releases: jakemorgangit/NineLives
Release list
v1.7.3
Six fixes since v1.7.2, all in the check that tells you which backups a container has not got. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
Four of the six made that check under-report: a backup declared present when it was not, or a container declared current when it was hours behind. A short list reads as an all-clear, which is the worst direction for it to be wrong in.
The whole history is read, with no cap anywhere
The instance's backup history was read as "the newest 500" — and not even that. The limit was applied after joining the table that holds one row per backup file, so it counted files rather than backups: a four-way striped setup got a quarter of what the number promised. The cut could also land inside a backup set, leaving an entry holding some of its stripes — which a RESTORE cannot use, and which this check reported as missing files that were neither missing nor beyond the limit.
Every caller feeds something a short answer quietly breaks: the restore chain, the CLI's inventory, and the missing-backups check itself. So there is no cap on any of them now. A database on a one-minute log schedule takes 1,440 backups a day; the old limit covered about eight hours, against containers routinely further behind than that.
Copied backups land where the app can find them
The check names the backups that never reached the container and writes you a script to copy them in. That script uploaded each file under its bare name, which puts it at the root of the container.
Nothing about the upload fails. But a blob's database and server are read back out of its path, by the container's own layout — so a file at the root belongs to no database, every question the app asks steps over it, and pressing "rescan and check again" reported none of them arrived after a copy in which every byte transferred.
The destination now follows the container's own pattern, worked out from what the instance recorded about each backup. The file keeps its name; only where it lands changes.
A backup is only "present" on evidence that it is
Matching is by LSN where both sides have one. Otherwise it falls back to type and timestamp within a few seconds — which is the ordinary path, because a container set only carries an LSN once it has been audited.
That tolerance assumed the timestamp came from the file name, where it is the backup's own start time. When the name cannot be parsed, the only time available is when the blob finished uploading — a different event, on a clock that can be hours out of position. Comparing the two as though they were the same kind of thing could match a backup against an unrelated one that happened to upload nearby, and report it as safely in the container when it was not.
An upload time now vouches for nothing. Auditing a container reads each backup's own header and settles it exactly; where that has not happened, the panel says how many sets it could not identify instead of quietly guessing.
"This container is N behind" can no longer disappear
The same reading was used to measure the gap, and the same two problems applied — with a worse outcome. An upload time always makes the container look more current than it is, and a UTC reading west of UTC runs ahead of the server's own clock. Both shrink the measured gap, and a gap that did not come out positive was not reported at all.
So a single such set could suppress the warning entirely, on a container genuinely hours behind. It is measured only from real backup times now, and where there are none it quotes no figure rather than a wrong one.
The button works
"Check its history" stayed disabled after choosing an instance, and the only way to wake it was to leave the screen and come back. It read a value nothing ever announced had changed, so the control kept the answer it was given while the list was still empty. It also greys out while a check is running now, so a second press cannot land on one already in flight.
Under the hood
2,291 tests. Every fix above was confirmed to fail against the unfixed code before the fix went in.
v1.7.2
Four fixes since v1.7.1. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
Every one of these was found by asking whether an already-fixed bug had siblings. It did. Three times.
The one worth upgrading for
The confirmation banner could name a different instance from the one the restore would run against.
The red panel above Execute is the last thing you read before an irreversible write, and it is worded the way it is for a reason: two hostnames can differ by a single character.
It named the instance the app last connected to. The restore ran against the instance selected as the target — and those are set in different places. Connect to SRV01 on the SQL Servers screen, come to Restore, change the target to SRV02 using the step this screen invites you to use, and arm: the banner said SRV01. The restore ran against SRV02.
Naming the wrong instance there is worse than naming none, because it actively reassures. The name now follows the target wherever it is set, and disconnecting clears it instead of leaving the last one standing.
Leaving a screen no longer throws away what you were doing
Navigating between screens re-reads the saved servers and containers, so that anything added elsewhere shows up. Each screen then re-selected what you had chosen — and could not tell that re-selection apart from you choosing something different, because the config hands back a fresh copy every time. So the screens did what they should do when you genuinely change your mind: they cleared.
On the Restore screen that meant the loaded backups, the timeline, the armed Execute, the missing-backup answer and its comparison — plus a fresh connection to the target instance, every visit. Reading a container of 98 headers takes minutes; glancing at Browse Backups to check a filename left "Load Backups" as the only thing on screen.
Worst on the missing-backup panel, because going away is how it is used: it names the transaction logs that never reached the container, hands you a script to copy them, and waits for you to come back and press rescan. Coming back was what cleared the answer the rescan compares against — so it reported everything as still missing however much had arrived.
On the Back Up screen it meant your database ticks. This is the screen for ticking twelve of forty databases before a patch window, and a glance at another screen emptied all twelve without saying anything.
Both now keep what you chose. Lists are still re-read, so a database or container created since your last visit still appears, and genuinely changing the server, container or source still clears everything — which is correct, and is why the clearing was there.
Finishing the missing-transaction-log check
The check that finds log backups which never reached your container now closes the loop. After you run the copy script it can answer the only question you actually have — did it work — instead of re-listing what is still missing. Five files still outstanding looks identical whether eighteen arrived or none did.
It counts by which files arrived rather than how many, so a retention job trimming an old log between checks is not mistaken for a successful copy, and backups taken since the last check are reported separately: nothing went wrong, there is simply more now.
Under the hood
Eleven new tests, each confirmed to fail against the unfixed code before the fix went in. 2,257 passing.
v1.7.1
Two new capabilities and two fixes since v1.7.0. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
The one worth upgrading for
A completed restore could surface as a crash. Reading the console to file a run's receipt was not thread-safe: the line collection could be enumerated while another thread was still appending to it, handing back a null and throwing. That happened inside the run's own finally block, unguarded, so it came out of the restore itself — the database restored, the target online, and an exception on screen.
It was found as a red build rather than by looking for it, which is the only reason it was found at all: it needs a particular interleaving, so it would have shown up on somebody's machine occasionally and never reproduced when they went looking.
The console now snapshots before reading and tolerates a torn one, and filing a receipt can no longer fail the thing it is filing — if the history cannot be written, it says so on the console and the restore stands.
Copy a database for a cutover, not just a refresh
A copy brought the target online. That is right for refreshing a test environment and wrong for a migration: online means no more log backups can be applied, so the copy is frozen at the moment it was taken, and the downtime is however long the full takes.
There is now an option to leave the target in RESTORING instead. With it ticked, the source can be asked which log backups it has taken since the copy — and where they went — with the statements to apply them. The long part happens in advance and the switch-over costs only the tail.
Off by default, because a database left in RESTORING is not usable and somebody refreshing a test environment wants the opposite. The screen says which it will be.
Both service accounts are proven against the shared folder, before the backup
The Copy screen has always said it: the source writes there as its own service account and the target reads there as its own — two different accounts, and routinely not the same one. What it also said, accurately, was that the target's read is checked after the backup, because the check ran on a file and there is no file until the backup has been written.
So a share the target could not read cost you the whole backup first. On a large database that is a long wait to be told something knowable in a second. The source's write was never checked ahead of time at all — it was discovered by the backup failing.
Both are now asked as themselves when the scripts are generated, before anything is written. The two verdicts are kept apart, because the fix for either is a permission grant to a specific account and one combined answer loses which one. It stays honest about what it proves: the folder is reachable by that account, not that a backup inside it will open — a share can be traversable and still refuse the read — so the file-level check after the backup remains the authoritative one.
The gap check can be found
The "what is this container missing" check shipped in 1.7.0 inside the restore options — behind a step you only reach after confirming a restore point computed from the very chain it exists to question. A feature whose whole point is telling you the chain is shorter than you think cannot require you to already suspect that.
It now sits in Select source, beside Audit these backups, reachable as soon as a database is picked.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.
Full detail in CHANGELOG.md.
v1.7.0
Three new capabilities and one fix reported from the app. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
The logs that never reached the container
The estate shape this is for: fulls and differentials go to blob, and the transaction logs go somewhere else — a local or cluster disk for throughput, or a share because log shipping already owns them.
Point Nine Lives at the container and it builds an honest chain out of what it can see. That chain stops at the last differential, and nothing tells you the logs exist, where they went, or that the restore on offer is discarding hours of recoverable time. Finding out requires already knowing to look, which is exactly what nobody has at 3am on a server they did not build.
The instance that took them knows. Under the advanced restore options there is now a source instance picker and a check: it reads that instance's own record, sets it against what the container holds, and names what is missing — the folder it went to, how many files, how big, over what window, and how far behind the container is in recovery time.
Then two ways out, because both are legitimate:
Bring them in. A PowerShell script to run on the machine that holds them — azcopy with an Az.Storage fallback for blob, aws s3 cp for a bucket. Every file is named individually rather than globbed: a wildcard over the folder takes other databases' backups, an unrelated job's output, and whatever is half-written at that moment.
It carries no credential. That would have been one string interpolation away, and the result would sit in a .ps1 on a production server. The SAS or key pair is a mandatory parameter supplied at run time — which is also the form that wires into a scheduled task without touching disk.
Or leave them where they are. The one to reach for mid-incident, when uploading twenty-three files is time you do not have. Fold them into the timeline and the restore point extends to the newest of them; one script then reads the container by URL and those logs from their own path by DISK. The target has to be able to open that path as its own service account — a different question from whether the container is reachable, and one now asked before anything is dropped.
Carrying a login across from the source
When a restored database holds a user whose SID matches no login here, the advice was to create the login first, with a password set by whoever owns that account. Honest — inventing one would be worse — and it still left you holding a database whose users could not log in, with no way forward that did not involve tracking down the account owner.
There is a third option: neither invent the password nor go asking for it. Fetch the real one.
Run the new script on the instance the backup came from and it prints the CREATE LOGIN to run here, carrying the original password hash and the original SID. Both halves earn their place — the hash means applications authenticate with the password they already have, and the SID means the restored user is not orphaned at all. Its SID already matches, so there is no ALTER USER afterwards and no window where its permissions are wrong.
It reads and prints; it executes nothing, on either server. DEFAULT_DATABASE, DEFAULT_LANGUAGE, CHECK_POLICY and CHECK_EXPIRATION travel too, and a login disabled on the source is re-disabled on the target rather than arriving enabled.
SQL logins only. A Windows login carries no password and takes its SID from Active Directory, so the target recreates it with CREATE LOGIN ... FROM WINDOWS and the SIDs match by construction — the script says so rather than quietly finding nothing.
The Copy screen tells you why it will not generate
Reported from the app, and a good catch: every visible field filled in, the red banner naming the database that would be overwritten, and both buttons dead with nothing on screen saying why.
The source database had been cleared out from under the form. That screen re-reads its server list on every visit and re-assigns the source server to a fresh object each time, which clears the database and reloads the list — while the target server, container, name and overwrite tick all survive. So navigating away and back left a form that looked complete, refused to work, and explained nothing.
Your own choice is now put back when the same server arrives again. Never when you switch servers: putting back what somebody chose is not the same as choosing for them, and inventing a source database is the one thing that screen must not do — the wrong choice reads a production database at full speed and overwrites one on another server.
And six things have to be true before Generate is live. The screen named none of them; it now names the first one missing, along with the step to go back to.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.
Full detail in CHANGELOG.md.
v1.6.4
Five fixes on top of 1.6.3, all from a code review of the audit-trail work that shipped in 1.6.3. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
The window stops freezing during a backup
Writing a receipt is synchronous file I/O: a cross-process lock whose backoff sleeps up to ten seconds when a scheduled 9lives run holds it, a whole-file read, a redaction pass over every script and log, then a serialize and a file replace. The screens did all of that on the UI thread, once per database — so a fifty-database backup did fifty whole-file rewrites there and the window could stop repainting for minutes.
Worse, the same write sat in the cancellation handler. Pressing Stop — the one control whose entire purpose is to respond immediately — froze the app instead of stopping the run.
Receipts now go off the dispatcher, and are still awaited one at a time. That last part is deliberate: a run that dies on the sixth database leaves the first five receipts on disk, and that half-finished run is exactly the incident somebody opens the history for. Batching the run into a single write was the alternative and trades that away.
Recovery actions leave a trace
The panel that appears after a restore has failed runs RESTORE ... WITH RECOVERY and DBCC CHECKDB against a production database. It is the highest-stakes statement this app sends, at the worst possible moment — and it wrote nothing to the history at all. An incident write-up showed the failed restore and not the statement that brought the database back.
All three endings are now filed, cancelled included: "I stopped it and the database was left alone" is exactly what a change ticket needs, and the panel said that only on screen, where it scrolls away. Recovery is also offered in the History screen's filter, so the receipts can actually be found.
A differential's file name no longer claims COPY_ONLY
COPY_ONLY is the default for backups this app takes, and the marker went into every destination name — but the generated statement omits the keyword on a differential, and rightly so: there is no copy-only differential, because what COPY_ONLY protects is the differential base and a differential does not move it.
The marker is load-bearing. The listing reads it back out of the name to classify what it finds. Backups already written keep their names and are still read correctly; this changes what gets written from here on.
Two internal fixes, and why they are in these notes
Neither changes anything you can see, but one of them is the reason a real bug reached a released build once already.
The config-store test double now behaves like the real one. The real store re-reads and deserializes on every call, so callers get fresh objects; the fake handed back cached instances. That single difference meant a screen which rebuilds its list and reselects by id saw "no change" under test and a genuine change in production — so every visit to the Backup or Copy screen silently opened a connection to a production instance and wiped the user's selections, with a fully green suite throughout. A test double that diverges from production does not merely fail to catch a bug; it makes that bug invisible.
The Restore screen can no longer default its history store to the real file. It was an optional argument defaulting to the installed app's own audit trail, and this screen hands it to the thing that writes — so one forgotten argument meant a test appending invented runs to a real history, with the entry cap trimming genuine receipts to make room. Filed as latent; it turned out one test file was already passing nothing at all.
Full detail in CHANGELOG.md.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.
v1.6.3
One fix, reported from the field within hours of the 1.6.2 announcement. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
Pressing Stop on a recovery step said the step had failed
A restore has already failed. You are on the recovery panel, RESTORE ... WITH RECOVERY is running against a large database, and you press Stop. What came back was:
FAILED at HH:mm:ss: A severe error occurred on the current command.
It had not failed. It had done exactly what you asked — and it said otherwise at the one moment somebody is trying to recover a broken database and needs to know what state it is actually in. That panel exists to tell you the truth about a database when things have already gone wrong, so this was the worst possible place to get a word wrong.
Why it happened. SqlClient reports a command cancelled mid-flight as a SqlException — "A severe error occurred on the current command" — not as the cancellation the panel was watching for. The panel had a correct handler saying "the database is in the same state as before this step", and execution walked straight past it into the general failure handler. The method behind that panel was written after the three call sites that carry the translation, and never got one.
The fix, in two places. The service now translates a cancelled command for every caller. The panel additionally trusts the token directly: whatever the driver throws on the way out, if you pressed Stop then what happened is that you pressed Stop. Both guards read the token and never the message — so a genuine severe error still reports as the failure it is. The same mistake pointing the other way would be worse: it would tell you your database was untouched when a recovery step had just failed against it.
The one other method of the same shape — public, CommandTimeout = 0, takes a cancellation token — has been given the same guard before it can matter, and the remaining execute paths audited.
Full detail in CHANGELOG.md and #427.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.
v1.6.2
Twelve fixes on top of 1.6.0/1.6.1. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
Two of these were reported from the app while the release was being prepared; the rest came out of a systematic review of the screens and the engine.
The ones that could cost you something
Execute stayed live while a restore was running. The button took its enabled state from the same property that supplies the "why you cannot press this" sentence — and that sentence is deliberately empty during a run, because mid-restore the control anybody wants is Stop. Empty read as "not blocked". Two presses armed the button and called the run again, and the first thing that does is begin a new cancellation: the restore in flight abandoned mid-chain, its target left in RESTORING, and the whole thing started from the top. The Backup and Copy Database screens both had this right already; this was the one where WITH REPLACE has already dropped the target.
Connect announced a different server from the one it proved. A connection attempt is exactly the operation slow enough for somebody to click another entry in the list beside it. Every line after the await re-read that selection, so the app proved it could reach one server and then marked the other connected — handing it out as the connected server, which is the object the restore screen executes against. It also wrote the proved server's version banner onto the other one's saved entry, where it persists and drives the version preflights.
The exposure dashboard judged one clock against another. msdb records a backup's finish time in the instance's local time; the sweep compared it against the clock of the machine running the app. The log warning threshold is one hour, so any offset measured in hours decided the verdict outright. A server behind the app raised false alarms on healthy databases; a server ahead of it made backups look newer than they were — downgrading an alarm to a warning, and where the offset exceeded the real age, going negative and showing green. Each server is now asked for its own clock in the same query.
An S3 listing could loop forever. The paging loops stopped when the continuation token was null, but an XML element that is present and empty reads as "", not null. A provider that writes <NextContinuationToken/> on the last page instead of omitting it sent the loop back for another page with an empty token — which S3 reads as start from the beginning — re-listing the whole bucket until the process ran out of memory. AWS omits the element, so this never fired against S3 proper; it is the S3-compatible providers this feature exists for where that detail differs.
Recovery actions could be started on top of each other — on the panel you are looking at after a restore has failed, where pressing the second button abandoned the RESTORE ... WITH RECOVERY you were waiting on.
Screens that said the wrong thing
- The Restore screen claimed there were no restore points before anything had been loaded — a red banner about a missing full backup, when the truth was that nobody had pressed Load Backups yet. It said it twice, too.
- Choosing a target never connected to it. Step 2 offers "otherwise pick a saved server here" as the alternative to connecting on the SQL Servers screen, and set everything except the flag that gates Execute — so every step ticked green and the button told you to go to another tab and do what you had just done. It now attempts the connection and says what happened, including the reason if it failed. A failure never blocks generation: producing a script for an instance this machine cannot reach is exactly what Save to File, Copy as Agent job and Export runbook are for.
- Four screens showed errors but never confirmations. A config import — a bulk operation over your server and container lists — reported what it had added and updated to nobody. An export's "the file holds no secrets" was never shown, which is the one thing worth knowing before emailing it to a colleague.
- A fresh install told you to select things that did not exist, including Browse Backups asking for a container when none were configured.
- Every container had a green dot whether or not it had a credential. That bites hardest right after a config import, since export carries no secrets by design — so a whole list came up green and could reach nothing.
- Verify Last Backup had no busy state, and a second press cancelled and restarted it.
- Ticking WITH MOVE cancelled a running chain verification.
Also
The saved execution log was going out unredacted; free space is reported on every restore rather than only after an optional button; the script verb can relocate; the CLI's exit-code contract is now checked against the code that implements it.
Full detail in CHANGELOG.md.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.
v1.6.1
Fixes on top of 1.6.0, plus two small additions. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
Four defects that generated wrong T-SQL
A backup set with no files generated a restore that ends the sequence. A set discovered with nothing recorded against it produced RESTORE DATABASE [x] WITH RECOVERY naming no device. That is not a syntax error — it is the valid recovery-only form, so nothing failed at run time. Against a target sitting in RESTORING (a log-shipping secondary, a paused restore) it brought the database online wherever it had reached and ended the restore sequence permanently: no further log could be applied and the whole chain had to start again. The validator now reports it, the restore screen explains it, and the generator refuses to emit it.
A striped set missing its first file passed validation. The stripe check returned early on a single-file set, so a set holding only ..._2.bak — stripe 1 purged by retention, or never uploaded — passed clean, while the same set as stripes 2 and 3 was correctly flagged. It was blind precisely at its worst case. The question is now asked of the stripe numbers rather than the file count.
One striped set written across two folders was offered as two restore points. Multi-directory striping arrived as two sets at the same instant, each holding half a media set and each looking complete. A chain that should have offered four restore points offered seven, every one of them restoring from half a set. The halves are now recognised by their stripe numbers; two genuinely complete backups sharing a timestamp are left alone as the warning they are.
KEEP_REPLICATION and the broker options rode on NORECOVERY statements. SQL Server refuses that combination outright — Msg 3031 — so the first statement failed and the option was unusable for any chain longer than one statement, which is every log-shipping and replication scenario it exists for. They now go only on the statement that recovers, which is deliberately not the same as the last statement.
A saved log that was going out unredacted
Save output on the execution console wrote the raw console text. The method that adds a header naming the server, the target and the outcome — and runs the result through the redactor, with a comment explaining that this file gets attached to tickets — existed, was documented, and was called from nowhere. SAS signatures in the console were going out in saved logs.
Screens that said nothing, or the wrong thing
- Browse Backups had no error banner and no status line, so an expired SAS, a 403, a DNS failure, a genuinely empty container and never having pressed the button all left the identical screen. Pressing Load Backups with nothing selected was a button that visibly did nothing.
- Connect on the SQL Servers screen wrote its failure to a banner that only renders while the edit form is open — the form its own button is hidden behind. On the last step of first run, a typo'd instance flashed "Connecting..." and returned to a screen with nothing on it, while Test, one button away, explained itself properly.
- The Backup and Copy consoles vanished when the run ended, taking SQL Server's own account of the failure with them. On the copy screen that console is where the literal
RESTORE ... WITH RECOVERYstatements needed to get the target out of RESTORING are printed — so the app hid them, and the only way back was to re-run a production operation. - A container that answered and held nothing reported "Connected! 0 files found (0 B)" in the voice of a tick. It is the commonest new-user failure there is, and the cost of dressing it as success is an inexplicably empty Browse Backups two screens later.
- Ctrl+0..9 stayed live behind the first-run mode cards, landing on a screen with the sidebar collapsed to zero width, no mode chosen and no way back — an app that had to be restarted.
- The saved-containers list turned white while the Add Container form was open, in every theme, because a disabled
ListBoxtakes a system colour while its rows keep drawing their text in white.
Added
scriptcan relocate.--target,--relocate,--data-pathand--log-path, so generate-here-hand-over-run-there stops silently dropping its WITH MOVE clauses. Relocation needs an instance to ask — the logical file names come from FILELISTONLY and the default directories from the instance — so those flags are refused without a--targetrather than quietly ignored.- The About screen links to the documentation, the issue tracker and the releases. Until now the only hyperlink anywhere in the app was blackcat.wales.
Also
Free space is reported on every restore rather than only after an optional button. SECURITY.md no longer claims Entra ID is unsupported, and lists all four credential-vault prefixes rather than two. The README front-loads all three storage media and finally documents the S3 key pair's permissions and the IDENTITY = 'S3 Access Key' credential. The four v1.0.0-era screenshots are reshot. And the CLI's exit-code contract is now checked against the code that implements it — the gap that let two exit-code bugs survive.
Full detail in CHANGELOG.md.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.
Nine Lives v1.6.0
Two things in this release are not increments: a second front end and a second storage provider. Both have been documented in the README for a while and existed in no downloadable build until now.
S3-compatible object storage (#51)
An s3:// container is just another entry in the containers list. AWS S3, Wasabi, Backblaze B2, Cloudflare R2, and appliances speaking the S3 API.
The URL's own scheme picks the provider, so nothing else has to be kept in sync and every existing config migrates by doing nothing. The credential is the pair AccessKeyId:SecretKey - which is literally the engine's own CREATE CREDENTIAL secret format - so it rides the existing credential pipeline unchanged, with a shape check at entry rather than an authentication failure at restore time.
Browsing a bucket uses a hand-rolled SigV4 signer and a minimal ListObjectsV2 client, pinned stage by stage against AWS's published test vectors. No SDK: the alternative was the largest package in the application in exchange for one HMAC chain.
Restoring from S3 needs SQL Server 2022 or later and is not available on Express. The app refuses before anything is dropped rather than erroring on the server afterwards.
Not yet exercised against a live bucket. Everything is verified offline - published signature vectors, a scripted endpoint, and a large set of tests - but the first real-world report will be the interesting one. The failure explainer turns the provider's own error codes into the next move, which should make that report a short one.
The CLI: 9lives.exe
9lives.exe ships beside the app. The same engine from a terminal, against the same configured containers, servers and credentials: read verbs (list, points, script, validate, exposure), execution verbs (restore, rehearse, backup), and provisioning verbs (add-server, add-container) so a freshly built VM can be configured with nobody at a screen.
--ephemeral resolves an estate entirely from environment variables, persisting nothing, for service accounts and CI agents. --json makes the endings machine-shaped. The full reference lives in 9lives help VERB, held to the parser by tests so it cannot drift.
A front door (#343)
Choosing a mode now lands on Home: what the application is, the three get-going steps as buttons, and pointers to the rest of what the chosen mode offers. Reachable any time from the sidebar, or Ctrl+0.
Correctness fixes worth naming
A code review before this release found several defects that could each have cost somebody a restore. All are fixed here:
- A striped backup read from an instance's own history was declared corrupt and the restore refused. msdb records a backup's size once per set rather than per stripe, and zero was being read as "failed upload" - so this fired at DR time, on precisely the large databases that get striped.
- A restore chain could take one server's full backup and another server's logs. Two servers backing up a same-named database to one container is the everyday DR pair; chains are now built per database and instance.
- Restore receipts could destroy each other. A writer that could not take the history lock wrote anyway, dropping whatever the other process had committed in between.
- A backup set id could break out of the comment naming it, leaving the remainder of the line standing as an executable statement.
restoreandrehearsenever created the server-side credential they authenticate with, so the documented provisioning template failed on its last line.rehearseran no preflights, so a misconfigured rehearsal host reported NOT PROVEN - which reads as "this backup is bad" when the backup was never in question. Blocked and disproven are now different answers.- A backup destination longer than SQL Server's 259-character limit is refused before the run rather than discovered mid-backup.
Also
The screens no longer assume Azure: a saved container is a saved container, whichever provider answers it. Failures in the early restore steps are shown rather than reaching only a grey status line. Editing a container can no longer save over a different one. The CLI's exit codes stopped contradicting their own documentation, and --keep-sessions exists for restores that must leave other connections alone.
The full list is in CHANGELOG.md.
Downloads
NineLives.exe and 9lives.exe for x64, -arm64 variants for Windows on ARM, zips of each pair, and SHA256SUMS.txt. Every asset carries build provenance:
gh attestation verify NineLives.exe --repo jakemorgangit/NineLives
The binaries are unsigned, so SmartScreen will warn on first run - see the README for what that is and how to check what you have downloaded.
v1.5.0
The proof release. v1.4.0 made Nine Lives a full backup-and-restore orchestrator with safety nets; 1.5.0 makes it prove things — that backups restore, how exposed every database is right now, and that the person responsible hears about it wherever they are. 1,542 tests, up from 1,485.
Proof, not hope
- Restore rehearsal — one button restores the chosen chain to a scratch database, proves the data with
DBCC CHECKDB, and drops the scratch copy. The History entry is the receipt an audit asks for, and the duration is your real RTO number. Safety by construction: generated name refused if it exists, neverWITH REPLACE, every file relocated, and the guarded DROP runs last so any failure retains the evidence (#238) - The Exposure dashboard — every user database on every configured server: "if this server died now, everything after 14:32 is gone — up to 47m of work", traffic-lit worst-first. Never-backed-up, FULL recovery with no log backups, stopped chains — and an unreachable server is itself an alarm, because unknown is not the same as fine. Rehearsal receipts appear as a Proven column (#239)
- Run notifications to Teams, Slack, or any JSON endpoint — when a backup, restore or copy starts, finishes, or hits a problem, including per-database failures in a multi-database backup at the moment they happen. Teams MessageCard (connectors and Power Automate), Slack Block Kit, and plain JSON. Webhook URLs are treated as the secrets they are and never leave the machine in a config export (#242)
For the worst day
- Export a restore runbook — one self-contained Markdown document per restore point: the chain file by file, prerequisites in worst-day order (credential, TDE certificates by thumbprint, disk space), the exact script, what to do when it stops part-way, what finishes the job (#240)
- The retention referee — what a keep-N-days rule keeps, deletes (with bytes reclaimed), must keep despite its age (base fulls, bridge logs), and what is already broken. Report-only: deleting stays a human act (#241)
- STOPATMARK — restore to a named transaction discovered from
logmarkhistory, stopping just before it by default. The mark is the target, not a clock time reconstructed from chat messages (#243)
Sharpened
- One console. The run's output lives in its window, and only there — a "View the last run's output" button reopens the full record (previously every line rendered twice)
- Every restore option in every mode — WITH MOVE, point-in-time, the advanced options: modes narrow which screens exist, never which restore options do
- The Generate Script button is gone — the script builds itself live; an empty pane says why, where the eye already is
- CHECKDB shows progress and speaks its outcome on the finishing panel — a bar from the server's own
percent_complete, and "found nothing wrong — the restore is proven, not just finished" stays on screen after the console scrolls on - The metadata inspector now states the taking version and the backup's protection — TDE, encrypted, or "Not encrypted", because absence is information too (#222)
Fixed
- The orphaned-user scan crashed with a collation conflict on cross-collation restores — both sides of the login-name comparison are now forced to one collation
- A drive the target does not have was described as "0.0 B free"; absent is now said as absent, with the fix named (MOVE, or a different target)
Full detail in the changelog.