Build Explainer #44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Explainer | |
| # Hosted runner for explainmyrepo: given a GitHub repo, build its explainer with the | |
| # owner's keys (repo secrets), deploy it to its own Netlify site, and write progress | |
| # back to a status gist that the website polls. Triggered by the /build Netlify function. | |
| # | |
| # RAILS (deterministic): 20-min hard timeout · serialized concurrency (no cost fan-out) · | |
| # --no-refine (caps the expensive vision passes) · --ship-best-effort (ALWAYS delivers a | |
| # URL) · --no-publish (no per-repo GitHub repo/invite, so no extra token needed). | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| target_repo: | |
| description: "owner/repo to explain" | |
| required: true | |
| build_id: | |
| description: "correlation id (matches the status gist)" | |
| required: false | |
| gist_id: | |
| description: "status gist to write progress into" | |
| required: false | |
| submitter_email: | |
| description: "email to notify when done (optional)" | |
| required: false | |
| concurrency: | |
| group: build-explainer | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout the explainer tool | |
| uses: actions/checkout@v4 | |
| - name: Set up Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Validate input (abuse guard) | |
| run: | | |
| REPO='${{ github.event.inputs.target_repo }}' | |
| echo "target: $REPO" | |
| if ! printf '%s' "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then | |
| echo "::error::invalid repo format (expected owner/name): $REPO"; exit 1 | |
| fi | |
| - name: Install dependencies | |
| run: | | |
| npm ci || npm install | |
| # Belt-and-suspenders: guarantee the 3 runtime deps are present (CI 'npm ci' was leaving | |
| # playwright unresolved -> quality-grade failed). EXACT pinned versions + --no-save so this | |
| # cannot drift to a newer/compromised patch or rewrite the committed lockfile (no supply- | |
| # chain bypass; these match package-lock.json). | |
| npm install --no-save playwright@1.61.1 @xenova/transformers@2.17.2 @ruvector/rvf@0.2.2 | |
| npx playwright install --with-deps chromium | |
| # System tools the pipeline shells out to (scanned from tools/*.mjs), not all preinstalled | |
| # on the runner: ImageMagick (magick/convert/identify — favicon, social card), xmllint | |
| # (libxml2-utils — diagram SVG validation), dot (graphviz — diagram layout), zip (AI pack). | |
| # Fonts help text render in generated imagery. | |
| sudo apt-get update && sudo apt-get install -y imagemagick libxml2-utils graphviz zip fonts-liberation fonts-dejavu-core | |
| - name: Build, deploy, and report status | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | |
| EXPLAINER_NONINTERACTIVE: "1" | |
| EXPLAINER_SKIP_INVITE: "1" | |
| # Email fallback (non-blocking): if these secrets exist, notify emails the submitter. | |
| SMTP_USER: ${{ secrets.GMAIL_USER }} | |
| SMTP_PASS: ${{ secrets.GMAIL_APP_PASSWORD }} | |
| EMAIL_TO: ${{ github.event.inputs.submitter_email }} | |
| # Gist status write-back (needs `gist` scope; same account that created the gist): | |
| GIST_TOKEN: ${{ secrets.EXPLAINER_GH_TOKEN }} | |
| BUILD_ID: ${{ github.event.inputs.build_id }} | |
| GIST_ID: ${{ github.event.inputs.gist_id }} | |
| REPO: ${{ github.event.inputs.target_repo }} | |
| run: | | |
| set +e | |
| patch_status() { # status step stepName liveUrl error | |
| [ -z "$GIST_ID" ] || [ -z "$GIST_TOKEN" ] && return 0 | |
| local content | |
| content=$(jq -n --arg b "$BUILD_ID" --argjson s "$1" --arg sn "$3" --arg st "$2" \ | |
| --arg u "$4" --arg e "$5" --arg r "$REPO" \ | |
| '{buildId:$b, step:$s, totalSteps:16, stepName:$sn, status:$st, repo:$r, | |
| result: (if $u=="" then null else {liveUrl:$u} end), | |
| error: (if $e=="" then null else $e end)}') | |
| curl -s -X PATCH "https://api.github.com/gists/$GIST_ID" \ | |
| -H "Authorization: Bearer $GIST_TOKEN" -H "Accept: application/vnd.github+json" \ | |
| -d "$(jq -n --arg c "$content" '{files:{"status.json":{content:$c}}}')" >/dev/null || true | |
| } | |
| patch_status 1 building "Reading and understanding the repo…" "" "" | |
| set -o pipefail | |
| node bin/explainmyrepo.mjs "$REPO" --ship-best-effort --no-publish --no-refine 2>&1 | tee /tmp/build.log | |
| RC=${PIPESTATUS[0]} | |
| URL=$(grep -oE 'https://[a-zA-Z0-9.-]+\.netlify\.app' /tmp/build.log | tail -1) | |
| if [ "$RC" -eq 0 ] && [ -n "$URL" ]; then | |
| patch_status 16 done "Done — your explainer is live." "$URL" "" | |
| echo "LIVE: $URL" | |
| echo "### Explainer ready → $URL" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| # SECURITY: never publish raw build-log tails to the (public) status gist — a log line | |
| # could contain an API key. Curated, fixed message to the gist; real detail stays in the | |
| # GitHub Actions run log (visible only to repo collaborators). | |
| patch_status 16 failed "The build couldn't finish." "" "It didn't complete this time. Try another repo, or try again in a bit." | |
| echo "::error::build failed (rc=$RC, url=$URL)" | |
| echo "---- build.log tail (Actions run log only — NOT published) ----" | |
| tail -c 2000 /tmp/build.log || true | |
| exit 1 | |
| fi |