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, and the 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, but I am not much good at javascript, and wanted to understand Actions better, so I built my own: 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, 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. 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:
- Check out my private repo containing my posts
- Publish the articles for dev.to
- 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
.
- I created a Personal Access Token, and named it
- Dev.to credentials to post on my behalf.
- These are created in the dev.to account settings 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
- 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
- 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.
- 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 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
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"
Top comments (0)