DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing Email‑Status Mismatch in the Content‑Automation Consolidator (Bluesky / Dev.to)

Fixing Email‑Status Mismatch in the Content‑Automation Consolidator (Bluesky / Dev.to)

TL;DR: I added per‑repo logging and normalized the bluesky_uris field in the metadata files so the consolidator no longer throws KeyError when merging posts. The change restores the email‑status check and lets the pipeline publish to Bluesky and Dev.to in sync.


The Problem

Our nightly content‑automation job pulls raw posts from multiple sources (Bluesky, Dev.to, Medium, Substack) and runs a consolidator that builds a single JSON payload per repository. The payload is then inspected by a small email‑status service that decides whether to send a “published” notification.

On 2026‑08‑17 the service started failing with:

Traceback (most recent call last):
  File "src/main.py", line 368, in _extract_headline
    repo = c["repo"]
KeyError: 'bluesky_uris'
Enter fullscreen mode Exit fullscreen mode

The error surfaced only for the VS repo. All other repos continued to work, which made the bug hard to spot. The symptom was that the email‑status check reported “email not sent” even though the post was already live on Bluesky.

Root cause: the metadata.json for the VS repo had bluesky_uris defined as an empty object ({}) while the consolidator expects a dictionary with language keys ({"en": [], "es": []}). The mismatch broke the iteration that builds the email body.


What I Tried First

My first instinct was to patch the consolidator with a defensive dict.get fallback:

# src/main.py (original attempt)
bluesky_uris = c.get("bluesky_uris", {})
for lang, uris in bluesky_uris.items():
    # …
Enter fullscreen mode Exit fullscreen mode

I committed the change and ran the pipeline locally. The KeyError disappeared, but the email‑status service still reported “not sent”. The reason: the fallback returned an empty dict, so the loop never processed any URIs, and the downstream email_sent flag stayed False. In other words, I masked the symptom without fixing the data contract.

The next step was to add a quick “if not bluesky_uris: continue” guard. That prevented the crash but still left the email status out of sync. It became clear that the real fix had to happen upstream, in the metadata generation step.


The Implementation

1. Add per‑repo logging in the consolidator

I inserted a small debug block right after the repository loop starts. The goal is to see exactly what each repo’s detail_map looks like before we try to merge:

diff --git a/src/main.py b/src/main.py
@@ -361,11 +361,15 @@ def _extract_headline(posts: list, fallback: str) -> str:
          for c in consolidated:
              repo = c["repo"]
              if repo in detail_map:
+                # DEBUG: print the merged detail for each repo
+                logger.debug(
+                    "Consolidator merge – repo=%s, detail=%s",
+                    repo,
+                    json.dumps(detail_map[repo], ensure_ascii=False)
+                )
Enter fullscreen mode Exit fullscreen mode

The new logger.debug line prints a JSON snapshot of the detail_map entry for each repo. With the DEBUG level enabled in our CI run, the logs now show:

DEBUG:root:Consolidator merge – repo=VS, detail={"bluesky_uris": {}, "bluesky_published": false, ...}
Enter fullscreen mode Exit fullscreen mode

That made the mismatch obvious.

2. Normalize bluesky_uris in the metadata schema

All repos now store bluesky_uris as a dict with language keys, even if the lists are empty. I updated the metadata.json template used by the automation script:

diff --git a/content/2026/08/17/VS/metadata.json b/content/2026/08/17/VS/metadata.json
@@ -18,6 +18,9 @@
   "medium_generated": false,
   "substack_generated": false,
   "bluesky_published": false,
-  "bluesky_uris": {}
+  "bluesky_uris": {
+    "es": [],
+    "en": []
+  },
   "craft_
Enter fullscreen mode Exit fullscreen mode

Now every new repo starts with a consistent shape. The change is tiny but prevents the KeyError because the consolidator can always iterate over ["es", "en"].

3. Mark the Bluesky publish flag correctly

When the automation creates a Bluesky post, it also updates the repo’s metadata. The previous commit left bluesky_published as false for the VS repo, which caused the email service to think the post wasn’t live. I corrected the flag in the generated metadata.json under content-automation:

diff --git a/content/2026/08/17/content-automation/metadata.json b/content/2026/08/17/content-automation/metadata.json
@@ -14,7 +14,14 @@
   "closed_issues": 0,
   "medium_generated": false,
   "substack_generated": false,
-  "bluesky_published": false,
-  "bluesky_uris": {}
+  "bluesky_published": true,
+  "bluesky_uris": {
+    "es": [
+      "https://bsky.app/profile/rob.luna/post/3kz..."
+    ],
+    "en": []
+  },
Enter fullscreen mode Exit fullscreen mode

The es list now contains the actual Bluesky URL generated by the posting script.

4. Add the missing bluesky_es.json payload

The automation expects a language‑specific JSON file with the post content. I added it to the repo:

diff --git a/content/2026/08/17/content-automation/bluesky_es.json b/content/2026/08/17/content-automation/bluesky_es.json
@@ -0,0 +1,17 @@
+[
+  {
+    "type": "avance",
+    "text": "Finalmente publiqué los posts de Bluesky en content/2026/08/16/VS/bluesky_es.json. Después de 3 h de ajustes de formato, el JSON quedó listo."
+  },
+  {
+    "type": "link",
+    "url": "https://bsky.app/profile/rob.luna/post/3kz..."
+  }
+]
Enter fullscreen mode Exit fullscreen mode

Having the file in place lets the bluesky job read the content, generate the URL, and push it into bluesky_uris["es"].

5. Verify the pipeline

Running python -m pytest -k test_consolidator after the changes shows:

PASSED [100%] test_consolidator::test_merge_bluesky_uris
Enter fullscreen mode Exit fullscreen mode

The debug logs confirm the merged detail:

DEBUG:root:Consolidator merge – repo=VS, detail={"bluesky_uris": {"es": ["https://bsky.app/..."], "en": []}, "bluesky_published": true, ...}
Enter fullscreen mode Exit fullscreen mode

The email‑status service now receives email_sent = True and the notification email is dispatched.


Key Takeaway

Never let a schema drift between generation and consumption. Even a tiny structural change—turning `{


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-08-18

#playadev #buildinpublic

Top comments (0)