Introduction
One of the most frustrating part of setting up CI/CD is that it is usually impossible to test and debug the pipelines locally. So, when I was tasked to move our CI/CD pipelines from AppVeyor to GitHub actions I found the opportunity to improve on this. One of my colleagues introduced me to Nuke, a cross-platform build automation system with C# domain specific language. This was perfect, since the project was already C# based and it meant that the whole team could develop, debug, and change our CI/CD pipelines.
It was easy to setup Nuke on the project and get it to build and run the tests. You could setup multiple targets with dependencies between them, it even provides a dependency graph to be able to trace and validate the different targets dependencies. A lot of tools are provided as tasks that can easily be set up and configured like the examples below(Check Nuke documentation for available tools):
// Build the solution with specified parameters
public Target Build => _ => _
.Executes(() =>
{
DotNetTasks.DotNetBuild(s => s
.SetProjectFile(Solution)
.SetConfiguration(Configuration)
.SetAssemblyVersion(BuildInfo.AssemblyVersion.ToString())
.SetInformationalVersion(BuildInfo.SemanticVersion.ToString())
.SetFileVersion(BuildInfo.FileVersion.ToString())
.SetCopyright($"Copyright {DateTime.Now.Year}")
.SetNoLogo(true)
);
});
// Does an npm install on the specified directory
public Target NpmInstall => _ => _
.Executes(() =>
{
NpmTasks.NpmCi(s =>
{
s = s.SetProcessWorkingDirectory(FrontendProjectDirectory);
if (IsServerBuild && !string.IsNullOrWhiteSpace(NodeAuthToken))
{
// token required for accessing private npm server
s = s.SetProcessEnvironmentVariable("NODE_AUTH_TOKEN", NodeAuthToken);
}
return s;
});
});
// Does an npm run build on the specified directory
public Target BuildFrontend => _ => _
.DependsOn(NpmInstall)
.Executes(() =>
{
NpmTasks.NpmRun(s => s
.SetProcessWorkingDirectory(FrontendProjectDirectory)
.SetCommand("build"));
});
Creating a release and publishing it
One of the tasks that was missing was a simple way to create releases. The closest thing that I found was the GitReleaseManagerTasks
, but that required creating milestones that was not used in our team. upon further investigation I found out that Nuke uses Octokit for .Net behind the scenes, which meant I could directly use Octokit for creating a release and uploading the assets.
First step we need to create a GitHub client with the correct credentials so it can access your repository. There is a static property available on GitHubTasks
that you can initialize on, so it is accessible in different targets. The GithubRepositoryAuthToken
is a personal access token that can be created on your profile.
GitHubTasks.GitHubClient = new GitHubClient(new ProductHeaderValue(nameof(NukeBuild)))
{
Credentials = new Credentials(GithubRepositoryAuthToken)
};
Next, we need to create a release object that will be used by GitHubClient
to create the actual release. At first, we will create the release as a draft, and when the release is ready and all the assets has been uploaded, we will publish the release.
The easiest way I found to get the repository id needed for creating a release was to open the repository in a browser like Chrome or Firefox and view the page source and search for octolytics-dimension-repository_id
in the page source. You should find something like <meta name="octolytics-dimension-repository_id" content="1234567">
where the repository id is the number specified in the content attribute.
var newRelease = new NewRelease(BuildInfo.TagName)
{
TargetCommitish = CommitSha,
Draft = true,
Name = $"Release version {BuildInfo.SemanticVersion}",
Prerelease = BuildInfo.SemanticVersion.IsPrerelease,
Body =
@$"See release notes in [docs](https://[YourSite]/{BuildInfo.SemanticVersion.Major}.{BuildInfo.nticVersion.Minor}/)"
};
var createdRelease = GitHubTasks.GitHubClient.Repository.Release.Create(RepositoryId, newRelease).Result;
Now that the release has been created in draft mode, it is time to upload assets to the release. For that I created a function, where you provide the created release and path to your asset, and it will upload the asset to the newly created release. It first check if the asset exists and then if it does, it will determine the content type of the asset and create a ReleaseAssetUpload
object that will be uploaded to the release.
private void UploadReleaseAssetToGithub(Release release, AbsolutePath asset)
{
if (!FileSystemTasks.FileExists(asset))
{
return;
}
if (!new FileExtensionContentTypeProvider().TryGetContentType(asset, out var assetContentType))
{
assetContentType = "application/x-binary";
}
var releaseAssetUpload = new ReleaseAssetUpload
{
ContentType = assetContentType,
FileName = Path.GetFileName(asset),
RawData = File.OpenRead(asset)
};
var _ = GitHubTasks.GitHubClient.Repository.Release.UploadAsset(release, releaseAssetUpload).Result;
}
Note that to determine the content type of the provided asset I used FileExtensionContentTypeProvider
from AspNetCore framework reference which is used in ASP.NET Core to detect static files content types. So, remember to add the following to the csproj
file of your project.
<FrameworkReference Include="Microsoft.AspNetCore.App" />
At the end we publish the release by editing the draft flag to false.
var _ = GitHubTasks.GitHubClient.Repository.Release
.Edit(RepositoryId, createdRelease.Id, new ReleaseUpdate { Draft = false }).Result;
Here is the full implementation with some extra loggings and checks:
public Target CreateRelease => _ => _
.Executes(() =>
{
Logger.Info("Started creating the release");
GitHubTasks.GitHubClient = new GitHubClient(new ProductHeaderValue(nameof(NukeBuild)))
{
Credentials = new Credentials(GithubRepositoryAuthToken)
};
var newRelease = new NewRelease(BuildInfo.TagName)
{
TargetCommitish = CommitSha,
Draft = true,
Name = $"Release version {BuildInfo.SemanticVersion}",
Prerelease = BuildInfo.SemanticVersion.IsPrerelease,
Body =
@$"See release notes in [docs](https://[YourSite]/{BuildInfo.SemanticVersion.Major}.{BuildInfo.nticVersion.Minor}/)"
};
var createdRelease = GitHubTasks.GitHubClient.Repository.Release.Create(RepositoryId, newRelease).Result;
if (ReleaseArtifactsDirectory.GlobDirectories("*").Count > 0)
{
Logger.Warn(
$"Only files on the root of {ReleaseArtifactsDirectory} directory will be uploaded as release assets.");
}
ReleaseArtifactsDirectory.GlobFiles("*").ForEach(p => UploadReleaseAssetToGithub(createdRelease, p));
var _ = GitHubTasks.GitHubClient.Repository.Release
.Edit(RepositoryId, createdRelease.Id, new ReleaseUpdate { Draft = false })
.Result;
});
private void UploadReleaseAssetToGithub(Release release, AbsolutePath asset)
{
if (!FileExists(asset))
{
return;
}
Logger.Info($"Started Uploading {Path.GetFileName(asset)} to the release.");
if (!new FileExtensionContentTypeProvider().TryGetContentType(asset, out var assetContentType))
{
assetContentType = "application/x-binary";
}
var releaseAssetUpload = new ReleaseAssetUpload
{
ContentType = assetContentType,
FileName = Path.GetFileName(asset),
RawData = File.OpenRead(asset)
};
var _ = GitHubTasks.GitHubClient.Repository.Release.UploadAsset(release, releaseAssetUpload).Result;
Logger.Success($"Done Uploading {Path.GetFileName(asset)} to the release.");
}
Conclusion
There are a lot of neat features in Nuke that can be used to improve your CI/CD, so I encourage you to read through their documentation and find which features are useful for your pipelines.
Comments