2017-03-26

Write The FAQ ‘n Manual (Part 4)

Automated Documentation in a CI/CD Pipeline for PowerShell Modules with PlatyPS, psake, AppVeyor, GitHub and ReadTheDocs



Part 4: Monitoring the Build, PowerShell Magic, Looking Forward, and Closing Thoughts


Monitor the Build Status


AppVeyor

Once the release has been pushed to GitHub, the behind the scenes webhook for AppVeyor is triggered and your build will be queued on AppVeyor. If you did your required reading, this part should be familiar to you. What we are looking for is the parts of the documentation build in the output. You can look at this build of the example project to see the full output: https://ci.appveyor.com/project/markekraus/autodocumentsexample/build/1.0.6

BuildDocs Task:

PostDeploy Task:


ReadTheDocs

ReadTheDocs will preform 2 builds. It will build once when your do you push your release to GitHub and again when AppVeyor pushes the build changes back to GitHub. The first build may only show as triggered if the AppVeyor build finishes before the first ReadTheDocs build completes. ReadTheDocs doesn't have a live feed of the build process like AppVeyor does, but you can see the results of builds. You can see the build that followed the above AppVeyor build here: https://readthedocs.org/projects/autodocumentsexample/builds/5198207/

You can get here by doing the following
  1. Go to your dashboard https://readthedocs.org/dashboard/
  2. Select your project
  3. Go to the Builds tab
  4. Click the desired build





Pull the Build Changes to your Local Git Repo

You will need to remember that since the build process pushes changes back to GtHub, you will need to refresh your local repo. This is done with git pull:
git pull




So, Where's the PowerShell?

This is a PowerShell blog and so far in this series not much PowerShell has been discussed. As I stated before, the magic is happening in /psake.ps1: https://github.com/markekraus/AutoDocumentsExample/blob/master/psake.ps1

Build Task

The Build task contains the code that Adds /RELEASE.md to the ReleaseNotes in the module manifest, maintains the /docs/ChanegLog.md and adds the version and date to /REALEASE.md.

https://github.com/markekraus/AutoDocumentsExample/blob/master/psake.ps1#L114
    # Update release notes with Version info and set the PSD1 release notes
    $parameters = @{
        Path = $ReleaseNotes
        ErrorAction = 'SilentlyContinue'
    }
    $ReleaseText = (Get-Content @parameters | Where-Object {$_ -notmatch '^# Version '}) -join "`r`n"
    if (-not $ReleaseText) {
        "Skipping realse notes`n"
        "Consider adding a RELEASE.md to your project.`n"
        return
    }
    $Header = "# Version {0} ({1})`r`n" -f $BuildVersion$BuildDate
    $ReleaseText = $Header + $ReleaseText
    $ReleaseText | Set-Content $ReleaseNotes
    Update-Metadata -Path $env:BHPSModuleManifest -PropertyName ReleaseNotes -Value $ReleaseText
    
    # Update the ChangeLog with the current release notes
    $releaseparameters = @{
        Path = $ReleaseNotes
        ErrorAction = 'SilentlyContinue'
    }
    $changeparameters = @{
        Path = $ChangeLog
        ErrorAction = 'SilentlyContinue'
    }
    (Get-Content @releaseparameters),"`r`n`r`n"(Get-Content @changeparameters) | Set-Content $ChangeLog


BuildDocs Task

The BuildDocs task is responsible for creating /mkdocs.yml, copying /RELEASE.md to /docs/RELEASE.md, and creating the function markdown files under /docs/functions/.

https://github.com/markekraus/AutoDocumentsExample/blob/master/psake.ps1#L174
Task BuildDocs -depends Test {
    $lines
    
    "Loading Module from $ENV:BHPSModuleManifest"
    Remove-Module $ENV:BHProjectName -Force -ea SilentlyContinue
    # platyPS + AppVeyor requires the module to be loaded in Global scope
    Import-Module $ENV:BHPSModuleManifest -force -Global
    
    #Build YAMLText starting with the header
    $YMLtext = (Get-Content "$ProjectRoot\header-mkdocs.yml") -join "`n"
    $YMLtext = "$YMLtext`n"
    $parameters = @{
        Path = $ReleaseNotes
        ErrorAction = 'SilentlyContinue'
    }
    $ReleaseText = (Get-Content @parameters) -join "`n"
    if ($ReleaseText) {
        $ReleaseText | Set-Content "$ProjectRoot\docs\RELEASE.md"
        $YMLText = "$YMLtext  - Realse Notes: RELEASE.md`n"
    }
    if ((Test-Path -Path $ChangeLog)) {
        $YMLText = "$YMLtext  - Change Log: ChangeLog.md`n"
    }
    $YMLText = "$YMLtext  - Functions:`n"
    # Drain the swamp
    $parameters = @{
        Recurse = $true
        Force = $true
        Path = "$ProjectRoot\docs\functions"
        ErrorAction = 'SilentlyContinue'
    }
    $null = Remove-Item @parameters
    $Params = @{
        Path = "$ProjectRoot\docs\functions"
        type = 'directory'
        ErrorAction = 'SilentlyContinue'
    }
    $null = New-Item @Params
    $Params = @{
        Module = $ENV:BHProjectName
        Force = $true
        OutputFolder = "$ProjectRoot\docs\functions"
        NoMetadata = $true
    }
    New-MarkdownHelp @Params | foreach-object {
        $Function = $_.Name -replace '\.md'''
        $Part = "    - {0}: functions/{1}" -f $Function$_.Name
        $YMLText = "{0}{1}`n" -f $YMLText$Part
        $Part
    }
    $YMLtext | Set-Content -Path "$ProjectRoot\mkdocs.yml"
}
You'll notice that the code imports the updated module into the Global scope. Some combination of PlatyPS, AppVeyor, and psake makes this a necessity. I suspect it is a PlatyPS issue, but I haven't had time to dig through their source code.

You will also notice that this deletes all of the current function markdown files. This is so functions removed from the project no longer had lingering documentation and because PlatyPS doesn't play nice with preexisting files (at least in my testing they did not).


PostDeploy Task

This task is slightly different. It's not really PowerShell. If someone has a good (this is the key word: good) PowerShell implementation of git, please let me know. All of the ones I have tried are just as terrible as doing what I have done here.  I yearn for a PowerShell native implementation of git. I wont post all of it here since it;s not truly PowerShell, but I will explain some of it. The code beings here: https://github.com/markekraus/AutoDocumentsExample/blob/master/psake.ps1#L251

https://github.com/markekraus/AutoDocumentsExample/blob/master/psake.ps1#L258
        "git config --global credential.helper store"
        cmd /c "git config --global credential.helper store 2>&1"
The first line is just to "echo" the command that is being run to AppVeyor. That makes it easier to trace down where something went wrong. Just be careful not to expose your GitHub access token and probably not the email address either.

All of the git commands are redirecting stderr to stdout and this is being done in CMD, not PowerShell. The reason for this is that I want verbose output from the git commands displayed in in the AppVeyor output. git.exe puts informational text into stderr. PowerShell interprets a non-empty stderr from an evaluated command as something went wrong with the command. Now, it's debatable whether git.exe putting info in stderr is bad or PowerShell interpreting stderr content as an exception is bad, but this is the mess we have to deal with.

I tried several different workarounds, but ultimately this got me where I wanted. It has some drawbacks. For example, this means there is no error checking. I realize there is a git.exe option that drops informative text and thus only error when there really is an error. As I indicated, I wanted verbose output. This came up in one of my build attempts:

https://ci.appveyor.com/project/markekraus/autodocumentsexample/build/1.0.5

You can see git had a fatal error, but since I'm crippling the errors on this and not implementing my own error checking the build passed even though git failed.


Help.Tests.ps1 Pester Test

I also indicated my Help.Tests.ps1 is slightly different form others. My looping is a little different. I loop around each function because I need to test for a HelpUri.

https://github.com/markekraus/AutoDocumentsExample/blob/master/Tests/Help.Tests.ps1#L10
    foreach($Function in $Functions){
        $help = Get-Help $Function.name
        Context $help.name {
            it "Has a HelpUri" {
                $Function.HelpUri | Should Not BeNullOrEmpty
            }

I am also testing for the existence of at least one .LINK
https://github.com/markekraus/AutoDocumentsExample/blob/master/Tests/Help.Tests.ps1#L16
            It "Has related Links" {
                $help.relatedLinks.navigationLink.uri.count | Should BeGreaterThan 0
            }




未来へ(To the Future)

There is much to be improved on. This is just a start for me. Well, more like a point just beyond the start as this is already several iterations in. There are several flaws in this process.

During the writing of this blog series it became apparent to me that prepending /RELEASE.md to /docs/ChangeLog.md on every build was probably a bad idea. It's probably better to do this part only on deployment builds. This way you could keep /RELEASE.md updated as you make minor changes to the code base without /docs/ChangeLog.md getting cluttered with a bunch of junk and repetitions. This of course means rethinking all of the documentation build logic to accommodate.

Another thing that needs improvement is figuring out a way to have ReadTheDocs only build after an AppVeyor commit instead of every GitHub commit. That would also mean some other build logic to handle documentation only repo updates.

I would also like to find a way to keep the default ReadTheDocs build to match the current version available on PowerShell Gallery. At least, I'd like a way to connect the published versions of the code back to the correct documentation version. I don't really see how that is possible though. Maybe further manipulation of /mkdocs.yml could achieve that. I need to research deeper.

I definitely need to get some error detection around my git code in /psake.ps1. I researched how other major projects were doing this. Many of them are just doing their git directly from /appveyor.yml. But, I want to keep /appveyor.yml as a configuration only and /psake.ps1 as code only. Which brings me to my final point:

I would like to move more of the configuration out of /psake.ps1 and into /appveyor.yml. Basically, anything static should be in /appveyor.yml (e.g. change log path) and anything that needs to be dynamically generated (e.g. build version) should be in /psake.ps1.



Closing Thoughts

I hope this series has been helpful and informative. I hope the amount of time and effort I put into it shows. Most of all, I really hope to see more documentation processes included in PowerShell build pipelines, even if what I have done here provides no help other than to raise it to the level of attention it deserves. If you have corrections, suggestion, or comments, please don't hesitate to let me know. Thanks for reading!

Go Back to Part 3