Skip to content

Deploy static content to Pages #759

Deploy static content to Pages

Deploy static content to Pages #759

Workflow file for this run

# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
paths:
- '.github/pages/**'
- '.github/workflows/pages.yaml'
- 'src/public/locales/**'
- 'src/public/pages/scripts/**'
- 'imgs/**'
- 'src/runner/main.sh'
- 'src/runner/main.ps1'
branches: ['master']
# Daily: fetch JotForm feedback and redeploy
schedule:
- cron: '0 0 * * *'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Resolve comments.json and skip unchanged (schedule only)
id: pages_skip
env:
JOTFORM_API_KEY: ${{ secrets.JOTFORM_API_KEY }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -e
PAGES='.github/pages'
DATA_DIR="$PAGES/data"
OUT="$DATA_DIR/comments.json"
PAGES_BASE='https://steve02081504.github.io/fount'
fetch_comments_to() {
local dest="$1"
mkdir -p "$(dirname "$dest")"
fallback() {
echo "Fetch failed or invalid response, falling back to existing deployment."
wget -q -O "$dest" "${PAGES_BASE}/data/comments.json" || echo '[]' > "$dest"
}
if [ -z "${JOTFORM_API_KEY:-}" ]; then
fallback
return
fi
curl -sf "https://api.jotform.com/form/260680715143050/submissions?apiKey=${JOTFORM_API_KEY}" | python3 -c "
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
sys.exit(1)
content = d.get('content') or []
out = []
for s in content:
a = s.get('answers') or {}
fullname = (a.get('2') or {}).get('prettyFormat') or ''
feedback = (a.get('4') or {}).get('answer') or ''
display_opt = (a.get('5') or {}).get('answer') or ''
show_name = 'Yes' in (display_opt or '')
avatar = (a.get('7') or {}).get('answer') or None
if (feedback or '').strip():
out.append({
'name': fullname if show_name else None,
'avatar': avatar,
'feedback': feedback.strip(),
'created_at': s.get('created_at')
})
print(json.dumps(out, ensure_ascii=False, indent=2))
" > "$dest" || fallback
}
json_equal() {
python3 -c "
import json, sys
with open(sys.argv[1], encoding='utf-8') as f: a = json.load(f)
with open(sys.argv[2], encoding='utf-8') as f: b = json.load(f)
def norm(x):
if isinstance(x, list):
return sorted(x, key=lambda i: json.dumps(i, sort_keys=True, ensure_ascii=False))
return x
sys.exit(0 if norm(a) == norm(b) else 1)
" "$1" "$2"
}
if [ "$EVENT_NAME" != "schedule" ]; then
mkdir -p "$DATA_DIR"
fetch_comments_to "$OUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
mkdir -p "$DATA_DIR"
fetch_comments_to /tmp/new_comments.json
wget -q -O /tmp/remote_comments.json "${PAGES_BASE}/data/comments.json" || echo '[]' > /tmp/remote_comments.json
if ! json_equal /tmp/new_comments.json /tmp/remote_comments.json; then
echo "Comments JSON differs from deployment; will deploy."
cp /tmp/new_comments.json "$OUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
svg_changed=0
shopt -s nullglob
for svg in imgs/*.svg; do
name=$(basename "$svg")
if ! curl -sf "${PAGES_BASE}/imgs/${name}" -o "/tmp/remote_${name}"; then
echo "Could not fetch deployed SVG: $name; will deploy."
svg_changed=1
break
fi
if ! python3 -c "
import pathlib, sys
a = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8').replace('\r\n', '\n')
b = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8').replace('\r\n', '\n')
sys.exit(0 if a == b else 1)
" "$svg" "/tmp/remote_${name}"; then
echo "SVG differs: $name; will deploy."
svg_changed=1
break
fi
done
shopt -u nullglob
if [ "$svg_changed" -eq 0 ]; then
echo "Schedule run: comments.json and all imgs/*.svg match live site; skipping deploy."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
cp /tmp/new_comments.json "$OUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Copy local files
if: steps.pages_skip.outputs.skip != 'true'
run: |
cp -r ./src/public/locales ./.github/pages/
cp -r ./imgs ./.github/pages/
cp -rn ./src/public/pages/scripts ./.github/pages/
cp ./src/runner/main.sh ./.github/pages/install.sh
cp ./src/runner/main.ps1 ./.github/pages/install.ps1
- name: PNG/ICO from live site or local render
if: steps.pages_skip.outputs.skip != 'true'
run: |
set -e
PAGES_BASE='https://steve02081504.github.io/fount'
IMG_DIR='.github/pages/imgs'
cd "$IMG_DIR"
shopt -s nullglob
svgs=(./*.svg)
if [ "${#svgs[@]}" -eq 0 ] || [ ! -f "${svgs[0]}" ]; then
echo "No SVG files under imgs; nothing to rasterize."
shopt -u nullglob
exit 0
fi
svg_match_live=true
for svg in "${svgs[@]}"; do
name=$(basename "$svg")
if ! curl -sf "${PAGES_BASE}/imgs/${name}" -o "/tmp/raster_cmp_${name}"; then
echo "Could not fetch live SVG: $name - will render locally."
svg_match_live=false
break
fi
if ! python3 -c "
import pathlib, sys
a = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8').replace('\r\n', '\n')
b = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8').replace('\r\n', '\n')
sys.exit(0 if a == b else 1)
" "$svg" "/tmp/raster_cmp_${name}"; then
echo "SVG differs from live: $name - will render locally."
svg_match_live=false
break
fi
done
if [ "$svg_match_live" = true ]; then
fetch_ok=true
for svg in "${svgs[@]}"; do
base="${svg%.svg}"
stem="${base#./}"
if ! curl -sf "${PAGES_BASE}/imgs/${stem}.png" -o "${stem}.png" \
|| ! curl -sf "${PAGES_BASE}/imgs/${stem}.ico" -o "${stem}.ico"; then
echo "Could not fetch live ${stem}.png/.ico - will render locally."
fetch_ok=false
break
fi
done
if [ "$fetch_ok" = true ]; then
echo "SVGs match live and PNG/ICO fetched from deployment."
shopt -u nullglob
exit 0
fi
fi
echo "Installing raster tools and rendering PNG/ICO locally."
sudo apt-get update -q && sudo apt-get install -y -q librsvg2-bin imagemagick
for svg in "${svgs[@]}"; do
base="${svg%.svg}"
rsvg-convert -w 512 "$svg" -o "${base}.png"
for dim in 16 32 48 64 128 256; do
rsvg-convert -w "$dim" "$svg" -o "/tmp/ico_${dim}.png"
done
convert /tmp/ico_16.png /tmp/ico_32.png /tmp/ico_48.png /tmp/ico_64.png /tmp/ico_128.png /tmp/ico_256.png "${base}.ico" \
|| magick /tmp/ico_16.png /tmp/ico_32.png /tmp/ico_48.png /tmp/ico_64.png /tmp/ico_128.png /tmp/ico_256.png "${base}.ico"
done
shopt -u nullglob
- name: Inline GitHub user-attachments to avoid CSP
if: steps.pages_skip.outputs.skip != 'true'
env:
REPO_NAME: ${{ github.event.repository.name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
PAGES='.github/pages'
ASSETS_DIR="$PAGES/assets"
mkdir -p "$ASSETS_DIR"
CURL_AUTH=( -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream, image/*" )
echo "::group::Collecting user-attachments URLs from $PAGES"
grep -rohE 'https://github\.com/user-attachments/assets/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' "$PAGES" 2>/dev/null | sort -u > /tmp/urls.txt || true
n=$(wc -l < /tmp/urls.txt)
echo "Found $n unique URL(s)"
cat /tmp/urls.txt
echo "::endgroup::"
# Pre-fetch rendered README HTML via GitHub API to get fresh JWT-signed CDN URLs.
# Some assets (e.g. GIFs from private-user-images CDN) return 404 without a browser
# session, but are embedded in rendered Markdown as pre-signed URLs with a short-lived JWT.
echo "::group::Pre-fetching rendered Markdown for JWT URLs"
JWT_HTML_FILE=/tmp/rendered_pages.html
for mdfile in README.md docs/Readme.*.md; do
[ -f "$mdfile" ] || continue
api_path=$(echo "$mdfile" | sed 's|^\./||')
gh api "/repos/$GITHUB_REPOSITORY/contents/$api_path" \
-H "Accept: application/vnd.github.html" >> "$JWT_HTML_FILE" 2>/dev/null || true
done
jwt_url_count=$(grep -oE 'https://private-user-images\.githubusercontent\.com/[^"]+' "$JWT_HTML_FILE" 2>/dev/null | wc -l)
echo "JWT-signed CDN URLs found: $jwt_url_count"
echo "::endgroup::"
while IFS= read -r url; do
[ -z "$url" ] && continue
uid=$(basename "$url")
echo "::group::Fetch $uid"
tmp="$ASSETS_DIR/${uid}.tmp"
# Primary: token → redirect → fetch pre-signed URL without Auth header.
location=$(curl -s -w '%{redirect_url}' "${CURL_AUTH[@]}" --max-redirs 0 -o /dev/null "$url" || echo '')
if [ -n "$location" ]; then
echo "Redirect obtained: ${location:0:80}..."
code=$(curl -sL -w '%{http_code}' -o "$tmp" "$location")
else
code=$(curl -sL -w '%{http_code}' "${CURL_AUTH[@]}" -o "$tmp" "$url")
fi
# Fallback: extract a fresh JWT URL from the pre-fetched rendered Markdown HTML.
if [ "$code" != "200" ] || [ ! -s "$tmp" ]; then
echo "Direct fetch failed (HTTP $code), trying JWT URL from rendered Markdown..."
rm -f "$tmp"
jwt_url=$(grep -oE "https://private-user-images\.githubusercontent\.com/[^\"]*${uid}[^\"]*" \
"$JWT_HTML_FILE" 2>/dev/null | head -1 || echo '')
if [ -n "$jwt_url" ]; then
echo "JWT URL: ${jwt_url:0:80}..."
code=$(curl -sL -w '%{http_code}' -o "$tmp" "$jwt_url")
fi
fi
if [ "$code" != "200" ] || [ ! -s "$tmp" ]; then
echo "Failed to fetch $url (HTTP $code)"
rm -f "$tmp"
echo "::endgroup::"
continue
fi
mime=$(file -b --mime-type "$tmp" || echo '')
case "$mime" in
image/png) ext=png;;
image/jpeg) ext=jpg;;
image/gif) ext=gif;;
image/webp) ext=webp;;
image/svg+xml) ext=svg;;
*) echo "Response not an image ($mime), skipping"; rm -f "$tmp"; echo "::endgroup::"; continue;;
esac
out="$ASSETS_DIR/${uid}.${ext}"
mv "$tmp" "$out"
echo "Saved $out ($mime)"
base_path="/$REPO_NAME/assets/${uid}.${ext}"
find "$PAGES" -type f \( -name '*.html' -o -name '*.css' -o -name '*.mjs' -o -name '*.json' \) -exec sed -i "s|$url|$base_path|g" {} +
echo "Replaced with $base_path"
echo "::endgroup::"
done < /tmp/urls.txt
- name: Setup Pages
if: steps.pages_skip.outputs.skip != 'true'
uses: actions/configure-pages@v5
- name: Upload artifact
if: steps.pages_skip.outputs.skip != 'true'
uses: actions/upload-pages-artifact@v5
with:
path: '.github/pages'
- name: Deploy to GitHub Pages
if: steps.pages_skip.outputs.skip != 'true'
id: deployment
uses: actions/deploy-pages@v5