π¨ CRITICAL GUIDELINES
Windows File Path Requirements
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (
) in file paths, NOT forward slashes (/
).
Examples:
-
β WRONG: D:/repos/project/file.tsx
-
β CORRECT: D:\repos\project\file.tsx
This applies to:
-
Edit tool file_path parameter
-
Write tool file_path parameter
-
All file operations on Windows systems
Documentation Guidelines
NEVER create new documentation files unless explicitly requested by the user.
-
Priority: Update existing README.md files rather than creating new documentation
-
Repository cleanliness: Keep repository root clean - only README.md unless user requests otherwise
-
Style: Documentation should be concise, direct, and professional - avoid AI-generated tone
-
User preference: Only create additional .md files when user specifically asks for documentation
SSDT CI/CD Best Practices 2025
Overview
This skill provides comprehensive guidance on implementing modern CI/CD pipelines for SQL Server database projects using SSDT, SqlPackage, and contemporary DevOps practices.
Key Principles (2025 Recommended Approach)
- State-Based Deployment (Recommended)
Definition: Source code represents the current database state, not migration scripts.
How it Works:
-
All database objects (tables, procedures, views, functions) stored in separate .sql files
-
SqlPackage automatically generates incremental deployment scripts
-
Declarative approach: "This is what the database should look like"
-
SSDT compares source to target and calculates differences
Advantages:
-
Easier to maintain and understand
-
No risk of missing migration scripts
-
Git history shows complete object definitions
-
Branching and merging simplified
-
Rollback by redeploying previous version
Implementation:
GitHub Actions example
-
name: Build DACPAC (State-Based) run: dotnet build Database.sqlproj -c Release
-
name: Deploy State to Target run: | sqlpackage /Action:Publish
/SourceFile:Database.dacpac
/TargetConnectionString:"${{ secrets.SQL_CONN }}"
/p:BlockOnPossibleDataLoss=True
Contrast with Migration-Based:
-
Migration-based: Sequential scripts (001_CreateTable.sql, 002_AddColumn.sql)
-
State-based: Object definitions (Tables/Customer.sql contains complete CREATE TABLE)
- tSQLt Unit Testing (Critical for CI/CD)
Why tSQLt:
-
Open-source SQL Server unit testing framework
-
Write tests in T-SQL language
-
Produces machine-readable XML/JSON results
-
Integrates seamlessly with CI/CD pipelines
Key Features:
-
Automatic Transactions: Each test runs in a transaction and rolls back
-
Schema Grouping: Group related tests in schemas
-
Mocking: Fake tables and procedures for isolated testing
-
Assertions: Built-in assertion methods (assertEquals, assertEmpty, etc.)
Pipeline Abort on Failure:
GitHub Actions with tSQLt
-
name: Run tSQLt Unit Tests run: |
Deploy test framework
sqlpackage /Action:Publish
/SourceFile:DatabaseTests.dacpac
/TargetConnectionString:"${{ secrets.TEST_SQL_CONN }}"Execute tests and capture results
sqlcmd -S test-server -d TestDB -Q "EXEC tSQLt.RunAll" -o test-results.txt
Parse results and fail pipeline if tests fail
if grep -q "Failure" test-results.txt; then echo "Unit tests failed!" exit 1 fi
echo "All tests passed!"
-
name: Deploy to Production (only runs if tests pass) run: | sqlpackage /Action:Publish
/SourceFile:Database.dacpac
/TargetConnectionString:"${{ secrets.PROD_SQL_CONN }}"
Test Structure:
-- tSQLt test example CREATE SCHEMA CustomerTests; GO
CREATE PROCEDURE CustomerTests.[test Customer Insert Sets Correct Defaults] AS BEGIN -- Arrange EXEC tSQLt.FakeTable 'dbo.Customers';
-- Act
INSERT INTO dbo.Customers (FirstName, LastName, Email)
VALUES ('John', 'Doe', 'john@example.com');
-- Assert
EXEC tSQLt.AssertEquals @Expected = 1,
@Actual = (SELECT COUNT(*) FROM dbo.Customers);
EXEC tSQLt.AssertNotEquals @Expected = NULL,
@Actual = (SELECT CreatedDate FROM dbo.Customers);
END; GO
-- Run all tests EXEC tSQLt.RunAll;
Azure DevOps Integration:
-
task: PowerShell@2 displayName: 'Run tSQLt Tests' inputs: targetType: 'inline' script: | # Execute tSQLt tests $results = Invoke-Sqlcmd -ServerInstance $(testServer)
-Database $(testDatabase)-Query "EXEC tSQLt.RunAll" ` -Verbose# Check for failures $failures = $results | Where-Object { $_.Class -eq 'Failure' } if ($failures) { Write-Error "Tests failed: $($failures.Count) failures" exit 1 }
- Windows Authentication Over SQL Authentication
Security Best Practice: Prefer Windows Authentication (Integrated Security) for CI/CD agents.
Why Windows Auth:
-
No passwords stored in connection strings
-
Leverages existing Active Directory infrastructure
-
Service accounts with minimal permissions
-
Audit trail via Windows Security logs
-
No credential rotation needed
Implementation:
Self-Hosted Agents (Recommended):
GitHub Actions with self-hosted Windows agent
runs-on: [self-hosted, windows, sql-deploy]
steps:
- name: Deploy with Windows Auth
run: |
sqlpackage /Action:Publish
/SourceFile:Database.dacpac
/TargetConnectionString:"Server=prod-sql;Database=MyDB;Integrated Security=True;"
/p:BlockOnPossibleDataLoss=True
Azure DevOps with Service Connection:
- task: SqlAzureDacpacDeployment@1 inputs: authenticationType: 'integratedAuth' # Uses Windows Auth serverName: 'prod-sql.domain.com' databaseName: 'MyDatabase' dacpacFile: '$(Build.ArtifactStagingDirectory)/Database.dacpac'
Alternative for Cloud Agents (Azure SQL):
Use Managed Identity instead of SQL auth
- name: Deploy with Managed Identity
run: |
sqlpackage /Action:Publish
/SourceFile:Database.dacpac
/TargetConnectionString:"Server=tcp:server.database.windows.net;Database=MyDB;Authentication=ActiveDirectoryManagedIdentity;"
/p:BlockOnPossibleDataLoss=True
Never Do This:
BAD: Plain text SQL auth password
TargetConnectionString: "Server=prod;Database=MyDB;User=sa;Password=P@ssw0rd123"
If SQL Auth Required:
Use secrets/variables (least preferred method)
- name: Deploy with SQL Auth (Not Recommended)
run: |
sqlpackage /Action:Publish
/SourceFile:Database.dacpac
/TargetServerName:"${{ secrets.SQL_SERVER }}"
/TargetDatabaseName:"${{ secrets.SQL_DATABASE }}"
/TargetUser:"${{ secrets.SQL_USER }}"
/TargetPassword:"${{ secrets.SQL_PASSWORD }}"
/p:BlockOnPossibleDataLoss=TrueStill not as secure as Windows Auth!
- Version Control Everything
What to Version:
DatabaseProject/ βββ Tables/ β βββ Customer.sql β βββ Order.sql βββ StoredProcedures/ β βββ GetCustomerOrders.sql βββ Tests/ β βββ CustomerTests/ β β βββ test_CustomerInsert.sql β βββ OrderTests/ β βββ test_OrderValidation.sql βββ Scripts/ β βββ Script.PreDeployment.sql β βββ Script.PostDeployment.sql βββ Database.sqlproj βββ Database.Dev.publish.xml βββ Database.Prod.publish.xml βββ .gitignore
.gitignore:
Build outputs
bin/ obj/ *.dacpac
User-specific files
*.user *.suo
Visual Studio folders
.vs/
Never commit credentials
*.publish.xml.user
Check-in Requirements:
-
Require code review for database changes
-
Mandate comments on all commits
-
Run automated tests before merge
-
Enforce naming conventions via branch policies
- Deployment Reports Always Required
Before Production Deployment:
-
name: Generate Deployment Report run: | sqlpackage /Action:DeployReport
/SourceFile:Database.dacpac
/TargetConnectionString:"${{ secrets.PROD_SQL_CONN }}"
/OutputPath:deploy-report.xml
/p:BlockOnPossibleDataLoss=True -
name: Parse and Review Report run: |
Extract key metrics from XML
echo "=== DEPLOYMENT REPORT ==="
Parse XML for operations count
Check for data loss warnings
Display to user or post to PR
-
name: Require Manual Approval uses: trstringer/manual-approval@v1 with: approvers: database-admins minimum-approvals: 1 instructions: "Review deploy-report.xml before approving"
-
name: Deploy After Approval run: | sqlpackage /Action:Publish
/SourceFile:Database.dacpac
/TargetConnectionString:"${{ secrets.PROD_SQL_CONN }}"
- Environment Promotion Strategy
Standard Flow: Dev β QA β Staging β Production
Consistent Deployment Options:
Define environment-specific properties
environments: dev: blockOnDataLoss: false dropObjectsNotInSource: true backupBeforeChanges: false
qa: blockOnDataLoss: true dropObjectsNotInSource: false backupBeforeChanges: true
staging: blockOnDataLoss: true dropObjectsNotInSource: false backupBeforeChanges: true
production: blockOnDataLoss: true dropObjectsNotInSource: false backupBeforeChanges: true requireApproval: true
Complete GitHub Actions Pipeline (2025 Best Practice)
name: SQL Server CI/CD Pipeline
on: push: branches: [main, develop] pull_request: branches: [main]
env: DOTNET_VERSION: '8.0.x' SQLPACKAGE_VERSION: '170.2.70'
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install SqlPackage
run: dotnet tool install -g Microsoft.SqlPackage --version ${{ env.SQLPACKAGE_VERSION }}
- name: Build Database Project
run: dotnet build src/Database.sqlproj -c Release
- name: Build Test Project
run: dotnet build tests/DatabaseTests.sqlproj -c Release
- name: Upload DACPAC Artifacts
uses: actions/upload-artifact@v4
with:
name: dacpacs
path: |
src/bin/Release/*.dacpac
tests/bin/Release/*.dacpac
test: runs-on: windows-latest # tSQLt requires SQL Server needs: build steps: - uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: dacpacs
- name: Setup Test Database
run: |
sqlcmd -S localhost -Q "CREATE DATABASE TestDB"
- name: Deploy Database to Test
run: |
sqlpackage /Action:Publish `
/SourceFile:Database.dacpac `
/TargetConnectionString:"Server=localhost;Database=TestDB;Integrated Security=True;"
- name: Deploy tSQLt Framework
run: |
sqlpackage /Action:Publish `
/SourceFile:DatabaseTests.dacpac `
/TargetConnectionString:"Server=localhost;Database=TestDB;Integrated Security=True;"
- name: Run tSQLt Unit Tests
run: |
$results = Invoke-Sqlcmd -ServerInstance localhost `
-Database TestDB `
-Query "EXEC tSQLt.RunAll" `
-Verbose
$failures = $results | Where-Object { $_.Class -eq 'Failure' }
if ($failures) {
Write-Error "Tests failed: $($failures.Count) failures"
exit 1
}
Write-Host "All tests passed!"
deploy-dev: runs-on: [self-hosted, windows, sql-deploy] needs: test if: github.ref == 'refs/heads/develop' environment: dev steps: - name: Download Artifacts uses: actions/download-artifact@v4 with: name: dacpacs
- name: Deploy to Dev (Windows Auth)
run: |
sqlpackage /Action:Publish `
/SourceFile:Database.dacpac `
/TargetConnectionString:"Server=dev-sql;Database=MyDB;Integrated Security=True;" `
/p:BlockOnPossibleDataLoss=False `
/p:DropObjectsNotInSource=True
deploy-staging: runs-on: [self-hosted, windows, sql-deploy] needs: test if: github.ref == 'refs/heads/main' environment: staging steps: - name: Download Artifacts uses: actions/download-artifact@v4 with: name: dacpacs
- name: Generate Deployment Report
run: |
sqlpackage /Action:DeployReport `
/SourceFile:Database.dacpac `
/TargetConnectionString:"Server=staging-sql;Database=MyDB;Integrated Security=True;" `
/OutputPath:deploy-report.xml
- name: Deploy to Staging (Windows Auth)
run: |
sqlpackage /Action:Publish `
/SourceFile:Database.dacpac `
/TargetConnectionString:"Server=staging-sql;Database=MyDB;Integrated Security=True;" `
/p:BlockOnPossibleDataLoss=True `
/p:BackupDatabaseBeforeChanges=True `
/p:DropObjectsNotInSource=False
deploy-production: runs-on: [self-hosted, windows, sql-deploy] needs: deploy-staging environment: production steps: - name: Download Artifacts uses: actions/download-artifact@v4 with: name: dacpacs
- name: Generate Deployment Report
run: |
sqlpackage /Action:DeployReport `
/SourceFile:Database.dacpac `
/TargetConnectionString:"Server=prod-sql;Database=MyDB;Integrated Security=True;" `
/OutputPath:prod-deploy-report.xml
- name: Manual Approval Required
uses: trstringer/manual-approval@v1
with:
approvers: database-admins,devops-leads
minimum-approvals: 2
instructions: "Review prod-deploy-report.xml and approve deployment"
- name: Deploy to Production (Windows Auth)
run: |
sqlpackage /Action:Publish `
/SourceFile:Database.dacpac `
/TargetConnectionString:"Server=prod-sql;Database=MyDB;Integrated Security=True;" `
/p:BlockOnPossibleDataLoss=True `
/p:BackupDatabaseBeforeChanges=True `
/p:DropObjectsNotInSource=False `
/p:DoNotDropObjectTypes=Users;Logins;RoleMembership `
/DiagnosticsFile:prod-deploy.log
- name: Upload Deployment Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: production-deployment-logs
path: prod-deploy.log
Azure DevOps Pipeline Example (2025)
trigger: branches: include: - main - develop
pool: vmImage: 'windows-2022'
variables: buildConfiguration: 'Release' dotnetVersion: '8.0.x' sqlPackageVersion: '170.2.70'
stages:
-
stage: Build jobs:
- job: BuildDatabase
steps:
-
task: UseDotNet@2 displayName: 'Install .NET 8' inputs: version: $(dotnetVersion)
-
task: DotNetCoreCLI@2 displayName: 'Build Database Project' inputs: command: 'build' projects: '**/*.sqlproj' arguments: '-c $(buildConfiguration)'
-
task: PublishBuildArtifacts@1 displayName: 'Publish DACPAC' inputs: PathtoPublish: '$(Build.SourcesDirectory)/bin/$(buildConfiguration)' ArtifactName: 'dacpacs'
-
- job: BuildDatabase
steps:
-
stage: Test dependsOn: Build jobs:
- job: RunUnitTests
steps:
-
task: DownloadBuildArtifacts@1 inputs: artifactName: 'dacpacs'
-
task: SqlAzureDacpacDeployment@1 displayName: 'Deploy to Test Database' inputs: authenticationType: 'integratedAuth' serverName: 'test-sql-server' databaseName: 'TestDB' dacpacFile: '$(System.ArtifactsDirectory)/dacpacs/Database.dacpac'
-
task: PowerShell@2 displayName: 'Run tSQLt Tests' inputs: targetType: 'inline' script: | $results = Invoke-Sqlcmd -ServerInstance 'test-sql-server'
-Database 'TestDB'-Query "EXEC tSQLt.RunAll"$failures = $results | Where-Object { $_.Class -eq 'Failure' } if ($failures) { throw "Tests failed: $($failures.Count) failures" }
-
- job: RunUnitTests
steps:
-
stage: DeployProduction dependsOn: Test condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs:
-
deployment: DeployToProduction environment: 'Production' strategy: runOnce: deploy: steps: - task: SqlAzureDacpacDeployment@1 displayName: 'Generate Deployment Report' inputs: deployType: 'DeployReport' authenticationType: 'integratedAuth' serverName: 'prod-sql-server' databaseName: 'ProductionDB' dacpacFile: '$(Pipeline.Workspace)/dacpacs/Database.dacpac' outputFile: 'deploy-report.xml'
- task: SqlAzureDacpacDeployment@1 displayName: 'Deploy to Production' inputs: authenticationType: 'integratedAuth' serverName: 'prod-sql-server' databaseName: 'ProductionDB' dacpacFile: '$(Pipeline.Workspace)/dacpacs/Database.dacpac' additionalArguments: '/p:BlockOnPossibleDataLoss=True /p:BackupDatabaseBeforeChanges=True'
-
Best Practices Checklist
Source Control
-
All database objects in source control
-
.gitignore configured for build outputs
-
No credentials committed
-
Test scripts versioned separately
-
Branching strategy defined (gitflow, trunk-based, etc.)
Testing
-
tSQLt framework deployed
-
Unit tests cover critical stored procedures
-
Tests grouped logically in schemas
-
Pipeline aborts on test failure
-
Test results published to dashboard
Security
-
Windows Authentication used for CI/CD
-
Service accounts follow principle of least privilege
-
Secrets stored in Azure Key Vault / GitHub Secrets
-
No plain text passwords
-
Audit logging enabled
Deployment
-
State-based deployment strategy
-
Deployment reports generated before production
-
Manual approval gates for production
-
Backup before changes (production)
-
BlockOnPossibleDataLoss enabled (production)
-
DoNotDropObjectTypes configured
-
Rollback plan documented
Monitoring
-
Deployment logs captured
-
Failed deployments trigger alerts
-
Performance metrics tracked
-
Schema drift detection automated
Resources
-
tSQLt Official Site
-
Microsoft.Build.Sql Documentation
-
SqlPackage Reference
-
Azure DevOps SQL Tasks
-
GitHub Actions for SQL