Split the public banner URL from the runner-internal clone URL
tests / pytest (push) Successful in 7s

Every auto-published page, in all three pipelines, carried a source link to
`localhost:3005`. The template was never at fault — it links `{{{source}}}`
verbatim; the bad URL was baked into each page's wikitext by the publisher.

Root cause was one variable serving two incompatible purposes. `URL_TO_GITEA`
has to be an address the runner container can clone from, and since the runner
uses host networking that is `http://localhost:3005`. All three workflows then
composed `GITEA_REPO_URL` — the base of the public banner link, written into
the wiki for anyone to read — out of that same loopback value.

Adds a second org variable, `PUBLIC_URL_TO_GITEA`, for the public base
(`https://git.novoyuuparosk.org`), and points the banner at it. Cloning keeps
using `URL_TO_GITEA`. The two are now documented as deliberately separate,
because collapsing them looks like tidying up.

`lib/wiki.public_source_base()` validates the banner base rather than trusting
it: unset, relative, loopback, mDNS (`*.local`) or private-range values raise
with the variable to set. A misconfiguration that would publish an unreachable
link now stops the run instead. Note the failure mode this creates on purpose:
until `PUBLIC_URL_TO_GITEA` exists, publishing aborts rather than proceeding
with a wrong URL.

Also builds the source path with `rel.as_posix()`, so the separator in a URL
does not depend on the OS the publisher runs on — it produced backslashes when
the renderer was exercised on Windows.

Prerequisites before this can publish:

1. Add the org variable `PUBLIC_URL_TO_GITEA` = https://git.novoyuuparosk.org
2. Flush the act_runner callee cache — `docker compose up -d --force-recreate
   act_runner`. Per the cross-cutting decisions log, edits to `publish-*.yml`
   keep executing the stale cached copy, and this change is in those files.

Pages already published still carry the old URL in their stored wikitext. They
are only corrected by re-publishing each pipeline in full.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 16:37:06 +09:00
parent 162c053377
commit e1810facd6
12 changed files with 166 additions and 18 deletions
+70
View File
@@ -15,6 +15,7 @@ from lib.wiki import (
expand_columns,
extract_md_link_targets,
markdown_to_wikitext,
public_source_base,
resolve_md_links,
strip_first_h1,
)
@@ -214,3 +215,72 @@ class TestPandocContract:
wikitext = markdown_to_wikitext("```columns\n**A**\nl1\n===\n**B**\nl2\n```")
assert wikitext.count("<poem>") == 2
assert "'''A'''" in wikitext
class TestPublicSourceBase:
"""The banner URL is read by the public, so a runner-only address must fail.
Every one of these rejections was a page published with a link to
localhost:3005, which is what the whole guard exists to prevent.
"""
def test_accepts_a_public_https_url(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "https://git.novoyuuparosk.org/org/repo")
assert public_source_base() == "https://git.novoyuuparosk.org/org/repo"
def test_accepts_a_public_host_with_a_port(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "http://git.novoyuuparosk.org:1551/org/repo")
assert public_source_base() == "http://git.novoyuuparosk.org:1551/org/repo"
def test_strips_a_trailing_slash(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "https://git.example.org/org/repo/")
assert public_source_base() == "https://git.example.org/org/repo"
def test_strips_surrounding_whitespace(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", " https://git.example.org/org/repo ")
assert public_source_base() == "https://git.example.org/org/repo"
def test_unset_is_rejected(self, monkeypatch):
monkeypatch.delenv("GITEA_REPO_URL", raising=False)
with pytest.raises(RuntimeError, match="is unset"):
public_source_base()
def test_empty_is_rejected(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "")
with pytest.raises(RuntimeError, match="is unset"):
public_source_base()
def test_a_bare_path_is_rejected(self, monkeypatch):
# What an unset PUBLIC_URL_TO_GITEA leaves the workflow composing.
monkeypatch.setenv("GITEA_REPO_URL", "/novoyuuparosk-wiki/ncmr-songs")
with pytest.raises(RuntimeError, match="not an absolute URL"):
public_source_base()
def test_localhost_is_rejected(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "http://localhost:3005/org/repo")
with pytest.raises(RuntimeError, match="loopback hostname"):
public_source_base()
def test_loopback_ip_is_rejected(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "http://127.0.0.1:3005/org/repo")
with pytest.raises(RuntimeError, match="loopback or private-range"):
public_source_base()
def test_private_range_ip_is_rejected(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "http://192.168.1.20:3005/org/repo")
with pytest.raises(RuntimeError, match="loopback or private-range"):
public_source_base()
def test_mdns_name_is_rejected(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "http://pi5-16.local:3005/org/repo")
with pytest.raises(RuntimeError, match="mDNS"):
public_source_base()
def test_rejection_names_the_variable_to_set(self, monkeypatch):
monkeypatch.setenv("GITEA_REPO_URL", "http://localhost:3005/org/repo")
with pytest.raises(RuntimeError, match="PUBLIC_URL_TO_GITEA"):
public_source_base()
def test_reads_the_named_variable(self, monkeypatch):
monkeypatch.setenv("OTHER_BASE", "https://git.example.org/org/repo")
assert public_source_base("OTHER_BASE") == "https://git.example.org/org/repo"