diff --git a/publish_episode.py b/publish_episode.py index 15fd1f5..951b17a 100755 --- a/publish_episode.py +++ b/publish_episode.py @@ -974,6 +974,31 @@ def sync_episode_media_to_bunny(episode_id: int, already_uploaded: set): Path(tmp_path).unlink(missing_ok=True) +def regenerate_website_pages() -> bool: + """Rebuild the static episode pages and sitemap after a publish. + + Never fatal. By the time this runs the audio is already live on Castopod + and the RSS feed has been rebuilt, so a generator failure must not abort + the publish — it just means the new episode's page lands on the next run. + """ + script = Path(__file__).parent / "generate_episode_pages.py" + try: + result = subprocess.run( + [sys.executable, str(script), "--sitemap"], + capture_output=True, text=True, timeout=300, + ) + except (subprocess.TimeoutExpired, OSError) as e: + print(f" Warning: could not run episode page generation: {e}") + return False + + if result.returncode != 0: + print(f" Warning: episode page generation failed: {result.stderr[-300:]}") + return False + + print(" Episode pages and sitemap regenerated") + return True + + def generate_social_image(episode_number: int, description: str, output_path: str) -> str: """Generate a social media image with cover art, episode number, and description.""" from PIL import Image, ImageDraw, ImageFont @@ -1817,7 +1842,9 @@ def main(): shutil.copy2(str(transcript_path), str(website_transcript_path)) print(f" Transcript copied to website/transcripts/") - # Sitemap is regenerated wholesale by generate_episode_pages.py --sitemap + # Build this episode's static page and regenerate the sitemap wholesale. + # generate_episode_pages.py is the only writer of sitemap.xml. + regenerate_website_pages() # Sync any remaining episode media to BunnyCDN (cover art, etc.) print(" Syncing remaining episode media to CDN...") diff --git a/tests/test_publish_regenerates_pages.py b/tests/test_publish_regenerates_pages.py new file mode 100644 index 0000000..41da4b3 --- /dev/null +++ b/tests/test_publish_regenerates_pages.py @@ -0,0 +1,80 @@ +"""Publishing must rebuild the static episode pages and the sitemap. + +Before static pages existed, publish_episode.py appended one entry to +sitemap.xml itself. That appender is gone — generate_episode_pages.py owns the +sitemap now — so the publish has to invoke it, or a newly published episode +would have no page and never reach the sitemap. + +A generator failure must never abort a publish: by the time this runs the audio +is already live on Castopod and the RSS feed has been rebuilt. +""" + +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import publish_episode + + +class _Result: + def __init__(self, returncode=0, stderr=""): + self.returncode = returncode + self.stderr = stderr + self.stdout = "" + + +def test_invokes_the_generator_with_sitemap(monkeypatch): + calls = [] + monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(cmd) or _Result()) + + assert publish_episode.regenerate_website_pages() is True + + assert len(calls) == 1 + cmd = calls[0] + assert "generate_episode_pages.py" in " ".join(cmd) + assert "--sitemap" in cmd + + +def test_uses_the_running_interpreter(monkeypatch): + """Must not shell out to a bare 'python' that may not have the venv.""" + calls = [] + monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(cmd) or _Result()) + + publish_episode.regenerate_website_pages() + + assert calls[0][0] == sys.executable + + +def test_generator_failure_is_not_fatal(monkeypatch): + monkeypatch.setattr(subprocess, "run", + lambda cmd, **kw: _Result(returncode=1, stderr="boom")) + + assert publish_episode.regenerate_website_pages() is False + + +def test_generator_timeout_is_not_fatal(monkeypatch): + def _boom(cmd, **kw): + raise subprocess.TimeoutExpired(cmd, 300) + + monkeypatch.setattr(subprocess, "run", _boom) + + assert publish_episode.regenerate_website_pages() is False + + +def test_missing_generator_is_not_fatal(monkeypatch): + def _boom(cmd, **kw): + raise OSError("no such file") + + monkeypatch.setattr(subprocess, "run", _boom) + + assert publish_episode.regenerate_website_pages() is False + + +def test_publish_flow_calls_it_after_copying_the_transcript(): + """Guards the wiring, not just the helper.""" + source = Path(publish_episode.__file__).read_text() + copy_at = source.index("Transcript copied to website/transcripts/") + call_at = source.index("regenerate_website_pages()", copy_at) + assert call_at > copy_at