# Creating a simple build server in F# to execute FAKE scripts

15 Jul 2014

Now that we have created a FAKE script to check out the latest code from TFS, build it, and deploy it to IISExpress, the next step is to run the script at regular intervals to ensure the build stays in good shape. We could use TFS for this by installing a build agent on a local server or install another build tool.

Alternatively, if we don't require anything fancy, we could write a build server ourselves. A simple mail with the log file attached and a summary in the mail body whenever the build fails might do.

For the CiServer.fsx script, first we need to set various configuration options:

open System
open System.IO
open System.Diagnostics
open System.Net
open System.Net.Mail
open System.Text
open System.Collections.Generic

let mailFrom = "foo@bugfree.dk"
let mailTo = ["bar@bugfree.dk"; "baz@bugfree.dk"]
let smtpServer = "smtp.gmail.com"
let failureSummaryLength = 30
let maxBuildDuration = 1000 * 60 * 10
let timeBetweenBuilds = 1000 * 60 * 15
let fakeExe = __SOURCE_DIRECTORY__ + "/packages/FAKE.2.15.4/tools/fake.exe"


As we want CiServer to send a mail notification upon build failure, we need to configure the outgoing SMTP server. The mailTo allows for the specification of a list of recipients to notify when the build fails, including in the mail the last failureSummaryLength lines of the log file. These lines often suffice to fix the build without having to look at the entire attached log file.

Since we want CiServer to spawn Fake.exe, we need to ensure Fake.exe doesn't get stuck for whatever reason, i.e., we need the ability to terminate the build if it takes longer than maxBuildDuration. We also want to run the build in an infinite loop with timeBetweenBuilds between when one build finishes and the next build starts.

Now for the crux of the CiServer:

let formatOutput message =
sprintf "%s: %s" (DateTime.Now.ToString("MM-dd-yyyy HH:mm:ss")) message

let logToConsole message =
message |> formatOutput |> printfn "%s"

let output = List()
let logToFile message =

let build() =
let nl = Environment.NewLine
let p =
new Process(
StartInfo =
ProcessStartInfo(
FileName = fakeExe,
Arguments = __SOURCE_DIRECTORY__ + "/Build.fsx deploy",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true))

logToConsole e.Data
logToFile e.Data

p.Start() |> ignore

let exited = p.WaitForExit(maxBuildDuration)
if not exited then logToConsole "Maximum build time exceeded. Build killed"

if p.ExitCode <> 0 then
logToConsole "Build failed. Sending mail"
let tail =
output
|> Seq.skip(output.Count - min failureSummaryLength output.Count)
|> Seq.reduce (fun acc s -> acc + nl + s)

use message =
new MailMessage(
Subject = "AcmeApp build failure",
Body = "See attachment for complete build log." + nl + nl +
"Below is the last part of the build log:" + nl + nl +
tail)
let bytes = Encoding.UTF8.GetBytes(String.Join(nl, output))
(new SmtpClient(
Host = smtpServer,
EnableSsl = true)).Send(message)


The build function is what spawns the Fake process and attaches listeners to its standard error and standard output. What Fake targets print while they execute typically go to standard output while any errors in the build go to standard error.

To make the build run in an infinite loop, we add the following piece of code at the end of the script. Because it's outside a function, it's executed immediately:

while true do
logToConsole "Build started"
build()
logToConsole "Build ended. Going to sleep"

PS> .\packages\FAKE.2.15.4\tools\Fsi.exe .\CiRunner.fsx