Skip to content

Blogging With GitHub #23

@github-actions

Description

@github-actions

Obligatory "wow it's been so long since I've written here!"

In an effort to reduce the (admittedly already low) friction of writing blog posts, I'm now hosting this site on
netlify, which makes it pretty effortless to generate site previews off of pull requests, as well
as perform the usual tasks like updating DNS, provisioning a Let's Encrypt cert, etc. I'm still using my own static site generator which was easy to integrate with netlify's GitHub App: I set my site's "build command" to make build, which runs:

SSGEN_BIN ?= ./ssgen

build: $(SSGEN_BIN)
	$(SSGEN_BIN) -in src -out build
	cp -R static/ build/

$(SSGEN_BIN):
	go get github.com/ktravis/ssgen
	go build -o $@ github.com/ktravis/ssgen

netlify will then publish static content in the build/ directory.

I wanted to make the process of publishing a new blog post feel a bit more dynamic by allowing people to "follow" and be
notified of new posts (this is probably a bit optimistic, but I'm mostly interested in the technical challenge). Since
the site's source is already hosted on GitHub, it seems natural that watching the repo should give you that
notification. Rather than spamming people with notifications on every commit, I'd like interested parties to be able to
watch "releases only" - meaning I need to create a new release when a new blog post is added. Simple enough, but why do
that manually when we can automate it?

A quick dive into the world of GitHub Actions led me to actions/github-script
which allows easy use of the Octokit API directly from a workflow definition - i.e. without creating your own custom
action. After a bit too much trial and error (thanks JavaScript) I ended up with this:

# .github/workflows/release.yml
name: blog-post-release
on:
  pull_request:
    branches: [ master ]
    types: [ opened, edited, closed, reopened ]
jobs:
  announce:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/github-script@v3
      with:
        github-token: ${{secrets.GITHUB_TOKEN}}
        script: |
          const pr = context.payload.pull_request
          if (pr.state !== "open" && pr.merged_at === null) {
            console.log("pr was not merged, skipping")
            return
          }
          let urlBase = "https://kylemtravis.com/blog"
          if (pr.state === "open")
            urlBase = `https://deploy-preview-${pr.number}--kylemtravis.netlify.app/blog`
          const result = await github.pulls.listFiles({
            pull_number: pr.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
          })
          const added = result.data.filter(f => f.status === "added")
          const newBlogPosts = added.filter(f => f.filename.match(/src\/blog\//) !== null)
          if (newBlogPosts.length < 1)
            return
          const releaseName = newBlogPosts[0].patch.match(/@name\s*=\s*(.+)/)[1]
          let slug = releaseName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9\-]/g, "")
          const slugMatch = newBlogPosts[0].patch.match(/@slug\s*=\s*(.+)/)
          if (slugMatch && slugMatch.length > 1)
            slug = slugMatch[1]
          const rel = {
            owner: context.repo.owner,
            repo: context.repo.repo,
            tag_name: `blog-pr-${pr.number}`,
            target_commitish: pr.head.sha,
            name: `[Blog Post] ${releaseName}`,
            body: `New blog post, read it [here](${urlBase}/${slug})!`,
            draft: pr.state === "open",
          }
          const rels = await github.repos.listReleases({
            owner: context.repo.owner,
            repo: context.repo.repo,
          })
          const found = rels.data.filter(r => r.draft && r.tag_name == rel.tag_name)
          await (found.length > 1 ?
            github.repos.updateRelease({
              release_id: found[0].id,
              ...rel
            }) :
            github.repos.createRelease(rel)
          )

That's it - the whole deal. It's more verbose that it needs to be, because a) my js is not great, and b) I have some
metadata in the contents of the blog post that I wanted to pick out. The result is an action that will run on pull
requests, creating or updating a release if that PR adds files under the path ./src/blog/. A PR that only changes
existing posts will not trigger a new release, and a draft release is created (pointing to the netlify preview) if the
PR is yet to be merged. One loose end, closed PR's will leave behind a draft release. This could easily be fixed with a
bit more logic, but I am happy with the tradeoff for now.

I'm convinced this solution could be made even simpler, but rather than prematurely optimize, I'm going to leave it
as-is for now; after all, it's no good if it's not being used yet.

Thank you for reading - if you're interested in following along, you can watch the repo
here!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions