Lab 4
Troubleshooting and Optimizing DevOps Workflows
Overview:
- 4.1: Creating a rollback point when the deployment is successful
- 4.2: Incorporating data cleaning pre-deployment scripts
- 4.3: Source control for a rogue database
4.1 Creating a rollback point when the deployment is successful
In this section, we will add a step to our deployment pipeline that gives us a rollback process in case of a failed deployment. We will archive the dacpac file on a successful deployment using a release and a git tag. The dacpac file allows us to create a dynamic rollback script that can be used to revert the database to a previous state. The git tag will give us quick visibility to the database code that matches that previous state.
- Open the
publish.ymlfile in the.github/workflowsdirectory in VS Code. - Modify the workflow trigger to include a user input for the
DBVersionparameter that is a required string.
on:
workflow_dispatch:
inputs:
DBVersion:
required: true
type: string
description: 'Version of the database to deploy, eg 1.0.0'
The DBVersion input parameter will be added to the GitHub interface such that the semantic version of the database can be specified when the workflow is run.

- Add a step to the end of the
Publishjob, after thePublish SQL projectstep, to create a GitHub release. We’ll use thesoftprops/action-gh-releaseaction because it facilitates the creation of a release and the uploading of Wingtips dacpac to that release.
- name: Archive release assets
uses: softprops/action-gh-release@v2
with:
files: Wingtips/bin/Debug/Wingtips.dacpac
tag_name: "v$"
name: "Wingtips v$"
make_latest: true
If a deployment were to fail, we could use the Wingtips.dacpac file from the latest release to revert the database to a previous state.
As time allows, run the workflow manually from the GitHub interface. You will be prompted to enter a version number for the database, something like 1.0.0. The workflow will run and create a release with the Wingtips.dacpac file.

Expanded publish workflow
name: Deploy SQL project
on:
workflow_dispatch:
inputs:
DBVersion:
required: true
type: string
description: 'Version of the database to deploy, eg 1.0.0'
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore Wingtips/Wingtips.sqlproj
- name: Build
run: dotnet build Wingtips/Wingtips.sqlproj --no-restore
- name: Get deployment script
run: |
sqlpackage /Action:Script /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /OutputPath:WingtipsDeploy.sql
- name: Upload SQL script
uses: actions/upload-artifact@v4
with:
name: ReviewDeploymentScript
path: WingtipsDeploy.sql
- name: Generate deploy report
run: |
sqlpackage /Action:DeployReport /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /OutputPath:WingtipsDeployReport.xml
- name: Prettify deploy report
shell: pwsh
run: .github/workflows/deployReportOutput.ps1 -deployReportFile WingtipsDeployReport.xml
- name: output step summary
run: |
cat deployreport.md >> $GITHUB_STEP_SUMMARY
deploy:
runs-on: ubuntu-22.04
environment: ProductionDeployment
needs: build
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore Wingtips/Wingtips.sqlproj
- name: Build
run: dotnet build Wingtips/Wingtips.sqlproj --no-restore
- name: Publish SQL project
run: |
sqlpackage /Action:Publish /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}"
- name: Archive release assets
uses: softprops/action-gh-release@v2
with:
files: Wingtips/bin/Debug/Wingtips.dacpac
tag_name: "v${{ github.event.inputs.DBVersion }}"
name: "Wingtips v${{ github.event.inputs.DBVersion }}"
make_latest: true
4.2 Incorporating data cleaning pre-deployment scripts
In this section, we will add a pre-deployment script to our SQL project such that we can incorporate data cleaning or data migration steps into our deployment process. To make the script more robust and enable executing it against multiple environments, we will also add a table to the database that tracks the version of the database.
Track the database version in a table
Tracking the database version is enabled through a new schema control in the database with a table DeployedVersions. A new row will be added to the table with the version of the database each time a deployment occurs through a deployment script. Inserting the new version into the table is facilitated through a SQLCMD variable that is passed to the deployment.
- Open the explorer view in VS Code and navigate to the Wingtips project.
- Create a new folder in the Wingtips project called
controland add a new filecontrol.sqlto the folder. - Add the following code to the
control.sqlfile:
CREATE SCHEMA control;
- Add a new folder to the
controlfolder calledTablesand add a new fileDeployedVersions.sqlto theTablesfolder. - Add the following code to the
DeployedVersions.sqlfile:
CREATE TABLE [control].[DeployedVersions] (
[VersionNumber] NVARCHAR(50) NOT NULL,
[DeployedOn] DATETIME2 NOT NULL DEFAULT (GETDATE()),
[Description] NVARCHAR(255) NULL,
CONSTRAINT [PK_DeployedVersions] PRIMARY KEY CLUSTERED ([VersionNumber])
);
- Add a new folder to the
controlfolder calledScalarFunctionsand add a new fileVersionCompare_IsGreaterThan.sqlto theScalarFunctionsfolder. - Add the following code to the
VersionCompare_IsGreaterThan.sqlfile:
CREATE FUNCTION control.VersionCompare_IsGreaterThan
(
@Version1 NVARCHAR(50),
@Version2 NVARCHAR(50)
)
RETURNS BIT -- 1 when @Version1 > @Version2, 0 when @Version1 <= @Version2
AS
BEGIN
DECLARE @greaterThan BIT = 0
SET @greaterThan = (SELECT CASE WHEN convert(int, parsename(@Version1, 4)) > convert(int, parsename(@Version2, 4)) THEN 1
WHEN convert(int, parsename(@Version1, 4)) < convert(int, parsename(@Version2, 4)) THEN 0
ELSE CASE
WHEN convert(int, parsename(@Version1, 3)) > convert(int, parsename(@Version2, 3)) THEN 1
WHEN convert(int, parsename(@Version1, 3)) < convert(int, parsename(@Version2, 3)) THEN 0
ELSE CASE
WHEN convert(int, parsename(@Version1, 2)) > convert(int, parsename(@Version2, 2)) THEN 1
WHEN convert(int, parsename(@Version1, 2)) < convert(int, parsename(@Version2, 2)) THEN 0
ELSE CASE
WHEN convert(int, parsename(@Version1, 1)) > convert(int, parsename(@Version2, 1)) THEN 1
WHEN convert(int, parsename(@Version1, 1)) < convert(int, parsename(@Version2, 1)) THEN 0
ELSE 0 -- 'equal'
END END END END)
RETURN @greaterThan
END
- Open the SQL projects extension in VS Code.
- Right-click on the SQLCMD Variables node under the Wingtips project and select Add SQLCMD Variable.
- Name the variable
DBVersionand set the default value to0.0.0. - Open the
StaticData.sqlfile in the Wingtips project and add the following code to the end of the file, which inserts a row into theDeployedVersionstable with the version of the database. TheDBVersionSQLCMD variable is used to set the version number.
INSERT INTO control.DeployedVersions (
VersionNumber,
DeployedOn,
Description
) VALUES (
'$(DBVersion)',
GETDATE(),
'sample Wingtips database'
);
Create a pre-deployment script that runs based on the version
- In the SQL projects extension in VS Code, right-click on the Wingtips project and select Add Pre-Deployment Script.
- Name the script
PreDeployment. - Open the
PreDeployment.sqlfile and add the following code to the file:
-- this predeploy script can be used to clean up data as part of the deployment
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'DeployedVersions')
BEGIN
-- if there are no deployed versions greater than 1.0.1, then we can run this cleanup section
IF (SELECT COUNT(*) FROM control.DeployedVersions WHERE control.VersionCompare_IsGreaterThan(VersionNumber, '1.0.1') = 1) = 0
BEGIN
select 'data cleanup with version 1.0.1'
END
-- if there are no deployed versions greater than 1.2.0, then we can run this cleanup section
IF (SELECT COUNT(*) FROM control.DeployedVersions WHERE control.VersionCompare_IsGreaterThan(VersionNumber, '1.2.0') = 1) = 0
BEGIN
select 'data cleanup with version 1.2.0'
END
END
Since we’ve added a new SQLCMD variable to the project, we need to update any deployments to pass the SQLCMD variable to the deployment.
- Open the
build-sql2022.ymlfile in the.github/workflowsdirectory in VS Code. - The
Publish SQL projectstep needs to be updated to pass the SQLCMD variable to the deployment. SqlPackage uses/v:VariableName=valuesyntax, so we will update thePublish SQL projectstep with the value ofDBVersionfrom the GitHub input ($).
- name: Publish SQL project
run: |
sqlpackage /Action:Publish /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"Data Source=localhost,1433;Database=Wingtips;User ID=sa;Password=${{ secrets.CONTAINER_SQL_PASSWORD }};TrustServerCertificate=True;" /p:AllowIncompatiblePlatform=true /v:DBVersion=${{ github.event.inputs.DBVersion }}
- Open the
publish.ymlfile in the.github/workflowsdirectory in VS Code. - Locate all (three) instances of SqlPackage - Get deploy script, Generate deploy report, and Publish SQL project. Append the
/v:DBVersion=$to the end of each of the three steps. ThePublish SQL projectstep should look like this:
- name: Publish SQL project
run: |
sqlpackage /Action:Publish /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /v:DBVersion=${{ github.event.inputs.DBVersion }}
As time allows, run the deploy workflow manually from the GitHub interface. Enter a new version number, like 1.1.0. The workflow will run and create a release with the Wingtips.dacpac file and you should be able to see the deployed version in the DeployedVersions table.
SELECT TOP (1000) [VersionNumber]
,[DeployedOn]
,[Description]
FROM [control].[DeployedVersions]

Expanded publish workflow
name: Deploy SQL project
on:
workflow_dispatch:
inputs:
DBVersion:
required: true
type: string
description: 'Version of the database to deploy, eg 1.0.0'
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore Wingtips/Wingtips.sqlproj
- name: Build
run: dotnet build Wingtips/Wingtips.sqlproj --no-restore
- name: Get deployment script
run: |
sqlpackage /Action:Script /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /OutputPath:WingtipsDeploy.sql /v:DBVersion=${{ github.event.inputs.DBVersion }}
- name: Upload SQL script
uses: actions/upload-artifact@v4
with:
name: ReviewDeploymentScript
path: WingtipsDeploy.sql
- name: Generate deploy report
run: |
sqlpackage /Action:DeployReport /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /OutputPath:WingtipsDeployReport.xml /v:DBVersion=${{ github.event.inputs.DBVersion }}
- name: Prettify deploy report
shell: pwsh
run: .github/workflows/deployReportOutput.ps1 -deployReportFile WingtipsDeployReport.xml
- name: output step summary
run: |
cat deployreport.md >> $GITHUB_STEP_SUMMARY
deploy:
runs-on: ubuntu-22.04
environment: ProductionDeployment
needs: build
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore Wingtips/Wingtips.sqlproj
- name: Build
run: dotnet build Wingtips/Wingtips.sqlproj --no-restore
- name: Publish SQL project
run: |
sqlpackage /Action:Publish /SourceFile:Wingtips/bin/Debug/Wingtips.dacpac /TargetConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /v:DBVersion=${{ github.event.inputs.DBVersion }}
- name: Archive release assets
uses: softprops/action-gh-release@v2
with:
files: Wingtips/bin/Debug/Wingtips.dacpac
tag_name: "v${{ github.event.inputs.DBVersion }}"
name: "Wingtips v${{ github.event.inputs.DBVersion }}"
make_latest: true
4.3 Source control for a rogue database
Some environments may involve a “rogue database,” or a database that is being modified by another team or application in its deployed state. These changes may be necessary and valuable, but we still want to track the changes in source control and be confident that they are the right changes. In this section, we will setup a pipeline that will create a pull request (PR) anytime a change is found in the database. The changes will be presented as a SQL project so that we can leverage the same validation processes we use for our other databases.
The pipeline would ideally run on a schedule, about once a week, but for this lab we will run it manually as well.
Enable workflows to open pull requests in the repository
For our soon-to-be-created pipeline to open pull requests in the repository, we need to enable a repository level setting to allow GitHub Actions to create and approve pull requests.
- Go to the Settings tab of the repository.
- In the left navigation pane, select Actions.
- In the General section, scroll down to the Workflow permissions section.
- Select the checkbox for Allow GitHub Actions to create and approve pull requests.
- Click the Save button below the option.

Create a new pipeline
- In the
.github/workflowsfolder, create a new file namedrogue-db.ymlfor the new workflow “rogue database” with aworkflow_dispatchtrigger and ascheduletrigger and permission to write to contents and pull requests in repository:
name: rogue database
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # every Sunday at midnight
permissions:
contents: write
pull-requests: write
- Add a new job to the workflow called
validate-dbthat starts by checking out the repo files and setting a variable for the current timestamp. The timestamp will be used to create a new branch for any changes that are found in the database:
jobs:
validate-db:
runs-on: ubuntu-22.04
steps:
- name: '1. Checkout repo files'
uses: actions/checkout@v4
- name: '2. Set timestamp variable'
id: timestamp
run: echo "branchtimestamp=$(date +'%Y-%m-%d-%H-%M-%S')" >> $GITHUB_OUTPUT
- We can use the SqlPackage extract command to get the current state of the database as a set of .sql files or as a .dacpac file. The .sql files are more useful for source control, so we will use the extract command with the
/p:ExtractTargetoption set toSchemaObjectType. The output will be a folder with the nameWingtipsFromLiveDBthat contains all of the database objects in a folder structure is similar to how we would lay out a SQL project. For the SqlPackage extract command to succeed, we have to remove the existingWingtipsFromLiveDBfolder if it exists. We will also add a step to create a new SQL project in the folder, which is optional.
After we run the database extract, we remove the student.sql file from the Security folder. SqlPackage hashes out SQL authentication passwords, resulting in this file always having a change in the user definition.
- name: '3. Reset existing database files in repo'
run: rm -f -r WingtipsFromLiveDB
- name: '4. Deconstruct database objects to files'
run: |
sqlpackage /Action:Extract /SourceConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /TargetFile:"WingtipsFromLiveDB" /p:ExtractTarget=SchemaObjectType
rm -f WingtipsFromLiveDB/Security/student.sql
- name: '4b. optional - add a sqlproj to folder'
run: |
dotnet new install microsoft.build.sql.templates
dotnet new sqlproj -o WingtipsFromLiveDB -tp SqlAzureV12
- When we’re in a UI tool like VS Code, we can see whether files have been changed from the source control view. In a pipeline, we’re able to use CLI options to check for changes. We can use the
git statuscommand to check for changes in theWingtipsFromLiveDBfolder. The--porcelainoption will give us a machine-readable output, and we can usewc -lto count the number of lines in the output. If there are any changes, we will create a new branch and commit the changes.
- name: '5. Check if there is any changes with git'
id: get_changes
run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT
- If there are changes, we will create a new branch and commit the changes. The step uses an
ifparameter to compare theGITHUB_OUTPUTfrom theget_changesstep. There’s 7 individual git commands to commit and push changes in a pipeline:git configto set the user emailgit configto set the user namegit switchto create a new branch based on the current timestampgit addto add the changes to the staging areagit committo commit the changesgit pushto push the changes to the repository
# if there are changes commit and push
- name: '6. Process changes into a git commit'
if: steps.get_changes.outputs.changed != 0
run: |
git config --global user.email "dbbot@email.com"
git config --global user.name "Database Status Bot"
git switch -c db-deconstruction-$
git add WingtipsFromLiveDB
git commit -m "Update database deconstruction"
git push -u origin db-deconstruction-$
- The last required step is to create a pull request with the changes. We will use the
vsoch/pull-request-actionaction to create a pull request from the new branch to the main branch. This action utilizes the automaticGITHUB_TOKENsecret on the repository, no setup is required to use the token.
# open pull request
- name: '7. open a pull request'
id: pull_request
if: steps.get_changes.outputs.changed != 0
uses: vsoch/pull-request-action@1.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST_FROM_BRANCH: "db-deconstruction-${{ steps.timestamp.outputs.branchtimestamp }}"
PULL_REQUEST_BRANCH: "main"
PULL_REQUEST_TITLE: "Update database state"
- Since we learned earlier about using the
$GITHUB_STEP_SUMMARYvariable to output information to the GitHub interface, we can use it here to output a summary of the changes. The summary will include a link to the pull request that was created. Thesteps.get_changes.outputs.changedvariable is used to determine whether there were any changes in the database and control which summary to output.
- name: '8a. output summary with PR link'
if: steps.get_changes.outputs.changed != 0
run: |
echo "### :bell: New pull request ${{ steps.pull_request.outputs.pull_request_url }}" >> $GITHUB_STEP_SUMMARY
- name: '8b. output summary with no changes'
if: steps.get_changes.outputs.changed == 0
run: |
echo "### :tada: No database drift detected" >> $GITHUB_STEP_SUMMARY
Complete rogue DB workflow
name: rogue database
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # every Sunday at midnight
permissions:
contents: write
pull-requests: write
jobs:
validate-db:
runs-on: ubuntu-22.04
steps:
- name: '1. Checkout repo files'
uses: actions/checkout@v4
- name: '2. Set timestamp variable'
id: timestamp
run: echo "branchtimestamp=$(date +'%Y-%m-%d-%H-%M-%S')" >> $GITHUB_OUTPUT
- name: '3. Reset existing database files in repo'
run: rm -f -r WingtipsFromLiveDB
- name: '4. Deconstruct database objects to files'
run: |
sqlpackage /Action:Extract /SourceConnectionString:"${{ secrets.SQL_CONNECTION_STRING }}" /TargetFile:"WingtipsFromLiveDB" /p:ExtractTarget=SchemaObjectType
rm -f WingtipsFromLiveDB/Security/student.sql
- name: '4b. optional - add a sqlproj to folder'
run: |
dotnet new install microsoft.build.sql.templates
dotnet new sqlproj -o WingtipsFromLiveDB -tp SqlAzureV12
- name: '5. Check if there is any changes with git'
id: get_changes
run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT
# if there are changes commit and push
- name: '6. Process changes into a git commit'
if: steps.get_changes.outputs.changed != 0
run: |
git config --global user.email "dbbot@email.com"
git config --global user.name "Database Status Bot"
git switch -c db-deconstruction-${{ steps.timestamp.outputs.branchtimestamp }}
git add WingtipsFromLiveDB
git commit -m "Update database deconstruction"
git push -u origin db-deconstruction-${{ steps.timestamp.outputs.branchtimestamp }}
# open pull request
- name: '7. open a pull request'
id: pull_request
if: steps.get_changes.outputs.changed != 0
uses: vsoch/pull-request-action@1.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST_FROM_BRANCH: "db-deconstruction-${{ steps.timestamp.outputs.branchtimestamp }}"
PULL_REQUEST_BRANCH: "main"
PULL_REQUEST_TITLE: "Update database state"
- name: '8a. output summary with PR link'
if: steps.get_changes.outputs.changed != 0
run: |
echo "### :bell: New pull request ${{ steps.pull_request.outputs.pull_request_url }}" >> $GITHUB_STEP_SUMMARY
- name: '8b. output summary with no changes'
if: steps.get_changes.outputs.changed == 0
run: |
echo "### :tada: No database drift detected" >> $GITHUB_STEP_SUMMARY
Test the workflow
To test this workflow, we need to run it once to capture the current state of the database, then make a change to the database and run it again.
- In the GitHub interface, navigate to the Actions tab and select the rogue database workflow.
- Click the Run workflow button to run the workflow manually.
- Once the workflow has completed, click on the New pull request link in the summary to view the pull request that was created.

- This initial pull request contains the entire state of the database written to the
WingtipsFromLiveDBfolder. Merge the pull request to the main branch of the repository.

- Once the pull request is merged, delete the temporary db-deconstruction branch that was created.
Make an “off-process change” to the database
- In VS Code, open the SQL Server extension view (object explorer) and connect to the Azure SQL Database.
- Expand the Databases node and locate the Wingtips database. Right-click on the database and select New Query.
- In the query window, run the following SQL code:
CREATE INDEX IX_Customers_PostalCode ON Customers (PostalCode);
GO
CREATE VIEW dbo.MusicVenues
AS
SELECT v.VenueId
, v.VenueName
, v.VenueType
, v.AdminEmail
, v.AdminPassword
, v.PostalCode
, v.CountryCode
, v.RowVersion
FROM dbo.Venues v
LEFT JOIN dbo.VenueTypes vt
ON v.VenueType = vt.VenueType
WHERE vt.EventTypeShortName IN ('Concert', 'Session', 'Opera')
GO
- Click the Execute button to modify the database.
- In the GitHub interface, navigate to the Actions tab and select the rogue database workflow.
- Click the Run workflow button to run the workflow manually.
- Once the workflow has completed, click on the New pull request link in the summary to view the pull request that was created from the ad-hoc changes we just made to the database.
- This time, the pull request contains only the changes that were made to the database. Go to the Files changed tab to see the changes that were made to the database. There are two files that are different:
WingtipsFromLiveDB/dbo/Views/MusicVenues.sql- new file for the new view that was createdWingtipsFromLiveDB/dbo/Tables/Customers.sql- the table definition for the Customers table was modified to add the new index

- Merge the pull request to the main branch of the repository.
- Once the pull request is merged, delete the temporary db-deconstruction branch that was created.
If we were actively developing this database further, we would integrate this workflow into our other CI/CD processes. For example:
- We could use code analysis check the changes for suggested revisions
- We could use SqlPackage to deploy these changes to our other test/staging/production environments
- We could create unit tests to validate the functionality of the database after the changes were made
The schema compare graphical UIs in Visual Studio and Azure Data Studio are also good tools to visualize the changes that were made to the database and bring these changes back into a SQL project. This workflow provides a way to automatically notice and capture changes to a database that is being modified outside of the normal development process, but additional graphical tools are available to help us understand and manage these changes.