Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions .github/workflows/newsletter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ on:
- 'blog/weekly/**'
workflow_dispatch:

# Serialize newsletter publishes so two pushes never race the same
# unpublished file through `publish-buttondown` and double-create the
# Buttondown draft. `cancel-in-progress: false` is important — we must
# never cancel mid-run, or we could land in a state where Buttondown has
# the draft but the buttondown_id never gets committed back, which would
# cause the next run to re-publish it.
concurrency:
group: newsletter-publish
cancel-in-progress: false

env:
SQLX_OFFLINE: true
APP_BASE_URL: https://coreyja.com
Expand All @@ -19,8 +29,31 @@ jobs:
env:
CARGO_INCREMENTAL: 0
steps:
# The default GITHUB_TOKEN can't push to main — the branch ruleset
# only lets configured bypass actors through, and the App is the
# bypass actor (matches the pattern in bluesky.yml). Without the
# App token, the `Commit buttondown_id updates` step at the bottom
# fails after a successful publish, leaving Buttondown with a
# draft but the local file with no buttondown_id — so the next push
# re-publishes and accumulates duplicate drafts.
- name: Generate App token
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Setup | Checkout
uses: actions/checkout@v4
with:
# Always check out the tip of main, not the SHA that triggered
# the workflow. Without this, a queued run for an older commit
# could see stale newsletter frontmatter (missing buttondown_id
# updates already pushed by an earlier run) and re-publish.
ref: main
# Authenticate git operations as the App so the push bypasses
# the main-branch ruleset (App is a bypass actor).
token: ${{ steps.app-token.outputs.token }}

- uses: awalsh128/cache-apt-pkgs-action@latest
with:
Expand Down Expand Up @@ -86,12 +119,15 @@ jobs:
- name: Commit buttondown_id updates
if: steps.find.outputs.paths != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]"
git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"
git add blog/weekly/
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Add buttondown_id to published newsletters"
# Rebase onto whatever main looks like *now* — covers the case
# where main moved during this workflow run.
git pull --rebase origin main
git push
fi
41 changes: 41 additions & 0 deletions server/src/commands/buttondown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,47 @@ Body here.
assert!(fm.bsky_url.is_some());
}

/// Regression test for the shape of frontmatter written by the
/// `publish-standard-site sync` step. PRs #404-#408 added `atproto_uri`,
/// `atproto_pub_cid`, `subtitle`, and `publication` fields. A working
/// `publish-buttondown` must still parse a newsletter that has those
/// fields and no `buttondown_id` yet — otherwise the publish step
/// errors out instead of creating the Buttondown draft.
#[test]
fn test_parse_frontmatter_with_atproto_fields() {
let content = r#"---
title: "coreyja.fm Episode 3: Lean Into the Latency"
author: Corey Alexander
date: 2026-04-07
is_newsletter: true
subtitle: Async is the natural mode for AI agent workflows
atproto_uri: at://did:plc:bg2gnrjiv6htfynausierbm2/site.standard.document/weekly-20260407
atproto_pub_cid: bafyreibn466glibcavfowito3sot5hosargmk4jmhg2ldx2lcw3ifcyxti
publication: blog
---

Hey Team!
"#;
let (fm, body) = parse_frontmatter(content).unwrap();
assert_eq!(fm.title, "coreyja.fm Episode 3: Lean Into the Latency");
assert!(fm.is_newsletter);
assert!(fm.buttondown_id.is_none());
assert!(body.contains("Hey Team!"));
}

/// Regression test for the exact byte shape that the syndication sync
/// produces on disk — `---` followed by blank lines before the first
/// key. Reproduces the layout of `blog/weekly/20260407/index.md` on
/// main as of 2026-06-05.
#[test]
fn test_parse_frontmatter_blank_lines_between_delimiter_and_first_key() {
let content =
"---\n\n\n\n\ntitle: Test\ndate: 2026-04-07\nis_newsletter: true\n---\n\nBody.\n";
let (fm, body) = parse_frontmatter(content).unwrap();
assert_eq!(fm.title, "Test");
assert!(body.contains("Body."));
}

#[test]
fn test_parse_frontmatter_missing_opening_delimiter() {
let content = r"title: Test
Expand Down
Loading