DEV Community

Jonathan Higgs
Jonathan Higgs

Posted on • Originally published at jonathanhiggs.github.io on

A Complete Cake Script for Wyam

Introduction

The previous post gave instructions for creating setting up a blog website that included a simple cake build script. This post will take that script, extract some of the implicit parameters and add some extra steps for clarity and utility

Jump to the end of the post to see the complete script

Original Script

First things first, here is the simple script from the previous post. Currently there are implicit parameters for the output directory and the branch that will be deployed

#addin nuget:?package=Cake.Wyam

var target = Argument("target", "Deploy");

Task("Build")
    .Does(() => Wyam());

Task("Preview")
    .Does(() => Wyam(new WyamSettings {
        Preview = true,
        Watch = true,
    }));

Task("Deploy")
    .IsDependentOn("Build")
    .Does(() => {
        StartProcess("git", "add .");
        StartProcess("git", "commit -m \"Output files generated for subtree\"");
        StartProcess("git", "subtree split --prefix output -b master");
        StartProcess("git", "push -f origin master");
    });

RunTarget(target);

Like any function, it is best to have all of the parameters that might vary specified at the start of the script. While it might initially seem strange that a 'parameter' is hard coded to a specific value in this script it is useful to extract them so that the script can be copy-pasted and reused in another project with minimal fuss and a single place in the script where parameters are set. Currently the input and output directories, and the branch that gets deployed are implicitly used in all the steps

var deployBranch = "master";
var output = "output";

Now the parameters are explicitly set to variables, all instances of them can be replaced in the rest of the script using a bit of string interpolation

Task("Deploy")
    .IsDependentOn("Build")
    .Does(() => {
        StartProcess("git", "add .");
        StartProcess("git", "commit -m \"Output files generated for subtree\"");
        StartProcess("git", $"subtree split --prefix {output} -b {deployBranch}");
        StartProcess("git", $"push -f origin {deployBranch}");
    });

Similarly to the parameters, the locations of directories and files can be extracted and set once. This script doesn't make use of these paths yet, but will do shortly. Again, the paths can be built on-top of each other so each part of the path is only specified in one place

var root = MakeAbsolute(Directory("."));
var inputDirectory = Directory($"{root}/input");
var outputDirectory = Directory($"{root}/{output}");

Continuing the theme of moving all configuration to the start of the script, the settings object passed into wyam are created at the point of usage and contain implicit parameters. They are moved to the start of the script and the paths can be passed in

var build = new WyamSettings {
    InputPaths = new [] { inputDirectory.Path },
    OutputPath = outputDirectory
};

var preview = new WyamSettings {
    InputPaths = new [] { inputDirectory.Path },
    OutputPath = outputDirectory,
    Preview = true,
    Watch = true,
};

Task("Preview")
    .Does(() => Wyam(preview));

Task("Build")
    .Does(() => Wyam(build));

Cake.Git Add-In

The script currently uses out-of-process calls to interact with git but there is a Cake.Git add-in which has methods for lots of the common git commands. Specifically the GitCommit method takes additional parameters for setting the username and email that will be used for the commit. This can be quite useful for scripts that create commits as part of the build process. Here the parameters are extracted to the start of the script, and another gitMessage parameter is used for the message

var gitMessage = Argument("message", "Output files generated for subtree");

var gitUser = "My Name";
var gitEmail = "myname@site.com";

Task("Deploy")
    .IsDependentOn("Build")
    .Does(() => {
        GitAddAll(root);
        GitCommit(root, gitUser, gitEmail, gitMessage);

        StartProcess("git", $"subtree split --prefix {output} -b {deployBranch}");
        StartProcess("git", $"push -f origin {deployBranch}");
    });

Unfortunately the git add-in does not contain any cake aliases for subtree so that step will still need to call out-of-process. Also, there is a long-running issue with the ssh library that Cake.Git uses which means that push and pull won't work if the repository is setup to an ssh url. I hope both of these get fixed one day

The git message will still default to the same value as the original script, but it can also be overwritten by passing it in to the script

$ powershell ".\build.ps1" --message="I did a thing"

Publish Workflow

The main functional change to this script is to create a workflow for working on posts but not publishing them until they are ready. This will be achieved by creating a new posts directory for the WIP posts, including it in the preview but not the main build, and adding a task that will move a post over when a valid post name is supplied

The first thing to do is create a work-in-progress directory that will be visible in preview but not publish. The wyam settings allow for multiple paths to be passed into the InputPaths parameter, and the Blog recipe will look in a posts sub-directory when creating the static pages

/input
    /posts
/output
/wip
    /posts
...

Path variables for both these directories will be needed in a moment, so they're added to the start of the script with the other path declarations. The build settings do not need to change, but the wip directory are passed into the preview settings to include the wip posts in preview content generation

var postsDirectory = Directory($"{root}/input/posts");
var wipDirectory = Directory($"{root}/wip/posts");

var build = new WyamSettings {
    InputPaths = new [] { inputDirectory.Path },
    ...
};

var preview = new WyamSettings {
    InputPaths = new [] { inputDirectory.Path, wipDirectory.Path },
    ...
};

Publish is a new task that will move posts from the wip directory into the input/posts directory. The first thing required will be a script argument into which the name of the post will be passed. Here the posts are kept organized by year so the script can ensure that the posts subdirectory with the current year exists before performing the actual move. File paths are built up from the path declarations which aid in making the script readable. Also note that the .md extension is being omitted from the argument and added by the task for brevity

var post = Argument("post", string.Empty);

Task("Publish")
    .Does(() => {
        EnsureDirectoryExists($"{postsDirectory}/{DateTime.Now.Year}");

        MoveFile(
          File($"{wipDirectory}/{post}.md"),
          File($"{postsDirectory}/{DateTime.Now.Year}/{post}.md")
        );
    });

Wyam blog posts have a published document metadata field that can be automatically set during this move. Cake.FileHelpers is another add-in that has the ReplaceRegexInFiles method to do this

#addin nuget:?package=Cake.FileHelpers

Task("Publish")
    .Does(() => {
        ReplaceRegexInFiles(
            $"{wipDirectory}/{post}.md",
            "Published:\\s*\\d{4}-\\d{2}-\\d{2}",
            $"Published: {DateTime.Now.Date.ToString("yyyy-MM-dd")}"
        );
        ...
    });

The only issue with this task so far is that it requires a post to be moved on every build, which will not always be the case. Cake allows conditional execution of tasks without blocking subsequent tasks from executing, and all that is required is a check on whether a post has been passed into the script to achieve this

Task("Publish")
    .WithCriteria(!string.IsNullOrEmpty(post))
    .Does(() => {
        ...
    });

Finally, the message passed to git can be modified with the name of post

Task("Publish")
    .Does(() => {
        ...
        gitMessage = $"Published: {post}";
    });

Assuming that there is a file in the wip/posts directory called my-post.md then the following command would make use of the Publish task to move it from wip and into the inputs directory, and hence it would be published to the main site. One thing to note is that there seems to be a bug in the command line argument parsing that will assume a second script name is being passed in when a dash appears in a argument value, so the value must be placed in double quotes

$ powershell ".\build.ps1" --post="my-post"

Info Task

The final step to add does not make a functional change, but simply dumps out parameters and variables. Generally, this can be very useful when porting the script to a new project and debugging when it doesn't quite work the first time, or if the script is being executed by a CI process rather than locally

Task("Info")
    .Does(() => {
        Information("Parameters:");
        Information($"-target {target}");
        Information($"-post {post}");
        Information("\nPaths:");
        Information($"Root Directory {root}");
        Information($"Output Directory {outputDirectory}");
        Information($"Post Directory {postsDirectory}");
        Information($"WIP Directory {wipDirectory}");
        Information($"\nGit Settings:");
        Information($"Git User {gitUser}");
        Information($"Git Email {gitEmail}");
    });

Final Script

Putting it all together, here is the final script

////////////////////////
// Add-ins & Tools
//////////////////////

#addin nuget:?package=Cake.FileHelpers
#addin nuget:?package=Cake.Git
#addin nuget:?package=Cake.Wyam

////////////////////////
// Parameters
//////////////////////

var target = Argument("target", "Deploy");
var post = Argument("post", string.Empty);
var gitMessage = Argument("message", "Output files generated for subtree");

var deployBranch = "master";
var gitUser = "My Name";
var gitEmail = "my.name@site.com";

var output = "output";

var root = MakeAbsolute(Directory("."));
var inputDirectory = Directory($"{root}/input");
var postsDirectory = Directory($"{root}/input/posts");
var outputDirectory = Directory($"{root}/{output}");
var wipDirectory = Directory($"{root}/wip/posts");

////////////////////////
// Settings
//////////////////////

var build = new WyamSettings {
    InputPaths = new [] { inputDirectory.Path },
    OutputPath = outputDirectory
};

var preview = new WyamSettings {
    InputPaths = new [] { inputDirectory.Path, wipDirectory.Path },
    OutputPath = outputDirectory,
    Preview = true,
    Watch = true,
};

////////////////////////
// Tasks
//////////////////////

Task("Info")
    .Does(() => {
        Information("Parameters:");
        Information($"-target {target}");
        Information($"-post {post}");
        Information("\nPaths:");
        Information($"Root Directory {root}");
        Information($"Output Directory {outputDirectory}");
        Information($"Post Directory {postsDirectory}");
        Information($"WIP Directory {wipDirectory}");
        Information($"\nGit Settings:");
        Information($"Git User {gitUser}");
        Information($"Git Email {gitEmail}");
    });

Task("Preview")
    .IsDependentOn("Info")
    .Does(() => Wyam(preview));

Task("Publish")
    .WithCriteria(!string.IsNullOrEmpty(post))
    .IsDependentOn("Info")
    .Does(() => {
        ReplaceRegexInFiles(
            $"{wipDirectory}/{post}.md",
            "Published:\\s*\\d{4}-\\d{2}-\\d{2}",
            $"Published: {DateTime.Now.Date.ToString("yyyy-MM-dd")}"
        );

        EnsureDirectoryExists($"{postsDirectory}/{DateTime.Now.Year}");

        MoveFile(
          File($"{wipDirectory}/{post}.md"),
          File($"{postsDirectory}/{DateTime.Now.Year}/{post}.md")
        );

        gitMessage = $"Published: {post}";
    });

Task("Build")
    .IsDependentOn("Publish")
    .Does(() => Wyam(build));

Task("Deploy")
    .IsDependentOn("Build")
    .Does(() => {
        GitAddAll(root);
        GitCommit(root, gitUser, gitEmail, gitMessage);

        StartProcess("git", $"subtree split --prefix {output} -b {deployBranch}");
        StartProcess("git", $"push -f origin {deployBranch}");
    });

////////////////////////
// Invoke
//////////////////////

RunTarget(target);

Top comments (0)