6 minute read

Recently I have been working on a project in ASP.NET Core and DotVVM accompanied by a WebJob using Azure WebJob SDK. The idea behind publishing was that whenever I push code to the repository (VSTS in my case), App Service would pull the code, build it and deploy it automatically (this is achieved by setting up Continuous Deployment). This method works just great for ASP.NET Core application, however when accompanied by a WebJob, things weren't as smooth as I was expecting them to be.

When you deploy a regular ASP.NET application with a WebJob, a special package is used in the main project (Microsoft.Web.WebJobs.Publish), which causes the WebJobs contained within the project to be built and put into their respective directories.

In the main application's .csproj file, special targets are imported which in combination with the configuration file provide the necessary information for the build process to build the jobs and place them to correct WebJob directories.

However this method isn't compatible with ASP.NET Core at the moment and while there is work in progress on enabling it to work together directly, you have to do it on your own.

Brief introduction to Git publishing to App Service

So what happens on the background when you deploy your code to App Service? When change is triggered and the repository is downloaded, Kudu (the engine behind App Service) tries to figure out what type of project you are deploying. It is done by various methods like checking for presence of .sln files and their content (you can look into it yourself here). Once the project type is figured out, a build script is assembled from templates (which can be found here). That build script is then run in the downloaded project and results in successful deployment (or fail :)).

Sample script, which gets generated for ASP.NET Core project can be found below:

@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off

:: ----------------------
:: KUDU Deployment Script
:: Version: 1.0.12
:: ----------------------

:: Prerequisites
:: -------------

:: Verify node.js installed
where node 2>nul >nul
IF %ERRORLEVEL% NEQ 0 (
  echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment.
  goto error
)

:: Setup
:: -----

setlocal enabledelayedexpansion

SET ARTIFACTS=%~dp0%..\artifacts

IF NOT DEFINED DEPLOYMENT_SOURCE (
  SET DEPLOYMENT_SOURCE=%~dp0%.
)

IF NOT DEFINED DEPLOYMENT_TARGET (
  SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot
)

IF NOT DEFINED NEXT_MANIFEST_PATH (
  SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest

  IF NOT DEFINED PREVIOUS_MANIFEST_PATH (
    SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest
  )
)

IF NOT DEFINED KUDU_SYNC_CMD (
  :: Install kudu sync
  echo Installing Kudu Sync
  call npm install kudusync -g --silent
  IF !ERRORLEVEL! NEQ 0 goto error

  :: Locally just running "kuduSync" would also work
  SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd
)
IF NOT DEFINED DEPLOYMENT_TEMP (
  SET DEPLOYMENT_TEMP=%temp%\___deployTemp%random%
  SET CLEAN_LOCAL_DEPLOYMENT_TEMP=true
)

IF DEFINED CLEAN_LOCAL_DEPLOYMENT_TEMP (
  IF EXIST "%DEPLOYMENT_TEMP%" rd /s /q "%DEPLOYMENT_TEMP%"
  mkdir "%DEPLOYMENT_TEMP%"
)

IF DEFINED MSBUILD_PATH goto MsbuildPathDefined
SET MSBUILD_PATH=%ProgramFiles(x86)%\MSBuild\14.0\Bin\MSBuild.exe
:MsbuildPathDefined
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

echo Handling ASP.NET Core Web Application deployment.

:: 1. Restore nuget packages
call :ExecuteCmd dotnet restore "Radius365.sln"
IF !ERRORLEVEL! NEQ 0 goto error

:: 2. Build and publish
call :ExecuteCmd dotnet publish "src\Radius365\Radius365.csproj" --output "%DEPLOYMENT_TEMP%" --configuration Release
IF !ERRORLEVEL! NEQ 0 goto error

:: 3. KuduSync
call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
IF !ERRORLEVEL! NEQ 0 goto error

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto end

:: Execute command routine that will echo out when error
:ExecuteCmd
setlocal
set _CMD_=%*
call %_CMD_%
if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_%
exit /b %ERRORLEVEL%

:error
endlocal
echo An error has occurred during web site deployment.
call :exitSetErrorLevel
call :exitFromFunction 2>nul

:exitSetErrorLevel
exit /b 1

:exitFromFunction
()

:end
endlocal
echo Finished successfully.

Whenever you deploy a project, the generated build script is saved to D:\home\site\deployments\tools as deploy.cmd. From there you can download it and make modifications to it.

So like you probably understood from the statement above, the script can be completely customized. In order to do so, you have to let Kudu know that you will be using a custom deployment script by creating a file in the project root called .deployment and putting the following content into it (much more information about deployment scripts can be found in Kudu's docs):

[config]
command = deploy.cmd

Once done, you also need to create the deploy.cmd file in the root (or specify its path). You can start with copying the contents of the pregenerated file (D:\home\site\deployments\tools\deploy.cmd). After that, you have to change it, so that the WebJob project gets built and put into correct folder. It is very simple and quite common sense:

:: 2.1 Build and publish WebJobs
echo Building and deploying Radius365.WebJob
call :ExecuteCmd dotnet publish "src\Radius365.WebJob\Radius365.WebJob.csproj" -o "%DEPLOYMENT_TEMP%\App_Data\Jobs\Continuous\Radius365.WebJob" -c Release
IF !ERRORLEVEL! NEQ 0 goto error

You just put this piece of code to your deploy.cmd, right after section 2.1 Build and publish, change the paths, names and done. Next you can do that for every other WebJob you have in your project.

The output path %DEPLOYMENT_TEMP%\App_Data\Jobs\Continuous\ applies to all cases when you are using the WebJobs SDK. If you need to deploy a triggered job, you would be using Triggered directory instead of Continuous (more info can be found in the docs).

I also found it quite handy to include run.cmd script with my WebJob to make sure that the correct executable is used when the job is run or triggered instead of relying on .exe detection.

Comments

To submit comments, go to GitHub Discussions.