Fixing the Bluesky Daily Posting Workflow in content‑automation (GitHub Actions + Metadata Flags)
TL;DR: I discovered a syntax error in .github/workflows/bluesky-daily.yml that stopped our automated Bluesky posts, and I tightened the metadata handling in the content‑automation pipeline so generated flags are reliable. The fix restores our Build‑in‑Public cadence and prevents duplicate posts.
The Problem
Our weekly “Build in Public” cadence relies on three GitHub Actions:
-
content‑automation – walks the
content/YYYY/MM/DD/*tree, renders Markdown for Medium/Substack, and flips the*_generatedflags inmetadata.json. - bluesky‑daily – reads the same tree and publishes a short status to Bluesky.
On 2026‑10‑02 the Bluesky job started failing with the following error in the Actions log:
Error: Unexpected token ':' at line 23, column 12 in .github/workflows/bluesky-daily.yml
The workflow never reached the run: npm start step, so no post was sent. At the same time, metadata.json for the VS project showed an inconsistent state:
{
"pull_requests": 5,
"releases": 0,
"closed_issues": 0,
- "medium_generated": false,
- "substack_generated": false,
+ "medium_generated": true,
+ "substack_generated":
}
The missing boolean for substack_generated caused the next run of the content‑automation script to think the Substack article was still pending, leading to duplicate drafts.
What I Tried First
My first instinct was to edit the workflow directly in the UI and replace the offending line with a proper boolean (true). I also manually patched metadata.json by adding "substack_generated": false. Both changes fixed the immediate syntax error, but the next push reverted the file because the repository’s content‑automation job rewrote metadata.json based on the stale state. In short: a quick UI edit didn’t survive the automation cycle.
The Implementation
1. Fix the YAML syntax
The root cause was a stray colon after the if: condition in the bluesky-daily.yml file:
# .github/workflows/bluesky-daily.yml (pre‑fix)
...
if: ${{ github.event_name == 'schedule' && github.ref == 'refs/heads/main' }}:
steps:
- uses: actions/checkout@v3
...
The trailing colon turned the whole block into an invalid mapping. I removed it and added an explicit jobs: block:
# .github/workflows/bluesky-daily.yml (post‑fix)
name: Bluesky Daily Post
on:
schedule:
- cron: '0 9 * * *' # 9 AM UTC daily
jobs:
post:
if: ${{ github.event_name == 'schedule' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run Bluesky publisher
run: npm run bluesky:post
Now the workflow parses correctly and reaches the npm run bluesky:post script.
2. Harden the metadata handling
The content‑automation script (written in Node.js) reads each metadata.json, updates the flags, and writes the file back. The bug stemmed from a missing default for substack_generated, which resulted in an undefined value being stringified as null (or omitted entirely). I added explicit defaults and a small validation helper:
// scripts/update-metadata.js
const fs = require('fs');
const path = require('path');
function loadMeta(file) {
const raw = fs.readFileSync(file, 'utf8');
const meta = JSON.parse(raw);
// Ensure flags exist and are booleans
meta.medium_generated = !!meta.medium_generated;
meta.substack_generated = !!meta.substack_generated;
return meta;
}
function saveMeta(file, meta) {
// Write pretty‑printed JSON with trailing newline
const data = JSON.stringify(meta, null, 2) + '\n';
fs.writeFileSync(file, data);
}
// Walk the content tree
const base = path.join(__dirname, '..', 'content');
fs.readdirSync(base, { withFileTypes: true }).forEach(dir => {
if (!dir.isDirectory()) return;
const metaPath = path.join(base, dir.name, 'metadata.json');
if (fs.existsSync(metaPath)) {
const meta = loadMeta(metaPath);
// Example: mark Medium as generated after successful publish
if (process.env.TARGET === 'medium') meta.medium_generated = true;
if (process.env.TARGET === 'substack') meta.substack_generated = true;
saveMeta(metaPath);
}
});
With this guard in place, metadata.json now always contains explicit boolean values:
{
"pull_requests": 5,
"releases": 0,
"closed_issues": 0,
"medium_generated": true,
"substack_generated": false
}
3. Add a sanity‑check step to the workflow
To avoid silent regressions, I added a lightweight job that validates all metadata.json files before the publishing steps:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate metadata flags
run: node scripts/validate-metadata.js
validate-metadata.js simply scans for missing boolean fields and exits with a non‑zero code if it finds any.
4. Commit summary
The final commit (hash 71e86968) added a JSON payload for Bluesky post tracking:
json
// content/2026/10/01/content-automation/bluesky_es.json
[
{
"type
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/content-automation` · 2026-10-02*
\#playadev #buildinpublic
Top comments (0)