News syndication: incident patterns and mitigations
Searchable SSOT for why automated outbound publishing fails in production and how Vox constrains it.
Common failure modes (industry + API behavior)
-
Wrong environment / credentials
Tokens scoped to the wrong org, expired OAuth, or CI secrets injected into a job that was assumed to be dry-run only. Mitigation: separate config keys, defaultdry_run = true, and require explicitpublish_armed+VOX_NEWS_PUBLISH_ARMEDfor live posts. -
Missing staging for write APIs
Many social/write APIs (e.g. X posting) do not offer a full “sandbox” identical to production; validation is often contract testing (local HTTP mocks) plus dry-run. Mitigation:vox-publishertests hit local Axum mocks; production paths stay behind gates. -
Retry / idempotency bugs
Marking a post as “done” before all channels succeed causes skipped retries on some channels; marking too late causes duplicate posts. Mitigation: each run recordsnews_publish_attemptswith per-channel outcomes, andpublished_newsis written only for successful live runs with no enabled-channel failures. -
GitHub releases trigger notifications
GitHub documents that creating a release can trigger notifications; rapid writes can hit secondary rate limits. Mitigation: default research/release templates usedraft: truefor GitHubRelease; prefer draft until human publish. See GitHub REST: create a release and best practices for using the REST API. -
Schema / feed regressions
Invalid RSS breaks subscribers silently. Mitigation: validatefeed.xmlstructure in CI where practical (e.g. W3C Feed Validator docs: validator.w3.org/feed/docs); keep links andpubDateRFC-2822-shaped viachrono. -
Insufficient human gates
Single-person publish from automation. Mitigation: two distinct approvers innews_publish_approvals_v2for the currentcontent_sha3_256digest before live syndication (enforced inNewsService; legacy id-only approvals are migration fallback).
Vox-specific controls (code pointers)
| Control | Location |
|---|---|
| Global + per-item dry run | vox_publisher::Publisher::publish_all |
| Recursive draft pickup | vox_orchestrator::services::news::collect_news_markdown_paths |
| Dual approval + armed gate | vox_orchestrator::services::news::NewsService::tick |
| Approval persistence | vox_db::VoxDb::record_news_approval_for_digest, has_dual_news_approval_with_fallback |
| MCP tools (no live by default) | vox_mcp::tools::news_tools |
| Canonical templates | crates/vox-publisher/news-templates/*.md |
References
- Open Collective API direction (GraphQL v2): Open Collective API →
https://graphql-docs-v2.opencollective.com/. - Cross-cutting env vars: env-vars.md.