@@ -22,21 +22,130 @@ jobs:
2222 runs-on : ubuntu-latest
2323 steps :
2424 - uses : actions/checkout@v4
25+ with :
26+ fetch-depth : 0
2527 - uses : ruby/setup-ruby@v1
2628 with :
2729 ruby-version : " 3.3"
2830 bundler-cache : true
29- - run : bundle exec jekyll build
31+ - name : Build previous site
32+ id : previous-build
33+ continue-on-error : true
34+ run : |
35+ set -euo pipefail
36+
37+ if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
38+ previous_ref="${{ github.event.before }}"
39+ else
40+ previous_ref="HEAD^"
41+ fi
42+
43+ git worktree add --detach "$RUNNER_TEMP/previous-site" "$previous_ref"
44+ bundle exec jekyll build \
45+ --source "$RUNNER_TEMP/previous-site" \
46+ --destination "$RUNNER_TEMP/previous-site-build"
47+ - name : Build current site
48+ run : bundle exec jekyll build
49+ - name : Find changed routes
50+ id : changed-routes
51+ env :
52+ SITE_URL : https://cb341.dev
53+ run : |
54+ set -euo pipefail
55+
56+ if [ "${{ steps.previous-build.outcome }}" != "success" ]; then
57+ : > "$RUNNER_TEMP/purge-urls.txt"
58+ echo "count=0" >> "$GITHUB_OUTPUT"
59+ echo "No previous build available; skipping route-specific Cloudflare purge."
60+ exit 0
61+ fi
62+
63+ python3 <<'PY'
64+ import hashlib
65+ import os
66+ from pathlib import Path
67+
68+ previous = Path(os.environ["RUNNER_TEMP"]) / "previous-site-build"
69+ current = Path("_site")
70+ output = Path(os.environ["RUNNER_TEMP"]) / "purge-urls.txt"
71+ site_url = os.environ["SITE_URL"].rstrip("/")
72+
73+ def digest(path):
74+ h = hashlib.sha256()
75+ with path.open("rb") as file:
76+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
77+ h.update(chunk)
78+ return h.hexdigest()
79+
80+ def files(root):
81+ return {
82+ path.relative_to(root)
83+ for path in root.rglob("*")
84+ if path.is_file()
85+ }
86+
87+ def url_for(path):
88+ parent = path.parent.as_posix()
89+ if path.name == "index.html" and parent == ".":
90+ return "/"
91+
92+ if path.name == "index.html":
93+ return f"/{parent}/"
94+
95+ return f"/{path.as_posix()}"
96+
97+ changed = []
98+ for path in sorted(files(previous) | files(current)):
99+ before = previous / path
100+ after = current / path
101+ if not before.exists() or not after.exists() or digest(before) != digest(after):
102+ changed.append(f"{site_url}{url_for(path)}")
103+
104+ output.write_text("\n".join(changed) + ("\n" if changed else ""))
105+
106+ with Path(os.environ["GITHUB_OUTPUT"]).open("a") as github_output:
107+ github_output.write(f"count={len(changed)}\n")
108+
109+ print(f"Found {len(changed)} changed route(s).")
110+ for url in changed:
111+ print(url)
112+ PY
30113 - uses : actions/configure-pages@v5
31114 - uses : actions/upload-pages-artifact@v3
32115 with :
33116 path : _site/
34117 - id : deployment
35118 uses : actions/deploy-pages@v4
36119 - name : Purge Cloudflare cache
37- if : steps.deployment.outcome == 'success'
120+ if : steps.deployment.outcome == 'success' && steps.changed-routes.outputs.count != '0'
121+ env :
122+ CLOUDFLARE_ZONE_ID : ${{ secrets.CLOUDFLARE_ZONE_ID }}
123+ CLOUDFLARE_API_TOKEN : ${{ secrets.CLOUDFLARE_API_TOKEN }}
38124 run : |
39- curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/purge_cache" \
40- -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
41- -H "Content-Type: application/json" \
42- --data '{"purge_everything":true}'
125+ set -euo pipefail
126+
127+ python3 <<'PY'
128+ import json
129+ import os
130+ import subprocess
131+ from pathlib import Path
132+
133+ urls = [
134+ line.strip()
135+ for line in (Path(os.environ["RUNNER_TEMP"]) / "purge-urls.txt").read_text().splitlines()
136+ if line.strip()
137+ ]
138+
139+ endpoint = f"https://api.cloudflare.com/client/v4/zones/{os.environ['CLOUDFLARE_ZONE_ID']}/purge_cache"
140+ headers = [
141+ "-H", f"Authorization: Bearer {os.environ['CLOUDFLARE_API_TOKEN']}",
142+ "-H", "Content-Type: application/json",
143+ ]
144+
145+ for start in range(0, len(urls), 30):
146+ payload = json.dumps({"files": urls[start:start + 30]})
147+ subprocess.run(
148+ ["curl", "-sS", "-X", "POST", endpoint, *headers, "--data", payload],
149+ check=True,
150+ )
151+ PY
0 commit comments