Setting created_at
with GitHub Actions when a PR is merged
The blog that you’re reading is a static site that’s generated on every push to main
. That means that whatever’s in the repo is the source of truth, including the created_at
date when a PR is merged.
I do a lot of my writing in batches, but don’t want to publish everything at the same time. Rather than keeping finished posts in Ulysses (my writing app of choice) and staging a new post every time I want to publish, I raise a pull request immediately and use GitHub Actions to set the created_at
date when publishing a post.
Here’s the rough workflow:
- Check if the
submit-post
label has been added - Fetch the list of changed files in the PR
- For each
added
file, fetch the file contents - Replace the
created_at
date (it’s calleddate
in the frontmatter) in each file (using.replace()
but I should probably usegray-matter
) - Use
github.repos.createOrUpdateFileContents
to update the contents of each file - Use
github.pulls.merge
withmerge_method: "squash"
to merge the PR as a single commit (createOrUpdateFileContents
adds multiple commits) - Delete the branch
I could write a new GitHub Action to do this, but as it’s not reusable and it’s only a small number of lines I’ve chosen to use actions/github-script
which lets me write JavaScript and use the GitHub API easily.
js
on:pull_request:types:- labeledjobs:merge-label:runs-on: ubuntu-lateststeps:- uses: actions/github-script@v4id: set-resultwith:github-token: $script: |const labelName = context.payload.label.name;if (labelName != "submit-post"){console.log(`${labelName} label was added. No action needed`);return;}const {data: files} = await github.pulls.listFiles({pull_number: context.issue.number,owner: context.repo.owner,repo: context.repo.repo,});// Load all new files + their contentsconst newPosts = await Promise.all(files.filter(f => {return f.status == 'added' && f.filename.endsWith(".md")}).map(async f => {const blob = await github.request(`GET ${f.contents_url}`);const content = Buffer.from(blob.data.content, 'base64').toString();return {sha: f.sha, filename: f.filename, content};}));// Replace the date + updated_at with the current dateconst now = (new Date).toISOString();await Promise.all(newPosts.map(async f => {f.content = f.content.replace(/date: "[^"]+"/mi, `date: "${now}"`);f.content = f.content.replace(/updated_at: "[^"]+"/mi, `updated_at: "${now}"`);return await github.repos.createOrUpdateFileContents({owner: context.repo.owner,repo: context.repo.repo,path: f.filename,message: "Update date",branch: context.payload.pull_request.head.ref,sha: f.sha,content: Buffer.from(f.content).toString('base64'),committer: {name: "Michael Heap",}});}));// Wait 10 seconds for changes to propogateawait new Promise((resolve) => {setTimeout(resolve, 10000);});// Squash and merge the PRawait github.pulls.merge({pull_number: context.issue.number,owner: context.repo.owner,repo: context.repo.repo,merge_method: "squash"});// Delete the branchgithub.git.deleteRef({repo: context.repo.repo,owner: context.repo.owner,ref: `heads/${context.payload.pull_request.head.ref}`});result-encoding: string
This action allows me to stage posts when I finish writing them rather than when I plan to publish. Once my intended publishing date arrives, I add the submit-post
label to the PR (usually via my phone) and the post is published as intended.