Building and deploying Hexo to an Azure App Service from Azure DevOps

This blog runs on the wonderful Hexo, a Markdown-based and NodeJS-powered blog framework. The code is stored in a project on Azure DevOps (https://***.visualstudio.com/) and the blog runs on an Azure App Service.

In this blog post I’ll show you how I build and deploy my blog to Azure.

Project and solution

My Hexo blog lives in an otherwise empty ASP.NET Web Application because I want to run the hexo generate command - that generates the static files that need to be deployed - via the Post-build event command line.

To do this right-click the Project, select Properties, go to Build Events and add the following in Post-build event command line:

hexo generate

This will also trigger the same command when you build the project in Visual Studio.

The Build

Get Sources

npm install

npm install hexo-cli -g

Command and arguments: install hexo-cli -g

Visual Studio Build

In the logs you should see the hexo generate PostBuildEvent being triggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PostBuildEvent:
hexo generate
INFO Start processing
INFO Files loaded in 342 ms
INFO Generated: index.html
INFO Generated: about/index.html
INFO Generated: archives/index.html
INFO Generated: archives/2018/11/index.html
INFO Generated: fancybox/blank.gif
INFO Generated: tags/automate-everything/index.html
[...]
INFO Generated: css/images/IMG_20181012_140103.jpg
INFO 54 files generated in 1.07 s
Done Building Project "D:\a\3\s\Blog.csproj" (default targets).
Done Building Project "D:\a\3\s\Blog.sln" (default targets).

Build succeeded.

For this step I first tried to run hexo generate via npm too, as described in posts like this, but I could not get that to work…

Copy Files

Target Folder: $(Build.ArtifactStagingDirectory)

Publish Build Artifacts

The Release

The release is a standard Azure App Service Deploy:

Building and deploying fractal.build to Azure from Team Foundation Server

This year we started with creating a Design Language System for Nextens. It runs on Fractal, “a tool to help you build and document web component libraries, and then integrate them into your projects”. We use it to create and document the atoms, molecules and organisms that make up our components.

The site is deployed continuously to an App Service in Azure from our on-premise Microsoft Visual Studio Team Foundation Server 2017.3.1. This post documents how this is done.

web.config

In the source folder we have a web.config that fixes the MIME types of fonts so they load correctly in Azure, and which adds a rewrite rule that enables extension-less URLs in the documentation pages of Fractal.

The latter is needed because we have this setting in \gulp\fractal-setup.js:

1
fractal.web.set('builder.urls.ext', null); // default is '.html'

The content of web.config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
</customHeaders>
</httpProtocol>
<staticContent>
<remove fileExtension=".svg" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<mimeMap fileExtension=".woff" mimeType="application/x-font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
</staticContent>
<rewrite>
<rules>
<rule name="docs/pattern-overview">
<match url="(^docs|^components)(\/.+)+$"/>
<action type="Rewrite" url="{R:0}.html"/>
<conditions>
<add input="{URL}" pattern=".(html|svg|scss|css)$" negate="true"/>
</conditions>
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

The web.config file is copied from the source folder to the public folder by a gulp task (gulp\gulp-tasks\copy.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* global module */
module.exports = function (gulp, config, plugins, build_base) {
'use strict';

gulp.task('copy:js', function (done) {
gulp.src(config.src.js)
.pipe(plugins.plumber({ errorHandler: config.onError }))
.pipe(plugins.include())
.pipe(plugins.uglify())
.pipe(gulp.dest(config.build.js))

gulp.src(config.fractal.docsImages)
.pipe(gulp.dest(build_base + 'docs/'))

gulp.src(config.src.path + '*.*') // favicon.ico and web.config
.pipe(gulp.dest(build_base))

done();
});
};

The config.src.path is __dirname + '/source/'

The Build tasks

NPM install

To install the npm packages defined in your package.json file add an npm task with these settings:

  • The working folder should point to the directory that contains the package.json file.
  • The npm command to use is install

Install gulp –save-dev

To install the gulp npm package add an npm task with these settings:

  • npm command: install
  • arguments: gulp --save-dev

Install gulp-cli –save-dev

To install the gulp-cli npm package add an npm task with these settings:

  • npm command: install
  • arguments: gulp-cli --save-dev

Gulp build

To run the actual Gulp build task add a Gulp task with these settings:

  • Gulp File Path: the path to your gulpfile.js
  • Gulp Task(s): build
  • Advanced > Working Directory: the same as the working folder in the first two tasks
  • Advanced > gulp.js location: node_modules/gulp/bin/gulp.js

The log of this task should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
##[section]Starting: gulp build
[...]
[00:44:44] Starting 'build'...
[...]
[00:45:41] Finished 'build:all' after 55 s
[00:45:41] Starting 'fractal:build'...
[?25l⚑ Exported 1 of 635 items
[...]
[?25l⚑ Exported 635 of 635 items
✔ Fractal build completed!
[00:51:17] Finished 'fractal:build' after 5.6 min
[00:51:17] Finished 'build' after 6.55 min
##[section]Finishing: gulp build

Publish Build Artifacts

Add a Publish Build Artifacts task with these settings:

Path to Publish

The path to publish is the folder named public created by the Gulp build.

1
$/Cumulus/Stitch-DLS/$(BranchName)/rbi-stitch/public

Artifact Name

1
drop

The Release

The release is done with an Azure App Service Deploy task.

Point the Package or folder to the public folder created by the build:
$(System.DefaultWorkingDirectory)/Dev05-Stitch/drop

The log of this task should look something like this:

1
2
3
4
5
6
Info: Updating file (*****\assets.html).
[...]
Info: Updating file (*****\themes\mandelbrot\js\mandelbrot.js.map).
Info: Updating file (*****\Web.config).
Total changes: 1211 (0 added, 0 deleted, 1211 updated, 0 parameters changed, 75632972 bytes copied)
Successfully deployed web package to App Service.

A PowerShell Script To Calculate The Perfect Time For Lunch

Because I don’t always start at the same time I found it hard to remember at what time I came into the office. To fix that I wrote a Powershell script.

  • It retrieves the time at which I started up my computer. It starts looking for the first start-up after 5 in the morning, so I can reboot during the day without influencing the output of the script.
  • From this start-up time it calculates the perfect time for lunch and the end of my working day.
1
2
3
4
5
6
7
8
9
10
11
12
13
$hoursPerWeek = 36
$daysPerWeek = 5
$lunchBreakHours = 0.5

$startedAt = (Get-EventLog -LogName "System" -After (Get-Date).Date.AddHours(5) | Sort-Object -Property TimeGenerated | Select-Object -First 1).TimeGenerated
$finishedAt = (Get-Date($startedAt)).AddHours(($hoursPerWeek/$daysPerWeek) + $lunchBreakHours)
$timeRemaining = New-Timespan -Start (Get-Date) -End $finishedAt
$lunchAt = (Get-Date($startedAt)).AddHours($hoursPerWeek/10)

Write-Output ("Your computer started up at {0} on {1} (day {2} of the year)," -f $startedAt.ToShortTimeString(), (Get-Date).ToString("D"), (Get-Date).DayOfYear)
Write-Output ("you work {0} hours in {1} days and have a {2} minute lunchbreak ({3} hours per day)," -f $hoursPerWeek, $daysPerWeek, ($lunchBreakHours*60), (($hoursPerWeek/$daysPerWeek) + $lunchBreakHours))
Write-Output ("which means you're finished today at {0} (that's in {1}:{2})." -f $finishedAt.ToShortTimeString(), $timeRemaining.Hours, $timeRemaining.Minutes)
Write-Output "The perfect time for lunch is $($lunchAt.ToShortTimeString())."

Output:

1
2
3
4
Your computer started up at 09:01 on Thursday, November 22, 2018 (day 326 of the year),
you work 36 hours in 5 days and have a 30 minute lunchbreak (7.7 hours per day),
which means you're finished today at 16:43 (that's in 0:10).
The perfect time for lunch is 12:37.

You should change the $hoursPerWeek, $daysPerWeek and $lunchBreakHours for your personal situation. So if you work 40 hours in 4 days and have a one hour lunch you change it to this:

1
2
3
$hoursPerWeek = 40
$daysPerWeek = 4
$lunchBreakHours = 1

Output:

1
2
3
4
Your computer started up at 09:01 on Thursday, November 22, 2018 (day 326 of the year),
you work 40 hours in 4 days and have a 60 minute lunchbreak (11 hours per day),
which means you're finished today at 20:01 (that's in 3:20).
The perfect time for lunch is 13:01.