44 push :
55 branches : ["main"]
66 workflow_dispatch :
7+ repository_dispatch :
8+ types : [visibility_changed]
79
810permissions :
911 contents : read
1012 pages : write
1113 id-token : write
14+ deployments : write
1215
1316concurrency :
1417 group : " pages"
1518 cancel-in-progress : false
1619
1720jobs :
21+ # ── Guard: check repo visibility ────────────────────────────────
22+ check-visibility :
23+ runs-on : ubuntu-latest
24+ outputs :
25+ is_private : ${{ steps.check.outputs.is_private }}
26+ steps :
27+ - name : Check repo visibility
28+ id : check
29+ run : |
30+ if [ "${{ github.event.repository.private }}" = "true" ]; then
31+ echo "is_private=true" >> "$GITHUB_OUTPUT"
32+ echo "::warning::Repository is private — Pages deployment will be skipped"
33+ else
34+ echo "is_private=false" >> "$GITHUB_OUTPUT"
35+ fi
36+
37+ # ── Teardown: remove Pages if repo is private ───────────────────
38+ teardown :
39+ runs-on : ubuntu-latest
40+ needs : check-visibility
41+ if : needs.check-visibility.outputs.is_private == 'true'
42+ steps :
43+ - name : Remove GitHub Pages deployments
44+ env :
45+ GH_TOKEN : ${{ github.token }}
46+ REPO : ${{ github.repository }}
47+ run : |
48+ echo "Repository is private — removing existing Pages deployments"
49+
50+ # List all deployments in the github-pages environment
51+ DEPLOYMENTS=$(gh api "repos/${REPO}/deployments?environment=github-pages&per_page=100" --jq '.[].id' 2>/dev/null || true)
52+
53+ if [ -z "$DEPLOYMENTS" ]; then
54+ echo "No existing Pages deployments found"
55+ exit 0
56+ fi
57+
58+ # Mark each deployment as inactive, then delete it
59+ for ID in $DEPLOYMENTS; do
60+ echo "Deactivating deployment ${ID}..."
61+ gh api \
62+ --method POST \
63+ -H "Accept: application/vnd.github+json" \
64+ "repos/${REPO}/deployments/${ID}/statuses" \
65+ -f state=inactive 2>/dev/null || true
66+
67+ echo "Deleting deployment ${ID}..."
68+ gh api \
69+ --method DELETE \
70+ -H "Accept: application/vnd.github+json" \
71+ "repos/${REPO}/deployments/${ID}" 2>/dev/null || true
72+ done
73+
74+ echo "All Pages deployments removed"
75+
76+ # ── Build (only if public) ──────────────────────────────────────
1877 build :
1978 runs-on : ubuntu-latest
79+ needs : check-visibility
80+ if : needs.check-visibility.outputs.is_private == 'false'
2081 steps :
2182 - name : Checkout
2283 uses : actions/checkout@v4
@@ -197,10 +258,6 @@ jobs:
197258 fi
198259
199260 # ── Fix relative links for Jekyll ─────────────────────────────
200- # Jekyll with permalink:pretty changes paths, so relative links
201- # like [X](CONTRIBUTING.md) or [X](LICENSE) would 404.
202- # Point them at their Jekyll permalinks instead.
203- # Only targets known community files in root-level markdown.
204261 for f in *.md; do
205262 [ -f "$f" ] || continue
206263 [ -f "CONTRIBUTING.md" ] && sed -i 's|\](CONTRIBUTING\.md)|\](contributing/)|g' "$f"
@@ -250,7 +307,6 @@ jobs:
250307 fi
251308
252309 # ── Releases page ─────────────────────────────────────────────
253- # Write the JS to a separate file to avoid heredoc escaping issues
254310 mkdir -p _includes
255311 cat > _includes/releases.js << 'JSEOF'
256312 (async function() {
@@ -262,6 +318,29 @@ jobs:
262318 var data = await res.json();
263319 if (!data.length) { c.innerHTML = '<p>No releases found.</p>'; return; }
264320
321+ function inline(s) {
322+ var codes = [];
323+ s = s.replace(/`([^`]+)`/g, function(m, code) {
324+ codes.push(code.replace(/</g,'<').replace(/>/g,'>'));
325+ return '\x00CODE' + (codes.length - 1) + '\x00';
326+ });
327+ s = s
328+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
329+ .replace(/(https?:\/\/[^\s<)]+)/g, function(m, url, offset, str) {
330+ if (str.charAt(offset - 1) === '"' || str.charAt(offset - 1) === '>' || str.substring(offset - 6, offset) === 'href="') return m;
331+ return '<a href="' + url + '">' + url + '</a>';
332+ })
333+ .replace(/(^|[\s(])@([a-zA-Z0-9_-]+)/g, function(m, pre, user) {
334+ return pre + '<a href="https://github.com/' + user + '">@' + user + '</a>';
335+ })
336+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
337+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
338+ s = s.replace(/\x00CODE(\d+)\x00/g, function(m, idx) {
339+ return '<code>' + codes[parseInt(idx)] + '</code>';
340+ });
341+ return s;
342+ }
343+
265344 function md(s) {
266345 if (!s) return '';
267346 var lines = s.split('\n');
@@ -272,29 +351,29 @@ jobs:
272351 var h3 = line.match(/^### (.+)$/);
273352 var h2 = line.match(/^## (.+)$/);
274353 var li = line.match(/^\* (.+)$/);
275- var cb = line.match(/^```/);
354+ var cb = line.match(/^```(.*)$ /);
276355 if (cb) {
277356 if (inList) { html += '</ul>'; inList = false; }
357+ var lang = cb[1].trim();
278358 var code = '';
279359 i++;
280360 while (i < lines.length && !lines[i].match(/^```/)) { code += lines[i] + '\n'; i++; }
281- html += '<pre><code>' + code + '</code></pre>';
361+ var langLabel = lang ? '<div style="font-size:11px;color:var(--text-muted);padding:4px 12px;border-bottom:1px solid var(--border);font-family:IBM Plex Mono,monospace">' + lang + '</div>' : '';
362+ html += '<div style="border:1px solid var(--border);border-radius:6px;overflow:hidden;margin:8px 0 16px">' + langLabel + '<pre style="margin:0;border:none;border-radius:0"><code>' + code.replace(/</g,'<').replace(/>/g,'>') + '</code></pre></div>';
282363 } else if (h2) {
283364 if (inList) { html += '</ul>'; inList = false; }
284- html += '<h2>' + h2[1] + '</h2>';
365+ html += '<h2>' + inline( h2[1]) + '</h2>';
285366 } else if (h3) {
286367 if (inList) { html += '</ul>'; inList = false; }
287- html += '<h3>' + h3[1] + '</h3>';
368+ html += '<h3>' + inline( h3[1]) + '</h3>';
288369 } else if (li) {
289370 if (!inList) { html += '<ul>'; inList = true; }
290- var text = li[1].replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
291- html += '<li>' + text + '</li>';
371+ html += '<li>' + inline(li[1]) + '</li>';
292372 } else {
293373 if (inList) { html += '</ul>'; inList = false; }
294374 if (line.trim() === '---') { html += '<hr>'; }
295375 else if (line.trim()) {
296- var text = line.replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
297- html += '<p>' + text + '</p>';
376+ html += '<p>' + inline(line) + '</p>';
298377 }
299378 }
300379 }
@@ -306,16 +385,52 @@ jobs:
306385 var d = new Date(r.published_at).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
307386 var latest = i === 0 ? '<span class="release-latest">latest</span>' : '';
308387 var openAttr = i === 0 ? ' open' : '';
388+
389+ // Commit SHA
390+ var sha = r.target_commitish || '';
391+ var shortSha = sha.substring(0, 7);
392+ var shaHtml = '';
393+ if (sha.length >= 7) {
394+ shaHtml = '<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:13px;color:var(--text-muted)">'
395+ + '<a href="https://github.com/' + repo + '/commit/' + sha + '" style="font-family:IBM Plex Mono,monospace;font-size:12px">' + shortSha + '</a>'
396+ + '</div>';
397+ }
398+
399+ // Assets
309400 var assets = '';
310401 if (r.assets.length) {
311- assets = '<details class="release-assets"><summary>Assets (' + r.assets.length + ')</summary>';
402+ var hashMap = {};
403+ var mainAssets = [];
312404 for (var j = 0; j < r.assets.length; j++) {
313405 var a = r.assets[j];
314- assets += '<div class="release-asset">' + a.name + ' <span style="color:var(--text-muted)">' + (a.size / 1024).toFixed(1) + ' KB</span></div>';
406+ if (a.name.match(/\.(sha256|sha512|md5|sha1)$/i)) {
407+ var base = a.name.replace(/\.(sha256|sha512|md5|sha1)$/i, '');
408+ hashMap[base] = a;
409+ } else {
410+ mainAssets.push(a);
411+ }
412+ }
413+
414+ var srcZip = 'https://github.com/' + repo + '/archive/refs/tags/' + r.tag_name + '.zip';
415+ var srcTar = 'https://github.com/' + repo + '/archive/refs/tags/' + r.tag_name + '.tar.gz';
416+ var totalAssets = mainAssets.length + 2;
417+
418+ assets = '<details class="release-assets"><summary>Assets (' + totalAssets + ')</summary>';
419+ for (var j = 0; j < mainAssets.length; j++) {
420+ var a = mainAssets[j];
421+ var hash = hashMap[a.name];
422+ var hashHtml = '';
423+ if (hash) {
424+ hashHtml = ' <span style="font-size:11px;color:var(--text-muted)">(' + hash.name.split('.').pop() + ')</span>';
425+ }
426+ assets += '<div class="release-asset"><a href="' + a.browser_download_url + '">' + a.name + '</a> <span style="color:var(--text-muted)">' + (a.size / 1024).toFixed(1) + ' KB</span>' + hashHtml + '</div>';
315427 }
428+ assets += '<div class="release-asset"><a href="' + srcZip + '">Source code</a> <span style="color:var(--text-muted)">(zip)</span></div>';
429+ assets += '<div class="release-asset"><a href="' + srcTar + '">Source code</a> <span style="color:var(--text-muted)">(tar.gz)</span></div>';
316430 assets += '</details>';
317431 }
318- return '<details class="release"' + openAttr + '><summary><span class="release-tag">' + r.tag_name + '</span>' + latest + '<span class="release-date">' + d + '</span></summary><div class="release-body">' + md(r.body) + assets + '</div></details>';
432+
433+ return '<details class="release"' + openAttr + '><summary><span class="release-tag">' + r.tag_name + '</span>' + latest + '<span class="release-date">' + d + '</span></summary><div class="release-body">' + md(r.body) + assets + shaHtml + '</div></details>';
319434 }).join('');
320435 } catch(e) {
321436 c.innerHTML = '<p>Unable to load releases. Visit <a href="https://github.com/' + repo + '/releases">GitHub</a> directly.</p>';
@@ -344,7 +459,6 @@ jobs:
344459 done
345460
346461 if [ -n "$LICENSE_SRC" ]; then
347- # Create LICENSE.md with front matter + full license text
348462 if ! ([ -f "LICENSE.md" ] && head -1 LICENSE.md | grep -q '^\-\-\-'); then
349463 TEMP=$(mktemp)
350464 printf -- '---\nlayout: default\ntitle: License\npermalink: /license/\n---\n\n# License\n\n```\n' > "$TEMP"
@@ -354,7 +468,6 @@ jobs:
354468 echo "Created LICENSE.md from ${LICENSE_SRC}"
355469 fi
356470 elif [ -f "LICENSE.md" ] && ! head -1 LICENSE.md | grep -q '^\-\-\-'; then
357- # LICENSE.md exists but has no front matter
358471 TEMP=$(mktemp)
359472 printf -- '---\nlayout: default\ntitle: License\npermalink: /license/\n---\n\n' > "$TEMP"
360473 cat LICENSE.md >> "$TEMP"
@@ -397,12 +510,14 @@ jobs:
397510 - name : Upload artifact
398511 uses : actions/upload-pages-artifact@v3
399512
513+ # ── Deploy (only if build succeeded) ────────────────────────────
400514 deploy :
401515 environment :
402516 name : github-pages
403517 url : ${{ steps.deployment.outputs.page_url }}
404518 runs-on : ubuntu-latest
405519 needs : build
520+ if : needs.build.result == 'success'
406521 steps :
407522 - name : Deploy to GitHub Pages
408523 id : deployment
0 commit comments