CI: actually run the kit in smoke tests, on oldest and newest SQL Server - #4047
Conversation
The smoke test installed only the changed sp_*.sql and ran it with @VersionCheckMode = 1, which is an unconditional early RETURN. No check body ever executed, so runtime breakage passed green -- PR #4045 had two such bugs reach human review. Now, per issue #4046: - Runs all 12 non-deprecated scripts through a 39-step parameter matrix covering the @output* table paths, sp_BlitzIndex modes 0-4, sp_kill's execute path and sp_DatabaseRestore. sp_BlitzUpdate is excluded because it rewrites the procs mid-run. - Installs the base branch's copy of every script, runs the same matrix, and compares. SQL errors this branch introduces fail the build; errors already present on base are reported and do not. Differences in sp_Blitz findings are printed for review and never fail, since changing a finding is often the point of the change. - Seeds a database, indexes, backup history and plan cache activity so the scripts have something real to read instead of an empty server. - Runs against SQL Server 2017 (oldest in support since 2016 aged out on 2026-07-14) and SQL Server 2025. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
This PR strengthens the SQL Server smoke-test CI by running the full (non-deprecated) First Responder Kit against seeded, realistic state, across both the oldest supported and newest SQL Server versions, and comparing base vs head to only fail on newly introduced SQL errors.
Changes:
- Expands the GitHub Actions workflow to run a SQL Server version matrix (2017 + 2025) with
fail-fast: falseand longer timeouts. - Adds CI SQL scripts to seed data and run a step-split execution matrix covering the kit’s main procedures and key parameter paths.
- Reworks the smoke-test runner to install and execute the kit for base and head revisions, diff results, and fail only on newly introduced SQL errors.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| .github/workflows/sql-server-smoke-tests.yml | Runs smoke tests across SQL Server 2017/2025 images with full git history for base vs head comparison. |
| .github/scripts/smoke-test-seed.sql | Seeds a realistic database, activity, and backup history for meaningful procedure execution. |
| .github/scripts/smoke-test-matrix.sql | Defines the 39-step execution matrix split into independently attributable steps. |
| .github/scripts/run-sql-server-smoke-tests.sh | Installs/runs base + head kits, splits and executes the matrix, captures findings, and enforces “new errors fail” logic. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
BrentOzar
left a comment
There was a problem hiding this comment.
The expanded matrix is a strong direction, but the current successful workflow run still records seven failed head steps on each SQL Server version. I found five correctness gaps that currently leave several advertised paths untested or can mask regressions.
All six findings from the first run's review: - Add sqlcmd -I so procedures install with QUOTED_IDENTIFIER ON. Without it sqlcmd's OFF default is captured at CREATE time and all three sp_BlitzFirst and both sp_BlitzLock steps died with Msg 1934. Every real client has it ON, so OFF was testing a configuration nobody runs. - Compare a normalized error signature per step, not just the failing label. Matching on the label alone turned every step failing on base into a permanent allowlist entry: a PR could introduce an unrelated error inside one and stay green. Server name and line numbers are stripped so a shifted line is not mistaken for a new error. - Install Ola Hallengren's CommandExecute, which sp_DatabaseRestore refuses to run without. Fetched in the workflow and checksum-verified, so an upstream change fails the build instead of silently running different code. - Constrain the sp_BlitzPlanCompare lookup to a marker query run in FRKSmokeTest's own context and filtered by the plan's dbid. The outer batch runs in master and contains the same marker text, which is what made the previous lookup pick a plan @DatabaseName could not match. Raises an error now if no plan is found, rather than skipping quietly. - Exclude CheckID 156 from the findings comparison. It embeds GETDATE() in Finding, so it reported one removal and one addition on every run. - Drop BlitzFirst_WaitStats_Categories and sp_BlitzLock's synonyms between passes, so the head pass exercises the same create paths the base pass did. Also: surface the actual Msg lines on failure instead of a blind tail, which hid errors behind the result sets these procs print; and wait out the instance's first minute of uptime, because sp_Blitz aborts with a divide-by-zero inside that window (issue #4048) and it landed on whichever pass ran first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
… PRs The 615fdab run took the baseline from 9 failing steps to 1 and the findings comparison came back clean (50 findings, identical to base). Two things left over, both pre-existing bugs rather than CI defects, now filed: - Issue #4050: CheckID 106 reads the live default trace with fn_trace_gettable. A read landing on a torn or rolling file raises Msg 568 and aborts the entire sp_Blitz run, not just that check. It hit one of six sp_Blitz calls on the 2017 job while 2025 passed, so it is a race, and it would mean red builds on pull requests that changed nothing. CI now skips 106 through @SkipChecksTable, which has the side benefit of exercising the skip-checks path on every run. Remove the row once #4050 is fixed. - Issue #4049: sp_DatabaseRestore's @MoveFiles = 1 path splits the filename off PhysicalName with a hardcoded backslash, so on Linux CHARINDEX returns 0 and LEFT(..., -1) raises Msg 537. That step is left failing on purpose: it fails identically on base and head, so the signature comparison classifies it as pre-existing and it does not fail the build. It turns green once #4049 is fixed. Only visible now because CommandExecute is installed; before that the proc exited earlier on the missing dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
BrentOzar
left a comment
There was a problem hiding this comment.
The earlier five findings were addressed, and the new run confirms that -I, the plan-cache lookup, cleanup additions, and volatile-finding filter work. The refreshed CI also exposes five follow-up correctness gaps: the uptime guard is not actually waiting, the restore fixture still lacks a required dependency and cannot handle this Linux path, error signatures discard the diagnostic text, and a transient error in unchanged code is falsely attributed to the PR.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.github/scripts/run-sql-server-smoke-tests.sh:363
- This call reruns the same full
sp_Blitzconfiguration that the matrix may just have recorded as a base failure. Becausecapture_findingsusessqlcmd -band the script hasset -e, such a pre-existing base error aborts the job here before the head pass and is never handled by the comparison logic. Guard the base capture, track whether findings are available, and skip only the informational findings diff when it fails.
if [[ -n "$BASE_REVISION" ]] && git -C "$REPO_ROOT" rev-parse --verify --quiet "$BASE_REVISION" >/dev/null; then
…bution
Second review round, all five findings:
- The uptime guard never waited. run_query only forwards $1, so the -h -1 -W
flags were dropped and the first output line was a column header, never '0'.
It returned in ~0.1s and the 2025 base pass then hit the exact Msg 8134
divide-by-zero it exists to avoid. Added run_scalar for bare values, and the
result must now parse as a number greater than zero -- an empty result, a
header or an error means keep waiting rather than assume the best.
- Error signatures now carry the message text, not just number/level/state.
Msg 50000 is whatever RAISERROR was handed, and CommandExecute raises it for
many unrelated failures, so keying on the number alone let one Msg 50000 be
swapped for another unnoticed. Server name, Procedure, Line and quoted
literals are still stripped, since those move without the error changing.
- Fetch and install CommandLog.sql alongside CommandExecute.sql, checksummed
the same way. sp_DatabaseRestore always passes @LogToTable = 'Y' and
CommandExecute refuses that mode without the table, so CommandExecute alone
never reached a restore.
- A base/head mismatch is re-confirmed before it is blamed on the branch. The
passes run minutes apart against one moving server, so a transient lands on
whichever is unlucky -- that turned a PR touching no stored procedure red.
Mismatches are now retried on head, and anything still failing is re-tested
against a reinstalled base. Only a failure surviving both is reported.
- Dropped the executing sp_DatabaseRestore step back to @Help. Its dependencies
are installed now, but it still cannot complete on a Linux fixture: the
@MoveFiles = 1 path uses a hardcoded backslash both to split the filename off
PhysicalName (Msg 537) and to join the backup directory to the file name
('/var/opt/mssql/data/\\FRKSmokeTest_Full2.bak'). Tracked in #4049. A
permanently-red step trains everyone to expect red and advertises coverage
that does not exist; the invocation is left commented for when #4049 lands.
The signature rewrite was caught by its own unit test before pushing: the first
attempt bounded the Line strip with [^,]* and, since the message has no commas,
ate the message text -- collapsing every error to one signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
…a new step Two more review findings: - sp_BlitzFirst's logging path creates four views alongside its tables -- <FileStats>_Deltas, <PerfmonStats>_Deltas, <PerfmonStats>_Actuals and <WaitStats>_Deltas (sp_BlitzFirst.sql:164-167). Dropping only the tables left the base pass's views behind, so the head pass skipped all four view-creation paths. They are dropped now, before their backing tables. - Both passes execute THIS branch's matrix, so a step the PR adds or edits also runs against base's procedures. A defect in the step itself -- a typo'd procedure name, a bad parameter -- therefore failed identically on both passes and was waved through as pre-existing. On this PR, where base has no matrix at all, that meant every step could be broken and the job would still pass. A failure is only grandfathered now when base has a step with the same label AND a byte-identical body; anything new or edited has to pass on its own merits. Unit-tested the comparison before pushing: unchanged body -> grandfatherable, edited body -> must pass, label absent from base -> must pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
capture_findings runs the same full sp_Blitz configuration the matrix just exercised, through sqlcmd -b under set -e. So whenever sp_Blitz is failing -- including failing on base, before this branch is involved at all -- the job died here, during the base pass, before the head pass ran. The comparison logic that exists to classify exactly that failure was never reached. Both captures are guarded now and tracked separately. A capture that fails warns, records that side as unavailable, and lets the run continue; the findings diff is skipped and says which revision it lost. The error classification is untouched, which is the half that decides the build. The findings diff is informational and survivable. Losing the error classification because the informational half failed first is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
The view cleanup added in 369418e emitted 'DROP VIEW FRKSmokeTest.dbo.[X]', which SQL Server rejects with Msg 166: 'DROP VIEW' does not allow specifying the database name as a prefix to the object name. DROP TABLE accepts one, which is why the table cleanup beside it worked and this did not. That aborted the run between the two passes on the 2017 job. The statement is built with two-part names now and executed inside the target database through its own sp_executesql, and filtered to the dbo schema. The rest of that run is the first fully clean baseline: the uptime guard waited properly (7 checks, 1 minute up) and base finished with 0 failing steps, down from 9 two rounds ago. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
BrentOzar
left a comment
There was a problem hiding this comment.
The current head is clean on both engines: 38/38 base and head steps pass, and findings are identical. I found four future-regression holes below. The PR description also needs a refresh before merge: it still says 39 steps and implies sp_DatabaseRestore execute-path coverage, while the current matrix has 38 steps and deliberately runs only @Help; it also still says the SQL has not been verified even though both live jobs are now clean.
…names Four more review findings: - Quoted literals are no longer blanked wholesale. A quoted name is often the only difference between two errors -- Msg 208 for 'OldTable' and 'NewTable' are different bugs -- so blanking them let a real regression inherit a grandfathered signature. Only quoted values containing a path separator are normalised now, which covers rotating trace files and per-run backup paths and leaves deterministic identifiers intact. - Each pass starts from absent kit objects. The base pass used to leave its procedures in master, so a script deleted on head was silently skipped by the install loop, and a script reduced to an empty file still made sqlcmd exit 0 -- in both cases the head matrix ran the stale base procedure and passed without touching the head artifact. Procedures are dropped before each install, and every expected one is verified to exist afterwards, because installing without error is not proof the procedure exists. - Grandfathering now also requires the harness to be unchanged. Both passes use THIS branch's runner, seed, matrix and workflow, so a PR that breaks the harness -- dropping -I, say -- breaks unchanged steps against base and head alike, and step-body equality alone would wave that through. Any difference in those four files means every head step must pass on its own merits. - Dropped the CommandLog/CommandExecute downloads. The only sp_DatabaseRestore invocation left is @Help = 1, which returns at sp_DatabaseRestore.sql:71, long before the CommandExecute check at :255 -- so they were two network dependencies and two checksums that could redden every PR while exercising nothing. The URL and both hashes are recorded in a comment for when #4049 lands and the execute-path step comes back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
…tzWho views Four findings from the latest Copilot review: - The findings-capture invocation combines @CheckUserDatabaseObjects, @CheckServerInfo and table output in a way no matrix step did, and its failure path is a warning by design so the informational diff can degrade without killing the run. A head-only error in that exact combination could therefore never reach the classifier. It is a labelled matrix step now, so a new failure there fails the build. - Re-confirmation replays the whole matrix instead of re-running the failing step in place. Steps conditionally create persistent tables and views, so replaying one alone could skip the create branch that failed and look transient; the base re-test had the same problem, running against head-mutated state. Both rounds now reset, reinstall, and run the full matrix so ordering and state match the original passes. - sp_BlitzWho creates <table>_Deltas for whatever table it logs to (sp_BlitzWho.sql:158), which means BlitzWho_Deltas via sp_BlitzFirst's @OutputTableNameBlitzWho and BlitzWho_Results_Deltas from the direct step. Neither was being dropped, so the head pass skipped both view-creation paths. - run_scalar's result is assigned in a command substitution, so under set -e a transient sqlcmd failure aborted the script -- contradicting the uptime guard's own contract that an error means keep waiting. Both call sites tolerate failure now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
BrentOzar
left a comment
There was a problem hiding this comment.
Reviewed the current head 734f71f. Both live jobs are clean: SQL Server 2017 and 2025 each passed all 39 base steps and all 39 head steps, with 49 and 43 findings respectively, identical to base. The latest push addresses the prior install, signature, harness, cleanup, retry-state, and findings-configuration comments. I found two remaining items below. The PR description also needs one final refresh: it still says 38 steps and that the Ola dependencies are installed, and its verification section is anchored to 8ddef6b rather than this 39-step run.
…dance - The matrix step added in 734f71f covered the sp_Blitz call and the table it writes, but not the SELECT the comparison reads it back with. A head change renaming or dropping CheckID, DatabaseName or Finding therefore failed only inside the warning-only capture path: head_findings_ok=0, diff reported as Skipped, job green. The readback now runs inside the labelled step, so that is a classified failure. The runner's volatile-CheckID filter is deliberately not duplicated there -- the step exists to prove the columns, not the filter. - The comment explaining how to re-enable the sp_DatabaseRestore execute path was written before 32e24f9 removed the dependency downloads, so following it would fail on the CommandExecute prerequisite. It now says both the workflow dependency step and the invocation have to come back together, and points at where the URL and hashes are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
Previously missed (3) — in code that hasn't changed since the last review.
.github/scripts/smoke-test-seed.sql:66
CHECKSUM(NEWID())can return-2147483648, andABSon thatINTraises arithmetic overflow. This makes the seed capable of randomly aborting the entire CI job before any smoke test runs; cast toBIGINTbefore applyingABS.
This issue also appears on line 72 of the same file.
ABS(CHECKSUM(NEWID())) % 50000,
DATEADD(DAY, -(ABS(CHECKSUM(NEWID())) % 3000), GETDATE())
.github/scripts/smoke-test-matrix.sql:202
- This does not exercise the advertised KILL construction/execution path. With a guaranteed-nonmatching application name,
sp_killsets@TotalKillsto 0 and exits through the “Nothing to kill” branch; the cursor andEXEC(@KillSQL)path atsp_kill.sql:793-840are never reached. Hold open a secondsqlcmdsession and target that session so regressions in the actual kill loop fail CI.
EXEC dbo.sp_kill @ExecuteKills = 'N';
.github/scripts/run-sql-server-smoke-tests.sh:455
sqlcmdformats this variable-length expression before it reachessed: by default it limits variable columns to 256 characters and uses an 80-character screen width. This serialized row can be about 340 characters (CheckID, a 128-character database name, and a 200-character finding), so output can be truncated or wrapped into multiple lines;sort/commcan then miss a changed suffix or compare fragments instead of findings. Request an untruncated, non-wrapping width for this machine-readable output.
"$SQLCMD" "${SQLCMD_ARGS[@]}" -d FRKSmokeTest -h -1 -W -s '|' -Q "
.github/scripts/smoke-test-seed.sql:74
- These three random expressions have the same
ABS(INT_MIN)failure mode:CHECKSUM(NEWID())can produce-2147483648, which makesABSraise arithmetic overflow and flakes the seed. Cast toBIGINTfirst.
ABS(CHECKSUM(NEWID())) % 2000 + 1,
ABS(CHECKSUM(NEWID())) % 100,
DATEADD(DAY, -(ABS(CHECKSUM(NEWID())) % 1000), GETDATE()),
- The findings readback is parsed, not read, but ran with sqlcmd's defaults: an 80-character screen width and variable-length columns clipped at 256. A row here reaches roughly 340 characters (CheckID + NVARCHAR(128) DatabaseName + VARCHAR(200) Finding), so rows could wrap or clip and sort/comm would compare fragments -- silently missing real differences in the half of the harness that exists to surface them. Added -y 0 -w 65535. - CHECKSUM(NEWID()) can return -2147483648, and ABS() of INT_MIN raises an arithmetic overflow, so the seed could abort at random and take the whole job with it. All five sites widen to BIGINT before ABS. - The sp_kill step ran with @ExecuteKills = 'Y' against a filter matching no session, so sp_kill sets @TotalKills to 0 and leaves through its nothing-to- kill branch: the cursor and EXEC(@KillSQL) at sp_kill.sql:793-840 are never reached. It covers parameter handling and the filter path, not the kill loop, and is now named for that. Real coverage needs a second session held open and respawned per matrix run -- tracked in #4051, deliberately not added here since it introduces a timing-dependent background process into a path that runs up to three times per job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
9abba81 added -y 0 -w 65535 to stop sqlcmd clipping the machine-read findings. Those are the 'unlimited' spellings, and this sqlcmd build rejected them: the readback failed outright, and because that path only warns, the run reported 'Skipped: sp_Blitz could not produce a findings set on either revision' while staying green. The previous commit compared 49 findings; this one compared none and said so quietly. Two changes: - Use -y 8000 -w 8000. Both are squarely inside the documented ranges and comfortably above the ~340-character maximum a row can reach, so the original truncation concern is still addressed without depending on how a particular build handles the edge values. - Stop discarding the readback's stderr. It was going to /dev/null, so a failure produced no explanation anywhere -- the reason this took a CI round to spot rather than being obvious in the log. It now warns and prints what sqlcmd said. The findings diff is informational by design (issue #4046, decision 1A) and should not fail the build, but it should never fail quietly either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
f2f0842 kept -W alongside -y, and sqlcmd refuses that combination outright: Sqlcmd: The y and the W options are mutually exclusive. So the readback still failed and the findings comparison was still skipped -- the same symptom as 9abba81, a different cause. The warning added in f2f0842 is what made this visible in one CI round instead of another guessing pass; without it the log said only 'Skipped' with no reason. -y is the option that lifts the 256-character truncation, so -W goes. Columns now come back padded to the display width, and the pipeline strips trailing whitespace before deduplicating. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
.github/scripts/smoke-test-matrix.sql:231
- This still says the dependencies are installed, but the workflow and the re-enabling instructions below explicitly say they are not. That contradiction can mislead maintainers about what the current help-only step covers; describe the dependencies as deliberately omitted.
Its dependencies are installed (CommandLog + CommandExecute, fetched by the
workflow), so the procedure runs -- but it cannot complete against a Linux
The sp_DatabaseRestore comment still said its dependencies were installed and fetched by the workflow, which 32e24f9 removed -- and which the re-enabling instructions six lines below it directly contradicted. It now says they are deliberately absent, and records that they were installed briefly and that this is how #4049 was found: with both present the procedure gets far enough to fail on Linux path handling instead of stopping at the missing-dependency check. A comment that overstates coverage is the same defect this PR exists to fix, just written in prose instead of SQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.github/scripts/run-sql-server-smoke-tests.sh:510
sp_BlitzLockalso createsFRKSmokeTest.dbo.BlitzLockFindingson its table-output path (sp_BlitzLock.sql:869-893). Leaving that table behind means the head pass sees it from the base pass and skips its creation branch, so a regression in that DDL can pass. Include it in the table cleanup list.
WHERE name IN (N'BlitzOutput', N'BlitzCache', N'BlitzFirst', N'BlitzFirst_FileStats',
N'BlitzFirst_PerfmonStats', N'BlitzFirst_WaitStats',
N'BlitzFirst_WaitStats_Categories', N'BlitzWho',
N'BlitzWho_Results', N'BlitzLock', N'BlitzIndex', N'BlitzFindings');
…ture - The reset list has now been wrong four times: sp_BlitzLock's synonyms, sp_BlitzFirst's four views, sp_BlitzWho's two, and now sp_BlitzLock's BlitzLockFindings. Any proc taking @OutputTableName can create objects derived from it, so naming them one at a time is structurally the losing approach -- each new output path is a silent gap until someone notices the head pass skipped its create branch. Cleanup now enumerates every Blitz-prefixed table and view in the test database instead, keeping BlitzChecksToSkip (seed-created, needed by both passes) and leaving the seed's own non-prefixed tables alone. - The replay branch compared signatures without requiring either to be non-empty, unlike the initial classification. error_signature returns empty whenever a failure produces no 'Msg ...' or 'Sqlcmd: Error' line, so two unrelated unknown failures compared equal and were written off as environmental. It now requires a non-empty signature, matching the initial check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
The value in this harness is executing the scripts at all. That alone found three real pre-existing bugs (#4048, #4049, #4050) and would have caught both of PR #4045's runtime breakages. It worked from the first commit and has needed almost no repair since. The base-vs-head comparison is the opposite story. Installing the base branch's copies, running everything twice, and classifying each failure as new or pre-existing needed error signatures, step-body comparison, harness-file comparison and replay-on-mismatch -- about 155 lines whose only job was answering 'is this failure the PR's fault?'. Nearly every defect found while reviewing this harness lived in that code, and several were regressions introduced while fixing others. It also never took a decision. Every run so far logged 'harness differs from base; every step must pass on its own merits', because the harness files themselves keep changing. Grandfathering only matters when the baseline is dirty, and the baseline is clean: 0 failing steps on both engine versions. So it is gone, along with the findings diff it fed. The runner drops from 792 lines to 281, CI drops from two passes to one, and what remains is: seed a database, install all 12 non-deprecated scripts, verify each procedure exists, run 39 labelled steps, fail on any SQL error. Git history keeps a working implementation of the comparison if a dirty baseline ever makes it necessary. Unchanged and still earning their place: sqlcmd -I (QUOTED_IDENTIFIER), per-step attribution so one round surfaces every failure, the uptime wait for #4048, the CheckID 106 skip for #4050, and both engine versions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
BrentOzar
left a comment
There was a problem hiding this comment.
Reviewed the current head 88f5e08. The earlier findings-readback and restore-guidance notes are fixed, as are the subsequent cleanup and empty-signature issues. Both live jobs passed all 39 base and 39 head steps with no SQL errors. SQL Server 2025 reported 43 findings identical to base; SQL Server 2017 showed one informational difference, CheckID 185 (Wait Stats Have Been Cleared), despite this PR changing no kit procedure. I found three remaining classifier/noise issues below.
Labels are display-only now that the classifier is gone, so a duplicate can no longer cause a false pass -- every step file runs and failures are counted per file. It would still make the output ambiguous about which step failed, and the check is three lines, so the splitter rejects duplicates outright. Verified both ways: a matrix with two identical labels exits with the offending label named, and the real 39-step matrix passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
.github/scripts/smoke-test-matrix.sql:81
- This explanation describes the removed findings comparison, classification, and volatile-CheckID filter. The current runner only executes the
SELECT; its purpose is now to make missing or renamed persisted columns fail the step.
/* The readback the comparison performs, so that renaming or removing CheckID,
DatabaseName or Finding fails this classified step instead of silently
downgrading the findings diff to "Skipped". The volatile-CheckID filter the
runner applies is deliberately not reproduced here -- this exists to prove the
columns the readback depends on, not to duplicate the filter. */
573c8ac deleted the base-vs-head comparison but left prose describing it in three places: - The sp_BlitzUpdate exclusion was justified by 'would invalidate the base-vs-head comparison'. The real and remaining reason is that it replaces the kit's procedures mid-run, so every later step would exercise whatever it just downloaded instead of the code under test. - The findings readback was explained in terms of the classifier and the volatile-CheckID filter, neither of which exists. It now says what it does: proves the persisted columns are still shaped the way callers expect, since writing the table only proves the table was created. - The seed said it runs 'before either pass'. There is one pass. Swept the rest of the harness for the same class; the only other hit was sp_BlitzPlanCompare's own plan comparison, which is accurate. Stale comments describing behaviour that no longer exists are a smaller version of the problem this PR was opened to fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
BrentOzar
left a comment
There was a problem hiding this comment.
Re-reviewed current head 87413ca.
The base-vs-head classifier and findings-diff subsystem that motivated my prior three comments has been removed. The remaining duplicate-label ambiguity is now explicitly rejected, and this latest commit cleans up the stale comments left by the one-pass simplification.
I found no additional actionable issues in the current runner, matrix, seed, or workflow. The live SQL Server 2017 and SQL Server 2025 jobs both passed all 39 matrix steps.
Brings in the smoke-test overhaul from #4047, so this branch's 51 rewritten cross-database blocks are executed against real SQL Server 2017 and 2025 for the first time. That is the boxed-SQL verification this PR's description says is missing, and the gap that let its Msg 137 and stranded-IF bugs through.
Answers the question code review cannot: does the rewritten sp_Blitz still report the same findings as the released one? Three reviewers have now read this change; nobody has run it and compared what it finds. Installs dev's sp_Blitz, runs it, records every (CheckID, DatabaseName, Finding). Installs this branch's, runs the same thing, records again, diffs. The Azure rewrite is meant to change how the code is written, not what it reports, so on boxed SQL the two lists should be identical. Runs on SQL Server 2017 and 2025. Excludes CheckIDs 156 and 185, whose text embeds a timestamp and a live wait counter respectively, so they differ between any two runs minutes apart regardless of the code. Deliberately not part of the permanent smoke tests -- on every PR this was noisy and never decided anything, which is why #4047 removed it. Gated to this branch, and both files come out once the answer is recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
Closes #4046.
The problem
The smoke test installed only the changed
sp_*.sqland ran it once, with@VersionCheckMode = 1— an unconditional earlyRETURNat sp_Blitz.sql#L75-L78. No check body ever executed, so it proved the script parsed and nothing more.#4045 is the evidence: two runtime breakages passed it green and had to be caught by human review. Neither was catchable at install time — one was a variable inside a string literal, the other left the outer T-SQL validly balanced.
What this does
Seeds a database, installs all 12 non-deprecated scripts, verifies each procedure actually exists, runs a 39-step matrix, and fails the build on any SQL error. On SQL Server 2017 and 2025.
.github/scripts/smoke-test-matrix.sql— the@Output*table paths,sp_BlitzIndexmodes 0-4,sp_ineachdb's three-part-name rewrite,sp_BlitzPlanCompareagainst a real cached plan. Seeded fromDocumentation/Development/Test in Azure.sql, kept separate so CI can carry setup a hand-run script shouldn't (decision 2B).sp_BlitzUpdateis excluded: it overwrites the procs mid-run (decision 4).--#STEP:markers, so a failure names one step and the rest still run — one CI round surfaces every problem, not just the first.sqlcmd -I, so procedures are created withQUOTED_IDENTIFIER ON. Without itsqlcmd's OFF default is captured atCREATEtime and thesp_BlitzFirst/sp_BlitzLockXML paths die withMsg 1934— a configuration no real client uses.sqlcmd0, so every expected procedure is checked withOBJECT_ID.fail-fast: false.What was removed, and why
An earlier revision of this PR also installed the base branch's copies, ran everything twice, and classified each failure as new or pre-existing — error signatures, step-body comparison, harness-file comparison, replay-on-mismatch. About 155 lines answering one question: is this failure the PR's fault?
It's gone. Three reasons:
harness differs from base; every step must pass on its own merits, because the harness files themselves keep changing. The machinery has not once been the thing that decided an outcome.The runner went from 792 lines to 281, and CI from two passes to one. Git history has a working implementation if a dirty baseline ever makes the question real.
What is deliberately not covered
Stated plainly, because a step whose name implies coverage it lacks is the same defect as the
@VersionCheckModecall this replaces:sp_kill's kill loop. The matrix runs@ExecuteKills = 'Y'against a filter matching no session, sosp_killsets@TotalKillsto 0 and exits its nothing-to-kill branch; the cursor andEXEC(@KillSQL)atsp_kill.sql:793-840are never reached. Parameter and filter handling are covered. Real coverage needs a second session held open — CI: sp_kill's actual kill loop is not covered by the smoke tests #4051.sp_DatabaseRestore's execute path. Stays at@Helpuntil sp_DatabaseRestore: @MoveFiles = 1 fails on Linux with Msg 537 (hardcoded backslash path separator) #4049; its Ola Hallengren dependencies aren't installed either, since nothing could reach them.@IsWindowsOperatingSystem = 0andxp_regread,xp_cmdshelland cluster DMVs aren't exercised. Binding still happens, so both sp_Blitz: make it install and run on Azure SQL Database (#4040) #4045 bugs would have been caught.Bugs this found
All pre-existing on
dev, all filed separately:sp_Blitzdivides by zero when the server has been up under a minute.@MsSinceWaitsClearedisDATEDIFF(MINUTE, ...), so it's0, and the zero-guard sits inside a branch that can't be taken while the value is0. Reproduced on both engine versions.sp_DatabaseRestore's@MoveFiles = 1(the default) is broken on Linux: two hardcoded backslashes, one splitting the filename offPhysicalName(Msg 537), one joining the backup directory to the file name.sp_Blitzaborts entirely when a read of the live default trace lands on a torn file (Msg 568), dropping every finding after CheckID 106.CI works around #4048 (waits out the uptime window) and #4050 (skips CheckID 106 via
@SkipChecksTable, which incidentally exercises that previously untested path). Both carry comments pointing at their issues and should come out when they're fixed.🤖 Generated with Claude Code
https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7