Continuous Delivery of dev.to articles with Github Actions
Tags: writing, github, cicd
Published: 2020-12-29 01:05:56

---
title: Continuous Delivery of dev.to articles with Github Actions
published: true
tags: writing, github, cicd
---

I'm a big proponent of Continuous Delivery: delivering value should be routine, fast, and easy. I've recently applied that mindset to my writing as well, thanks to [Github Actions](https://github.com/features/actions), and the [dev.to API](https://docs.dev.to/api).

## But Why?

My typical workflow for writing is to start brainstorming in a text file (usually with vim or vscode), and gradually shape that into a coherent piece of writing. I prefer to write in markdown, since the format is simple enough to do by hand, and is easy to render as html, or just paste into Google Docs.

The publishing part is usually where I'd get bogged down. I'd debate with myself what the right platform was, and then have to reformat to make sure links worked. What if I needed to make a correction? Repeat the process.
Often I'd just stop working on a piece, because the process was annoying. This felt exactly like a problem Continuous Delivery could solve.

## A writer's CI/CD with Github Actions

My normal writing workflow starts with markdown and text files, so there was no change needed for me to keep my writing in a git repo. In fact, I had a mostly-empty repo already laying around, with some old writing already in it!

### Creating the Github Action

I will admit up front that creating my own Github Action for publishing was unnecessary, and I only did it as a learning exercise. There was already [an action to publish to dev.to](https://github.com/marketplace/actions/publish-to-dev-to), but I am not much good at javascript, and wanted to understand Actions better, so I built my own: [muncus/devto-publish-action](http://github.com/muncus/devto-publish-action).

The Golang code to call the Dev.to API is fairly straightforward, and I spent more time with the Dockerfile and the Github Actions YAML files.

The binary basically takes 3 inputs: a directory full of markdown files, a dev.to api key, and a json file in which to store article ids (which allows us to re-publish the same file when there are changes).

These arguments need to be declared in the `inputs` section of [action.yaml](https://github.com/muncus/devto-publish-action/blob/master/action.yaml), and referenced in the `args` section.

As an extra bonus, I added a workflow to build and test the go code for the action as [.github/workflows/build-go.yml](https://github.com/muncus/devto-publish-action/blob/master/.github/workflows/build-go.yml). This workflow is taken from an Actions tutorial (but I forgot the source, sorry!).

Now that our action repo is created and published, it is time to use it in our writing repo, by creating another workflow.

### Calling the Action from a Workflow

The full workflow for publishing an article has 3 steps:

1. Check out my private repo containing my posts
2. Publish the articles for dev.to
3. Update `ids.json` with newly published article IDs, so I can edit posts after initial publishing.

#### Setting up some Secrets

Before we can set up this workflow, we need a safe place to store two key pieces of secret information.
[Github Secrets]() is perfect for this.

- Github credentials to read my private repository
  - I created a Personal Access Token, and named it `MY_GH_PAT`.
- Dev.to credentials to post on my behalf.
  - These are created in the [dev.to account settings](https://dev.to/settings/account) under "DEV API Keys".
  - This secret is named `DEVTO_API_TOKEN`

With these secrets created, we can create a workflow that references them, without exposing our precious credentials! Let's look at each step individually, and the full example is below.

#### Step 1: Check out private repo

```yaml
    - name: Check out repo
      uses: actions/checkout@v2
      with:
        token: "${{ secrets.MY_GH_PAT }}"
```

This uses the built-in action to check out a repo, which accepts a `token` input. Because I keep my writing in a private repo, we must specify an access token.

#### Step 2: Publish with our custom action
```yaml
    - name: Publish to Dev.to
      uses: muncus/devto-publish-action@release/v1
      with:
        directory: "$GITHUB_WORKSPACE/dev.to/"
        api-key: "${{ secrets.DEVTO_API_TOKEN }}"
        state-file: "$GITHUB_WORKSPACE/ids.json"
```
This references my custom action, and provides the inputs it needs. Note that `$GITHUB_WORKSPACE` is a built-in environment variable that is the base directory of the repo we checked out in the previous step.

#### Step 3: Make a PR with local changes

When my publishing action runs, it updates a "state file" which maps filenames to dev.to article IDs. This means after a new article is added, there will be local changes. To preserve those changes, we'll need to create a Pull Request from those local changes.

```yaml
    - name: Create PR to update state file
      uses: peter-evans/create-pull-request@v3
      with:
        title: "[CI] Update state file with published article ids"
        reviewers: ${{ github.actor }}
        token: "${{ secrets.MY_GH_PAT }}"
        base: master
        branch: "ci/state-file-update"
```

There are many options for the [peter-evans/create-pull-request](github.com/peter-evans/create-pull-request) action, but lets run through the ones I'm using here:

- `title`: entirely cosmetic, but this helps me approve PRs quickly when I know they're from my CI system.
- `reviewers`: to clarify that whoever pushed the new article is responsible for approving the PR, they are listed as the reviewer.
- `token`: this is the github access token created above. it uses my credentials in this private repo, so all these PRs look like they were made by me. Its not ideal for a shared repo, but fine for my needs.
- `base`: the branch against which to make the PR. We check out master to publish, so that's what we're comparing to.
- `branch`: using a stable PR branch name keeps merges simpler, and helps "stack up" multiple changes to the state file, in case I forget to approve a previous PR before publishing a new article.

### Conclusions

In my experience, the hardest part of the Action development process was the trial-and-error needed to get the YAML "just right". There's still room for improvement in the Dockerfile, and I could use a pre-built image to save another 30 seconds on each article publication. But it works well enough to get us back to writing prose instead of code.

I hope this inspires you to use Github Actions in a non-traditional way!

### Appendix: full workflow yaml

```yaml
name: Publish
# Only trigger this workflow on master, and a feature branch specifically for testing the workflow.
on:
  push:
    branches: [ master, ci-action ]

jobs:
  build:
    name: Publish
    runs-on: ubuntu-latest
    steps:
    # First, check out the private repo.
    - name: Check out repo
      uses: actions/checkout@v2
      with:
        token: "${{ secrets.MY_GH_PAT }}"
    # Then publish articles.
    - name: Publish to Dev.to
      uses: muncus/devto-publish-action@release/v1
      with:
        directory: "$GITHUB_WORKSPACE/dev.to/"
        api-key: "${{ secrets.DEVTO_API_TOKEN }}"
        state-file: "$GITHUB_WORKSPACE/ids.json"
    # Last, update the state file by making a PR.
    - name: Create PR to update state file
      uses: peter-evans/create-pull-request@v3
      with:
        title: "[CI] Update state file with published article ids"
        reviewers: ${{ github.actor }}
        token: "${{ secrets.MY_GH_PAT }}"
        base: master
        branch: "ci/state-file-update"
```