curious builders

Using GitHub Actions to Publish Astro Drafts

|

I love it when I manage to see a problem as an opportunity. A chance learn and challenge my skills. This story is a simple recount of such instance. Follow along and you’ll see how I solved my publishing problem with a few tools from my kit.

I’ve used Astro for a few months now. And so far I have enjoyed the experience.

But I have an issue with the way Astro handles draft pages. I can add a draft: true flag to the blog post front matter and Astro will exclude the post when building.

Fine.

But what about when I want to publish a post?

As far as I can tell, the only way right now is to remove the flag again. Manually. Which means I can’t schedule a post for the future using Astro and simple markdown pages. Which is a problem. Especially as my AI powered blog experiment is supposed to publish blog posts daily.

How I publish Astro blog posts using GitHub Actions

To solve this problem I decided to write a small script.

The process is straightforward:

  1. Find all drafts
  2. Read the front matter of each post
  3. If pubDate is earlier than now, remove the draft flag

It isn’t pretty, but here is the full script:

import matter from "gray-matter";
import { getPathToBlog } from "./configuration";
import { readDir, readFromFile, writeToFile } from "./fileUtil";

function publishDraft(path: string) {
  readDir(path).forEach((file) => {
    const filePath = path + file;

    const { data: frontMatter, content } = matter(readFromFile(filePath));

    if (frontMatter["draft"]) {
      const pubDate = frontMatter["pubDate"];
      const parsedDate = new Date(pubDate);

      if (parsedDate < now) {
        console.log(`PUBLISH: ${file}. Publish date: ${pubDate}.`);

        delete frontMatter["draft"];

        writeToFile(filePath, matter.stringify(content, frontMatter));
      } else {
        console.log(`DRAFT: ${file}. Publish date: ${pubDate}.`);
      }
    }
  });
}

const now = new Date();

const blogs = [getPathToBlog(), "path/to/other/blog"];

blogs.forEach(publishDraft)

To run the script I added the following line to the scripts section of my package.json file:

"publish-drafts": "ts-node src/publishDrafts.ts",

The command uses ts-node to execute TypeScript. So I added that to devDependencies:

"ts-node": "^10.9.1",

And with that I can publish my Asto blog posts with a single command:

npm run publish-drafts

Almost.

The script will remove draft flags on all blog posts that should be published. But I still have to:

  1. Run the script
  2. Commit the changes
  3. Push the commit to GitHub

And I have to remember to run this whenever I have something to publish.

Still not great.

Let’s automate those 3 steps using GitHub actions.

I’ll explain the code in details, but here is the full code for a better overview:

name: Publish Blog Posts

on:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

defaults:
  run:
    working-directory: scripts

jobs:
  run:
    name: Remove Draft Tags to Publish Posts
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3

      - name: Install dependencies
        run: npm install

      - name: Remove draft tags
        run: npm run publish-drafts

      - name: Commit changes
        uses: EndBug/add-and-commit@v9
        with:
          message: 'Publish Blog Posts'
          committer_name: GitHub Actions
          committer_email: actions@github.com
          add: "['blog/src/pages/ai-overlord/blog/*.md']"

First up we need the action to run on a schedule. The schedule configuration lets us do that using a cron expression. "0 0 * * *" is every day at midnight.

on:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

The workflow_dispatch configuration allows me to also run the action via the GitHub UI. This was useful for testing and also if I want to publish a draft right away.

GitHub workflow dispatch

The GitHub action has one job. To publish blog posts. The first few steps set up the environment: checkout code, set up Node.js, and install npm dependencies.

- name: Checkout repo
  uses: actions/checkout@v3

- name: Set up Node.js
  uses: actions/setup-node@v3

- name: Install dependencies
  run: npm install

The next step runs my script:

- name: Remove draft tags
  run: npm run publish-drafts

After this the GitHub action has a checked out version of my repository with changes to some files — the blog posts that are to be published. To get these changes to the live blog we need to commit them. And luckily there is an action for that: Add & Commit.

Using this action I can specify the files I want to commit and have the action do it:

- name: Commit changes
  uses: EndBug/add-and-commit@v9
  with:
    message: 'Publish Blog Posts'
    committer_name: GitHub Actions
    committer_email: actions@github.com
    add: "['blog/src/pages/ai-overlord/blog/*.md']"

And that’s it.

Once a day, at 00:00, the bots over at GitHub will run my code and commit any changes they have made to markdown files inside the blog/src/pages/ai-overlord/blog/ directory. Other bots will then detect these changes and rebuild my website.

Beautiful.

…or, well it works.

Epilogue

I have previously used Jekyll as my static site generator. It treats posts with a publish date in the future as drafts. This means rebuilding is enough to publish scheduled posts.

I could have implemented something similar for my Astro site, but the draft flag seems like the default supported option at the moment (e.g., the rss package also supports drafts).

With Astro 2.0 introducing content collections I may have to rethink my approach. Content collections seem more complex to use but they will also open up more options — like the ability to not publish posts with a date in the future.