Auto-publish scheduled blog posts on their date - #622
Conversation
Adds an hourly workflow that releases blog posts marked draft = true once their frontmatter date has passed, by removing the draft line and committing to main. This lets maintainers merge posts ahead of time without them going live immediately. Uses Zola's native draft support, so pending posts are excluded from the build, blog listing, sitemap, and RSS feed until released. The workflow pushes with the valkeyrie-bot app token so the existing deploy workflow triggers on the resulting commit. This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
📝 WalkthroughWalkthroughThe pull request adds a script that publishes due blog drafts, a scheduled and manually dispatched workflow that runs and commits releases, and contributor documentation for date-based scheduling and draft publication. ChangesScheduled Blog Publication
Possibly related issues
Suggested reviewers: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/publish-scheduled-blogs.yml:
- Line 49: Update the publishing step around PUBLISHED to stage the content/blog
directory directly instead of piping published paths through default xargs and
git add. Preserve the existing clean-checkout assumptions and ensure filenames
containing spaces are handled safely.
- Around line 25-30: Update the Generate token step using
actions/create-github-app-token to explicitly request only the contents
permission, preserving the existing app-id and private-key inputs so the token
remains limited to checkout and pushing repository files.
In `@build/publish-scheduled-blogs.py`:
- Around line 42-49: Update strip_draft to preserve the input frontmatter’s
original line-ending style when removing the matched top-level draft line. Avoid
relying on read_text() and "\n".join() normalization in the surrounding release
flow, including the code paths around lines 64–97, so CRLF-authored posts retain
CRLF endings and unchanged content is not rewritten.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c352e5cf-7240-4fae-8362-1ef25a390d84
📒 Files selected for processing (3)
.github/workflows/publish-scheduled-blogs.ymlCONTRIBUTING-BLOG-POST.mdbuild/publish-scheduled-blogs.py
- Scope the app token with permission-contents: write, since the workflow permissions block only constrains GITHUB_TOKEN. - Stage content/blog directly instead of piping paths through xargs, which splits on whitespace and would break on filenames containing spaces. - Read and write with newline="" so CRLF-authored posts keep their line endings and produce a one-line diff. content/blog already contains one CRLF file, which previously would have been rewritten whole. Also strip the resulting lone trailing carriage return before parsing, which tomllib rejects. This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
stockholmux
left a comment
There was a problem hiding this comment.
I'm willing to give this a try but I think this is going to be a foot-gun for unintended publishing.
As for the code itself, I'd rather not see regular expression parsing on toml but rather the proper parsing library you're already using.
Finally, I think we have some process issues: we shouldn't merge work in progress and we need to make sure the blog process is very, very explicit about UTC posting times.
Parse frontmatter entirely with tomllib. The regex module is no longer imported: the fences are found by string search, and each candidate line is validated by tomllib itself, which is what authoritatively distinguishes the real top-level 'draft = true' from one in a table, a comment, or a string. Quoted date values are re-parsed as bare TOML instead of a strptime loop, so both forms follow identical rules. This also fixes two files the regex mishandled: a post with a leading blank line before the fence, and one with a multi-line array in its frontmatter. Publish once a day at 16:00 UTC (08:00 PST / 09:00 PDT) instead of hourly. Posts are conventionally dated 00:00:00 or 01:01:01, so an hourly job would have published them in the middle of the night PT. Document that dates are UTC and that the time of day in 'date' does not control publication, and drop the suggestion that work-in-progress can be parked on main. This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
The publish job ignores the time of day, so a bare date is the clearer convention. 16 posts already use one. This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@build/publish-scheduled-blogs.py`:
- Around line 117-125: Update the date comparison in the scheduled publication
flow around publish_at and now to normalize both aware datetimes to UTC, then
compare their date() values rather than full timestamps. Preserve the existing
future-date hold behavior while allowing posts dated today to publish regardless
of their publication time.
In `@CONTRIBUTING-BLOG-POST.md`:
- Around line 74-77: Clarify the date guidance in the `date` field section so it
uses one consistent rule: identify whether contributors should enter the
intended publication date or maintainers assign the final publication date, and
align the wording with the scheduling behavior that publishes drafts when `date`
arrives.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 74b47526-97ec-4594-a529-e6274cacc0d2
📒 Files selected for processing (3)
.github/workflows/publish-scheduled-blogs.ymlCONTRIBUTING-BLOG-POST.mdbuild/publish-scheduled-blogs.py
The line-scan approach had a real bug: a 'draft = true' inside a multi-line string was removed instead of the actual key, corrupting the value and leaving the post a draft. 8 existing posts use multi-line frontmatter values, so this was reachable. Now a candidate line is removed and the result re-parsed, accepting the edit only if it drops 'draft' and changes nothing else. Invalid TOML, a missing date, and an unparseable date now exit non-zero instead of warning and skipping. 'zola build' already fails on bad TOML, so there is no reason to accept it quietly here. This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
tomlkit is a round-trip TOML parser, so 'del frontmatter["draft"]' replaces the hand-rolled line scanning and the remove-then-re-parse verification. Reading the key directly only sees the top-level table, so the table-header scan is gone too, along with the multi-line-value corruption it was working around. Verified tomlkit round-trips all 59 existing posts byte-identically, and preserves comments and CRLF endings. Adds a pip install step; tomlkit is MIT with no dependencies. This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
|
@stockholmux Aight. Tried to make less of a foot gun. Always publishes at 15:00 UTC on the defined date, so we can schedule stuff out and not have to worry about individual availability. Rewrote a chunk of TOML parsing to use more of another dependency, to do less stuff manual. |
This was generated by AI but verified, with love, by a human. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
A post dated 2026-07-31 23:00:00 was held until the next day even though the docs say the time of day is ignored. Compare date() values instead, converting offset-aware datetimes to UTC first. Also align the frontmatter comment with the scheduling section: date is the publish date, not the day writing started. Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
build/publish-scheduled-blogs.py (1)
32-38: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMatch only a complete closing frontmatter fence.
text.find(FENCE, start)matches+++anywhere in TOML content. A valid value such astitle = "C+++ guide"makesendpoint into the string.tomlkit.parsethen receives truncated TOML and exits the release workflow.Match the closing
+++only when it is a complete delimiter line. Support both LF and CRLF line endings.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@build/publish-scheduled-blogs.py` around lines 32 - 38, Update the frontmatter parsing logic around the closing-fence lookup in the relevant parser function so `+++` is recognized only when it forms a complete delimiter line, not when embedded in TOML values. Match valid closing fences with both LF and CRLF line endings, while preserving the existing `None` behavior when no complete closing fence is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@build/publish-scheduled-blogs.py`:
- Around line 32-38: Update the frontmatter parsing logic around the
closing-fence lookup in the relevant parser function so `+++` is recognized only
when it forms a complete delimiter line, not when embedded in TOML values. Match
valid closing fences with both LF and CRLF line endings, while preserving the
existing `None` behavior when no complete closing fence is found.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c8befeab-8b97-475d-9df9-2bf33ce527af
📒 Files selected for processing (3)
.github/workflows/publish-scheduled-blogs.ymlCONTRIBUTING-BLOG-POST.mdbuild/publish-scheduled-blogs.py
🚧 Files skipped from review as they are similar to previous changes (2)
- CONTRIBUTING-BLOG-POST.md
- .github/workflows/publish-scheduled-blogs.yml
What
Adds an hourly GitHub Actions workflow that publishes blog posts once their date has arrived, so maintainers can merge a post ahead of time without it going live immediately.
How
draft = truein a post's frontmatter and setdateto the intended publish date/time (UTC)..github/workflows/publish-scheduled-blogs.ymlruns hourly.build/publish-scheduled-blogs.pyscanscontent/blog, and for any draft whosedatehas passed it removes thedraftline and commits tomain, which triggers the existing deploy.This uses Zola's native
draftsupport (not a custom field), so a pending post is excluded from the build, the blog listing, the sitemap, and the RSS feed until it is released.zola serve --draftsstill previews it locally.Why the app token
The commit is pushed with the
valkeyrie-botapp token rather thanGITHUB_TOKEN. Pushes made withGITHUB_TOKENdo not trigger other workflows, which would commit the post without ever rebuilding the site.Notes / things to check before merge
secrets.VALKEYRIE_BOT_*usable from ascheduletrigger and the app to havecontents: writeon this repo. Recommend a first run via Run workflow withdry_runticked to confirm.mainis currently unprotected, so the bot can push directly. If branch protection is added later, this will need a bypass or a PR-based flow.dateare never auto-published, so work-in-progress can safely sit onmain.Verified
date =/date=, with/without times), nestedindex.mdposts,draftunder a[extra]table (correctly ignored), missing dates, and idempotency.zola servewith and without--draftsconfirmed the live vs. preview behavior.This was generated by AI but verified, with love, by a human.