From 9a4e34268f1687971c22e9c78667af01e6fde820 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Tue, 20 Oct 2026 22:33:29 +0000 Subject: [PATCH] shots: a workflow that renders shots/desktop.png with headless Chromium and commits it Teahub pages serves this repo at gandora.yolopreneur.lol; the yolopreneur.lol venture list shows shots/desktop.png as the card image. Actions -> shots -> Run workflow (also runs on a push to main that touches index.html). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Hn2DWhV7Z1qUxjhCprDb88 --- .gitea/workflows/shots.yml | 48 ++++++++++++++++++++++++++++++++++++++ tools/build.sh | 4 ++-- tools/shots.sh | 39 +++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/shots.yml create mode 100644 tools/shots.sh diff --git a/.gitea/workflows/shots.yml b/.gitea/workflows/shots.yml new file mode 100644 index 0000000..56947fc --- /dev/null +++ b/.gitea/workflows/shots.yml @@ -0,0 +1,48 @@ +name: shots +# Render shots/desktop.png (the venture-list card image on yolopreneur.lol) from this repo's own index.html with +# headless Chromium, and commit it when it changed. Same recipe as Time's refresh workflow; Playwright's Chromium is +# installed when the runner has no Chrome. +on: + workflow_dispatch: + push: + branches: [main] + paths: [index.html, brain.js, favicon.svg] +permissions: + contents: write +concurrency: + group: shots + cancel-in-progress: true +jobs: + shots: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 1 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: find Chrome, or install Playwright's Chromium + run: | + set -eu + for c in google-chrome google-chrome-stable chromium chromium-browser; do + if command -v "$c" >/dev/null 2>&1; then echo "CHROME=$(command -v "$c")" >> "$GITHUB_ENV"; exit 0; fi + done + npx -y playwright@1.55.0 install --with-deps chromium > /tmp/pw.log 2>&1 || { tail -20 /tmp/pw.log; exit 1; } + c=$(find "$HOME/.cache/ms-playwright" -type f -name chrome -path '*chrome-linux*' | head -1) + [ -n "$c" ] && [ -x "$c" ] || { echo "::error::no Chromium after the install"; exit 1; } + echo "CHROME=$c" >> "$GITHUB_ENV" + - name: shoot + env: + CHROME_FLAGS: "--no-sandbox --disable-dev-shm-usage --disable-gpu" + run: sh tools/shots.sh + - name: commit + run: | + set -eu + git add shots/desktop.png + if git diff --cached --quiet; then echo "shot unchanged"; exit 0; fi + git -c user.name='gitea-actions[bot]' -c user.email='gitea-actions[bot]@users.noreply.teahub.xyz' commit -q -m "shots: desktop.png" + git push -q origin HEAD:main + - name: tell Teahub pages to pull + run: curl -fsS -X POST "https://gandora.yolopreneur.lol/_hook/gandora/site" || echo "pages pulls by itself within five minutes" diff --git a/tools/build.sh b/tools/build.sh index dac481e..09d90c0 100644 --- a/tools/build.sh +++ b/tools/build.sh @@ -1,3 +1,3 @@ #!/bin/sh -# gandora is a single static page; nothing to build. -exit 0 +# gandora is a single static page; the only made file is the card image. See tools/shots.sh. +exec sh "$(dirname "$0")/shots.sh" diff --git a/tools/shots.sh b/tools/shots.sh new file mode 100644 index 0000000..c3011f2 --- /dev/null +++ b/tools/shots.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# tools/shots.sh: shots/desktop.png, the first screen at 1440x900, rendered from this folder by headless Chrome. +# Needs python3 (for a local server) and Chrome or Chromium: set CHROME=/path/to/chrome if it isn't found, and +# CHROME_FLAGS=--no-sandbox when running as root. +set -eu +cd "$(dirname "$0")/.." + +CHROME=${CHROME:-} +for c in "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" "$(command -v chromium || true)" \ + "$(command -v chromium-browser || true)" "$(command -v google-chrome || true)" "$(command -v google-chrome-stable || true)"; do + if [ -z "$CHROME" ] && [ -n "$c" ] && [ -x "$c" ]; then CHROME=$c; fi +done +[ -n "$CHROME" ] || { echo "no Chrome or Chromium found; set CHROME" >&2; exit 1; } + +PORT=${PORT:-8794} +python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1 /dev/null || true' EXIT INT TERM +n=0 +until curl -fs -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/"; do + n=$((n + 1)); [ "$n" -lt 40 ] || { echo "the local server on port $PORT never answered" >&2; exit 1; }; sleep 0.5 +done + +mkdir -p shots +out=$PWD/shots/desktop.png +prof=$(mktemp -d) +rm -f "$out" +# shellcheck disable=SC2086 +"$CHROME" --headless=new --hide-scrollbars --no-first-run --no-default-browser-check --disable-background-networking \ + --disable-component-update --user-data-dir="$prof" --virtual-time-budget=6000 --window-size=1440,900 \ + --screenshot="$out" ${CHROME_FLAGS:-} "http://127.0.0.1:$PORT/" >"$prof.log" 2>&1 & +pid=$! +n=0 +while kill -0 "$pid" 2>/dev/null && [ ! -s "$out" ]; do n=$((n + 1)); [ "$n" -lt 120 ] || break; sleep 0.5; done +sleep 1 +kill "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true +[ -s "$out" ] || { echo "Chrome made no screenshot:" >&2; tail -5 "$prof.log" >&2; exit 1; } +rm -rf "$prof" "$prof.log" +echo "built: shots/desktop.png"