Setting up CI on GitHub Actions, and deploying docs for Julia

I didn't know this when I first started learning Julia, but if you write a function; e.g.

function my_sum(a::T...) where {T <: Number}
    return reduce(+, a)
end

If you add a string immediately before the function definition:

"This is my bad version of a sum function because it takes any number of values, but they all must be the same type of number."
function my_sum(a::T...) where {T <: Number}
    return reduce(+, a)
end

"""
This is my...

...multi-line comment.

Pretty cool, huh?  Even has *markdown* `support`.
"""
my_identity_function(a) = a

When you go into help mode (? in the Julia REPL), your string will display as documentation for that function:

help?> my_sum
search: my_sum

  This is my bad version of a sum function because it takes any number of values, but they all must be the same type of number.

help?> my_identity_function
search: my_identity_function

  This is my...

  ...multi-line comment.

  Pretty cool, huh? Even has markdown support.

Adding these, which are called "docstrings", are very useful for anyone using your package. Furthermore, they are useful for you to come back to six months later, to remind yourself how exactly you use your functions and structs, etc.

There is a very helpful tool called Documenter.jl which takes advantage of those docstrings, and builds a documentation static website. All you need to do is make five or so folders (directories), about three files, and of course have Documenter.jl installed.

1. Installing Documenter.jl

To install Documenter.jl, you need to enter Julia's REPL and type

julia> import Pkg; Pkg.add("Documenter")

2. Creating the necessary folders for Documenter.jl

From the command line, enter the directory in which your package lies. For example, I might be in the folder ~/projects/MyCoolPackage.jl/, which will probably look like this:

.
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── src
│   └── MyCoolPackage.jl
└── test
    └── runtests.jl

Now run the following from the command line:

$ mkdir -p docs/build/; mkdir -p docs/src/; mkdir -p .github/workflows/

Now your directory structure should look something like this:

.
├── .github
│   └── workflows
├── .gitignore
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── docs
│   ├── build
│   └── src
├── src
│   └──  MyCoolPackage.jl
└── test
    └── runtests.jl

3. Creating the necessary files for Documenter.jl

From the command line, run the following:

$ touch docs/src/index.md; touch docs/make.jl; touch .github/workflows/CI.yml

Now (this is an important step) you need to run

$ cd docs

$ julia --project=

julia> import Pkg; Pkg.activate(".")

julia> Pkg.add("Documenter")

julia> Pkg.add("") # HERE YOU NEED TO INSTALL ANY DEPENDENCIES YOUR PACKAGE HAS

Now that you have installed any dependencies your package has in the docs folder, your directory structure will look something like this:

.
├── .github
│   └── workflows
│       ├── CI.yml
│       ├── CompatHelper.yml
│       └── TagBot.yml
├── .gitignore
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── docs
│   ├── Manifest.toml
│   ├── Project.toml
│   ├── build
│   ├── make.jl
│   └── src
│       └── index.md
├── examples
│   ├── Manifest.toml
│   ├── Project.toml
│   ├── basic.jl
│   └── tm.jl
├── src
│   └── MyCoolPackage.jl
└── test
    └── runtests.jl

You may notice some other files in .github/workflows/. Scroll to the end to find out what they contain and how they work.

This is good. We are making good progress.

4. Writing to the files

Now that all necessary files are made, it is time to fill them up. I will paste here the bare-bones of working code:

docs/make.jl

This file is the file that is run when the documentation is being made. This is arguably the most important file, and should contain at least one function call on how to make the documentation. Usually you will also want a function call to tell Documenter.jl how to deploy the docs (which is where .github/workflows/CI.yml come into play, but I will get to that soon). This is an example of the make.jl file:

include(joinpath(dirname(@__DIR__), "src", "MyCoolPackage.jl"))
using Documenter, .MyCoolPackage

Documenter.makedocs(
    clean = true,
    doctest = true,
    modules = Module[MyCoolPackage],
    repo = "",
    highlightsig = true,
    sitename = "MyCoolPackage Documentation",
    expandfirst = [],
    pages = [
        "Index" => "index.md",
    ]
)

deploydocs(;
    repo  =  "github.com/username/MyCoolPackage.jl.git",
)

docs/src/index.md

This file will tell Documenter.jl how to structure your documentation. Once again, here is an extremely simple example:

# MyCoolPackage.jl Documentation

```@contents
```

```@meta
CurrentModule = MyCoolPackage
DocTestSetup = quote
    using MyCoolPackage
end
```

## Adding MyCoolPackage.jl
```@repl
using Pkg
Pkg.add("MyCoolPackage")
```

## Documentation
```@autodocs
Modules = [MyCoolPackage]
```

## Index

```@index
```

.github/workflows/CI.yml

Finally, this is the CI (continuous integration) part, which actually does the deploying. All you need to have is something like the following:

name: CI
# Run on master, tags, or any pull request
on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM UTC (8 PM CST)
  push:
    branches: [master]
    tags: ["*"]
  pull_request:
jobs:
  test:
    name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        version:
          - "1.5"   # current
          - "nightly"   # Latest Release
        os:
          - ubuntu-latest
          - macOS-latest
          - windows-latest
        arch:
          - x64
          - x86 # 32-bit; i686
        exclude:
          # 32-bit Julia binaries are not available on macOS
          - os: macOS-latest
            arch: x86
    steps:
      - uses: actions/checkout@v2
      - uses: julia-actions/setup-julia@v1
        with:
          version: ${{ matrix.version }}
          arch: ${{ matrix.arch }}
      - uses: actions/cache@v1
        env:
          cache-name: cache-artifacts
        with:
          path: ~/.julia/artifacts
          key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
          restore-keys: |
            ${{ runner.os }}-test-${{ env.cache-name }}-
            ${{ runner.os }}-test-
            ${{ runner.os }}-
      - uses: julia-actions/julia-buildpkg@latest
      - run: |
          git config --global user.name Tester
          git config --global user.email te@st.er
      - uses: julia-actions/julia-runtest@latest

  docs:
    name: Documentation
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: julia-actions/setup-julia@v1
        with:
          version: '1'
      - run: |
          git config --global user.name name
          git config --global user.email email
          git config --global github.user username
      - run: |
          julia --project=docs -e '
            using Pkg;
            Pkg.develop(PackageSpec(path=pwd()));
            Pkg.instantiate();'
      - run: julia --project=docs docs/make.jl
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The first part of this file will run some tests, and the second part will deploy the documentation.

Push all of this to the repo.

6. Allowing branch gh-pages to be accessed by username.github.io to deploy docs — the final step

The last thing we need to do is to go to your repository at https://github.com/username/MyCoolPackage.jl/, go into Settings > Options > GitHub Pages, and select the gh-pages branch, then press save. This will allow GitHub Pages to access the gh-pages branch created by Documenter.jl.

And you are done!

Post Script: other notes.

You might want to add some tags to your README.md:

<h1 align="center">
    MyCoolPackage.jl
</h1>

<!-- [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://username.github.io/MyCoolPackage.jl/stable) -->
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://username.github.io/MyCoolPackage.jl/dev)
[![CI](https://github.com/invenia/PkgTemplates.jl/workflows/CI/badge.svg)](https://github.com/username/MyCoolPackage.jl/actions?query=workflow%3ACI)
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)
![Project Status](https://img.shields.io/badge/status-maturing-green)

There are two other workflows I really like to have: one is a CompatHelper, which ensures your package's dependencies stay up-to-date; and one is TagBot, which will automatically update the version number based on the version of your package.

Here are the files:

CompatHelper.yml

name: CompatHelper
on:
  schedule:
    - cron: 0 0 * * *
  workflow_dispatch:
jobs:
  CompatHelper:
    runs-on: ubuntu-latest
    steps:
      - name: Pkg.add("CompatHelper")
        run: julia -e 'using Pkg; Pkg.add("CompatHelper")'
      - name: CompatHelper.main()
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }}
        run: julia -e 'using CompatHelper; CompatHelper.main()'

TagBot.yml

name: TagBot
on:
  schedule:
    - cron: 0 0 * * *
  workflow_dispatch:
jobs:
  TagBot:
    runs-on: ubuntu-latest
    steps:
      - uses: JuliaRegistries/TagBot@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          ssh: ${{ secrets.DOCUMENTER_KEY }}
Top