diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml
index 0104ba7..a80943b 100644
--- a/.github/workflows/jekyll.yml
+++ b/.github/workflows/jekyll.yml
@@ -11,7 +11,41 @@ permissions:
contents: read
jobs:
+ update-asset-version:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.head_ref }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Update asset version
+ run: python script/asset_version.py
+
+ - name: Commit asset version if changed
+ run: |
+ if git diff --quiet -- _data/assets.yml; then
+ echo "Asset version already up to date"
+ exit 0
+ fi
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add _data/assets.yml
+ git commit -m "Update asset version hash for CSS/JS changes."
+ git push
+
build:
+ needs: update-asset-version
+ if: always() && (needs.update-asset-version.result == 'success' || needs.update-asset-version.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -30,8 +64,9 @@ jobs:
- name: Check WebP derivatives
run: python script/generate_webp.py --check
- - name: Generate asset version
- run: python script/asset_version.py
+ - name: Check asset version
+ if: github.event_name != 'pull_request'
+ run: python script/asset_version.py --check
- name: Setup Pages
uses: actions/configure-pages@v5
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index d7f5ac8..0000000
--- a/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-_data/assets.yml
diff --git a/README.md b/README.md
index 017a867..ff5dd67 100644
--- a/README.md
+++ b/README.md
@@ -97,9 +97,13 @@ CI runs `python script/generate_webp.py --check` to ensure WebP copies are prese
### CSS/JS cache busting
-CI runs `python script/asset_version.py` before each Jekyll build. It hashes `css/style.css`, `css/github_markdown.css`, and `js/gallery.js`, then writes `_data/assets.yml`. Layout templates append `?v=` that hash to stylesheet and script URLs so browsers fetch new copies when those files change.
+`_data/assets.yml` stores a content hash for `css/style.css`, `css/github_markdown.css`, and `js/gallery.js`. Layout templates append it as `?v=` on stylesheet and script URLs. The file must be in the repo because GitHub Pages runs its own Jekyll build.
-For a local Jekyll preview, run the same script first:
+On pull requests, CI runs `script/asset_version.py` and commits an updated `_data/assets.yml` to the PR branch when CSS or JS changed, so the hash is ready before you merge.
+
+Pushes to `master` run `script/asset_version.py --check` instead (the merged PR should already include the updated file).
+
+To update locally:
```bash
python script/asset_version.py
diff --git a/_data/assets.yml b/_data/assets.yml
new file mode 100644
index 0000000..62d4178
--- /dev/null
+++ b/_data/assets.yml
@@ -0,0 +1 @@
+assets_version: "8a29461d"
diff --git a/_layouts/default.html b/_layouts/default.html
index 711f509..224bb3b 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -44,7 +44,7 @@
- {% assign assets_version = site.data.assets.assets_version | default: 'dev' %}
+ {% assign assets_version = site.data.assets.assets_version | default: site.github.build_revision | default: 'dev' %}
diff --git a/script/asset_version.py b/script/asset_version.py
index 98b6e23..7c79bfa 100644
--- a/script/asset_version.py
+++ b/script/asset_version.py
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
-"""Write _data/assets.yml with a content hash for CSS/JS cache busting."""
+"""Manage _data/assets.yml content hash for CSS/JS cache busting."""
from __future__ import annotations
+import argparse
import hashlib
+import re
import sys
from pathlib import Path
@@ -15,20 +17,60 @@
OUTPUT = Path("_data/assets.yml")
-def main() -> int:
+def compute_version() -> str:
hasher = hashlib.sha256()
for path in ASSET_FILES:
if not path.is_file():
- print(f"Missing asset file: {path}", file=sys.stderr)
- return 1
+ raise FileNotFoundError(path)
hasher.update(path.read_bytes())
hasher.update(b"\0")
- version = hasher.hexdigest()[:8]
+ return hasher.hexdigest()[:8]
+
+
+def read_version() -> str | None:
+ if not OUTPUT.is_file():
+ return None
+
+ match = re.search(r'^assets_version:\s*"([0-9a-f]+)"\s*$', OUTPUT.read_text(encoding="utf-8"), re.MULTILINE)
+ return match.group(1) if match else None
+
+
+def write_version(version: str) -> None:
OUTPUT.parent.mkdir(exist_ok=True)
OUTPUT.write_text(f'assets_version: "{version}"\n', encoding="utf-8")
- print(f"Wrote {OUTPUT} (assets_version: {version})")
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="Fail if _data/assets.yml is missing or does not match current CSS/JS files",
+ )
+ args = parser.parse_args()
+
+ try:
+ expected = compute_version()
+ except FileNotFoundError as exc:
+ print(f"Missing asset file: {exc}", file=sys.stderr)
+ return 1
+
+ if args.check:
+ actual = read_version()
+ if actual != expected:
+ print(
+ f"_data/assets.yml is out of date (have {actual!r}, expected {expected!r}). "
+ "Run: python script/asset_version.py",
+ file=sys.stderr,
+ )
+ return 1
+ print(f"Asset version OK ({expected})")
+ return 0
+
+ write_version(expected)
+ print(f"Wrote {OUTPUT} (assets_version: {expected})")
return 0