--- name: ssdt-cicd-best-practices-2025 description: Modern CI/CD best practices for SQL Server database development with tSQLt, state-based deployment, and 2025 patterns --- ## 🚨 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) ### 1. 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**: ```yaml # 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) ### 2. 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**: ```yaml # 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**: ```sql -- 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**: ```yaml - 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 } ``` ### 3. 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)**: ```yaml # 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**: ```yaml - 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)**: ```yaml # 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**: ```yaml # BAD: Plain text SQL auth password TargetConnectionString: "Server=prod;Database=MyDB;User=sa;Password=P@ssw0rd123" ``` **If SQL Auth Required**: ```yaml # 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=True # Still not as secure as Windows Auth! ``` ### 4. 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 ### 5. Deployment Reports Always Required **Before Production Deployment**: ```yaml - 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 }}" ``` ### 6. Environment Promotion Strategy **Standard Flow**: Dev → QA → Staging → Production **Consistent Deployment Options**: ```yaml # 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) ```yaml 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) ```yaml 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' - 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" } - 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](https://tsqlt.org/) - [Microsoft.Build.Sql Documentation](https://learn.microsoft.com/sql/tools/sql-database-projects/) - [SqlPackage Reference](https://learn.microsoft.com/sql/tools/sqlpackage/) - [Azure DevOps SQL Tasks](https://learn.microsoft.com/azure/devops/pipelines/tasks/deploy/sql-azure-dacpac-deployment) - [GitHub Actions for SQL](https://github.com/marketplace?type=actions&query=sql+)