From e0b155ade1dbe71bbd409037b6524548e3eaefa5 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 10:35:58 -0400 Subject: [PATCH 1/3] Only validate feed fields the publish script uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flightcast now emits a second enclosure per episode (the cover image) with no type attribute, which the copied-from-rss.ts schema required — so every publish run died in FeedSchema parse. Trim the schema to the fields the script reads (title, published, description, content_encoded, itunes_episodeType) so irrelevant feed changes can't break publishing. Co-Authored-By: Claude Fable 5 --- scripts/publish-episodes.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts index e2a1126..2dfa870 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-episodes.ts @@ -62,24 +62,17 @@ async function waitForPage(url: string) { } } +// Only validate the fields this script actually uses — requiring unused +// fields makes publishing break whenever the feed host changes something +// irrelevant (e.g. Flightcast's image enclosures have no `type`). const FeedSchema = object({ items: array( object({ - id: string(), title: string(), published: number(), description: string(), content_encoded: optional(string()), - itunes_duration: number(), - itunes_episode: optional(number()), - itunes_episodeType: optional(string()), - itunes_image: optional(object({ href: optional(string()) })), - enclosures: array( - object({ - url: string(), - type: string() - }) - ) + itunes_episodeType: optional(string()) }) ) }); From 5db61a65e197e5510f16261f8b0d798f4632c7ba Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 10:43:59 -0400 Subject: [PATCH 2/3] Rename publish script to publish-atproto-episodes; rebuild once per feed change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script only publishes standard.site documents to ATProto — episode pages come from the Astro build — so name it accordingly (pnpm scripts are now publish:atproto / publish:atproto:backfill). The gate also now records which feed content it already triggered a Vercel rebuild for, separately from the publish-success hash. A persistently failing publish (like the recent FeedSchema crash) keeps retrying every poll but no longer fires a fresh build each time; the publish script still waits on the pages from the one build it did trigger. Co-Authored-By: Claude Fable 5 --- .github/workflows/backfill-episodes.yml | 2 +- .github/workflows/publish-episodes.yml | 45 ++++++++++++++++--- README.md | 4 +- package.json | 4 +- ...pisodes.ts => publish-atproto-episodes.ts} | 6 +-- 5 files changed, 47 insertions(+), 14 deletions(-) rename scripts/{publish-episodes.ts => publish-atproto-episodes.ts} (97%) diff --git a/.github/workflows/backfill-episodes.yml b/.github/workflows/backfill-episodes.yml index e3486de..1504488 100644 --- a/.github/workflows/backfill-episodes.yml +++ b/.github/workflows/backfill-episodes.yml @@ -57,7 +57,7 @@ jobs: - name: Install dependencies run: pnpm install - name: Backfill all episodes - run: pnpm tsx scripts/publish-episodes.ts --backfill + run: pnpm tsx scripts/publish-atproto-episodes.ts --backfill env: ATPROTO_HANDLE: ${{ secrets.ATPROTO_HANDLE }} ATPROTO_APP_PASSWORD: ${{ secrets.ATPROTO_APP_PASSWORD }} diff --git a/.github/workflows/publish-episodes.yml b/.github/workflows/publish-episodes.yml index e0deea6..cbafa48 100644 --- a/.github/workflows/publish-episodes.yml +++ b/.github/workflows/publish-episodes.yml @@ -79,20 +79,53 @@ jobs: # the site build runs while the publish job is installing dependencies. # The publish script then waits for the new episode pages to be live # before publishing documents that link to them. + # + # The rebuild-hash cache tracks which feed content we already rebuilt + # for. The publish-success hash (.feed-hash) is intentionally separate: + # a persistently failing publish must keep retrying, but must not + # trigger a fresh Vercel build on every 30-minute poll. + - name: Restore last rebuild trigger hash + if: steps.config.outputs.configured == 'true' + uses: actions/cache/restore@v4 + with: + path: .rebuild-hash + key: rebuild-hash- + restore-keys: | + rebuild-hash- - name: Trigger site rebuild if: steps.config.outputs.configured == 'true' && steps.feed.outputs.changed == 'true' id: rebuild env: REBUILD_WEBHOOK: ${{ secrets.REBUILD_WEBHOOK }} + FEED_HASH: ${{ steps.feed.outputs.hash }} run: | - if [ -n "$REBUILD_WEBHOOK" ]; then - curl -sf -X POST "$REBUILD_WEBHOOK" > /dev/null - echo "triggered=true" >> "$GITHUB_OUTPUT" - echo "Triggered site rebuild." - else + if [ -z "$REBUILD_WEBHOOK" ]; then echo "triggered=false" >> "$GITHUB_OUTPUT" + echo "fired=false" >> "$GITHUB_OUTPUT" echo "::notice::REBUILD_WEBHOOK not set — publishing without waiting for a site rebuild." + exit 0 + fi + if [ -n "$FEED_HASH" ] && [ "$FEED_HASH" = "$(cat .rebuild-hash 2>/dev/null || true)" ]; then + # triggered=true so the publish script still waits for the pages + # from that earlier build before publishing. + echo "triggered=true" >> "$GITHUB_OUTPUT" + echo "fired=false" >> "$GITHUB_OUTPUT" + echo "Site already rebuilt for this feed content — not triggering again." + exit 0 fi + curl -sf -X POST "$REBUILD_WEBHOOK" > /dev/null + echo "triggered=true" >> "$GITHUB_OUTPUT" + echo "fired=true" >> "$GITHUB_OUTPUT" + echo "Triggered site rebuild." + - name: Record rebuild trigger hash + if: steps.rebuild.outputs.fired == 'true' && steps.feed.outputs.hash != '' + run: printf '%s' "${{ steps.feed.outputs.hash }}" > .rebuild-hash + - name: Save rebuild trigger hash + if: steps.rebuild.outputs.fired == 'true' && steps.feed.outputs.hash != '' + uses: actions/cache/save@v4 + with: + path: .rebuild-hash + key: rebuild-hash-${{ steps.feed.outputs.hash }}-${{ github.run_id }}-${{ github.run_attempt }} publish: needs: gate @@ -119,7 +152,7 @@ jobs: - name: Install dependencies run: pnpm install - name: Publish new episodes - run: pnpm tsx scripts/publish-episodes.ts + run: pnpm tsx scripts/publish-atproto-episodes.ts env: ATPROTO_HANDLE: ${{ secrets.ATPROTO_HANDLE }} ATPROTO_APP_PASSWORD: ${{ secrets.ATPROTO_APP_PASSWORD }} diff --git a/README.md b/README.md index 4c9416d..8c51e0d 100644 --- a/README.md +++ b/README.md @@ -179,10 +179,10 @@ You can also publish locally: ```bash # Publish only new episodes -pnpm publish:episodes +pnpm publish:atproto # Backfill all episodes -pnpm publish:episodes:backfill +pnpm publish:atproto:backfill ``` ##### Verification diff --git a/package.json b/package.json index 948d2c8..4e9de4f 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "lint:fix": "eslint . --fix", "preview": "astro preview", "start": "astro dev", - "publish:episodes": "tsx scripts/publish-episodes.ts", - "publish:episodes:backfill": "tsx scripts/publish-episodes.ts --backfill", + "publish:atproto": "tsx scripts/publish-atproto-episodes.ts", + "publish:atproto:backfill": "tsx scripts/publish-atproto-episodes.ts --backfill", "test": "concurrently \"pnpm:test:*(!fix)\" --names \"test:\"", "test:e2e": "pnpm exec playwright test", "test:unit": "vitest" diff --git a/scripts/publish-episodes.ts b/scripts/publish-atproto-episodes.ts similarity index 97% rename from scripts/publish-episodes.ts rename to scripts/publish-atproto-episodes.ts index 2dfa870..c6f47e3 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-atproto-episodes.ts @@ -12,8 +12,8 @@ * STANDARD_SITE_PUBLICATION_RKEY - The publication record key * * Usage: - * pnpm publish:episodes # publish new episodes only - * pnpm publish:episodes:backfill # publish all episodes (backfill) + * pnpm publish:atproto # publish new episodes only + * pnpm publish:atproto:backfill # publish all episodes (backfill) */ import { htmlToText } from 'html-to-text'; @@ -26,7 +26,7 @@ import { } from '@bryanguffey/astro-standard-site'; import starpodConfig from '../starpod.config'; -import { dasherize } from '../src/utils/dasherize'; +import { dasherize } from 'starpod/src/utils/dasherize'; const BACKFILL = process.argv.includes('--backfill'); // Set by the GitHub workflow when it has just triggered a site rebuild: From 0cb103e214fdae3c51ed7e281f520222dd2d4fd3 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 10:48:48 -0400 Subject: [PATCH 3/3] Restore relative dasherize import An in-progress workspace import path ('starpod/src/utils/dasherize') was accidentally captured during the rename; main still has src/ at the repo root, so astro check failed with ts(2307). Co-Authored-By: Claude Fable 5 --- scripts/publish-atproto-episodes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish-atproto-episodes.ts b/scripts/publish-atproto-episodes.ts index c6f47e3..4010972 100644 --- a/scripts/publish-atproto-episodes.ts +++ b/scripts/publish-atproto-episodes.ts @@ -26,7 +26,7 @@ import { } from '@bryanguffey/astro-standard-site'; import starpodConfig from '../starpod.config'; -import { dasherize } from 'starpod/src/utils/dasherize'; +import { dasherize } from '../src/utils/dasherize'; const BACKFILL = process.argv.includes('--backfill'); // Set by the GitHub workflow when it has just triggered a site rebuild: