cover image

Creating an Automated Documentation Pipeline in PHP with Autodocs and GitHub Actions

In this tutorial, learn how to create an automated documentation pipeline in PHP with Autodocs, Minicli, and GitHub Actions

on January 16, 2024

Originally published on dev.to.

Autodocs is a PHP library designed to facilitate building highly customizable automated docs based on markdown templates. Combined with a Minicli application, it provides a layer of abstraction and structure on top of which you can create your own documentation factory.

I created this project to help me keep up with the documentation needs for Chainguard Images, which now helps keeping more than a thousand doc pages up-to-date in a GitHub-based workflow with nightly runs.

Pages are defined as classes that follow a known interface. Data sources can be autoloaded as JSON cache files, a design that facilitates distributed setups where data comes from varied sources. This distributed approach facilitates integrating Autodocs with existing workflows and pipelines, especially when using GitHub Actions.

In this tutorial we'll create a demo Autodocs application to generate personal GitHub READMEs. If you'd prefer to skip the tutorial and go straight to the code, you can check the demo repository on GitHub.

1. Bootstrapping the Demo App

Let's start by creating the demo application. We're using the Minicli application template to provide more resources and organization when building out the app.

composer create-project minicli/minicli autodocs-demo

Once Composer is finished installing the new application dependencies, you should be able to enter the directory and run a quick test:

cd autodocs-demo
./minicli

You should get output like this, indicating that the Minicli application runs as expected:

❯ ./minicli


███╗   ███╗██╗███╗   ██╗██╗ ██████╗██╗     ██╗
████╗ ████║██║████╗  ██║██║██╔════╝██║     ██║
██╔████╔██║██║██╔██╗ ██║██║██║     ██║     ██║
██║╚██╔╝██║██║██║╚██╗██║██║██║     ██║     ██║
██║ ╚═╝ ██║██║██║ ╚████║██║╚██████╗███████╗██║
╚═╝     ╚═╝╚═╝╚═╝  ╚═══╝╚═╝ ╚═════╝╚══════╝╚═╝

Minimalist, dependency-free framework for building CLI-centric PHP applications

Now you can rename the executable to autodocs or a different name if you'd like:

mv minicli autodocs

Next, we want to include the autodocs library as a dependency within Composer:

composer require erikaheidi/autodocs

You now have all requirements set up and can proceed to configuring Autodocs.

2. Configuring Autodocs Options

Create a new configuration file to hold Autodocs settings. This should go in the config folder of your Minicli applicaiton:

touch config/autodocs.php

Autodocs expects an autodocs configuration entry with a few required settings:

  • templates_dir: where to find .tpl files that may be used as page templates. The use of templates is not mandatory, but it can be helpful to facilitate content layout updates without having to change page classes.
  • cache_dir: .json files in this directory will be automatically registered as a DataFeed object within the main Autodocs service.
  • output: location where to save built pages.
  • pages: an array with classes that should be registered as Reference Pages. These are the documentation pages that will be built at each run.
  • storage: (Optional) a class that implements StorageInterface, able to handle filesystem access to save and retrieve files. The default FileStorage is used when no option is defined.

For our demo, let's create a storage directory for templates, content, and cache:

mkdir -p storage/cache storage/templates storage/content

Next, open the file we created previously, config/autodocs.php, using your PHP editor of choice. Copy the following configuration to your file:

<?php

declare(strict_types=1);

use Autodocs\Page\ExamplePage;

return [
    'autodocs' => [
        // Pages to Build
        'pages' => [
            ExamplePage::class
        ],
        // Build Output Folder
        'output' => envconfig('AUTODOCS_OUTPUT', __DIR__.'/../storage/content'),
        // Cache Folder - where to look for cache json files
        'cache_dir' => envconfig('AUTODOCS_CACHE', __DIR__.'/../storage/cache'),
        // Templates directory
        'templates_dir' => envconfig('AUTODOCS_TEMPLATES', __DIR__.'/../storage/templates')
    ]
];

Save the file. Notice we registered an ExamplePage that is included for tests. We'll update this later on to include our custom pages.

3. Registering the Autodocs Service

With the config in place, you can now register the Autodocs service within the Minicli application.

Open the config/services.php file and make sure it looks like this, with the line including the Autodocs service class registered as autodocs:

<?php

declare(strict_types=1);

use Autodocs\Service\AutodocsService;

return [
    /****************************************************************************
     * Application Services
     * --------------------------------------------------------------------------
     *
     * The services to be loaded for your application.
     *****************************************************************************/

    'services' => [
        'autodocs' => AutodocsService::class,
    ],
];

Save the file. You can now start working on your build command, which we'll see in the next step.

4. Creating a Build Command

We'll now create a command to build our custom automated docs. This will build all pages defined in the pages autodocs config, which for now should be set to just include the ExamplePage. Once this simple example page builds, we can start working on our actual doc pages.

In Minicli, creating a command is just a matter of setting up some classes and directories in a pre-defined format. Start by creating a directory for your commands inside app/Commands:

mkdir app/Command/Build

Next, we'll create the command controller that will handle building the docs.

Create a new file called DefaultController.php inside the newly created app/Command/Build directory. In this class, we'll set up a method that will be called whenever we run ./autodocs build.

touch app/Command/Build/DefaultController.php

Open this file on your PHP editor of choice. You can start by copying the following skeleton for your controller class:

<?php

declare(strict_types=1);

namespace App\Command\Build;

use Minicli\Command\CommandController;

class DefaultController extends CommandController
{
    public function handle(): void
    {
        //build command action
    }
}

From the handle method, we can access the application, the configuration container, and all registered services. The logic here will depend on your documentation needs.

To build the included ExamplePage, update your handle method to the following code:

    public function handle(): void
    {
        /** @var AutodocsService $autodocs */
        $autodocs = $this->getApp()->autodocs;

        $this->info("Starting Build...");
        $autodocs->buildPages($this->getParam('pages') ?? "all");
        $this->info("Build finished. Output saved to " . $autodocs->config['output']);

    }

This will build the example page and save it in the location defined by output in Autodocs config.

Building the Docs

Now you should be able to run the build command with:

./autodocs build

You should get output like the following:

❯ ./autodocs build

Starting Build...

Build finished. Output saved to /home/erika/Projects/autodocs-demo/config/../storage/content

Check the output folder. You should find an example.md page there.

5. Creating Documentation Pages

In the previous step we created a build command and used it to build the included ExamplePage, which is a really simple page that basically outputs a title and a description. After having a look at how pages are defined, we'll create a custom page for our demo app.

5.1 Reference Pages

Reference Pages are models that represent a document and how it should be built. They should extend from the ReferencePage class (or implement ReferencePageInterface) and implement the following methods:

  • loadData - this method receives an optional $parameters array and should load any additional data required to build the page.
  • getName - must return a unique identifier that can be used later on to build only this type of page.
  • getContent - the actual content that will be saved.
  • getSavePath - the path where to save this file, to be used by the page builder.

It is easier to see how it works in practice, so this is what the ExamplePage looks like:

<?php

declare(strict_types=1);

namespace Autodocs\Page;

use Autodocs\DataFeed\JsonDataFeed;

class ExamplePage extends ReferencePage
{
    public JsonDataFeed $dataFeed;
    public function loadData(array $parameters = []): void
    {
        $this->dataFeed = new JsonDataFeed();
        $this->dataFeed->loadFromArray([
            'title' => 'example',
            'description' => 'description'
        ]);
    }

    public function getName(): string
    {
        return "example";
    }

    public function getContent(): string
    {
        return $this->dataFeed->json['title'].' - '.$this->dataFeed->json['description'];
    }

    public function getSavePath(): string
    {
        return 'example.md';
    }
}

This example creates a JsonDataFeed and loads it with static data. Instead, you could use JSON data files and have them automatically loaded by Autodocs - you just need to place the files in the path defined by the cache configuration option.

Next, we'll create our own custom documentation page using a simple JSON data source. For this demo, we'll generate a markdown page that can be used as your personal README on GitHub (the one that is rendered in your profile).

5.1 Creating a ReadmePage

The following ReadmePage class loads data from a JsonDataFeed that was pre-loaded into the application container. We'll set that JSON file in the next step.

The getContent method uses Stencil to render a template called readme.tpl with values obtained from the JSON data source.

<?php

declare(strict_types=1);

namespace App;

use Autodocs\DataFeed\JsonDataFeed;
use Autodocs\Page\ReferencePage;

class ReadmePage extends ReferencePage
{
    public JsonDataFeed $dataFeed;
    public function loadData(array $parameters = []): void
    {
        $this->dataFeed = $this->autodocs->getDataFeed('profile.json');
    }

    public function getName(): string
    {
        return "readme";
    }

    public function getContent(): string
    {
        return $this->autodocs->stencil->applyTemplate('readme', [
            'title' => $this->dataFeed->json['user'],
            'about' => $this->dataFeed->json['bio'],
            'projects_list' => $this->getProjects(),
        ]);
    }

    public function getProjects(): string
    {
        $content = "";
        foreach ($this->dataFeed->json['projects'] as $project => $info) {
            $content .= "- [{$project}]({$info['link']}): {$info['description']}\n"; // returns Markdown list
        }

        return $content;
    }

    public function getSavePath(): string
    {
        return 'README.md';
    }
}

When you're ready, copy these contents into your own app/ReadmePage.php and save the file. But don't run the build just yet; we still need to set up the JSON data source and the template file used by the ReadmePage class.

5.2 Setting up the profile.json Data Source

Next, we'll create a simple JSON file with some data to use when building the Readme page.

Create a new file called profile.json in your cache_path:

touch storage/cache/profile.json

Open the file in your editor of choice. Copy the following JSON skeleton and update the data with your own info:

{
  "user": "Erika Heidi",
  "bio": "Software and Documentation Engineer",
  "projects": {
    "minicli/minicli": {
      "description": "CLI framework for PHP",
      "link": "https://docs.minicli.dev"
     },
    "erikaheidi/autodocs": {
      "description": "Tiny framework for automating documentation",
      "link": "https://github.com/erikaheidi/autodocs/wiki"
    }
  }
}

Save the file when you're finished.

5.3 Setting Up the readme.tpl Template

Next, create the template file:

touch storage/templates/readme.tpl

Copy the following content to your template:

## {{ title }}

{{ about }}

## My Projects

{{ projects_list }}

Save the file when you're done.

5.4 Registering the ReadmePage within Autodocs Configuration

Now you just need to register your custom page within Autodocs configuration. Edit the config/autodocs.php file, and change the pages entry to include the new page. You can also remove the ExamplePage while you're at it. It should look like this when you're finished:


    'autodocs' => [
        // Pages to Build
        'pages' => [
            ReadmePage::class
        ],

Save the file when you're finished.

5.5 Building the ReadmePage

With the new page registered within the documentation, you can now proceed to build the ReadmePage:

./autodocs build

You should get output indicating that the page was successfully built. If should now have a readme.md file in your output folder with contents similar to this:

## Erika Heidi

Software and Documentation Engineer

## My PHP Projects

- [minicli/minicli](https://docs.minicli.dev): CLI framework for PHP
- [erikaheidi/autodocs](https://github.com/erikaheidi/autodocs/wiki): Tiny framework for automating documentation

6. Running the Demo with GitHub Actions (Optional)

We'll now create a workflow to run this demo on GitHub Actions. This is optional and requires you to set up your own repository with a copy of the project. You can also fork the original demo to your GitHub account and change the JSON data file to have your own info. Once you have your repository set up, go to Settings -> Actions -> General, scroll to the bottom of the page where you'll find the Workflow permissions section.

enable workflows to create pull requests

Then, enable "Read and Write permissions" and "Allow GitHub Actions to send Pull requests", so that the workflow can make changes and create a PR with the generated doc(s).

Our workflow will:

  • set up some environment variables that will overwrite default configuration values for Autodocs,
  • check out the repository,
  • install Composer dependencies using cache when available,
  • build the docs,
  • and send a pull request only when changes are detected.
name: Build Docs

on:
  workflow_dispatch:

env:
  AUTODOCS_OUTPUT: "${{ github.workspace }}"
  AUTODOCS_CACHE: "${{ github.workspace }}/storage/cache"
  AUTODOCS_TEMPLATES: "${{ github.workspace }}/storage/templates"
jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Cache Composer packages
      id: composer-cache
      uses: actions/cache@v2
      with:
        path: vendor
        key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
        restore-keys: |
          ${{ runner.os }}-php-

    # Install Dependencies
    - name: Install dependencies
      run: composer install --prefer-dist --no-progress

    # Build Docs
    - name: Run Autodocs
      run: ./autodocs build

    # Send a Pull Request
    - name: Create a PR
      uses: peter-evans/create-pull-request@v5
      id: cpr
      with:
        path: "${{ github.workspace }}"
        commit-message: Update content
        title: "[AutoDocs] Updated README"
        body: "README auto-update"
        labels: |
          documentation
          automated

You can save this file to .github/workflows/autodocs.yaml. Don't forget to commit and push it to your repository. Once the file is there, go to Actions and select the Build Docs action on the left. This workflow is not set to run on schedule, instead it uses the workflow_dispatcher trigger which can only be triggered from the repository page.

Click on the "Run Workflow" button on the right, then confirm to run the workflow for the first time. If the build passes as it should, you'll get a new pull request on your repository:

Autodocs pull request

Based on this simplified demo, you can elaborate your workflow to auto-generate your profile.json data and have your README always fresh. Maybe pull your latest posts from DEV or a blog feed? Show off your favorite projects, or highlight your sponsors? The only limit is your imagination :)

Final Considerations

Documentation is a living organism, it changes and evolves as projects grow. After quite a few years working in the field of technical writing and documentation, I believe good documentation needs flexibility, context, and human input. These are hard to combine in a one-size-fits-all software to magically create docs.

Autodocs doesn't magically generate docs for you, that was never the intention. Instead, it provides a very thin layer of abstractions to give you enough structure to engineer your super custom docs and also to scale up your documentation using a distributed approach to data sourcing.

If you want to see a more complex implementation to have an idea of what Autodocs is capable, check Chainguard's Images Autodocs project on GitHub.

Built with Librarian by @erikaheidi.

Back to top