From 166657ea57808232179d9d585c3eca198332b40a Mon Sep 17 00:00:00 2001 From: rederik76 <127075662+rederik76@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:59:21 +1000 Subject: [PATCH] fix(docs): resolve main branch ref on tag-triggered CI checkouts Versioned docs builds failed when GitHub Pages workflow ran on a tag push because the checkout is detached HEAD with origin/main but no local main. --- docs/scripts/build_versioned_docs.py | 37 +++++++++++++++++++++++----- tests/test_select_versions.py | 29 +++++++++++++++++++++- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/scripts/build_versioned_docs.py b/docs/scripts/build_versioned_docs.py index 2eaa43e..55939b4 100755 --- a/docs/scripts/build_versioned_docs.py +++ b/docs/scripts/build_versioned_docs.py @@ -50,6 +50,27 @@ def _run_capture(command: list[str], *, cwd: Path | None = None) -> str: return result.stdout.strip() +def _resolve_git_ref(repo_root: Path, ref: str) -> str: + """Return a git ref that exists in the local clone. + + Tag-triggered CI checkouts are often detached HEAD with ``origin/main`` but + no local ``main`` branch. Branch-like refs try ``ref`` then ``origin/ref``. + """ + candidates = [ref] + if not ref.startswith("refs/") and "/" not in ref: + candidates.append(f"origin/{ref}") + for candidate in candidates: + probe = subprocess.run( + ["git", "rev-parse", "--verify", f"{candidate}^{{commit}}"], + cwd=repo_root, + capture_output=True, + text=True, + ) + if probe.returncode == 0: + return candidate + return ref + + def _repo_root(docs_dir: Path) -> Path: return docs_dir.parent @@ -123,13 +144,15 @@ def _conf_ref_for(version_name: str) -> str | None: def _release_date_for_ref(repo_root: Path, ref: str) -> str: # ISO-style date (YYYY-MM-DD) for deterministic display in docs headers. - return _run_capture(["git", "log", "-1", "--format=%cs", ref], cwd=repo_root) + resolved = _resolve_git_ref(repo_root, ref) + return _run_capture(["git", "log", "-1", "--format=%cs", resolved], cwd=repo_root) def _version_for_ref(repo_root: Path, ref: str, fallback: str) -> str: # Resolve display version from the VERSION file in the selected ref. + resolved = _resolve_git_ref(repo_root, ref) try: - raw = _run_capture(["git", "show", f"{ref}:VERSION"], cwd=repo_root) + raw = _run_capture(["git", "show", f"{resolved}:VERSION"], cwd=repo_root) value = raw.splitlines()[0].strip().lstrip("v") return value or fallback except Exception: @@ -178,8 +201,9 @@ def _ensure_worktree( path = worktrees_root / name if path.exists(): return path + resolved = _resolve_git_ref(repo_root, ref) try: - _run(["git", "worktree", "add", "--detach", str(path), ref], cwd=repo_root) + _run(["git", "worktree", "add", "--detach", str(path), resolved], cwd=repo_root) except subprocess.CalledProcessError: # Recover from a previous run that deleted the dir without unregistering. subprocess.run( @@ -189,7 +213,7 @@ def _ensure_worktree( capture_output=True, ) _run( - ["git", "worktree", "add", "--force", "--detach", str(path), ref], + ["git", "worktree", "add", "--force", "--detach", str(path), resolved], cwd=repo_root, ) return path @@ -319,7 +343,8 @@ def main() -> None: worktrees_root.mkdir(parents=True, exist_ok=True) tags = _selected_tags(repo_root) - versions: list[dict[str, str]] = [{"name": "current", "ref": "main"}] + main_ref = _resolve_git_ref(repo_root, "main") + versions: list[dict[str, str]] = [{"name": "current", "ref": main_ref}] versions.extend({"name": t, "ref": t} for t in tags) if args.preview: preview_branch = _current_branch(repo_root) @@ -415,7 +440,7 @@ def main() -> None: (output_root / "index.html").write_text(index_html, encoding="utf-8") # Basic crawl artifacts for search engines. - sitemap_entries: list[tuple[str, str]] = [("", _release_date_for_ref(repo_root, "main"))] + sitemap_entries: list[tuple[str, str]] = [("", _release_date_for_ref(repo_root, main_ref))] for item in links: sitemap_entries.append((f"{item['name']}/index.html", item.get("release_date", ""))) diff --git a/tests/test_select_versions.py b/tests/test_select_versions.py index e8d940a..3bde40e 100644 --- a/tests/test_select_versions.py +++ b/tests/test_select_versions.py @@ -2,12 +2,13 @@ from __future__ import annotations +import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "docs" / "scripts")) -from build_versioned_docs import _superset_versions +from build_versioned_docs import _resolve_git_ref, _superset_versions from select_versions import MIN_MINOR_MAJOR_0, select_versions @@ -15,6 +16,32 @@ def _tags(*versions: str) -> list[str]: return [f"v{version}" if not version.startswith("v") else version for version in versions] +def test_resolve_git_ref_uses_origin_main_when_local_main_missing(tmp_path): + origin = tmp_path / "origin.git" + work = tmp_path / "work" + work.mkdir() + subprocess.run(["git", "init", "--bare", str(origin)], check=True, capture_output=True) + subprocess.run(["git", "init"], cwd=work, check=True, capture_output=True) + subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=work, check=True) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=work, check=True) + (work / "VERSION").write_text("0.21.0\n", encoding="utf-8") + subprocess.run(["git", "add", "VERSION"], cwd=work, check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "init"], cwd=work, check=True, capture_output=True) + subprocess.run(["git", "branch", "-M", "main"], cwd=work, check=True, capture_output=True) + subprocess.run( + ["git", "remote", "add", "origin", str(origin)], + cwd=work, + check=True, + capture_output=True, + ) + subprocess.run(["git", "push", "-u", "origin", "main"], cwd=work, check=True, capture_output=True) + subprocess.run(["git", "checkout", "--detach", "HEAD"], cwd=work, check=True, capture_output=True) + subprocess.run(["git", "branch", "-D", "main"], cwd=work, check=True, capture_output=True) + + assert _resolve_git_ref(work, "main") == "origin/main" + assert _resolve_git_ref(work, "v9.9.9") == "v9.9.9" + + def test_major_0_includes_minors_back_to_0_12_latest_patch_only(): tags = _tags( "0.16.0",