Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for build and publish database project #65

Merged
merged 23 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ad1bfa0
Add sqlproj input
zijchen Dec 4, 2021
c07ee32
Add sqlproj input
zijchen Dec 4, 2021
3b5d165
Merge branch 'master' of https://github.com/Azure/sql-action into sql…
zijchen Jan 3, 2022
c45a72c
Initial commit
zijchen Jan 3, 2022
0a7831e
Merge branch 'master' of https://github.com/Azure/sql-action into sql…
zijchen Jan 3, 2022
b0a4800
Add tests
zijchen Jan 4, 2022
c852a4c
Add DACPAC action test to PR check
zijchen Jan 4, 2022
998da41
Escape database name
zijchen Jan 4, 2022
782eecc
Truncate table instead of dropping DB
zijchen Jan 4, 2022
1acc9bf
Cleanup
zijchen Jan 4, 2022
d9f00b9
Merge branch 'master' of https://github.com/Azure/sql-action into dep…
zijchen Jan 5, 2022
5f63eea
Add build and publish test to PR check
zijchen Jan 5, 2022
952a27b
Merge branch 'master' of https://github.com/Azure/sql-action into sql…
zijchen Jan 5, 2022
139a837
Merge branch 'deploy-dacpac' of https://github.com/Azure/sql-action i…
zijchen Jan 5, 2022
ed99d2a
Resolve merge conflicts
zijchen Jan 5, 2022
7ff406d
Merge branch 'master' of https://github.com/Azure/sql-action into sql…
zijchen Feb 22, 2022
9b95140
Remove check on github.event
zijchen Feb 22, 2022
0cb8da0
Merge branch 'master' of https://github.com/Azure/sql-action into sql…
zijchen Feb 22, 2022
b6dd74a
Rename variable
zijchen Feb 22, 2022
473efb6
Use placeholder passwords
zijchen Feb 22, 2022
c3dc3ba
PR comments
zijchen Feb 22, 2022
5ce8487
Fix connection string for build and publish
zijchen Feb 22, 2022
127baa7
Missing awaits
zijchen Feb 23, 2022
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
8 changes: 8 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ jobs:
connection-string: '${{ secrets.AZURE_SQL_CONNECTION_STRING_NO_DATABASE }}Initial Catalog=${{ env.TEST_DB }};'
dacpac-package: ./__testdata__/sql-action.dacpac

# Build and publish sqlproj that should create a new view
- name: Test Build and Publish
uses: ./
with:
server-name: sql-action.database.windows.net
connection-string: ${{ secrets.AZURE_SQL_CONNECTION_STRING }}
project-file: ./__testdata__/TestProject/sql-action.sqlproj

# Execute testsql.sql via SQLCMD on server
- name: Test SQL Action
uses: ./
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ typings/
/.vs/sql-action/v16/.suo
/.vs/ProjectSettings.json
/.vs/slnx.sqlite

# Test sqlproj build artifacts
/__testdata__/TestProject/bin/*
/__testdata__/TestProject/obj/*
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ az ad sp create-for-rbac --name "mySQLServer" --role contributor \
}
```

### Sample workflow to deploy to an Azure SQL database
### Sample workflow to deploy a database project to an Azure SQL database

```yaml
# .github/workflows/sql-deploy.yml
Expand All @@ -88,7 +88,33 @@ jobs:
- uses: azure/sql-action@v1
with:
server-name: REPLACE_THIS_WITH_YOUR_SQL_SERVER_NAME
connection-string: ${{ secrets.AZURE_SQL_CONNECTION_STRING }}
connection-string: ${{ secrets.AZURE_SQL_CONNECTION_STRING }}
project-file: './Database.sqlproj'
build-arguments: '-c Release' # Optional arguments passed to dotnet build
arguments: '/p:DropObjectsNotInSource=true' # Optional parameters for SqlPackage Publish
```

**Note:**
The database project must use the [Microsoft.Build.Sql](https://www.nuget.org/packages/microsoft.build.sql/) SDK.

### Sample workflow to deploy a DACPAC to an Azure SQL database

```yaml
# .github/workflows/sql-deploy.yml
on: [push]

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v1
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: azure/sql-action@v1
with:
server-name: REPLACE_THIS_WITH_YOUR_SQL_SERVER_NAME
connection-string: ${{ secrets.AZURE_SQL_CONNECTION_STRING }}
dacpac-package: './Database.dacpac'
```

Expand Down
5 changes: 5 additions & 0 deletions __testdata__/TestProject/Table1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE [dbo].[Table1]
(
[Id] INT NOT NULL PRIMARY KEY,
[Column1] NVARCHAR(10) NULL
)
2 changes: 2 additions & 0 deletions __testdata__/TestProject/View1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE VIEW [dbo].[View1]
AS SELECT * FROM [Table1]
9 changes: 9 additions & 0 deletions __testdata__/TestProject/sql-action.sqlproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="Current">
<Sdk Name="Microsoft.Build.Sql" Version="0.1.1-alpha" />
<PropertyGroup>
<Name>sql-action</Name>
<DSP>Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider</DSP>
<ModelCollation>1033, CI</ModelCollation>
</PropertyGroup>
</Project>
5 changes: 4 additions & 1 deletion __testdata__/testsql.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
-- This script is used by pr-check.yml to test the SQLCMD action

-- This should successfully insert data into the table created in the DACPAC step
INSERT INTO [Table1] VALUES(1, 'test');
INSERT INTO [Table1] VALUES(1, 'test');

-- This should successfully SELECT from the view created by the sqlproj
SELECT * FROM [View1];
69 changes: 66 additions & 3 deletions __tests__/AzureSqlAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as path from 'path';
import * as exec from '@actions/exec';
import AzureSqlAction, { IDacpacActionInputs, ISqlActionInputs, ActionType, SqlPackageAction } from "../src/AzureSqlAction";
import AzureSqlAction, { IBuildAndPublishInputs, IDacpacActionInputs, ISqlActionInputs, ActionType, SqlPackageAction } from "../src/AzureSqlAction";
import AzureSqlActionHelper from "../src/AzureSqlActionHelper";
import DotnetUtils from '../src/DotnetUtils';
import SqlConnectionStringBuilder from '../src/SqlConnectionStringBuilder';

let sqlConnectionStringBuilderMock = jest.mock('../src/SqlConnectionStringBuilder', () => {
Expand Down Expand Up @@ -40,7 +42,7 @@ describe('AzureSqlAction tests', () => {
let getSqlPackagePathSpy = jest.spyOn(AzureSqlActionHelper, 'getSqlPackagePath').mockResolvedValue('SqlPackage.exe');
jest.spyOn(exec, 'exec').mockRejectedValue(1);

expect(action.execute().catch(() => null)).rejects;
expect(await action.execute().catch(() => null)).rejects;
expect(getSqlPackagePathSpy).toHaveBeenCalledTimes(1);
});

Expand All @@ -66,9 +68,61 @@ describe('AzureSqlAction tests', () => {
let getSqlCmdPathSpy = jest.spyOn(AzureSqlActionHelper, 'getSqlCmdPath').mockResolvedValue('SqlCmd.exe');
jest.spyOn(exec, 'exec').mockRejectedValue(1);

expect(action.execute().catch(() => null)).rejects;
expect(await action.execute().catch(() => null)).rejects;
expect(getSqlCmdPathSpy).toHaveBeenCalledTimes(1);
});

it('should build and publish database project', async () => {
const inputs = getInputs(ActionType.BuildAndPublish) as IBuildAndPublishInputs;
const action = new AzureSqlAction(inputs);
const expectedDacpac = path.join('./bin/Debug', 'TestProject.dacpac');

const parseCommandArgumentsSpy = jest.spyOn(DotnetUtils, 'parseCommandArguments').mockResolvedValue({});
const findArgumentSpy = jest.spyOn(DotnetUtils, 'findArgument').mockResolvedValue(undefined);
const getSqlPackagePathSpy = jest.spyOn(AzureSqlActionHelper, 'getSqlPackagePath').mockResolvedValue('SqlPackage.exe');
const execSpy = jest.spyOn(exec, 'exec').mockResolvedValue(0);

await action.execute();

expect(parseCommandArgumentsSpy).toHaveBeenCalledTimes(1);
expect(findArgumentSpy).toHaveBeenCalledTimes(2);
expect(getSqlPackagePathSpy).toHaveBeenCalledTimes(1);
expect(execSpy).toHaveBeenCalledTimes(2);
expect(execSpy).toHaveBeenNthCalledWith(1, `dotnet build "./TestProject.sqlproj" -p:NetCoreBuild=true --verbose --test "test value"`);
expect(execSpy).toHaveBeenNthCalledWith(2, `"SqlPackage.exe" /Action:Publish /TargetConnectionString:"${inputs.connectionString.connectionString}" /SourceFile:"${expectedDacpac}"`);
});

it('throws if dotnet fails to build sqlproj', async () => {
const inputs = getInputs(ActionType.BuildAndPublish) as IBuildAndPublishInputs;
const action = new AzureSqlAction(inputs);

const parseCommandArgumentsSpy = jest.spyOn(DotnetUtils, 'parseCommandArguments').mockResolvedValue({});
jest.spyOn(exec, 'exec').mockRejectedValueOnce(1);

expect(await action.execute().catch(() => null)).rejects;
expect(parseCommandArgumentsSpy).toHaveBeenCalledTimes(1);
});

it('throws if build succeeds but fails to publish', async () => {
const inputs = getInputs(ActionType.BuildAndPublish) as IBuildAndPublishInputs;
const action = new AzureSqlAction(inputs);

const parseCommandArgumentsSpy = jest.spyOn(DotnetUtils, 'parseCommandArguments').mockResolvedValue({});
const getSqlPackagePathSpy = jest.spyOn(AzureSqlActionHelper, 'getSqlPackagePath').mockResolvedValue('SqlPackage.exe');
const execSpy = jest.spyOn(exec, 'exec').mockImplementation((commandLine) => {
// Mock implementation where dotnet build is successful but fails the SqlPackage publish
if (commandLine.indexOf('dotnet build') >= 0) {
return Promise.resolve(0);
} else {
return Promise.reject(1);
}
});

expect(await action.execute().catch(() => null)).rejects;
expect(parseCommandArgumentsSpy).toHaveBeenCalledTimes(1);
expect(getSqlPackagePathSpy).toHaveBeenCalledTimes(1); // Verify build succeeds and calls into Publish
expect(execSpy).toHaveBeenCalledTimes(2);
});
});

function getInputs(actionType: ActionType) {
Expand All @@ -95,5 +149,14 @@ function getInputs(actionType: ActionType) {
additionalArguments: '-t 20'
} as ISqlActionInputs;
}
case ActionType.BuildAndPublish: {
return {
serverName: 'testServer.database.windows.net',
actionType: ActionType.BuildAndPublish,
connectionString: new SqlConnectionStringBuilder('Server=testServer.database.windows.net;Initial Catalog=testDB;User Id=testUser;Password=testPassword'),
zijchen marked this conversation as resolved.
Show resolved Hide resolved
projectFile: './TestProject.sqlproj',
buildArguments: '--verbose --test "test value"'
} as IBuildAndPublishInputs
}
}
}
34 changes: 34 additions & 0 deletions __tests__/DotnetUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import DotnetUtils from "../src/DotnetUtils";

describe('DotnetUtils tests', () => {
it('parseCommandArguments should parse dotnet parameters', async () => {
let dotnetParams = '';
let parsedArgs = await DotnetUtils.parseCommandArguments(dotnetParams);
expect(parsedArgs).toEqual({});

dotnetParams = `-o C:\\test\\ --configuration 'Debug' --verbose --test "test value"`;
parsedArgs = await DotnetUtils.parseCommandArguments(dotnetParams);
expect(parsedArgs).toEqual({
'-o': 'C:\\test\\',
'--configuration': `'Debug'`,
'--verbose': undefined,
'--test': `"test value"`
});
});

it ('should find arguments parsed by parseCommandArguments', async () => {
const dotnetParams = `-o C:\\test\\ --configuration 'Debug' --verbose --test "test value"`;
const parsedArgs = await DotnetUtils.parseCommandArguments(dotnetParams);

expect(await DotnetUtils.findArgument(parsedArgs, '')).toEqual(undefined);
expect(await DotnetUtils.findArgument(parsedArgs, '-o')).toEqual('C:\\test\\');
expect(await DotnetUtils.findArgument(parsedArgs, '--output', '-o')).toEqual('C:\\test\\');
zijchen marked this conversation as resolved.
Show resolved Hide resolved
expect(await DotnetUtils.findArgument(parsedArgs, '--configuration', '-c')).toEqual(`'Debug'`);
expect(await DotnetUtils.findArgument(parsedArgs, '--verbose')).toEqual(undefined);
expect(await DotnetUtils.findArgument(parsedArgs, '--test')).toEqual(`"test value"`);

// Edge case
expect(await DotnetUtils.findArgument({}, '')).toEqual(undefined);
});

});
42 changes: 38 additions & 4 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@ describe('main.ts tests', () => {
jest.restoreAllMocks();
})

it('gets inputs and executes build and publish action', async () => {
const resolveFilePathSpy = jest.spyOn(AzureSqlActionHelper, 'resolveFilePath').mockReturnValue('./TestProject.sqlproj');
const getInputSpy = jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
switch(name) {
case 'server-name': return 'test2.database.windows.net';
case 'connection-string': return 'Server=testServer.database.windows.net;Initial Catalog=testDB;User Id=testUser;Password=testPassword;';
case 'project-file': return './TestProject.sqlproj';
default : return '';
}
});

const getAuthorizerSpy = jest.spyOn(AuthorizerFactory, 'getAuthorizer');
const addFirewallRuleSpy = jest.spyOn(FirewallManager.prototype, 'addFirewallRule');
const actionExecuteSpy = jest.spyOn(AzureSqlAction.prototype, 'execute');
const removeFirewallRuleSpy = jest.spyOn(FirewallManager.prototype, 'removeFirewallRule');
const setFailedSpy = jest.spyOn(core, 'setFailed');
const detectIPAddressSpy = SqlUtils.detectIPAddress = jest.fn().mockImplementationOnce(() => {
return "";
});

await run();

expect(AzureSqlAction).toHaveBeenCalled();
expect(detectIPAddressSpy).toHaveBeenCalled();
expect(getAuthorizerSpy).not.toHaveBeenCalled();
expect(getInputSpy).toHaveBeenCalledTimes(7);
expect(SqlConnectionStringBuilder).toHaveBeenCalled();
expect(resolveFilePathSpy).toHaveBeenCalled();
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
expect(actionExecuteSpy).toHaveBeenCalled();
expect(removeFirewallRuleSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});

it('gets inputs and executes dacpac action', async () => {
let resolveFilePathSpy = jest.spyOn(AzureSqlActionHelper, 'resolveFilePath').mockReturnValue('./TestDacpacPackage.dacpac');
let getInputSpy = jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
Expand All @@ -30,7 +64,7 @@ describe('main.ts tests', () => {
}

return '';
});
});

let getAuthorizerSpy = jest.spyOn(AuthorizerFactory, 'getAuthorizer');
let addFirewallRuleSpy = jest.spyOn(FirewallManager.prototype, 'addFirewallRule');
Expand Down Expand Up @@ -64,7 +98,7 @@ describe('main.ts tests', () => {
case 'sql-file': return './TestSqlFile.sql';
default: return '';
}
});
});

let setFailedSpy = jest.spyOn(core, 'setFailed');
let getAuthorizerSpy = jest.spyOn(AuthorizerFactory, 'getAuthorizer');
Expand Down Expand Up @@ -93,15 +127,15 @@ describe('main.ts tests', () => {
jest.spyOn(AzureSqlActionHelper, 'resolveFilePath').mockImplementation(() => {
throw new Error(`Unable to find file at location`);
});

jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
switch(name) {
case 'server-name': return 'test1.database.windows.net';
case 'connection-string': return 'Server=testServer.database.windows.net;Initial Catalog=testDB;User Id=testUser;Password=testPassword;';
case 'sql-file': return './TestSqlFile.sql';
default: return '';
}
});
});
let detectIPAddressSpy = SqlUtils.detectIPAddress = jest.fn().mockImplementationOnce(() => {
return "";
});
Expand Down
18 changes: 12 additions & 6 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
name: 'Azure SQL Deploy'
description: 'Deploy a DACPAC or a SQLscript to AzureSQLdatabase'
description: 'Deploy a database project, DACPAC, or a SQL script to Azure SQL database'
inputs:
server-name:
description: 'Name of theAzureSQLServername,like Fabrikam.database.windows.net.'
description: 'Name of the Azure SQL Server name, like Fabrikam.database.windows.net.'
required: false
connection-string:
description: 'Theconnectionstring,includingauthenticationinformation,fortheAzureSQLServer database.'
description: 'The connection string, including authentication information, for the Azure SQL Server database.'
required: true
dacpac-package:
description: 'Path to DACPACfile todeploy'
description: 'Path to DACPAC file to deploy'
zijchen marked this conversation as resolved.
Show resolved Hide resolved
required: false
sql-file:
description: 'Path to SQL script file to deploy'
description: 'Path to SQL script file to deploy'
required: false
project-file:
description: 'Path to the SQL database project file to deploy'
required: false
arguments:
description: 'In case DACPAC option is selected, additional SqlPackage arguments that will be applied. When SQL query option is selected, additional sqlcmd arguments will be applied.'
description: 'In case DACPAC option is selected, additional SqlPackage arguments that will be applied. When SQL query option is selected, additional sqlcmd arguments will be applied.'
required: false
build-arguments:
description: 'In case Build and Publish option is selected, additional arguments that will be applied to dotnet build when building the database project.'
required: false
runs:
using: 'node12'
Expand Down
2 changes: 1 addition & 1 deletion lib/main.js

Large diffs are not rendered by default.

Loading