Parse the deployment summary report
You just walked Failures.log line by line to name a broken tenant, then cross-referenced its checkpoint to see how far it got. That’s a sharp read for one incident. But it’s still a read — you, scrolling text, doing the correlation by eye. Now put yourself in your release manager’s chair: they don’t want the story, they want the receipt. Every target’s fate, every millisecond, every verified change to the schema — in one file a script can parse without you in the loop.
Hey folks. I’m Forge Barrett, master of the Content Forge here at SchemaSmith.
Module 5 taught you to hunt a failure through logs and checkpoints. Today we open the file that was sitting there the whole time — SchemaQuench - Summary.json — and read it the way a dashboard would: outcome first, then where the time went, then exactly what changed. Let’s fire it up.
One report, two shapes, every exit path
Section titled “One report, two shapes, every exit path”Every quench writes this. You don’t switch it on, and you don’t have to ask for it — it lands next to your logs whether the run succeeds, partially fails, or hard-aborts. Two files, one model: Summary.json for machines, Summary.md for humans, both drawn from the same in-memory data, so they never disagree.
The lab’s fleet run pins the report at a chosen path for the sandbox to pick up, and tightens the bottleneck bar way down so a five-tenant run still surfaces a long pole worth looking at:
schemaquench --ConfigFile:quench.settings.after.json --report ./out/deploy-summary --BottleneckThresholdMs=800--report ./out/deploy-summary writes ./out/deploy-summary.json and ./out/deploy-summary.md — no extension in the path you give it, SchemaSmith appends both. Skip the switch on a normal run and the pair falls back to SchemaQuench - Summary.json / .md right beside Progress.log and Failures.log, archived as part of the same log bundle. One command, one receipt, no separate step to remember.
run — the fleet’s verdict in five fields
Section titled “run — the fleet’s verdict in five fields”Open the JSON and the top is the verdict, not the play-by-play:
"run": { "product": "Shop", "platform": "SqlServer", "durationMs": 5295, "mode": "Quench", "outcome": "PartialFailure", "exitCode": 2, "resumedFromCheckpoint": false}outcome: "PartialFailure", exitCode: 2 — that’s the same verdict Template 'Main' had 1 failed work unit(s) gave you at the terminal, except now it’s a field you can branch on in a pipeline instead of a string you’d have to grep. mode: "Quench" confirms this was a real deploy, not a WhatIf dry run. Just over five seconds wall-clock for the whole fleet.
targets[] names every tenant the run touched and how each one landed:
| Database | Outcome | Duration |
|---|---|---|
fleet_tenant_001 | Success | 2930ms |
fleet_tenant_002 | Success | 2609ms |
fleet_tenant_003 | Failed | 4551ms |
fleet_tenant_004 | Success | 4624ms |
fleet_tenant_005 | Success | 2882ms |
Four green, one red — fleet_tenant_003, the one seeded with duplicate emails before this run. Where Failures.log made you count FAILED to quench: lines, targets[] just hands you the tally: filter on outcome != "Success" and you’ve got your failed set, no text-scanning required.
timing — where the milliseconds went, and the bottleneck bar
Section titled “timing — where the milliseconds went, and the bottleneck bar”timing.bySlot rolls every target’s per-slot time into one bucket per convergence step:
"bySlot": [ { "slot": "ModifiedTables", "totalMs": 12150, "targetCount": 5 }, { "slot": "ForeignKeys", "totalMs": 104, "targetCount": 4 }, { "slot": "TableDataDelivery", "totalMs": 12, "targetCount": 4 }, { "slot": "IndexesAndConstraints", "totalMs": 1270, "targetCount": 4 }]Read the targetCount column before you read the milliseconds. ModifiedTables shows 5 — every tenant reached it. The other three slots show 4 — only the tenants that made it past the phase where fleet_tenant_003 died. One column, and you already know how deep the failure cut without opening a single log.
timing.bottlenecks lists the individual slot-on-a-target measurements that ran long enough to flag — anything exceeding BottleneckThresholdMs. Default is 30 seconds; on a five-tenant lab run nothing gets close to that, so the lab dials it to 800:
"bottlenecks": [ { "scope": "[localhost,11433].[fleet_tenant_003]", "slot": "ModifiedTables", "durationMs": 3401 }, { "scope": "[localhost,11433].[fleet_tenant_004]", "slot": "ModifiedTables", "durationMs": 3444 }, { "scope": "[localhost,11433].[fleet_tenant_001]", "slot": "ModifiedTables", "durationMs": 1835 }, { "scope": "[localhost,11433].[fleet_tenant_005]", "slot": "ModifiedTables", "durationMs": 1764 }, { "scope": "[localhost,11433].[fleet_tenant_002]", "slot": "ModifiedTables", "durationMs": 1706 }]Every single bottleneck is the same slot: ModifiedTables — the new table, the widened column, the new column. That’s the long pole on this run, named five times over, once per tenant. On a real fleet with hundreds of targets, this is the list you’d actually read; on this lab it’s proof the threshold is doing its job. Raise BottleneckThresholdMs on a heavy release where a minute a slot is normal, lower it here to catch a five-tenant lab actually working.
failures[] — the same story, now data
Section titled “failures[] — the same story, now data”This is the same shape of read Failures.log gave you in Module 5 — a duplicate key, a named tenant, a phase — now sitting in failures[] as data instead of text (the contextTail phase trail is trimmed to its key line here for space):
"failures": [ { "phase": "Template:Main", "scopeKey": "[localhost,11433].[fleet_tenant_003]", "error": "The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.Customer' and the index name 'UQ_Customer_Email'. The duplicate key value is (dupe@shop.example).", "contextTail": [ "Creating index [dbo].[Customer].[UQ_Customer_Email]" ], "artifactPath": "./artifacts\\SchemaQuench - Quench Indexes localhost,11433.fleet_tenant_003.sql" }]Same error text, same phase trail in contextTail, same artifactPath pointing at the resolved-SQL evidence — no new exposure, just the identical content the log gave you, now keyed and parseable. One entry per failed scope; an empty array on a clean run. If you built tooling around Failures.log, failures[] is that tooling’s structured twin.
objectChanges — what actually happened to your schema
Section titled “objectChanges — what actually happened to your schema”Timing tells you where the run spent its seconds. objectChanges tells you what it did. This is the section a DBA reads after a release, and it draws a hard line between changes SchemaSmith verified against the engine and scripts it merely ran:
"objectChanges": { "instrumented": true, "created": { "tables": 5, "columns": 5, "indexes": 4, "constraints": 4, "foreignKeys": 0, "procedures": 0, "views": 0, "functions": 0 }, "modified": { "tables": 0, "columns": 5 }, "dropped": { "tables": 0, "indexes": 5, "constraints": 0, "foreignKeys": 0 }, "scriptsRan": 5}instrumented: true is the honest per-run marker — it means the audit read succeeded and every count below is real, drained from the engine’s own record of what it did. (It only flips to false, all-zeros, when the audit can’t be read — and even then it’s telling you unknown, not nothing happened.)
Read these counts as fleet-wide totals, not per-tenant. tables: 5 isn’t five different tables — it’s dbo.ShipmentEvent, created once on each of the five tenants. created.columns: 5 is one new column, Customer.Region, added once per tenant. modified.columns: 5 is a different column, Customer.FullName, widened NVARCHAR(200) to NVARCHAR(300), once per tenant. dropped.indexes: 5 is the retired IX_Customer_FullName, dropped once per tenant. scriptsRan: 5 is the vw_ActiveProducts view, re-applied once per tenant. One change, times five targets, every time.
Now look at the two counts that don’t say 5: created.indexes and created.constraints both sit at 4. That’s UQ_Customer_Email and PK_ShipmentEvent — and both land on 4 for the same reason. fleet_tenant_003 reached the indexes-and-constraints phase, started building the new unique index over its seeded duplicate emails, and died right there. Two buckets short by exactly one, both belonging to the one tenant that never finished that phase. The counts alone name the failure before you’ve read a single error message.
“Ran” isn’t a euphemism for “we don’t know.” Object scripts — procedures, views, functions — get re-applied idempotently on every run, whether or not their body changed. So SchemaSmith refuses to guess: it won’t call a re-applied view “created” or “modified” when all it honestly knows is that the script executed. created.views sits at 0 even though vw_ActiveProducts ran on every tenant — that’s not a bug, it’s the report telling the truth. The view’s five executions show up as scriptsRan: 5 instead, and as "action": "ran" rows in details[]. Reporting a re-applied script as “modified” every run would make every report look busier than it really is.
created.columns and modified.columns count different work. Both read 5 here, and they’re two different columns. created.columns is Customer.Region — the new column this package adds, once per tenant. modified.columns is Customer.FullName — an ALTER of a column that was already there, once per tenant. One bucket is what the package added, the other is what it reshaped, and a release that does both shows both. Each column also gets its own row in details[], tagged "created" or "modified" to match.
details[] — the itemized list, one tenant’s worth
Section titled “details[] — the itemized list, one tenant’s worth”Where the counts are the summary, details[] is the itemized trail — one row per recorded change or run. Here’s a full tenant’s set from the certified run, trimmed from the 33-row array (four tenants clear all seven steps; fleet_tenant_003 stops at five):
{ "objectType": "constraint", "objectName": "[dbo].[ShipmentEvent].[PK_ShipmentEvent]", "action": "created" },{ "objectType": "index", "objectName": "[dbo].[Customer].[UQ_Customer_Email]", "action": "created" },{ "objectType": "column", "objectName": "[dbo].[Customer].[FullName]", "action": "modified" },{ "objectType": "index", "objectName": "[dbo].[Customer].IX_Customer_FullName", "action": "dropped" },{ "objectType": "column", "objectName": "[dbo].[Customer].[Region]", "action": "created" },{ "objectType": "table", "objectName": "[dbo].[ShipmentEvent]", "action": "created" },{ "objectType": "view", "objectName": ".\\after\\Templates\\Main\\Views\\vw_ActiveProducts.sql", "action": "ran" }Notice what’s missing from the row shape: no scope, no database, no tenant tag anywhere. details[] is one flat list for the whole fleet — it tells you what happened and, by counting rows, how many times, but not where. To pin a row to a tenant you go back to targets[] and failures[], exactly like you just did to explain why created.indexes reads 4 instead of 5. The counts and the detail rows are two views of the same audit; neither one alone tells the whole story.
Check yourself: objectChanges.created shows tables: 5 but constraints: 4 and indexes: 4. What does that gap tell you, and which file in the report would you check to confirm it?
A fleet-wide count that’s short by exactly one for two buckets at once — created.constraints and created.indexes — points at a single tenant that reached the phase where those two objects get built (indexes and constraints, in this lab PK_ShipmentEvent and UQ_Customer_Email) but never finished it. tables: 5 staying at the full count confirms every tenant got at least that far — the gap starts one phase later. To confirm which tenant, check targets[] for the one with outcome: "Failed" (here, fleet_tenant_003), or go straight to failures[], which names the same tenant, the same phase, and the engine’s own error. The counts tell you that something didn’t finish; targets[] and failures[] tell you which target and why.
Two files, one model — pick your reader
Section titled “Two files, one model — pick your reader”Everything above came from Summary.json. Summary.md carries the identical facts, rendered to read at a glance instead of parse:
## Object Changes- Created: tables=5, columns=5, indexes=4, constraints=4, foreignKeys=0, procedures=0, views=0, functions=0- Modified: tables=0, columns=5- Dropped: tables=0, indexes=5, constraints=0, foreignKeys=0- Ran (object scripts): 5Same numbers, same source, different reader. Point your CI parser at the JSON, point a human at the Markdown, and trust that neither one is ever telling a story the other one contradicts — they’re written from the same in-memory model in the same pass. For every field’s full contract — whatIf, migrationScripts, engine-specific details[] object types, and how the shape holds across SQL Server, PostgreSQL, MySQL, and MariaDB — the Deployment Summary Report reference is the authoritative word. This lesson taught you to read it; that page is what to keep open while you do.
Reading the receipt
Section titled “Reading the receipt”Failures.log names who broke. Summary.json names everything — every target’s fate in targets[], where the run’s milliseconds went in timing, the same failures as structured data in failures[], and — the centerpiece — exactly what changed against your schema in objectChanges, with an honest line drawn between what SchemaSmith verified and what it merely ran. Read the outcome first, then the counts, then let a discrepancy in the counts point you at the target that needs a closer look. That’s the whole method: one file, no scrolling required.
A smith doesn’t judge a batch by walking past the anvil and eyeballing the pile. He weighs it, counts the pieces, and knows before he picks up a single one which came out true and which came out short. The summary report is that scale. Read the counts, trust the gap they show you, and go straight to the piece that needs the hammer.
Got a Summary.json from your own fleet that doesn’t add up the way you expected? Email me at forgebarrett@schemasmith.com — send me the objectChanges block and I’ll help you read what it’s telling you.
Until then, may your counts always add up, and your gaps always point straight at the fix.
— Forge