public/Publish-DacPac.ps1

function Publish-DacPac {
<#
    .SYNOPSIS
    Publish-DacPac allows you to deploy a SQL Server Database using a DacPac to a SQL Server instance.
 
    .DESCRIPTION
    Publishes a SSDT DacPac using a specified DacPac publish profile from your solution.
    Basically deploys the DacPac by invoking SqlPackage.exe using a DacPac Publish profile.
    The SqlPackage.exe publish operation incrementally updates the schema of a target database to match the structure of a source database.
 
    Note that the XML of the DAC Publish Profile will updated with the Server, Database and SqlCmdVariables variables and a new file written to same folder as the DACPAC called
    "$Database.deploy.publish.xml" where $Database is the value passed to the -Database parameter.
 
    More information on SqlPackage.exe can be found here: https://docs.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage
    This module requires SqlPackage.exe to be installed on the host machine. See https://docs.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-download
 
    .PARAMETER DacPacPath
    Full path to your database DACPAC (e.g. C:\Dev\YourDB\bin\Debug\YourDB.dacpac)
 
    .PARAMETER DacPublishProfile
    Name of the DAC Publish Profile to be found in the same folder as your DACPAC (e.g. YourDB.CI.publish.xml)
    You can also provide the full path to an alternative DAC Publish Profile.
 
    .PARAMETER Server
    Name of the target server, including instance and port if required. Note that this overwrites the server defined in
    the DAC Publish Profile
 
    .PARAMETER Database
    Normally, the database will be named the same as your DACPAC. However, by adding the -Database parameter, you can name the database anything you like.
    Note that this overwrites the database name defined in the DAC Publish Profile.
 
    .PARAMETER SqlCmdVariables
    A string array containing SqlCmd variables to be updated in the DAC Publish Profile. These should be name/value pairs with no delimiters. For example:
 
        var1=varvalue1
        var2=varvalue2
        var3=varvalue3
 
    The simplest way of creating this in PowerShell is
 
        [string[]]$SqlCmdVariables = @();
        $SqlCmdVariables += "var1=varvalue1";
        $SqlCmdVariables += "var2=varvalue2";
        $SqlCmdVariables += "var3=varvalue3";
     
    And pass $SqlCmdVariables to the -SqlCmdVariables parameter.
 
    .PARAMETER PreferredVersion
    Defines the preferred version of SqlPackage.exe you wish to use. Use 'latest' for the latest version, or do not provide the parameter at all.
    It is recommended you use the latest version of SqlPackage.exe as this will deploy to all previous version of SQL Server.
 
    Valid values for -Version are:
 
        latest = use the latest version of SqlPackage.exe
        17 = SQL Server 2025
        16 = SQL Server 2022
        15 = SQL Server 2019
        14 = SQL Server 2017
        13 = SQL Server 2016
        12 = SQL Server 2014
        11 = SQL Server 2012
 
    .PARAMETER DeployScriptPath
    Specifies an optional file path to output the deployment script. For Azure deployments, if there are T-SQL commands to create or modify the master database, a script will be written to the same path but with "Filename_Master.sql" as the output file name.
    Note that providing the DeployScriptPath parameter will cause SqlPackage.exe to be called with the /Action:Script parameter and the database will NOT be deployed but scripted out.
 
    .PARAMETER DeployReportPath
    Specifies an optional file path to output the deployment report xml file.
 
    .PARAMETER AuthenticationMethod
    Indicates which method to use to connect to the target SQL Server instance in order to deploy the database DacPac.
    Valid options are:
 
        windows - Windows authentication (default) will be used to deploy the DacPac to the target SQL Server instance
        sqlauth - SQL Server authentication will be used to deploy the DacPac to the target SQL Server instance either on-premise or in Azure
 
    .PARAMETER AuthenticationUser
    UserID for the AuthenticationUser
    Only required if AuthenticationMethod = sqlauth
     
    .PARAMETER AuthenticationPassword
    Password for the AuthenticationUser
    Only required if AuthenticationMethod = sqlauth
 
    .PARAMETER EncryptConnection
    Specifies if SQL encryption should be used for the target database connection.
 
    .EXAMPLE
    Publish-DacPac -Server 'YourDBServer' -Database 'NewDatabaseName' -DacPacPath 'C:\Dev\YourDB\bin\Debug\YourDB.dacpac' -DacPublishProfile 'YourDB.CI.publish.xml'
 
    Publish your database to server 'YourDBServer' with the name 'NewDatabaseName', using the DACPAC 'C:\Dev\YourDB\bin\Debug\YourDB.dacpac' and the DAC Publish profile 'YourDB.CI.publish.xml'.
 
    .EXAMPLE
    Publish-DacPac -Server 'YourDBServer' -DacPacPath 'C:\Dev\YourDB\bin\Debug\YourDB.dacpac' -DacPublishProfile 'YourDB.CI.publish.xml'
 
    Simplist form
 
    .EXAMPLE
    Publish-DacPac -Server 'YourDBServer' -DacPacPath 'C:\Dev\YourDB\bin\Debug\YourDB.dacpac' -DacPublishProfile 'YourDB.CI.publish.xml' -PreferredVersion 130;
 
    Request a specific version of SqlPackage.exe
 
    .EXAMPLE
    [string[]]$SqlCmdVariables = @();
    $SqlCmdVariables += "var1=varvalue1";
    $SqlCmdVariables += "var2=varvalue2";
    $SqlCmdVariables += "var3=varvalue3";
    Publish-DacPac -Server 'YourDBServer' -DacPacPath 'C:\Dev\YourDB\bin\Debug\YourDB.dacpac' -DacPublishProfile 'YourDB.CI.publish.xml' -SqlCmdVariables $SqlCmdVariables;
 
    Shows how to pass values to the -SqlCmdVariables parameter. These will be written to the SqlCmdVariable section of the DAC publish profile.
     
    .LINK
    https://github.com/DrJohnT/PublishDacPac
 
    .NOTES
    Written by (c) Dr. John Tunnicliffe, 2019-2025 https://github.com/DrJohnT/PublishDacPac
    This PowerShell script is released under the MIT license http://www.opensource.org/licenses/MIT
 
#>


    [CmdletBinding()]
    param
    (
        [String] [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $DacPacPath,

        [String] [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $DacPublishProfile,

        [String] [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Server,

        [String] [Parameter(Mandatory = $false)]
        $Database,

        [String[]] [Parameter(Mandatory = $false)]
        $SqlCmdVariables,

        [String] [Parameter(Mandatory = $false)]
        [ValidateSet('17', '16', '15', '14', '13', '12', '11', 'latest')]
        $PreferredVersion = 'latest',

        [String] [Parameter(Mandatory = $false)]        
        $DeployScriptPath,

        [String] [Parameter(Mandatory = $false)]
        $DeployReportPath,

        [String] [Parameter(Mandatory = $false)]
        [ValidateSet('windows', 'sqlauth')]
        $AuthenticationMethod = 'windows',

        [Alias("Username","UserID")]
        [String] [Parameter(Mandatory = $false)]
        $AuthenticationUser,

        [Alias("Password")]
        [String] [Parameter(Mandatory = $false)]
        $AuthenticationPassword,

        [bool] [Parameter(Mandatory = $false)]
        $EncryptConnection = $false
    )

    $global:ErrorActionPreference = 'Stop';

    try {
        if ([string]::IsNullOrEmpty($PreferredVersion)) {
            $PreferredVersion = 'latest';
        }
        # find the specific version of SqlPackage or the latest if not available
        [string]$SqlPackageExePath = Get-SqlPackagePath -Version $PreferredVersion;
        $SqlPackageExePath = $SqlPackageExePath.Trim(); # remove any whitespace before / after the path

        if (!(Test-Path -Path $SqlPackageExePath)) {
            Write-Error "Could not find SqlPackage.exe in order to deploy the database DacPac!";
            Write-Warning "For install instructions, see https://docs.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-download/";
            throw "Could not find SqlPackage.exe in order to deploy the database DacPac!";
        }

        [String]$ProductVersion = (Get-Item $SqlPackageExePath).VersionInfo.ProductVersion;
        if (!(Test-Path -Path $DacPacPath)) {
            throw "DacPac path does not exist in $DacPacPath";
        }

        $DacPacName = Split-Path $DacPacPath -Leaf;
        $OriginalDbName = $DacPacName -replace ".dacpac", ""
        $DacPacFolder = Split-Path $DacPacPath -Parent;
        if ([string]::IsNullOrEmpty($Database)) {
            $Database = $OriginalDbName;
        }

        # figure out if we have a full path to the DAC Publish Profile or just the filename of the DAC Publish Profile in the same folder as the DACPAC
        if (Test-Path($DacPublishProfile)) {
            $DacPacPublishProfilePath = $DacPublishProfile;
        } else {
            try {
                $DacPacPublishProfilePath = Resolve-Path "$DacPacFolder\$DacPublishProfile";
            } catch {
                throw "DAC Publish Profile does not exist";
            }
        }

        $ProfileName = Split-Path $DacPacPublishProfilePath -Leaf;

        Write-Output "Publish-DacPac resolved the following parameters:";
        Write-Output "DacPacPath : $DacPacName from $DacPacFolder";
        Write-Output "DacPublishProfile : $ProfileName from $DacPacPublishProfilePath";
        Write-Output "Server : $Server";
        Write-Output "Database : $Database";
        Write-Output "SqlPackage.exe : $Version (v$ProductVersion) from $SqlPackageExePath";
        Write-Output "AuthenticationMethod : $AuthenticationMethod";

        [xml]$DacPacDacPublishProfile = [xml](Get-Content $DacPacPublishProfilePath);

        # update the database name and deployment server connection string in the DAC Publish Profile
        $DacPacDacPublishProfile.Project.PropertyGroup.TargetDatabaseName = $Database;
        $ExistingConnectionString = $DacPacDacPublishProfile.Project.PropertyGroup.TargetConnectionString
        $ConnBuilder = New-Object System.Data.OleDb.OleDbConnectionStringBuilder($ExistingConnectionString);
        $ConnBuilder["Data Source"] = $Server;
        $DacPacDacPublishProfile.Project.PropertyGroup.TargetConnectionString = $ConnBuilder.ConnectionString;

        # update the SqlCmdVariables (if any)
        if ($SqlCmdVariables.Count -gt 0) {
            $namesp = 'http://schemas.microsoft.com/developer/msbuild/2003';
            [System.Xml.XmlNamespaceManager] $nsmgr = $DacPacDacPublishProfile.NameTable;
            $nsmgr.AddNamespace('n', $namesp);

            <#
                # adding new nodes it not a good idea as they come up as warnings during deployment
                $ItemNode = $DacPacDacPublishProfile.SelectSingleNode('//n:ItemGroup', $nsmgr);
                if ($null -eq $ItemNode) {
                    Write-Information 'Creating ItemGroup to contain SqlCmdVariables';
                    $NewElement = $DacPacDacPublishProfile.CreateNode('element', 'ItemGroup', $namesp);
                    $ItemNode = $DacPacDacPublishProfile.DocumentElement.AppendChild($NewElement);
                }
            #>

            foreach ($SqlCmdVariable in $SqlCmdVariables) {
                [string[]]$NameValuePair = $SqlCmdVariable -split "=" | ForEach-Object { $_.trim() }
                $name = $NameValuePair[0];
                $value = $NameValuePair[1];

                # find the matching node (if any)
                $SqlCmdVariableNode = $DacPacDacPublishProfile.SelectNodes('//n:ItemGroup/n:SqlCmdVariable', $nsmgr) | Where-Object { ($_.Include -eq $name) };

                if ($null -eq $SqlCmdVariableNode) {
                    <#
                        # adding new nodes it not a good idea as they come up as warnings during deployment
                        # note missing, so create it
                        Write-Output "Adding SqlCmdVariable name: $name value: $value";
                        $NewSqlCmdVariableElement = $DacPacDacPublishProfile.CreateNode('element', 'SqlCmdVariable', $namesp);
                        $IncludeAttr = $DacPacDacPublishProfile.CreateAttribute('Include');
                        $IncludeAttr.Value = $name;
                        $NewSqlCmdVariableElement.Attributes.Append($IncludeAttr) | Out-Null; # do this to stop write to std output
                        $ItemNode.AppendChild($NewSqlCmdVariableElement) | Out-Null; # do this to stop write to std output
                        # add inner Value element
                        $NewValueElement = $DacPacDacPublishProfile.CreateNode('element', 'Value', $namesp);
                        $NewValueElement.InnerText = $value;
                        $NewSqlCmdVariableElement.AppendChild($NewValueElement) | Out-Null; # do this to stop write to std output
 
                    #>

                    Write-Warning "SqlCmdVariable '$name' was not found in DAC publish profile";
                } else {
                    # node present, so update it
                    Write-Output "Updating SqlCmdVariable name: $name value: $value";
                    $SqlCmdVariableNode.Value = $value;
                }
            }
        }
        $DacPacUpdatedProfilePath = "$DacPacFolder\$Database.deploy.publish.xml";
        $DacPacDacPublishProfile.Save($DacPacUpdatedProfilePath);
        Write-Output "Updated DacPublishProfile : $DacPacUpdatedProfilePath";

        Write-Output "Following output generated by SqlPackage.exe";
        Write-Output "==============================================================================";

        $global:lastexitcode = 0;

        $ArgList = @(
            "/SourceFile:$DacPacPath",
            "/Profile:$DacPacUpdatedProfilePath",
            "/Diagnostics:true"
        );
        if ("$DeployScriptPath" -eq "") {
            $ArgList += "/Action:Publish";
            Write-Verbose "Publish-DacPac: Deploying database '$Database' to server '$Server' using DacPac '$DacPacName'"
        } else {
            $ArgList += "/Action:Script";
            $ArgList += "/DeployScriptPath:$DeployScriptPath";
            Write-Verbose "Publish-DacPac: Scripting database '$Database' for deployment to server '$Server' using DacPac '$DacPacName'"
        }
        if ("$DeployReportPath" -ne "") {
            $ArgList += "/DeployReportPath:$DeployReportPath";
        }
        if ($AuthenticationMethod -eq "sqlauth") { 
            # For SQL Server Auth scenarios, defines the SQL Server user/password to use to access the target database instance.
            $ArgList += "/TargetUser:$AuthenticationUser";
            $ArgList += "/TargetPassword:$AuthenticationPassword";            
        }
        if ($EncryptConnection) {
            $ArgList += "/TargetEncryptConnection:true";
        }
        
        Invoke-ExternalCommand -Command "$SqlPackageExePath" -Arguments $ArgList;
        
    } catch {
        Write-Error "Publish-DacPac Error: $_";
    }
}