diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 08886e8..372d215 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -21,21 +21,21 @@ jobs:
with:
persist-credentials: false
- - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
- with:
- python-version: "3.13"
-
- # The generators use Jinja for raw Markdown and PyYAML for structured frontmatter.
- - name: Install linter dependencies
- run: pip install pyyaml==6.0.3 jinja2==3.1.6
+ - name: Install uv
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- name: Script tests
- run: python -m unittest discover -s tests -p "test_*.py"
+ run: uv run --frozen python -m unittest discover -s tests -p "test_*.py"
- name: Lint documentation
- run: python scripts/lint_docs.py --baseline
+ run: uv run --frozen python scripts/lint_docs.py --baseline
+
+ - name: Build documentation
+ env:
+ GA_MEASUREMENT_ID: ""
+ run: uv run --frozen python scripts/build_docs.py
# Advisory: shows the full Phase 0 backlog without affecting the job result.
- name: Backlog (informational)
if: always()
- run: python scripts/lint_docs.py || true
+ run: uv run --frozen python scripts/lint_docs.py || true
diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml
new file mode 100644
index 0000000..05be45c
--- /dev/null
+++ b/.github/workflows/pr-preview.yml
@@ -0,0 +1,170 @@
+name: PR Preview
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+
+permissions:
+ contents: read
+
+jobs:
+ preview:
+ if: >-
+ github.event.repository.private == true &&
+ vars.ENABLE_PR_PREVIEWS == 'true' &&
+ vars.PAGES_PREVIEW_ORIGIN != '' &&
+ github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: ubuntu-latest
+ concurrency:
+ group: "pages"
+ cancel-in-progress: false
+ permissions:
+ contents: write # Update gh-pages-previews branch.
+ pages: write # Deploy GitHub Pages bundle.
+ id-token: write # Mint OIDC token for actions/deploy-pages.
+ pull-requests: write # Post sticky preview link on PR.
+ environment:
+ name: github-pages
+ url: ${{ steps.comment.outputs.preview_url }}
+ steps:
+ - if: github.event.action != 'closed'
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+
+ - if: github.event.action != 'closed'
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+
+ - name: Build preview
+ if: github.event.action != 'closed'
+ env:
+ SITE_URL: "${{ vars.PAGES_PREVIEW_ORIGIN }}/pr-${{ github.event.pull_request.number }}"
+ COLAB_REPO: ${{ github.repository }}
+ COLAB_BRANCH: ${{ github.event.pull_request.head.ref }}
+ run: uv run --frozen python scripts/build_docs.py
+
+ - name: Update preview branch
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ EVENT_ACTION: ${{ github.event.action }}
+ run: |
+ set -euo pipefail
+ [[ "${PR_NUMBER}" =~ ^[1-9][0-9]*$ ]]
+ [[ "${GH_REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]
+
+ OPEN_PRS="$(gh api --paginate "repos/${GH_REPO}/pulls?state=open&per_page=100" --jq '.[].number')"
+ AUTH_B64="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w0)"
+ export GIT_TERMINAL_PROMPT=0
+ export GIT_CONFIG_COUNT=1
+ export GIT_CONFIG_KEY_0="http.https://github.com/.extraheader"
+ export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${AUTH_B64}"
+ REMOTE="https://github.com/${GH_REPO}.git"
+
+ rm -rf _pages
+ if ! git clone --depth=1 --single-branch --branch gh-pages-previews "${REMOTE}" _pages 2>/dev/null; then
+ rm -rf _pages
+ git init _pages
+ git -C _pages remote add origin "${REMOTE}"
+ git -C _pages checkout --orphan gh-pages-previews
+ fi
+
+ for dir in _pages/pr-*; do
+ [[ -d "${dir}" ]] || continue
+ num="${dir#_pages/pr-}"
+ if ! [[ "${num}" =~ ^[1-9][0-9]*$ ]] || [[ "${num}" == "${PR_NUMBER}" ]] || ! grep -Fxq -- "${num}" <<<"${OPEN_PRS}"; then
+ rm -rf -- "${dir}"
+ fi
+ done
+ if [[ "${EVENT_ACTION}" != "closed" ]]; then
+ mv site "_pages/pr-${PR_NUMBER}"
+ fi
+
+ {
+ printf ''
+ printf '
PR PreviewsPR Previews (%s)
' "${GH_REPO}"
+ found=0
+ for dir in _pages/pr-*; do
+ [[ -d "${dir}" ]] || continue
+ num="${dir#_pages/pr-}"
+ [[ "${num}" =~ ^[1-9][0-9]*$ ]] || continue
+ printf '- PR #%s
' "${num}" "${num}"
+ found=1
+ done
+ [[ "${found}" -eq 1 ]] || printf '- None
'
+ printf '
\n'
+ } > _pages/index.html
+ : > _pages/.nojekyll
+ printf 'User-agent: *\nDisallow: /\n' > _pages/robots.txt
+
+ git -C _pages config user.name "github-actions[bot]"
+ git -C _pages config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git -C _pages add -A
+ if [[ -n "$(git -C _pages status --porcelain)" ]]; then
+ git -C _pages commit -m "PR #${PR_NUMBER} (${EVENT_ACTION})"
+ git -C _pages push origin HEAD:refs/heads/gh-pages-previews
+ fi
+ rm -rf _pages/.git
+
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
+ with:
+ path: _pages
+
+ - id: deployment
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
+
+ - name: Update PR comment
+ id: comment
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ EVENT_ACTION: ${{ github.event.action }}
+ PAGE_URL: ${{ steps.deployment.outputs.page_url }}
+ run: |
+ python3 - <<'PY'
+ import json, os, subprocess
+
+ pr_num = int(os.environ["PR_NUMBER"])
+ action = os.environ["EVENT_ACTION"].strip()
+ gh_repo = os.environ["GH_REPO"].strip()
+ base_url = os.environ["PAGE_URL"].strip().rstrip("/")
+ preview_url = f"{base_url}/pr-{pr_num}/"
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
+ fh.write(f"preview_url={preview_url if action != 'closed' else base_url + '/'}\n")
+
+ marker = ""
+ if action == "closed":
+ body = f"{marker}\nPreview `/pr-{pr_num}/` removed (PR closed)."
+ else:
+ sha = os.environ["HEAD_SHA"].strip()[:7]
+ pages_raw = subprocess.check_output(
+ ["gh", "api", "--paginate", "--slurp", f"repos/{gh_repo}/pulls/{pr_num}/files?per_page=100"],
+ text=True,
+ )
+ routes = []
+ for f in (item for p in (json.loads(pages_raw) if pages_raw.strip() else []) for item in p):
+ name = str(f.get("filename", ""))
+ if f.get("status") == "removed" or not name.startswith("docs/") or not name.endswith(".md") or name.startswith("docs/contributing/"):
+ continue
+ rel = name[5:-3]
+ route = "" if rel == "index" else (f"{rel[:-6]}/" if rel.endswith("/index") else f"{rel}/")
+ routes.append(f"- `{name}` → {preview_url}{route}")
+ lines = [marker, f"**PR Preview:** {preview_url} (`{sha}`)"]
+ if routes:
+ lines.extend(["", "**Modified pages:**", *routes[:20]])
+ if len(routes) > 20:
+ lines.append(f"- *…and {len(routes) - 20} more*")
+ body = "\n".join(lines)
+
+ comments_raw = subprocess.check_output(
+ ["gh", "api", "--paginate", "--slurp", f"repos/{gh_repo}/issues/{pr_num}/comments?per_page=100"],
+ text=True,
+ )
+ comments = [c for p in (json.loads(comments_raw) if comments_raw.strip() else []) for c in p]
+ cid = next((c["id"] for c in comments if isinstance(c, dict) and marker in str(c.get("body", ""))), None)
+ endpoint = f"repos/{gh_repo}/issues/comments/{cid}" if cid else f"repos/{gh_repo}/issues/{pr_num}/comments"
+ subprocess.run(["gh", "api", "--method", "PATCH" if cid else "POST", endpoint, "--input", "-"], input=json.dumps({"body": body}), text=True, check=True)
+ PY
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..569a603
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,89 @@
+name: Publish
+
+on:
+ workflow_dispatch:
+ inputs:
+ source_branch:
+ description: "Source branch name"
+ required: true
+ type: string
+ commit_message:
+ description: "Commit message"
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+concurrency:
+ group: publish
+ cancel-in-progress: false
+
+jobs:
+ publish:
+ if: github.event.repository.private == true
+ runs-on: ubuntu-latest
+ environment: publish
+ steps:
+ - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
+ id: app-token
+ with:
+ client-id: ${{ vars.PUBLISHER_APP_CLIENT_ID }}
+ private-key: ${{ secrets.PUBLISHER_APP_PRIVATE_KEY }}
+ owner: ${{ github.repository_owner }}
+ repositories: "docs,${{ github.event.repository.name }}"
+ permission-contents: write
+ permission-pull-requests: write
+
+ - name: Verify approval and push squashed commit
+ env:
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
+ SOURCE_REPO: ${{ github.repository }}
+ REPO_OWNER: ${{ github.repository_owner }}
+ SOURCE_BRANCH: ${{ inputs.source_branch }}
+ COMMIT_MESSAGE: ${{ inputs.commit_message }}
+ ACTOR_LOGIN: ${{ github.actor }}
+ ACTOR_ID: ${{ github.actor_id }}
+ run: |
+ set -euo pipefail
+ [[ "${SOURCE_REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ && "${SOURCE_REPO}" != "google-gemma/docs" ]]
+ [[ "${SOURCE_BRANCH}" != "main" && "${SOURCE_BRANCH}" =~ ^[A-Za-z0-9_./-]+$ ]]
+ git check-ref-format --branch "${SOURCE_BRANCH}" >/dev/null
+ [[ -n "${COMMIT_MESSAGE// /}" ]]
+
+ git init repo && cd repo
+ AUTH_B64="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w0)"
+ git config "http.https://github.com/.extraheader" "AUTHORIZATION: basic ${AUTH_B64}"
+ git remote add upstream https://github.com/google-gemma/docs.git
+ git remote add origin "https://github.com/${SOURCE_REPO}.git"
+ git fetch --no-tags upstream main
+ git fetch --no-tags origin "refs/heads/${SOURCE_BRANCH}:refs/remotes/origin/target"
+ UPSTREAM_SHA="$(git rev-parse refs/remotes/upstream/main)"
+ TARGET_SHA="$(git rev-parse refs/remotes/origin/target)"
+ git merge-base --is-ancestor "${UPSTREAM_SHA}" "${TARGET_SHA}"
+ [[ "$(git rev-parse "${TARGET_SHA}^{tree}")" != "$(git rev-parse "${UPSTREAM_SHA}^{tree}")" ]]
+
+ PR_JSON="$(gh api -X GET "repos/${SOURCE_REPO}/pulls" -f state=open -f base=main -f "head=${REPO_OWNER}:${SOURCE_BRANCH}")"
+ PR_NUMBER="$(printf '%s' "${PR_JSON}" | jq -r '.[0].number // empty')"
+ PR_AUTHOR="$(printf '%s' "${PR_JSON}" | jq -r '.[0].user.login // empty')"
+ PR_HEAD="$(printf '%s' "${PR_JSON}" | jq -r '.[0].head.sha // empty')"
+ [[ -n "${PR_NUMBER}" && "${PR_HEAD}" == "${TARGET_SHA}" ]]
+
+ REVIEWS_JSON="$(gh api --paginate --slurp "repos/${SOURCE_REPO}/pulls/${PR_NUMBER}/reviews?per_page=100")"
+ APPROVERS="$(printf '%s' "${REVIEWS_JSON}" | jq -r --arg a "${PR_AUTHOR}" \
+ 'flatten | map(select(.user.login != null and .user.login != $a and (.state == "APPROVED" or .state == "CHANGES_REQUESTED" or .state == "DISMISSED"))) | group_by(.user.login) | map(sort_by(.id) | last) | .[] | select(.state == "APPROVED") | .user.login')"
+ VALID_LGTM=0
+ for reviewer in ${APPROVERS}; do
+ perm="$(gh api "repos/${SOURCE_REPO}/collaborators/${reviewer}/permission" --jq '.permission')"
+ case "${perm}" in admin|maintain|write) VALID_LGTM=1; break ;; esac
+ done
+ [[ "${VALID_LGTM}" -ge 1 ]]
+
+ export GIT_AUTHOR_NAME="${ACTOR_LOGIN}"
+ export GIT_AUTHOR_EMAIL="${ACTOR_ID}+${ACTOR_LOGIN}@users.noreply.github.com"
+ export GIT_COMMITTER_NAME="${GIT_AUTHOR_NAME}"
+ export GIT_COMMITTER_EMAIL="${GIT_AUTHOR_EMAIL}"
+ SQUASH_SHA="$(git commit-tree "${TARGET_SHA}^{tree}" -p "${UPSTREAM_SHA}" -m "${COMMIT_MESSAGE}")"
+ git push upstream "${SQUASH_SHA}:refs/heads/main" --ff-only
+ git push origin "${SQUASH_SHA}:refs/heads/main" --ff-only
+ gh api -X PATCH "repos/${SOURCE_REPO}/pulls/${PR_NUMBER}" -f state=closed
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
new file mode 100644
index 0000000..f955e2b
--- /dev/null
+++ b/.github/workflows/sync.yml
@@ -0,0 +1,42 @@
+name: Sync
+
+on:
+ schedule:
+ - cron: "0 6 * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: publish
+ cancel-in-progress: false
+
+jobs:
+ sync:
+ if: github.event.repository.private == true
+ runs-on: ubuntu-latest
+ environment: publish
+ steps:
+ - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
+ id: app-token
+ with:
+ client-id: ${{ vars.PUBLISHER_APP_CLIENT_ID }}
+ private-key: ${{ secrets.PUBLISHER_APP_PRIVATE_KEY }}
+ owner: ${{ github.repository_owner }}
+ repositories: "docs,${{ github.event.repository.name }}"
+ permission-contents: write
+ permission-workflows: write
+
+ - name: Fast-forward main
+ env:
+ GH_APP_TOKEN: ${{ steps.app-token.outputs.token }}
+ TARGET_REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ [[ "${TARGET_REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ && "${TARGET_REPO}" != "google-gemma/docs" ]]
+ git init repo && cd repo
+ AUTH_B64="$(printf 'x-access-token:%s' "${GH_APP_TOKEN}" | base64 -w0)"
+ git config "http.https://github.com/.extraheader" "AUTHORIZATION: basic ${AUTH_B64}"
+ git fetch --no-tags https://github.com/google-gemma/docs.git main:refs/remotes/upstream/main
+ git push "https://github.com/${TARGET_REPO}.git" refs/remotes/upstream/main:refs/heads/main --ff-only
diff --git a/overrides/main.html b/overrides/main.html
index 4799cdb..bb40fee 100644
--- a/overrides/main.html
+++ b/overrides/main.html
@@ -65,7 +65,7 @@
{% endif %}
{% endif %}
- {% if page and page.meta and page.meta.search and page.meta.search.exclude %}
+ {% if (page and page.meta and page.meta.search and page.meta.search.exclude) or (config.site_url and (config.site_url | trim("/")) != "https://gemma.ai") %}
{% else %}
diff --git a/overrides/partials/page-context.html b/overrides/partials/page-context.html
index dc56660..f249171 100644
--- a/overrides/partials/page-context.html
+++ b/overrides/partials/page-context.html
@@ -21,7 +21,7 @@
{%- set requires = page.meta.requires if page.meta and page.meta.requires else "" -%}
{%- set frameworks = page.meta.frameworks if page.meta and page.meta.frameworks else [] -%}
{%- set colab = page.meta.colab if page.meta and page.meta.colab else "" -%}
-{%- set markdown_url = ("/" ~ (page.url | trim("/")) ~ ".md.txt") if page and page.url else "" -%}
+{%- set markdown_url = (((page.url | trim("/")) ~ ".md.txt") | url) if page and page.url else "" -%}
{%- set ns = namespace(families=[]) -%}
{%- for id in models -%}
{%- set family = id | split("/") | first -%}
@@ -37,7 +37,8 @@
{%- for family in ns.families %}
{%- set url = gemma_model_urls[family] | default("") %}
{%- if url %}
- {{ gemma_model_names[family] | default(family) }}
+ {%- set resolved_url = (((url | trim("/")) ~ "/") | url) if url[:1] == "/" else url %}
+ {{ gemma_model_names[family] | default(family) }}
{%- else %}
{{ gemma_model_names[family] | default(family) }}
{%- endif %}
diff --git a/scripts/build_docs.py b/scripts/build_docs.py
index eabc2ae..84a9f73 100644
--- a/scripts/build_docs.py
+++ b/scripts/build_docs.py
@@ -25,12 +25,17 @@
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator
+from urllib.parse import quote
import agent_docs
ANALYTICS_PROPERTY_RE = re.compile(
r'(\[project\.extra\.analytics\][\s\S]*?\bproperty\s*=\s*)"[^"]*"'
)
+SITE_URL_RE = re.compile(r'^(\s*site_url\s*=\s*)"[^"]*"', re.MULTILINE)
+COLAB_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
+COLAB_BRANCH_RE = re.compile(r"^[A-Za-z0-9_./-]+$")
+PUBLIC_COLAB_PREFIX = "https://colab.research.google.com/github/google-gemma/docs/blob/main/"
def inject_analytics_property(config_text: str, measurement_id: str) -> str:
@@ -40,17 +45,64 @@ def inject_analytics_property(config_text: str, measurement_id: str) -> str:
return ANALYTICS_PROPERTY_RE.sub(rf'\g<1>"{measurement_id}"', config_text, count=1)
+def inject_site_url(config_text: str, site_url: str) -> str:
+ """Return config_text with project.site_url set to site_url."""
+ if not SITE_URL_RE.search(config_text):
+ raise ValueError("Could not locate site_url in zensical.toml")
+ normalized = re.sub(r"(? int:
+ """Rewrite canonical in-repo Colab links in built HTML files to target repo and branch."""
+ clean_repo = repo.strip()
+ clean_branch = branch.strip()
+ if not COLAB_REPO_RE.fullmatch(clean_repo):
+ raise ValueError(f"Invalid COLAB_REPO: {repo!r}")
+ if (
+ not COLAB_BRANCH_RE.fullmatch(clean_branch)
+ or clean_branch.startswith("/")
+ or clean_branch.endswith("/")
+ or ".." in clean_branch
+ ):
+ raise ValueError(f"Invalid COLAB_BRANCH: {branch!r}")
+
+ # URL-encode "/" as "%2F" so compound branch names stay a single path segment in Colab.
+ encoded_branch = quote(clean_branch, safe="._-")
+ target_prefix = f"https://colab.research.google.com/github/{clean_repo}/blob/{encoded_branch}/"
+ if target_prefix == PUBLIC_COLAB_PREFIX:
+ return 0
+
+ rewritten_files = 0
+ for html_path in site_dir.rglob("*.html"):
+ text = html_path.read_text(encoding="utf-8")
+ if PUBLIC_COLAB_PREFIX in text:
+ html_path.write_text(
+ text.replace(PUBLIC_COLAB_PREFIX, target_prefix),
+ encoding="utf-8",
+ )
+ rewritten_files += 1
+ return rewritten_files
+
+
@contextmanager
-def build_config(measurement_id: str | None) -> Iterator[Path | None]:
- """Yield a temporary Zensical config path with GA4 measurement ID injected if set."""
- if measurement_id is None:
+def build_config(
+ measurement_id: str | None,
+ site_url: str | None = None,
+) -> Iterator[Path | None]:
+ """Yield a temporary Zensical config path with optional GA4 and site_url overrides."""
+ if measurement_id is None and site_url is None:
yield None
return
source_config = agent_docs.REPO_ROOT / "zensical.toml"
temp_config = agent_docs.REPO_ROOT / ".zensical.build.toml"
content = source_config.read_text(encoding="utf-8")
- temp_config.write_text(inject_analytics_property(content, measurement_id), encoding="utf-8")
+ if measurement_id is not None:
+ content = inject_analytics_property(content, measurement_id)
+ if site_url is not None:
+ content = inject_site_url(content, site_url)
+ temp_config.write_text(content, encoding="utf-8")
try:
yield temp_config
finally:
@@ -87,19 +139,32 @@ def main() -> int:
agent_docs.write_generated()
raw_measurement_id = os.environ.get("GA_MEASUREMENT_ID")
measurement_id = raw_measurement_id.strip() if raw_measurement_id is not None else None
+ raw_site_url = os.environ.get("SITE_URL")
+ site_url = raw_site_url.strip() if raw_site_url is not None and raw_site_url.strip() else None
+ is_non_prod_site = site_url is not None and site_url.rstrip("/") != "https://gemma.ai"
+ if is_non_prod_site:
+ measurement_id = ""
+ raw_colab_repo = os.environ.get("COLAB_REPO")
+ colab_repo = raw_colab_repo.strip() if raw_colab_repo is not None and raw_colab_repo.strip() else None
+ raw_colab_branch = os.environ.get("COLAB_BRANCH")
+ colab_branch = (
+ raw_colab_branch.strip() if raw_colab_branch is not None and raw_colab_branch.strip() else None
+ )
args = sys.argv[1:]
site_dir = agent_docs.REPO_ROOT / "site"
for prefix in agent_docs.HIDDEN_PREFIXES:
stale = site_dir / prefix.strip("/")
if stale.exists():
shutil.rmtree(stale)
- with hidden_docs(), build_config(measurement_id) as temp_config:
+ with hidden_docs(), build_config(measurement_id, site_url) as temp_config:
if temp_config and "-f" not in args and "--config-file" not in args:
args = ["-f", str(temp_config), *args]
command = [sys.executable, "-m", "zensical", "build", *args]
result = subprocess.run(command, cwd=agent_docs.REPO_ROOT, check=False)
if result.returncode:
return result.returncode
+ if is_non_prod_site and colab_repo and colab_branch:
+ rewrite_colab_links(site_dir, colab_repo, colab_branch)
agent_docs.publish(site_dir)
return 0
diff --git a/tests/test_analytics.py b/tests/test_analytics.py
index 5ad400d..5208def 100644
--- a/tests/test_analytics.py
+++ b/tests/test_analytics.py
@@ -18,6 +18,7 @@
import os
import subprocess
import sys
+import tempfile
import unittest
from pathlib import Path
@@ -36,6 +37,46 @@ def test_inject_analytics_property_updates_config(self):
cleared = build_docs.inject_analytics_property(source, "")
self.assertIn('property = ""', cleared)
+ def test_inject_site_url_updates_config(self):
+ source = (build_docs.agent_docs.REPO_ROOT / "zensical.toml").read_text(encoding="utf-8")
+ self.assertIn('site_url = "https://gemma.ai"', source)
+ injected = build_docs.inject_site_url(source, "https://example.invalid//pr-42")
+ self.assertIn('site_url = "https://example.invalid/pr-42"', injected)
+
+ def test_rewrite_colab_links_handles_simple_and_compound_branches(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ site_dir = Path(tmp)
+ page = site_dir / "page" / "index.html"
+ page.parent.mkdir(parents=True)
+ sample_html = (
+ 'Colab\n'
+ 'External\n'
+ )
+ page.write_text(sample_html, encoding="utf-8")
+ md_txt = site_dir / "page.md.txt"
+ md_txt.write_text(sample_html, encoding="utf-8")
+
+ count = build_docs.rewrite_colab_links(site_dir, "org/repo", "sub/branch")
+ self.assertEqual(count, 1)
+ rewritten = page.read_text(encoding="utf-8")
+ self.assertIn(
+ "https://colab.research.google.com/github/org/repo/blob/"
+ "sub%2Fbranch/docs/notebooks/notebook.ipynb",
+ rewritten,
+ )
+ self.assertIn(
+ "https://colab.research.google.com/github/google-gemma/cookbook/blob/main/demo.ipynb",
+ rewritten,
+ )
+ self.assertEqual(md_txt.read_text(encoding="utf-8"), sample_html)
+
+ with self.assertRaises(ValueError):
+ build_docs.rewrite_colab_links(site_dir, "bad repo", "main")
+ with self.assertRaises(ValueError):
+ build_docs.rewrite_colab_links(site_dir, "org/repo", "../bad")
+
@unittest.skipUnless(HAS_ZENSICAL, "zensical is required for full build tests")
def test_build_without_measurement_id_omits_tracking_script(self):
env = {**os.environ, "GA_MEASUREMENT_ID": ""}
@@ -73,6 +114,28 @@ def test_build_with_measurement_id_includes_ga4_and_consent(self):
# Ensure temporary config file is cleaned up
self.assertFalse((build_docs.agent_docs.REPO_ROOT / ".zensical.build.toml").exists())
+ @unittest.skipUnless(HAS_ZENSICAL, "zensical is required for full build tests")
+ def test_non_prod_site_url_disables_analytics_and_sets_noindex(self):
+ env = {
+ **os.environ,
+ "GA_MEASUREMENT_ID": "G-TEST123456",
+ "SITE_URL": "https://example.invalid/pr-1",
+ }
+ result = subprocess.run(
+ [sys.executable, "scripts/build_docs.py"],
+ cwd=build_docs.agent_docs.REPO_ROOT,
+ env=env,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ index_html = (build_docs.agent_docs.REPO_ROOT / "site" / "index.html").read_text(encoding="utf-8")
+ self.assertNotIn("googletagmanager.com/gtag/js", index_html)
+ self.assertNotIn("G-TEST123456", index_html)
+ self.assertNotIn("G-CWMC2KTTGQ", index_html)
+ self.assertIn('', index_html)
+
if __name__ == "__main__":
unittest.main()