Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions .github/scripts/run-azure-smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
#
# Installs and runs sp_Blitz against a real Azure SQL Database.
#
# Azure SQL DB refuses to compile a module that names another database, which is
# the whole subject of issue #4040: sp_Blitz's ALTER PROCEDURE aborted on the
# first of 88 cross-database references and the procedure was never created.
# Worse, the CREATE stub in the preceding batch succeeded, so users were left
# with an sp_Blitz that ran, returned nothing, and raised no error.
#
# That failure mode is invisible to a boxed SQL Server test, and invisible to
# "did it install" -- the install genuinely succeeds. So this checks three
# things: the procedure installs without error, it runs without error, and it
# actually returns findings.

set -Eeuo pipefail

: "${AZURE_SQL_SERVER:?AZURE_SQL_SERVER must be set}"
: "${AZURE_SQL_DATABASE:?AZURE_SQL_DATABASE must be set}"
: "${AZURE_SQL_USER:?AZURE_SQL_USER must be set}"
: "${AZURE_SQL_PASSWORD:?AZURE_SQL_PASSWORD must be set}"
: "${SQLCMD:=sqlcmd}"

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT

# -I for QUOTED_IDENTIFIER ON, matching every real client. No -C: Azure presents
# a valid certificate, so the connection is verified rather than trusted blindly.
SQLCMD_ARGS=(
-S "tcp:$AZURE_SQL_SERVER,1433"
-d "$AZURE_SQL_DATABASE"
-U "$AZURE_SQL_USER"
-P "$AZURE_SQL_PASSWORD"
-N
-b
-I
-l 60
-t 600
)

run_query() { "$SQLCMD" "${SQLCMD_ARGS[@]}" -Q "$1"; }

# ---------------------------------------------------------------------------
# Read a single integer out of a batch.
#
# Not "the first line of output": sqlcmd prints every result set the batch
# produced, and sp_Blitz emits a version-check result set ("Component ... is
# outdated") ahead of its real output. Taking the first line picked up that
# warning text and reported it as the finding count. So take the last line that
# is nothing but digits, which the warning text can never be.
#
# `|| true` because grep exits 1 when it matches nothing, and pipefail would
# turn that into an abort instead of letting the caller report a clear error.
# ---------------------------------------------------------------------------
run_scalar_int() {
"$SQLCMD" "${SQLCMD_ARGS[@]}" -h -1 -W -Q "$1" \
| sed 's/[[:space:]]//g' \
| grep -E '^[0-9]+$' \
| tail -1 || true
}

# ---------------------------------------------------------------------------
# Serverless Azure SQL DB auto-pauses when idle, and the connection that wakes it
# is itself rejected while it resumes. A first failure means nothing; only a
# sustained one does.
# ---------------------------------------------------------------------------
wake_database() {
echo "Connecting to Azure SQL Database (it auto-pauses; early failures are expected)..."
for attempt in {1..30}; do
if run_query "SET NOCOUNT ON; SELECT 1;" >/dev/null 2>&1; then
Comment thread
BrentOzar marked this conversation as resolved.
Outdated
echo "Connected on attempt $attempt."
return 0
fi
echo " attempt $attempt: not up yet, waiting..."
sleep 20
done

echo "::error::Could not connect to Azure SQL Database after 30 attempts (10 minutes)." >&2
echo "::error::Check that the server is reachable and that its firewall admits GitHub runner IPs." >&2
return 1
}

wake_database

echo
echo "=== Engine confirms this really is Azure SQL Database ==="
# EngineEdition 5 is Azure SQL Database. If this is anything else, the test is
# passing for the wrong reasons and should say so rather than look green.
edition="$(run_scalar_int "SET NOCOUNT ON; SELECT CONVERT(INT, SERVERPROPERTY('EngineEdition'));")"

echo "EngineEdition: $edition"
if [[ "$edition" != "5" ]]; then
echo "::error::Expected EngineEdition 5 (Azure SQL Database), got '$edition'. Refusing to report success." >&2
exit 1
fi

echo
echo "=== Installing ==="
# sp_ineachdb first: sp_Blitz calls it to iterate databases.
for script in sp_ineachdb sp_Blitz; do
Comment thread
BrentOzar marked this conversation as resolved.
echo " installing $script"
if ! "$SQLCMD" "${SQLCMD_ARGS[@]}" -i "$REPO_ROOT/$script.sql" > "$WORK_DIR/$script.log" 2>&1; then
echo "::error::$script failed to install on Azure SQL Database"
grep -E -A2 '^(Msg [0-9]+,|Sqlcmd: Error)' "$WORK_DIR/$script.log" | head -30 || tail -30 "$WORK_DIR/$script.log"
exit 1
fi
done

# The install "succeeding" is exactly what made #4040 invisible: the CREATE stub
# is its own batch and always works, while only the ALTER carrying the real body
# fails. So confirm the procedure has a body, not merely a name.
echo
echo "=== Confirming sp_Blitz is more than the stub ==="
body_length="$(run_scalar_int "SET NOCOUNT ON;
SELECT LEN(OBJECT_DEFINITION(OBJECT_ID('dbo.sp_Blitz')));")"

echo "sp_Blitz definition length: $body_length characters"
if ! [[ "$body_length" =~ ^[0-9]+$ ]] || (( body_length < 10000 )); then
echo "::error::sp_Blitz exists but its body is '$body_length' characters -- that is the RETURN 0 stub, not the real procedure." >&2
echo "::error::This is the #4040 failure: the CREATE stub succeeded and the ALTER carrying the body did not." >&2
exit 1
fi

echo
echo "=== Running sp_Blitz ==="
if ! run_query "EXEC dbo.sp_Blitz;" > "$WORK_DIR/run.log" 2>&1; then
echo "::error::sp_Blitz raised an error on Azure SQL Database"
grep -E -A2 '^(Msg [0-9]+,|Sqlcmd: Error)' "$WORK_DIR/run.log" | head -40 || tail -40 "$WORK_DIR/run.log"
exit 1
fi

echo " ran without error"

# A silent clean bill of health is the bug, not the goal. Any real server has
# something to say, so zero findings means the checks are not running.
echo
echo "=== Counting findings ==="
findings="$(run_scalar_int "SET NOCOUNT ON; EXEC dbo.sp_Blitz @OutputType = 'COUNT';")"
Comment thread
BrentOzar marked this conversation as resolved.
Outdated

echo "sp_Blitz returned $findings finding(s)"
if ! [[ "$findings" =~ ^[0-9]+$ ]] || (( findings < 1 )); then
echo "::error::sp_Blitz returned no findings ('$findings'). Running clean with nothing to say is the #4040 symptom, not a pass." >&2
exit 1
fi

{
echo "### Azure SQL Database"
echo
echo "- EngineEdition \`5\` confirmed"
echo "- \`sp_Blitz\` installed, definition $body_length characters (not the stub)"
echo "- ran with no errors"
echo "- returned **$findings findings**"
} >> "${GITHUB_STEP_SUMMARY:-/dev/null}"

echo
echo "Azure SQL Database smoke test passed."
63 changes: 63 additions & 0 deletions .github/workflows/azure-sql-smoke-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Azure SQL Database smoke test

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

# One at a time. The target is a single shared Azure SQL Database, and this
# installs procedures into it; concurrent runs would overwrite each other
# mid-test. Queue rather than cancel, so a run already talking to the database
# is allowed to finish.
concurrency:
group: azure-sql-smoke-test
cancel-in-progress: false

permissions:
contents: read

jobs:
azure-smoke-test:
name: Azure SQL Database
runs-on: ubuntu-22.04
# Generous: serverless Azure SQL DB auto-pauses, and waking it can take
# several minutes before the first successful connection.
timeout-minutes: 30

# Skipped rather than failed on forks, where secrets are not available.
# A missing secret would otherwise look like a broken Azure database.
if: github.event.pull_request.head.repo.full_name == github.repository
Comment thread
BrentOzar marked this conversation as resolved.
Outdated

env:
AZURE_SQL_SERVER: ${{ secrets.AZURE_SQL_SERVER }}
AZURE_SQL_DATABASE: ${{ secrets.AZURE_SQL_DATABASE }}
AZURE_SQL_USER: ${{ secrets.AZURE_SQL_USER }}
AZURE_SQL_PASSWORD: ${{ secrets.AZURE_SQL_PASSWORD }}

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Confirm the Azure credentials are present
run: |
missing=""
[[ -n "$AZURE_SQL_SERVER" ]] || missing+="AZURE_SQL_SERVER "
[[ -n "$AZURE_SQL_DATABASE" ]] || missing+="AZURE_SQL_DATABASE "
[[ -n "$AZURE_SQL_USER" ]] || missing+="AZURE_SQL_USER "
[[ -n "$AZURE_SQL_PASSWORD" ]] || missing+="AZURE_SQL_PASSWORD "
if [[ -n "$missing" ]]; then
echo "::error::Missing repository secrets: $missing"
echo "::error::Set them under Settings -> Secrets and variables -> Actions."
exit 1
fi
echo "All four Azure secrets are present."

- name: Install sqlcmd
run: |
curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo env ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev
echo "/opt/mssql-tools18/bin" >> "$GITHUB_PATH"

- name: Install and run sp_Blitz on Azure SQL Database
run: bash .github/scripts/run-azure-smoke-test.sh
Loading