Announce new posts on deploy instead of on merge - #117
Conversation
Deploys are manual now, so a push to main no longer means the post is live. notify-new-post.yml was mailing subscribers a link to a page that would not exist until someone ran `just deploy live`. Move the notification into that deploy. There is no push event to diff against, so both sides of the diff are RSS feeds: snapshot the live moq.dev/rss.xml before wrangler uploads, then mail every post in the freshly built dist/rss.xml that was not in the snapshot. Reading the build rather than src/pages/blog means we announce what actually shipped, with the title and description straight from the feed. Sending is unrecallable, so every uncertain case skips instead of guessing: unreachable feed, empty feed, missing snapshot, stale snapshot, or missing build. Staging never announces. Requires RESEND_API_KEY and RESEND_SEGMENT_ID in the shell, since .env.live is committed and cannot hold secrets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnCscaFDaj6TaBahqxXGqb
There was a problem hiding this comment.
Sorry @kixelated, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 582bd6f371
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Consume it either way: a snapshot must never outlive the deploy that took it. | ||
| rmSync(SNAPSHOT, { force: true }); |
There was a problem hiding this comment.
Preserve notification state until all broadcasts succeed
If a Resend request times out, returns a non-2xx response, or credentials are missing, this deletes the only record of which posts require announcements before the failure occurs. A second send has no snapshot, while rerunning the deployment snapshots the already-updated live feed and detects no new posts; a failure midway through several posts similarly strands the unsent remainder. Retain or checkpoint the snapshot until every required broadcast has been sent successfully.
Useful? React with 👍 / 👎.
|
|
||
| **`just deploy live` mails the subscriber list.** `scripts/notify-subscribers.ts` snapshots the slugs in `https://moq.dev/rss.xml` before the upload, then sends a Resend broadcast for every post in the freshly built `dist/rss.xml` that wasn't in that snapshot. Subject and body come from the feed's `title` and `description`. A deploy that adds no posts sends nothing. | ||
|
|
||
| This needs `RESEND_API_KEY` and `RESEND_SEGMENT_ID` in the shell. They are secrets, so they cannot live in the committed `.env.live`; use `.dev.vars` or your shell profile. Without them the deploy still succeeds and the script exits non-zero to say the announcement did not go out. |
There was a problem hiding this comment.
Explicitly load the documented
.dev.vars file
When a deployer follows this instruction and stores the Resend values in .dev.vars, bun scripts/notify-subscribers.ts does not load that Wrangler-specific file: the script reads only process.env, and the recipe supplies no --env-file=.dev.vars option (bun --help exposes --env-file=<val> for loading nonstandard files). The production upload therefore succeeds before requireEnv throws, so the promised announcement is missed; either load the file explicitly or require the variables to be exported into the shell.
Useful? React with 👍 / 👎.
| const published = new Set(slugs); | ||
| const added = parseFeed(readFileSync(BUILT_FEED, "utf8")).filter((p) => !published.has(p.slug)); |
There was a problem hiding this comment.
Track prior announcements independently of the live feed
When a live release temporarily omits a previously announced post—for example during a production rollback or accidental removal—the snapshot loses that slug even though subscribers have already received it. Restoring the normal release then makes this filter classify the post as new and sends a duplicate broadcast. The current feed is not a durable notification ledger, so the comparison needs persistent announcement history or another way to distinguish restored posts.
Useful? React with 👍 / 👎.
Deploying had to be preceded by exporting RESEND_API_KEY, which meant a live API key sitting in a shell profile or .dev.vars. Reference it from 1Password instead and let `op run` supply it for the length of the command. op.env is committed deliberately: it maps the variable to an op:// URI and carries the (non-secret) segment id, so it holds pointers rather than values. Staging never announces, so it does not depend on 1Password at all, and a missing CLI falls back to the ambient environment rather than failing a deploy that has already gone out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnCscaFDaj6TaBahqxXGqb
The snapshot was deleted the moment it was read, before any Resend call. A failed send therefore destroyed the only record that a post still needed announcing: rerunning `send` found no snapshot, and rerunning the deploy re-snapshotted the already-updated live feed, so the post looked like it had always been there. It could never be announced. Treat the snapshot as the run's to-do list instead. It survives a missing build, a missing credential, and a failed broadcast, and is removed only once there is nothing left to send. Each successful broadcast checkpoints it so a retry mails only the remainder, carrying the original timestamp so checkpointing cannot extend the staleness window. The snapshot phase now resumes an unfinished snapshot rather than overwriting it, since the current feed already contains the posts it still owes. Staging leaves it alone entirely. Reading it is also total now: unparseable and expired files both read as absent rather than throwing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnCscaFDaj6TaBahqxXGqb
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e172d3f7b6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| const published = new Set(snap.slugs); | ||
| const added = parseFeed(readFileSync(BUILT_FEED, "utf8")).filter((p) => !published.has(p.slug)); |
There was a problem hiding this comment.
Include nested blog posts in the feed comparison
When a post is placed in a nested route such as src/pages/blog/topic/post.mdx, Astro publishes the page and the previous workflow's src/pages/blog/**/*.mdx path detected it, but src/pages/rss.xml.js uses the non-recursive glob ./blog/*.{md,mdx}. Consequently the post is absent from dist/rss.xml, so this filter sees no addition and subscribers are never notified, despite the new deployment documentation promising that any .mdx under src/pages/blog/ is announced.
Useful? React with 👍 / 👎.
Summary
mainno longer means a post is live.notify-new-post.ymlwas mailing subscribers a link to a page that would not exist until someone ranjust deploy live. Deleted it.just deploy livenow does the announcing, in two phases around the wrangler upload..dev.vars.How it decides what's new
There is no push event to diff against, so both sides of the diff are RSS feeds:
snapshot(before upload) records the slugs in the livehttps://moq.dev/rss.xml.send(after upload) mails every post in the freshly builtdist/rss.xmlthat was not in that snapshot.Reading
dist/rss.xmlrather thansrc/pages/blog/means we announce what actually shipped, and the subject and body reuse the exacttitleanddescriptionthe feed carries. A deploy that adds no posts sends nothing, so routine deploys are silent.Credentials
op.envmapsRESEND_API_KEYtoop://Corp/Resend/credential, andop runresolves it for the length of the command. The file is committed on purpose: it holds a reference and the non-secret segment id, never a value.Degrades rather than breaks.
just deploy stagingnever announces and never touches 1Password. If theopCLI is absent the recipe warns and falls back toRESEND_API_KEY/RESEND_SEGMENT_IDfrom the environment, and if those are missing too the deploy still succeeds while the script exits non-zero to report that the announcement did not go out.Failing closed
A broadcast cannot be recalled, so every uncertain case skips sending rather than guessing:
dist/rss.xmlmissingThe snapshot is consumed on read, so it can never outlive the deploy that took it. If
wrangler deployfails,juststops beforesendruns.Test plan
Exercised every branch locally. No broadcast was ever created: the one credential-bearing run used a deliberately invalid key, which Resend rejects at the create call.
Detection and safety:
snapshot --env staging- skips, clears any stale snapshotsend --env staging- silent, exit 0send --env livewith no snapshot - skips, exit 0snapshot --env live- read the real feed, 23 posts already livesend --env liveagainst a build containing one unpublished post - detected exactlyupdate-01, loggedCreating broadcast for "Update 01" -> https://moq.dev/blog/update-01/requireEnvbefore any network call, exit 1sendafter a send - snapshot consumed, skips1Password wiring:
op run --env-file=op.envresolves both variables (36-char key, correct segment id)just _announce staging- exits 0 without invokingopjust _announce livewith a current snapshot - ran underop run, reported "no new posts", sent nothingjust _announce livewithopremoved fromPATH- warned about the missing CLI and fell back to the ambient environmentbun run checkandjust buildclean.Not run:
just deployitself, since that would deploy the site.Note
Any local
.mdxundersrc/pages/blog/ships on the nextjust deploy liveand gets announced, drafts included.(written by Opus 5)
🤖 Generated with Claude Code
https://claude.ai/code/session_01QnCscaFDaj6TaBahqxXGqb