DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing the Bluesky Daily Posting Workflow in content‑automation (GitHub Actions + Metadata Flags)

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:

  1. content‑automation – walks the content/YYYY/MM/DD/* tree, renders Markdown for Medium/Substack, and flips the *_generated flags in metadata.json.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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": 
}
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)